670 lines
17 KiB
C++
670 lines
17 KiB
C++
/*
|
|
Mosh: the mobile shell
|
|
Copyright 2012 Keith Winstein
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
In addition, as a special exception, the copyright holders give
|
|
permission to link the code of portions of this program with the
|
|
OpenSSL library under certain conditions as described in each
|
|
individual source file, and distribute linked combinations including
|
|
the two.
|
|
|
|
You must obey the GNU General Public License in all respects for all
|
|
of the code used other than OpenSSL. If you modify file(s) with this
|
|
exception, you may extend this exception to your version of the
|
|
file(s), but you are not obligated to do so. If you do not wish to do
|
|
so, delete this exception statement from your version. If you delete
|
|
this exception statement from all source files in the program, then
|
|
also delete it here.
|
|
*/
|
|
|
|
#include <assert.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
|
|
#include "terminalframebuffer.h"
|
|
|
|
using namespace Terminal;
|
|
|
|
Cell::Cell( color_type background_color )
|
|
: contents(),
|
|
renditions( background_color ),
|
|
wide( false ),
|
|
fallback( false ),
|
|
wrap( false )
|
|
{}
|
|
|
|
void Cell::reset( color_type background_color )
|
|
{
|
|
contents.clear();
|
|
renditions = Renditions( background_color );
|
|
wide = false;
|
|
fallback = false;
|
|
wrap = false;
|
|
}
|
|
|
|
void DrawState::reinitialize_tabs( unsigned int start )
|
|
{
|
|
assert( default_tabs );
|
|
for ( unsigned int i = start; i < tabs.size(); i++ ) {
|
|
tabs[ i ] = ( (i % 8) == 0 );
|
|
}
|
|
}
|
|
|
|
DrawState::DrawState( int s_width, int s_height )
|
|
: width( s_width ), height( s_height ),
|
|
cursor_col( 0 ), cursor_row( 0 ),
|
|
combining_char_col( 0 ), combining_char_row( 0 ), default_tabs( true ), tabs( s_width ),
|
|
scrolling_region_top_row( 0 ), scrolling_region_bottom_row( height - 1 ),
|
|
renditions( 0 ), save(),
|
|
next_print_will_wrap( false ), origin_mode( false ), auto_wrap_mode( true ),
|
|
insert_mode( false ), cursor_visible( true ), reverse_video( false ),
|
|
bracketed_paste( false ), mouse_reporting_mode( MOUSE_REPORTING_NONE ), mouse_focus_event( false ),
|
|
mouse_alternate_scroll( false ), mouse_encoding_mode( MOUSE_ENCODING_DEFAULT ), application_mode_cursor_keys( false )
|
|
{
|
|
reinitialize_tabs( 0 );
|
|
}
|
|
|
|
Framebuffer::Framebuffer( int s_width, int s_height )
|
|
: rows(), icon_name(), window_title(), clipboard(), bell_count( 0 ), title_initialized( false ), ds( s_width, s_height )
|
|
{
|
|
assert( s_height > 0 );
|
|
assert( s_width > 0 );
|
|
const size_t w = s_width;
|
|
const color_type c = 0;
|
|
rows = rows_type(s_height, row_pointer(make_shared<Row>( w, c )));
|
|
}
|
|
|
|
Framebuffer::Framebuffer( const Framebuffer &other )
|
|
: rows( other.rows ), icon_name( other.icon_name ), window_title( other.window_title ),
|
|
clipboard( other.clipboard ), bell_count( other.bell_count ),
|
|
title_initialized( other.title_initialized ), ds( other.ds )
|
|
{
|
|
}
|
|
|
|
Framebuffer & Framebuffer::operator=( const Framebuffer &other )
|
|
{
|
|
if ( this != &other ) {
|
|
rows = other.rows;
|
|
icon_name = other.icon_name;
|
|
window_title = other.window_title;
|
|
clipboard = other.clipboard;
|
|
bell_count = other.bell_count;
|
|
title_initialized = other.title_initialized;
|
|
ds = other.ds;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
void Framebuffer::scroll( int N )
|
|
{
|
|
if ( N >= 0 ) {
|
|
delete_line( ds.get_scrolling_region_top_row(), N );
|
|
} else {
|
|
insert_line( ds.get_scrolling_region_top_row(), -N );
|
|
}
|
|
}
|
|
|
|
void DrawState::new_grapheme( void )
|
|
{
|
|
combining_char_col = cursor_col;
|
|
combining_char_row = cursor_row;
|
|
}
|
|
|
|
void DrawState::snap_cursor_to_border( void )
|
|
{
|
|
if ( cursor_row < limit_top() ) cursor_row = limit_top();
|
|
if ( cursor_row > limit_bottom() ) cursor_row = limit_bottom();
|
|
if ( cursor_col < 0 ) cursor_col = 0;
|
|
if ( cursor_col >= width ) cursor_col = width - 1;
|
|
}
|
|
|
|
void DrawState::move_row( int N, bool relative )
|
|
{
|
|
if ( relative ) {
|
|
cursor_row += N;
|
|
} else {
|
|
cursor_row = N + limit_top();
|
|
}
|
|
|
|
snap_cursor_to_border();
|
|
new_grapheme();
|
|
next_print_will_wrap = false;
|
|
}
|
|
|
|
void DrawState::move_col( int N, bool relative, bool implicit )
|
|
{
|
|
if ( implicit ) {
|
|
new_grapheme();
|
|
}
|
|
|
|
if ( relative ) {
|
|
cursor_col += N;
|
|
} else {
|
|
cursor_col = N;
|
|
}
|
|
|
|
if ( implicit ) {
|
|
next_print_will_wrap = (cursor_col >= width);
|
|
}
|
|
|
|
snap_cursor_to_border();
|
|
if ( !implicit ) {
|
|
new_grapheme();
|
|
next_print_will_wrap = false;
|
|
}
|
|
}
|
|
|
|
void Framebuffer::move_rows_autoscroll( int rows )
|
|
{
|
|
/* don't scroll if outside the scrolling region */
|
|
if ( (ds.get_cursor_row() < ds.get_scrolling_region_top_row())
|
|
|| (ds.get_cursor_row() > ds.get_scrolling_region_bottom_row()) ) {
|
|
ds.move_row( rows, true );
|
|
return;
|
|
}
|
|
|
|
if ( ds.get_cursor_row() + rows > ds.get_scrolling_region_bottom_row() ) {
|
|
int N = ds.get_cursor_row() + rows - ds.get_scrolling_region_bottom_row();
|
|
scroll( N );
|
|
ds.move_row( -N, true );
|
|
} else if ( ds.get_cursor_row() + rows < ds.get_scrolling_region_top_row() ) {
|
|
int N = ds.get_cursor_row() + rows - ds.get_scrolling_region_top_row();
|
|
scroll( N );
|
|
ds.move_row( -N, true );
|
|
}
|
|
|
|
ds.move_row( rows, true );
|
|
}
|
|
|
|
Cell *Framebuffer::get_combining_cell( void )
|
|
{
|
|
if ( (ds.get_combining_char_col() < 0)
|
|
|| (ds.get_combining_char_row() < 0)
|
|
|| (ds.get_combining_char_col() >= ds.get_width())
|
|
|| (ds.get_combining_char_row() >= ds.get_height()) ) {
|
|
return NULL;
|
|
} /* can happen if a resize came in between */
|
|
|
|
return get_mutable_cell( ds.get_combining_char_row(), ds.get_combining_char_col() );
|
|
}
|
|
|
|
void DrawState::set_tab( void )
|
|
{
|
|
tabs[ cursor_col ] = true;
|
|
}
|
|
|
|
void DrawState::clear_tab( int col )
|
|
{
|
|
tabs[ col ] = false;
|
|
}
|
|
|
|
int DrawState::get_next_tab( int count ) const
|
|
{
|
|
if ( count >= 0 ) {
|
|
for ( int i = cursor_col + 1; i < width; i++ ) {
|
|
if ( tabs[ i ] && --count == 0 ) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
for ( int i = cursor_col - 1; i > 0; i-- ) {
|
|
if ( tabs[ i ] && ++count == 0 ) {
|
|
return i;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void DrawState::set_scrolling_region( int top, int bottom )
|
|
{
|
|
if ( height < 1 ) {
|
|
return;
|
|
}
|
|
|
|
scrolling_region_top_row = top;
|
|
scrolling_region_bottom_row = bottom;
|
|
|
|
if ( scrolling_region_top_row < 0 ) scrolling_region_top_row = 0;
|
|
if ( scrolling_region_bottom_row >= height ) scrolling_region_bottom_row = height - 1;
|
|
|
|
if ( scrolling_region_bottom_row < scrolling_region_top_row )
|
|
scrolling_region_bottom_row = scrolling_region_top_row;
|
|
/* real rule requires TWO-line scrolling region */
|
|
|
|
if ( origin_mode ) {
|
|
snap_cursor_to_border();
|
|
new_grapheme();
|
|
}
|
|
}
|
|
|
|
int DrawState::limit_top( void ) const
|
|
{
|
|
return origin_mode ? scrolling_region_top_row : 0;
|
|
}
|
|
|
|
int DrawState::limit_bottom( void ) const
|
|
{
|
|
return origin_mode ? scrolling_region_bottom_row : height - 1;
|
|
}
|
|
|
|
void Framebuffer::apply_renditions_to_cell( Cell *cell )
|
|
{
|
|
if (!cell) {
|
|
cell = get_mutable_cell();
|
|
}
|
|
cell->set_renditions( ds.get_renditions() );
|
|
}
|
|
|
|
SavedCursor::SavedCursor()
|
|
: cursor_col( 0 ), cursor_row( 0 ),
|
|
renditions( 0 ),
|
|
auto_wrap_mode( true ),
|
|
origin_mode( false )
|
|
{}
|
|
|
|
void DrawState::save_cursor( void )
|
|
{
|
|
save.cursor_col = cursor_col;
|
|
save.cursor_row = cursor_row;
|
|
save.renditions = renditions;
|
|
save.auto_wrap_mode = auto_wrap_mode;
|
|
save.origin_mode = origin_mode;
|
|
}
|
|
|
|
void DrawState::restore_cursor( void )
|
|
{
|
|
cursor_col = save.cursor_col;
|
|
cursor_row = save.cursor_row;
|
|
renditions = save.renditions;
|
|
auto_wrap_mode = save.auto_wrap_mode;
|
|
origin_mode = save.origin_mode;
|
|
|
|
snap_cursor_to_border(); /* we could have resized in between */
|
|
new_grapheme();
|
|
}
|
|
|
|
void Framebuffer::insert_line( int before_row, int count )
|
|
{
|
|
if ( (before_row < ds.get_scrolling_region_top_row())
|
|
|| (before_row > ds.get_scrolling_region_bottom_row() + 1) ) {
|
|
return;
|
|
}
|
|
|
|
int scroll = ds.get_scrolling_region_bottom_row() + 1 - before_row;
|
|
if ( count < scroll ) {
|
|
scroll = count;
|
|
}
|
|
|
|
if ( scroll == 0 ) {
|
|
return;
|
|
}
|
|
|
|
// delete old rows
|
|
rows_type::iterator start = rows.begin() + ds.get_scrolling_region_bottom_row() + 1 - scroll;
|
|
rows.erase( start, start + scroll );
|
|
// insert new rows
|
|
start = rows.begin() + before_row;
|
|
rows.insert( start, scroll, newrow());
|
|
}
|
|
|
|
void Framebuffer::delete_line( int row, int count )
|
|
{
|
|
if ( (row < ds.get_scrolling_region_top_row())
|
|
|| (row > ds.get_scrolling_region_bottom_row()) ) {
|
|
return;
|
|
}
|
|
|
|
int scroll = ds.get_scrolling_region_bottom_row() + 1 - row;
|
|
if ( count < scroll ) {
|
|
scroll = count;
|
|
}
|
|
|
|
if ( scroll == 0 ) {
|
|
return;
|
|
}
|
|
|
|
// delete old rows
|
|
rows_type::iterator start = rows.begin() + row;
|
|
rows.erase( start, start + scroll );
|
|
// insert a block of dummy rows
|
|
start = rows.begin() + ds.get_scrolling_region_bottom_row() + 1 - scroll;
|
|
rows.insert( start, scroll, newrow());
|
|
}
|
|
|
|
Row::Row( const size_t s_width, const color_type background_color )
|
|
: cells( s_width, Cell( background_color ) ), gen( get_gen() )
|
|
{}
|
|
|
|
uint64_t Row::get_gen() const
|
|
{
|
|
static uint64_t gen_counter = 0;
|
|
return gen_counter++;
|
|
}
|
|
|
|
void Row::insert_cell( int col, color_type background_color )
|
|
{
|
|
cells.insert( cells.begin() + col, Cell( background_color ) );
|
|
cells.pop_back();
|
|
}
|
|
|
|
void Row::delete_cell( int col, color_type background_color )
|
|
{
|
|
cells.push_back( Cell( background_color ) );
|
|
cells.erase( cells.begin() + col );
|
|
}
|
|
|
|
void Framebuffer::insert_cell( int row, int col )
|
|
{
|
|
get_mutable_row( row )->insert_cell( col, ds.get_background_rendition() );
|
|
}
|
|
|
|
void Framebuffer::delete_cell( int row, int col )
|
|
{
|
|
get_mutable_row( row )->delete_cell( col, ds.get_background_rendition() );
|
|
}
|
|
|
|
void Framebuffer::reset( void )
|
|
{
|
|
int width = ds.get_width(), height = ds.get_height();
|
|
ds = DrawState( width, height );
|
|
rows = rows_type( height, newrow() );
|
|
window_title.clear();
|
|
clipboard.clear();
|
|
/* do not reset bell_count */
|
|
}
|
|
|
|
void Framebuffer::soft_reset( void )
|
|
{
|
|
ds.insert_mode = false;
|
|
ds.origin_mode = false;
|
|
ds.cursor_visible = true; /* per xterm and gnome-terminal */
|
|
ds.application_mode_cursor_keys = false;
|
|
ds.set_scrolling_region( 0, ds.get_height() - 1 );
|
|
ds.add_rendition( 0 );
|
|
ds.clear_saved_cursor();
|
|
}
|
|
|
|
void Framebuffer::resize( int s_width, int s_height )
|
|
{
|
|
assert( s_width > 0 );
|
|
assert( s_height > 0 );
|
|
|
|
int oldheight = ds.get_height();
|
|
int oldwidth = ds.get_width();
|
|
ds.resize( s_width, s_height );
|
|
|
|
row_pointer blankrow( newrow());
|
|
if ( oldheight != s_height ) {
|
|
rows.resize( s_height, blankrow );
|
|
}
|
|
if (oldwidth == s_width) {
|
|
return;
|
|
}
|
|
for ( rows_type::iterator i = rows.begin();
|
|
i != rows.end() && *i != blankrow;
|
|
i++ ) {
|
|
*i = make_shared<Row>( **i );
|
|
(*i)->set_wrap( false );
|
|
(*i)->cells.resize( s_width, Cell( ds.get_background_rendition() ) );
|
|
}
|
|
}
|
|
|
|
void DrawState::resize( int s_width, int s_height )
|
|
{
|
|
if ( (width != s_width)
|
|
|| (height != s_height) ) {
|
|
/* reset entire scrolling region on any resize */
|
|
/* xterm and rxvt-unicode do this. gnome-terminal only
|
|
resets scrolling region if it has to become smaller in resize */
|
|
scrolling_region_top_row = 0;
|
|
scrolling_region_bottom_row = s_height - 1;
|
|
}
|
|
|
|
tabs.resize( s_width );
|
|
if ( default_tabs ) {
|
|
reinitialize_tabs( width );
|
|
}
|
|
|
|
width = s_width;
|
|
height = s_height;
|
|
|
|
snap_cursor_to_border();
|
|
|
|
/* saved cursor will be snapped to border on restore */
|
|
|
|
/* invalidate combining char cell if necessary */
|
|
if ( (combining_char_col >= width)
|
|
|| (combining_char_row >= height) ) {
|
|
combining_char_col = combining_char_row = -1;
|
|
}
|
|
}
|
|
|
|
Renditions::Renditions( color_type s_background )
|
|
: foreground_color( 0 ), background_color( s_background ),
|
|
attributes( 0 )
|
|
{}
|
|
|
|
/* This routine cannot be used to set a color beyond the 16-color set. */
|
|
void Renditions::set_rendition( color_type num )
|
|
{
|
|
if ( num == 0 ) {
|
|
clear_attributes();
|
|
foreground_color = background_color = 0;
|
|
return;
|
|
}
|
|
|
|
if ( num == 39 ) {
|
|
foreground_color = 0;
|
|
return;
|
|
} else if ( num == 49 ) {
|
|
background_color = 0;
|
|
return;
|
|
}
|
|
|
|
if ( (30 <= num) && (num <= 37) ) { /* foreground color in 8-color set */
|
|
foreground_color = num;
|
|
return;
|
|
} else if ( (40 <= num) && (num <= 47) ) { /* background color in 8-color set */
|
|
background_color = num;
|
|
return;
|
|
} else if ( (90 <= num) && (num <= 97) ) { /* foreground color in 16-color set */
|
|
foreground_color = num - 90 + 38;
|
|
return;
|
|
} else if ( (100 <= num) && (num <= 107) ) { /* background color in 16-color set */
|
|
background_color = num - 100 + 48;
|
|
return;
|
|
}
|
|
|
|
bool value = num < 9;
|
|
switch ( num ) {
|
|
case 1: case 22: set_attribute(bold, value); break;
|
|
case 3: case 23: set_attribute(italic, value); break;
|
|
case 4: case 24: set_attribute(underlined, value); break;
|
|
case 5: case 25: set_attribute(blink, value); break;
|
|
case 7: case 27: set_attribute(inverse, value); break;
|
|
case 8: case 28: set_attribute(invisible, value); break;
|
|
default: break; /* ignore unknown rendition */
|
|
}
|
|
}
|
|
|
|
void Renditions::set_foreground_color( int num )
|
|
{
|
|
if ( (0 <= num) && (num <= 255) ) {
|
|
foreground_color = 30 + num;
|
|
} else if ( is_true_color( num ) ) {
|
|
foreground_color = num;
|
|
}
|
|
}
|
|
|
|
void Renditions::set_background_color( int num )
|
|
{
|
|
if ( (0 <= num) && (num <= 255) ) {
|
|
background_color = 40 + num;
|
|
} else if ( is_true_color( num ) ) {
|
|
background_color = num;
|
|
}
|
|
}
|
|
|
|
std::string Renditions::sgr( void ) const
|
|
{
|
|
std::string ret;
|
|
|
|
ret.append( "\033[0" );
|
|
if ( get_attribute( bold ) ) ret.append( ";1" );
|
|
if ( get_attribute( italic ) ) ret.append( ";3" );
|
|
if ( get_attribute( underlined ) ) ret.append( ";4" );
|
|
if ( get_attribute( blink ) ) ret.append( ";5" );
|
|
if ( get_attribute( inverse ) ) ret.append( ";7" );
|
|
if ( get_attribute( invisible ) ) ret.append( ";8" );
|
|
|
|
if ( foreground_color
|
|
&& (foreground_color <= 37) ) {
|
|
/* ANSI foreground color */
|
|
char col[ 8 ];
|
|
snprintf( col, 8, ";%u", static_cast<unsigned int>( foreground_color ) );
|
|
ret.append( col );
|
|
}
|
|
|
|
if ( background_color
|
|
&& (background_color <= 47) ) {
|
|
char col[ 8 ];
|
|
snprintf( col, 8, ";%u", static_cast<unsigned int>( background_color ) );
|
|
ret.append( col );
|
|
}
|
|
|
|
ret.append( "m" );
|
|
|
|
if ( is_true_color( foreground_color ) ) {
|
|
char col[64];
|
|
snprintf( col, 64, "\033[38;2;%u;%u;%um",
|
|
(foreground_color >> 16) & 0xff,
|
|
(foreground_color >> 8) & 0xff,
|
|
foreground_color & 0xff);
|
|
ret.append( col );
|
|
} else if ( foreground_color > 37 ) { /* use 256-color set */
|
|
char col[ 64 ];
|
|
snprintf( col, 64, "\033[38;5;%um", foreground_color - 30 );
|
|
ret.append( col );
|
|
}
|
|
|
|
if ( is_true_color( background_color ) ) {
|
|
char col[64];
|
|
snprintf( col, 64, "\033[48;2;%u;%u;%um",
|
|
(background_color >> 16) & 0xff,
|
|
(background_color >> 8) & 0xff,
|
|
background_color & 0xff);
|
|
ret.append( col );
|
|
} else if ( background_color > 47 ) { /* use 256-color set */
|
|
char col[ 64 ];
|
|
snprintf( col, 64, "\033[48;5;%um", background_color - 40 );
|
|
ret.append( col );
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
void Row::reset( color_type background_color )
|
|
{
|
|
gen = get_gen();
|
|
for ( cells_type::iterator i = cells.begin();
|
|
i != cells.end();
|
|
i++ ) {
|
|
i->reset( background_color );
|
|
}
|
|
}
|
|
|
|
void Framebuffer::prefix_window_title( const title_type &s )
|
|
{
|
|
if ( icon_name == window_title ) {
|
|
/* preserve equivalence */
|
|
icon_name.insert(icon_name.begin(), s.begin(), s.end() );
|
|
}
|
|
window_title.insert(window_title.begin(), s.begin(), s.end() );
|
|
}
|
|
|
|
std::string Cell::debug_contents( void ) const
|
|
{
|
|
if ( contents.empty() ) {
|
|
return "'_' ()";
|
|
}
|
|
std::string chars( 1, '\'' );
|
|
print_grapheme( chars );
|
|
chars.append( "' [" );
|
|
const char *lazycomma = "";
|
|
char buf[64];
|
|
for ( content_type::const_iterator i = contents.begin();
|
|
i < contents.end();
|
|
i++ ) {
|
|
|
|
snprintf( buf, sizeof buf, "%s0x%02x", lazycomma, static_cast<uint8_t>(*i) );
|
|
chars.append( buf );
|
|
lazycomma = ", ";
|
|
}
|
|
chars.append( "]" );
|
|
return chars;
|
|
}
|
|
|
|
bool Cell::compare( const Cell &other ) const
|
|
{
|
|
bool ret = false;
|
|
|
|
std::string grapheme, other_grapheme;
|
|
|
|
print_grapheme( grapheme );
|
|
other.print_grapheme( other_grapheme );
|
|
|
|
if ( grapheme != other_grapheme ) {
|
|
ret = true;
|
|
fprintf( stderr, "Graphemes: '%s' vs. '%s'\n",
|
|
grapheme.c_str(), other_grapheme.c_str() );
|
|
}
|
|
|
|
if ( !contents_match( other ) ) {
|
|
// ret = true;
|
|
fprintf( stderr, "Contents: %s (%ld) vs. %s (%ld)\n",
|
|
debug_contents().c_str(),
|
|
static_cast<long int>( contents.size() ),
|
|
other.debug_contents().c_str(),
|
|
static_cast<long int>( other.contents.size() ) );
|
|
}
|
|
|
|
if ( fallback != other.fallback ) {
|
|
// ret = true;
|
|
fprintf( stderr, "fallback: %d vs. %d\n",
|
|
fallback, other.fallback );
|
|
}
|
|
|
|
if ( wide != other.wide ) {
|
|
ret = true;
|
|
fprintf( stderr, "width: %d vs. %d\n",
|
|
wide, other.wide );
|
|
}
|
|
|
|
if ( !(renditions == other.renditions) ) {
|
|
ret = true;
|
|
fprintf( stderr, "renditions differ\n" );
|
|
}
|
|
|
|
if ( wrap != other.wrap ) {
|
|
ret = true;
|
|
fprintf( stderr, "wrap: %d vs. %d\n",
|
|
wrap, other.wrap );
|
|
}
|
|
|
|
return ret;
|
|
}
|