Rework Terminal::Display to improve performance and treat passed Framebuffers as const.

* Refactor put_cell() and parts of new_frame(), and associated state, into put_row().
* Optimize display and line wrap handling code/output.
* Make last_frame a const ref, to eliminate a costly copy of the framebuffer on every screen refresh.
* In new_frame()'s scroll optimization, don't copy rows, use pointers instead.
* Don't check entire frame buffer for scrolling if first line hasn't scrolled.
* Add a generation counter on Row objects to allow quicker/better identification of scroll regions
* Use at() for bounds-checking on framebuffers, because they can be resized.
* Copy and resize scroll buffer on window resize.
This commit is contained in:
John Hood
2013-12-15 18:39:23 -05:00
parent 32afa96111
commit c090d257f2
4 changed files with 285 additions and 200 deletions
+238 -165
View File
@@ -123,9 +123,7 @@ std::string Display::new_frame( bool initialized, const Framebuffer &last, const
|| (f.ds.get_width() != frame.last_frame.ds.get_width())
|| (f.ds.get_height() != frame.last_frame.ds.get_height()) ) {
/* reset scrolling region */
snprintf( tmp, 64, "\033[%d;%dr",
1, f.ds.get_height() );
frame.append( tmp );
frame.append( "\033[r" );
/* clear screen */
frame.append( "\033[0m\033[H\033[2J" );
@@ -138,15 +136,47 @@ std::string Display::new_frame( bool initialized, const Framebuffer &last, const
frame.current_rendition = frame.last_frame.ds.get_renditions();
}
/* shortcut -- has display moved up by a certain number of lines? */
frame.y = 0;
/* is cursor visibility initialized? */
if ( !initialized ) {
frame.cursor_visible = false;
frame.append( "\033[?25l" );
}
int frame_y = 0;
Row blankrow( 0, 0 );
Framebuffer::rows_p_type rows(frame.last_frame.get_p_rows());
/* Extend rows if we've gotten a resize and new is wider than old */
if ( frame.last_frame.ds.get_width() < f.ds.get_width() ) {
for ( Framebuffer::rows_p_type::iterator p = rows.begin(); p != rows.end(); p++ ) {
Row *bigger_row = new Row( **p );
bigger_row->cells.resize( f.ds.get_width(), Cell( f.ds.get_background_rendition() ) );
*p = bigger_row;
}
}
/* Add rows if we've gotten a resize and new is taller than old */
if ( static_cast<int>( rows.size() ) < f.ds.get_height() ) {
size_t orig_size = rows.size();
// get a proper blank row
blankrow = Row( f.ds.get_width(), 0 );
rows.resize( f.ds.get_height() );
for ( Framebuffer::rows_p_type::iterator p = rows.begin() + orig_size; p != rows.end(); p++ ) {
*p = new Row( blankrow );
}
}
/* shortcut -- has display moved up by a certain number of lines? */
if ( initialized ) {
int lines_scrolled = 0;
int scroll_height = 0;
for ( int row = 0; row < frame.last_frame.ds.get_height(); row++ ) {
if ( *(f.get_row( 0 )) == *(frame.last_frame.get_row( row )) ) {
for ( int row = 0; row < f.ds.get_height(); row++ ) {
const Row *new_row = f.get_row( 0 );
const Row *old_row = rows.at( row );
if ( new_row == old_row || *new_row == *old_row ) {
/* if row 0, we're looking at ourselves and probably didn't scroll */
if ( row == 0 ) {
break;
}
/* found a scroll */
lines_scrolled = row;
scroll_height = 1;
@@ -155,8 +185,8 @@ std::string Display::new_frame( bool initialized, const Framebuffer &last, const
for ( int region_height = 1;
lines_scrolled + region_height < f.ds.get_height();
region_height++ ) {
if ( *(f.get_row( region_height ))
== *(frame.last_frame.get_row( lines_scrolled + region_height )) ) {
if ( *f.get_row( region_height )
== *rows.at( lines_scrolled + region_height ) ) {
scroll_height = region_height + 1;
} else {
break;
@@ -168,112 +198,73 @@ std::string Display::new_frame( bool initialized, const Framebuffer &last, const
}
if ( scroll_height ) {
frame.y = scroll_height;
frame_y = scroll_height;
if ( lines_scrolled ) {
if ( !(frame.current_rendition == initial_rendition()) ) {
frame.append( "\033[0m" );
frame.current_rendition = initial_rendition();
}
frame.update_rendition( initial_rendition(), true );
int top_margin = 0;
int bottom_margin = top_margin + lines_scrolled + scroll_height - 1;
assert( bottom_margin < f.ds.get_height() );
/* set scrolling region */
snprintf( tmp, 64, "\033[%d;%dr",
top_margin + 1, bottom_margin + 1);
frame.append( tmp );
/* Common case: if we're already on the bottom line and we're scrolling the whole
* screen, just do a CR and LFs.
*/
if ( (scroll_height + lines_scrolled == f.ds.get_height() ) && frame.cursor_y + 1 == f.ds.get_height() ) {
frame.append( '\r' );
frame.append( lines_scrolled, '\n' );
frame.cursor_x = 0;
} else {
/* set scrolling region */
snprintf( tmp, 64, "\033[%d;%dr",
top_margin + 1, bottom_margin + 1);
frame.append( tmp );
/* go to bottom of scrolling region */
frame.append_silent_move( bottom_margin, 0 );
/* go to bottom of scrolling region */
frame.cursor_x = frame.cursor_y = -1;
frame.append_silent_move( bottom_margin, 0 );
/* scroll */
for ( int i = 0; i < lines_scrolled; i++ ) {
frame.append( '\n' );
/* scroll */
frame.append( lines_scrolled, '\n' );
/* reset scrolling region */
frame.append( "\033[r" );
/* invalidate cursor position after unsetting scrolling region */
frame.cursor_x = frame.cursor_y = -1;
}
/* do the move in memory */
/* Create a full-width blank row to represent newly-scrolled area */
blankrow = Row( f.ds.get_width(), 0 );
/* do the move in our local index */
for ( int i = top_margin; i <= bottom_margin; i++ ) {
if ( i + lines_scrolled <= bottom_margin ) {
*(frame.last_frame.get_mutable_row( i )) = *(frame.last_frame.get_row( i + lines_scrolled ));
rows.at( i ) = rows.at( i + lines_scrolled );
} else {
frame.last_frame.get_mutable_row( i )->reset( 0 );
rows.at( i ) = &blankrow;
}
}
/* reset scrolling region */
snprintf( tmp, 64, "\033[%d;%dr",
1, f.ds.get_height() );
frame.append( tmp );
/* invalidate cursor position after unsetting scrolling region */
frame.cursor_x = frame.cursor_y = -1;
}
}
}
/* iterate for every cell */
for ( ; frame.y < f.ds.get_height(); frame.y++ ) {
int last_x = 0;
for ( frame.x = 0;
frame.x < f.ds.get_width(); /* let put_cell() handle advance */ ) {
last_x = frame.x;
put_cell( initialized, frame, f );
}
/* To hint that a word-select should group the end of one line
with the beginning of the next, we let the real cursor
actually wrap around in cases where it wrapped around for us. */
if ( (frame.y < f.ds.get_height() - 1)
&& f.get_row( frame.y )->get_wrap() ) {
frame.x = last_x;
while ( frame.x < f.ds.get_width() ) {
frame.force_next_put = true;
put_cell( initialized, frame, f );
}
/* next write will wrap */
frame.cursor_x = 0;
frame.cursor_y++;
frame.force_next_put = true;
}
/* Turn off wrap */
if ( (frame.y < f.ds.get_height() - 1)
&& (!f.get_row( frame.y )->get_wrap())
&& (!initialized || frame.last_frame.get_row( frame.y )->get_wrap()) ) {
frame.x = last_x;
if ( initialized ) {
frame.last_frame.reset_cell( frame.last_frame.get_mutable_cell( frame.y, frame.x ) );
}
snprintf( tmp, 64, "\033[%d;%dH\033[K", frame.y + 1, frame.x + 1 );
frame.append( tmp );
frame.cursor_x = frame.x;
frame.force_next_put = true;
put_cell( initialized, frame, f );
}
/* Now update the display, row by row */
bool wrap = false;
for ( ; frame_y < f.ds.get_height(); frame_y++ ) {
wrap = put_row( initialized, frame, f, frame_y, *rows.at( frame_y ), wrap );
}
/* has cursor location changed? */
if ( (!initialized)
|| (f.ds.get_cursor_row() != frame.cursor_y)
|| (f.ds.get_cursor_col() != frame.cursor_x) ) {
snprintf( tmp, 64, "\033[%d;%dH", f.ds.get_cursor_row() + 1,
f.ds.get_cursor_col() + 1 );
frame.append( tmp );
frame.cursor_x = f.ds.get_cursor_col();
frame.cursor_y = f.ds.get_cursor_row();
frame.append_move( f.ds.get_cursor_row(), f.ds.get_cursor_col() );
}
/* has cursor visibility changed? */
if ( (!initialized)
|| (f.ds.cursor_visible != frame.last_frame.ds.cursor_visible) ) {
|| (f.ds.cursor_visible != frame.cursor_visible) ) {
if ( f.ds.cursor_visible ) {
frame.append( "\033[?25h" );
} else {
@@ -282,11 +273,7 @@ std::string Display::new_frame( bool initialized, const Framebuffer &last, const
}
/* have renditions changed? */
if ( (!initialized)
|| !(f.ds.get_renditions() == frame.current_rendition) ) {
frame.append( f.ds.get_renditions().sgr() );
frame.current_rendition = f.ds.get_renditions();
}
frame.update_rendition( f.ds.get_renditions(), !initialized );
/* has bracketed paste mode changed? */
if ( (!initialized)
@@ -338,115 +325,201 @@ std::string Display::new_frame( bool initialized, const Framebuffer &last, const
return frame.str;
}
void Display::put_cell( bool initialized, FrameState &frame, const Framebuffer &f ) const
bool Display::put_row( bool initialized, FrameState &frame, const Framebuffer &f, int frame_y, const Row &old_row, bool wrap ) const
{
char tmp[ 64 ];
int frame_x = 0;
const Cell *cell = f.get_cell( frame.y, frame.x );
const Row &row = *f.get_row( frame_y );
const Row::cells_type &cells = row.cells;
const Row::cells_type &old_cells = old_row.cells;
if ( !frame.force_next_put ) {
/* If we're forced to write the first column because of wrap, go ahead and do so. */
if ( wrap ) {
const Cell &cell = cells.at( 0 );
frame.update_rendition( cell.renditions );
frame.append_cell( cell );
frame_x += cell.width;
frame.cursor_x += cell.width;
}
/* If rows are the same object, we don't need to do anything at all. */
if (initialized && &row == &old_row ) {
return false;
}
int clear_count = 0;
bool wrote_last_cell = false;
Renditions blank_renditions = initial_rendition();
/* iterate for every cell */
while ( frame_x < f.ds.get_width() ) {
const Cell &cell = cells.at( frame_x );
/* Does cell need to be drawn? Skip all this. */
if ( initialized
&& ( *cell == *(frame.last_frame.get_cell( frame.y, frame.x )) ) ) {
frame.x += cell->width;
return;
&& !clear_count
&& ( cell == old_cells.at( frame_x ) ) ) {
frame_x += cell.width;
continue;
}
}
if ( (frame.x != frame.cursor_x) || (frame.y != frame.cursor_y) ) {
frame.append_silent_move( frame.y, frame.x );
}
if ( !(frame.current_rendition == cell->renditions) ) {
/* print renditions */
frame.append( cell->renditions.sgr() );
frame.current_rendition = cell->renditions;
}
if ( cell->contents.empty() ) {
/* see how far we can stretch a clear */
int clear_count = 0;
for ( int col = frame.x; col < f.ds.get_width(); col++ ) {
const Cell *other_cell = f.get_cell( frame.y, col );
if ( (cell->renditions == other_cell->renditions)
&& (other_cell->contents.empty()) ) {
/* Slurp up all the empty cells */
if ( cell.contents.empty() ) {
if ( !clear_count ) {
blank_renditions = cell.renditions;
}
if ( cell.renditions == blank_renditions ) {
/* Remember run of blank cells */
clear_count++;
frame_x++;
continue;
}
}
/* Clear or write cells within the row (not to end). */
if ( clear_count ) {
/* Move to the right position. */
frame.append_silent_move( frame_y, frame_x - clear_count );
frame.update_rendition( blank_renditions );
bool can_use_erase = has_bce || ( frame.current_rendition == initial_rendition() );
if ( can_use_erase && has_ech ) {
snprintf( tmp, 64, "\033[%dX", clear_count );
frame.append( tmp );
} else {
break;
frame.append( clear_count, ' ' );
frame.cursor_x = frame_x;
}
clear_count = 0;
// If the current character is *another* empty cell in a different rendition,
// we restart counting and continue here
if ( cell.contents.empty() ) {
blank_renditions = cell.renditions;
clear_count = 1;
frame_x++;
continue;
}
}
assert( frame.x + clear_count <= f.ds.get_width() );
bool can_use_erase = has_bce || (cell->renditions == initial_rendition());
if ( frame.force_next_put ) {
frame.append( ' ' );
frame.cursor_x++;
frame.x++;
frame.force_next_put = false;
return;
/* Now draw a character cell. */
/* Move to the right position. */
frame.append_silent_move( frame_y, frame_x );
frame.update_rendition( cell.renditions );
frame.append_cell( cell );
frame_x += cell.width;
frame.cursor_x += cell.width;
if ( frame_x >= f.ds.get_width() ) {
wrote_last_cell = true;
}
}
/* can we go to the end of the line? */
if ( (frame.x + clear_count == f.ds.get_width())
&& can_use_erase ) {
/* End of line. */
/* Clear or write empty cells at EOL. */
if ( clear_count ) {
/* Move to the right position. */
frame.append_silent_move( frame_y, frame_x - clear_count );
frame.update_rendition( blank_renditions );
bool can_use_erase = !row.get_wrap() && ( has_bce || ( frame.current_rendition == initial_rendition() ) );
if ( can_use_erase ) {
frame.append( "\033[K" );
frame.x += clear_count;
} else {
if ( has_ech && can_use_erase ) {
if ( clear_count == 1 ) {
frame.append( "\033[X" );
} else {
snprintf( tmp, 64, "\033[%dX", clear_count );
frame.append( tmp );
}
frame.x += clear_count;
} else { /* no ECH, so just print a space */
/* unlike erases, this will use background color irrespective of BCE */
frame.append( ' ' );
frame.cursor_x++;
frame.x++;
}
frame.append( clear_count, ' ' );
frame.cursor_x = frame_x;
wrote_last_cell = true;
}
}
if ( wrote_last_cell
&& (frame_y < f.ds.get_height() - 1) ) {
/* To hint that a word-select should group the end of one line
with the beginning of the next, we let the real cursor
actually wrap around in cases where it wrapped around for us. */
if ( row.get_wrap() ) {
/* Update our cursor, and ask for wrap on the next row. */
frame.cursor_x = 0;
frame.cursor_y++;
return true;
} else {
/* Resort to CR/LF and update our cursor. */
frame.append( "\r\n" );
frame.cursor_x = 0;
frame.cursor_y++;
}
}
return false;
}
FrameState::FrameState( const Framebuffer &s_last )
: str(), cursor_x(0), cursor_y(0), current_rendition( 0 ),
cursor_visible( s_last.ds.cursor_visible ),
last_frame( s_last )
{
/* Preallocate for better performance. Make a guess-- doesn't matter for correctness */
str.reserve( last_frame.ds.get_width() * last_frame.ds.get_height() * 4 );
}
/* Write a character cell */
void FrameState::append_cell(const Cell & cell)
{
if ( cell.contents.empty() ) {
append( ' ' );
return;
}
/* cells that begin with combining character get combiner attached to no-break space */
if ( cell->fallback ) {
frame.append( "\xC2\xA0" );
if ( cell.fallback ) {
append( "\xC2\xA0" );
}
frame.append( cell->contents );
frame.x += cell->width;
frame.cursor_x += cell->width;
frame.force_next_put = false;
append( cell.contents );
}
void FrameState::append_silent_move( int y, int x )
{
char tmp[ 64 ];
if ( cursor_x == x && cursor_y == y ) return;
/* turn off cursor if necessary before moving cursor */
if ( last_frame.ds.cursor_visible ) {
if ( cursor_visible ) {
append( "\033[?25l" );
last_frame.ds.cursor_visible = false;
cursor_visible = false;
}
append_move( y, x );
}
snprintf( tmp, 64, "\033[%d;%dH", y + 1, x + 1 );
append( tmp );
void FrameState::append_move( int y, int x )
{
do {
// If cursor pos is unknown, of course we can't optimize
if ( cursor_x != -1 && cursor_y != -1 ) {
// Can we use CR and/or LF? They're cheap and easier to trace.
if ( x == 0 && y - cursor_y >= 0 && y - cursor_y < 5 ) {
if ( cursor_x != 0 ) {
append( '\r' );
}
append( y - cursor_y, '\n' );
continue;
}
// Backspaces are good too.
if ( y == cursor_y && x - cursor_x < 0 && x - cursor_x > -5 ) {
append( cursor_x - x, '\b' );
continue;
}
// More optimizations are possible.
}
char tmp[ 64 ];
snprintf( tmp, 64, "\033[%d;%dH", y + 1, x + 1 );
append( tmp );
} while( 0 );
cursor_x = x;
cursor_y = y;
}
FrameState::FrameState( const Framebuffer &s_last )
: x(0), y(0),
force_next_put( false ),
str(), cursor_x(0), cursor_y(0), current_rendition( 0 ),
last_frame( s_last )
{
/* just a guess-- doesn't matter for correctness */
str.reserve( last_frame.ds.get_width() * last_frame.ds.get_height() * 4 );
void FrameState::update_rendition(const Renditions &r, bool force) {
if ( force || !(current_rendition == r) ) {
/* print renditions */
append_string( r.sgr() );
current_rendition = r;
}
}
+7 -4
View File
@@ -39,24 +39,27 @@ namespace Terminal {
/* variables used within a new_frame */
class FrameState {
public:
int x, y;
bool force_next_put;
std::string str;
int cursor_x, cursor_y;
Renditions current_rendition;
bool cursor_visible;
Framebuffer last_frame;
const Framebuffer &last_frame;
FrameState( const Framebuffer &s_last );
void append( const char c ) { str.append( 1, c ); }
void append( const size_t s, const char c ) { str.append( s, c ); }
void append( const wchar_t wc ) { Cell::append_to_str( str, wc ); }
void append( const char * s ) { str.append( s ); }
void append( const Cell::content_type &contents ) { str.append( contents.begin(), contents.end() ); }
void append_string( const std::string &append ) { str.append(append); }
void append_cell(const Cell & cell);
void append_silent_move( int y, int x );
void append_move( int y, int x );
void update_rendition( const Renditions &r, bool force = false );
};
class Display {
@@ -76,7 +79,7 @@ namespace Terminal {
const char *smcup, *rmcup; /* enter and exit alternate screen mode */
void put_cell( bool initialized, FrameState &frame, const Framebuffer &f ) const;
bool put_row( bool initialized, FrameState &frame, const Framebuffer &f, int frame_y, const Row &old_row, bool wrap ) const;
public:
void downgrade( Framebuffer &f ) const { if ( posterize_colors ) { f.posterize(); } }
+16 -22
View File
@@ -42,20 +42,16 @@ Cell::Cell( color_type background_color )
: contents(),
renditions( background_color ),
width( 1 ),
fallback( false ),
wrap( false )
fallback( false )
{}
#if 0
Cell::Cell() /* default constructor required by C++11 STL */
: contents(),
renditions( 0 ),
width( 1 ),
fallback( false ),
wrap( false )
fallback( false )
{
assert( false );
}
#endif
void Cell::reset( color_type background_color )
{
@@ -63,7 +59,6 @@ void Cell::reset( color_type background_color )
renditions = Renditions( background_color );
width = 1;
fallback = false;
wrap = false;
}
void DrawState::reinitialize_tabs( unsigned int start )
@@ -184,7 +179,7 @@ Cell *Framebuffer::get_combining_cell( void )
return NULL;
} /* can happen if a resize came in between */
return &rows[ ds.get_combining_char_row() ].cells[ ds.get_combining_char_col() ];
return get_mutable_cell( ds.get_combining_char_row(), ds.get_combining_char_col() );
}
void DrawState::set_tab( void )
@@ -331,16 +326,20 @@ void Framebuffer::delete_line( int row, int count )
}
Row::Row( size_t s_width, color_type background_color )
: cells( s_width, Cell( background_color ) )
: cells( s_width, Cell( background_color ) ), wrap( false ), gen( get_gen() )
{}
#if 0
Row::Row() /* default constructor required by C++11 STL */
: cells( 1, Cell() )
: cells( 1, Cell() ), wrap( false ), gen( get_gen() )
{
assert( false );
}
#endif
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 )
{
@@ -356,12 +355,12 @@ void Row::delete_cell( int col, color_type background_color )
void Framebuffer::insert_cell( int row, int col )
{
rows[ row ].insert_cell( col, ds.get_background_rendition() );
rows.at( row ).insert_cell( col, ds.get_background_rendition() );
}
void Framebuffer::delete_cell( int row, int col )
{
rows[ row ].delete_cell( col, ds.get_background_rendition() );
rows.at( row ).delete_cell( col, ds.get_background_rendition() );
}
void Framebuffer::reset( void )
@@ -402,6 +401,8 @@ void Framebuffer::resize( int s_width, int s_height )
assert( s_width > 0 );
assert( s_height > 0 );
ds.resize( s_width, s_height );
rows.resize( s_height, newrow() );
for ( rows_type::iterator i = rows.begin();
@@ -410,8 +411,6 @@ void Framebuffer::resize( int s_width, int s_height )
i->set_wrap( false );
i->cells.resize( s_width, Cell( ds.get_background_rendition() ) );
}
ds.resize( s_width, s_height );
}
void DrawState::resize( int s_width, int s_height )
@@ -604,6 +603,7 @@ void Renditions::posterize( void )
void Row::reset( color_type background_color )
{
gen = get_gen();
for ( cells_type::iterator i = cells.begin();
i != cells.end();
i++ ) {
@@ -660,11 +660,5 @@ bool Cell::compare( const Cell &other ) const
fprintf( stderr, "renditions differ\n" );
}
if ( wrap != other.wrap ) {
ret = true;
fprintf( stderr, "wrap: %d vs. %d\n",
wrap, other.wrap );
}
return ret;
}
+23 -8
View File
@@ -89,7 +89,6 @@ namespace Terminal {
Renditions renditions;
uint8_t width;
bool fallback; /* first character is combining character */
bool wrap; /* if last cell, wrap to next line */
Cell( color_type background_color );
Cell(); /* default constructor required by C++11 STL */
@@ -101,8 +100,7 @@ namespace Terminal {
return ( (contents == x.contents)
&& (fallback == x.fallback)
&& (width == x.width)
&& (renditions == x.renditions)
&& (wrap == x.wrap) );
&& (renditions == x.renditions) );
}
bool operator!=( const Cell &x ) const { return !operator==( x ); }
@@ -149,6 +147,11 @@ namespace Terminal {
public:
typedef std::vector<Cell> cells_type;
cells_type cells;
bool wrap; /* if last cell, wrap to next line */
// gen is a generation counter. It can be used to quickly rule
// out the possibility of two rows being identical; this is useful
// in scrolling.
uint64_t gen;
Row( size_t s_width, color_type background_color );
Row(); /* default constructor required by C++11 STL */
@@ -160,11 +163,13 @@ namespace Terminal {
bool operator==( const Row &x ) const
{
return ( cells == x.cells );
return ( gen == x.gen && cells == x.cells && wrap == x.wrap );
}
bool get_wrap( void ) const { return cells.back().wrap; }
void set_wrap( bool w ) { cells.back().wrap = w; }
bool get_wrap( void ) const { return wrap; }
void set_wrap( bool w ) { wrap = w; }
uint64_t get_gen() const;
};
class SavedCursor {
@@ -285,6 +290,7 @@ namespace Terminal {
class Framebuffer {
public:
typedef std::vector<wchar_t> title_type;
typedef std::vector<const Row *> rows_p_type;
private:
typedef std::vector<Row> rows_type;
@@ -303,11 +309,20 @@ namespace Terminal {
void scroll( int N );
void move_rows_autoscroll( int rows );
rows_p_type get_p_rows() const
{
rows_p_type retval;
for ( size_t i = 0; i < rows.size(); i++ ) {
retval.push_back( &rows.at(i) );
}
return retval;
}
const Row *get_row( int row ) const
{
if ( row == -1 ) row = ds.get_cursor_row();
return &rows[ row ];
return &rows.at( row );
}
inline const Cell *get_cell( int row = -1, int col = -1 ) const
@@ -315,7 +330,7 @@ namespace Terminal {
if ( row == -1 ) row = ds.get_cursor_row();
if ( col == -1 ) col = ds.get_cursor_col();
return &rows[ row ].cells[ col ];
return &rows.at( row ).cells.at( col );
}
Row *get_mutable_row( int row )