diff --git a/premake4.lua b/premake4.lua index 4f307bf7d..20c93fd8f 100644 --- a/premake4.lua +++ b/premake4.lua @@ -1795,6 +1795,8 @@ solution "eepp" project "eepp-unit_tests" kind "ConsoleApp" targetdir("./bin/unit_tests") + links { "eterm-static", "languages-syntax-highlighting-static" } + includedirs { "src/modules/eterm/include/" } language "C++" files { "src/tests/unit_tests/*.cpp" } build_link_configuration( "eepp-unit_tests", true ) diff --git a/premake5.lua b/premake5.lua index 827aed538..124efc44e 100644 --- a/premake5.lua +++ b/premake5.lua @@ -1663,6 +1663,8 @@ workspace "eepp" project "eepp-unit_tests" kind "ConsoleApp" targetdir(_MAIN_SCRIPT_DIR .. "/bin/unit_tests") + links { "eterm-static", "languages-syntax-highlighting-static" } + incdirs { "src/modules/eterm/include/" } language "C++" files { "src/tests/unit_tests/*.cpp" } build_link_configuration( "eepp-unit_tests", true ) diff --git a/src/modules/eterm/include/eterm/terminal/iterminaldisplay.hpp b/src/modules/eterm/include/eterm/terminal/iterminaldisplay.hpp index 21d28b89e..295be3a37 100644 --- a/src/modules/eterm/include/eterm/terminal/iterminaldisplay.hpp +++ b/src/modules/eterm/include/eterm/terminal/iterminaldisplay.hpp @@ -54,6 +54,9 @@ class ITerminalDisplay { virtual int resetColor( const Uint32& index, const char* name ); + virtual bool getColor( const Uint32& index, unsigned char* r, unsigned char* g, + unsigned char* b ); + virtual void setMode( TerminalWinMode mode, int set ); inline bool getMode( TerminalWinMode mode ) const { return mMode & mode; } diff --git a/src/modules/eterm/include/eterm/terminal/terminalcolorscheme.hpp b/src/modules/eterm/include/eterm/terminal/terminalcolorscheme.hpp index 4bf3ae16d..b3bd2ccc8 100644 --- a/src/modules/eterm/include/eterm/terminal/terminalcolorscheme.hpp +++ b/src/modules/eterm/include/eterm/terminal/terminalcolorscheme.hpp @@ -52,6 +52,14 @@ class TerminalColorScheme { size_t getPaletteSize() const; + void setForeground( const Color& foreground ); + + void setBackground( const Color& background ); + + void setCursor( const Color& cursor ); + + void setPaletteIndex( const size_t& index, const Color& color ); + protected: std::string mName; Color mForeground; diff --git a/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp b/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp index ae44fe48d..90aaf2852 100644 --- a/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp +++ b/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp @@ -161,6 +161,8 @@ class TerminalDisplay : public ITerminalDisplay { virtual void resetColors(); virtual int resetColor( const Uint32& index, const char* name ); + virtual bool getColor( const Uint32& index, unsigned char* r, unsigned char* g, + unsigned char* b ); virtual void setTitle( const char* title ); virtual void setIconTitle( const char* title ); @@ -374,7 +376,6 @@ class TerminalDisplay : public ITerminalDisplay { void drawBg( bool toFBO = false ); void sanitizeInput( std::string& input ); - }; }} // namespace eterm::Terminal diff --git a/src/modules/eterm/include/eterm/terminal/terminalemulator.hpp b/src/modules/eterm/include/eterm/terminal/terminalemulator.hpp index 391c9ba39..1e1cbc15e 100644 --- a/src/modules/eterm/include/eterm/terminal/terminalemulator.hpp +++ b/src/modules/eterm/include/eterm/terminal/terminalemulator.hpp @@ -36,6 +36,7 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. #include +#include #include #include #include @@ -48,6 +49,7 @@ using namespace EE; using namespace EE::Math; using namespace EE::Window; +using namespace EE::System; using namespace eterm::System; namespace eterm { namespace Terminal { @@ -67,6 +69,8 @@ struct Term { int histcursize{ 0 }; /* history current size */ int histsize{ 0 }; /* history max size */ int histi{ 0 }; /* history index */ + int histlen{ 0 }; /* history valid length */ + int max_width{ 0 }; /* max width of lines in history */ int scr{ 0 }; /* scroll back */ int* dirty{ nullptr }; /* dirtyness of lines */ TerminalCursor c{}; /* cursor */ @@ -83,6 +87,7 @@ struct Term { Rune lastc{ 0 }; /* last printed char outside of sequence, 0 if control */ std::string title; std::vector title_stack; + bool is_syncing{ false }; // Track DEC mode 2026 ~Term(); }; @@ -256,6 +261,11 @@ class TerminalEmulator final { PtyPtr mPty; ProcPtr mProcess; + bool mPendingPtyResize{ false }; + int mPendingPtyColumns{ 0 }; + int mPendingPtyRows{ 0 }; + Clock mPendingPtyResizeClock; + bool mDirty{ true }; bool mAllowMemoryTrimnming{ false }; int mExitCode; @@ -282,7 +292,6 @@ class TerminalEmulator final { PromptState mPromptState{ PromptState::Unknown }; PromptStateChangedCb mPromptStateChangedCb; - void resizeHistory(); void setClipboard( const char* str ); void loadColors(); @@ -308,14 +317,15 @@ class TerminalEmulator final { void tdumpsel(); void tdumpline( int ); void tdump(); - void tclearregion( int, int, int, int ); + void tclearregion( int, int, int, int, bool skip_clear = false ); void tcursor( int ); void tdeletechar( int ); void tdeleteline( int ); - void tinsertblank( int ); - void tinsertblankline( int ); - int tlinelen( int ) const; - int tiswrapped( int ); + void tinsertblank( int n ); + void tinsertblankline( int n ); + int tlinelen( int y ) const; + int tlinelen( Line line, int col ) const; + int tiswrapped( int y ); void tmoveto( int, int ); void tmoveato( int, int ); void tnewline( int ); @@ -323,7 +333,10 @@ class TerminalEmulator final { void tputc( Rune ); void treset(); void tscrollup( int, int, int ); - void tscrolldown( int, int, int ); + void tscrolldown( int, int ); + void historyPush( Line line, int col ); + void historyReflow( int old_col, int new_col ); + void historyPopToScreen( int loaded, int col ); void tsetattr( int*, int ); void tsetchar( Rune, TerminalGlyph*, int, int ); void tsetdirt( int, int ); diff --git a/src/modules/eterm/src/eterm/terminal/iterminaldisplay.cpp b/src/modules/eterm/src/eterm/terminal/iterminaldisplay.cpp index e3905cedd..e170fe452 100644 --- a/src/modules/eterm/src/eterm/terminal/iterminaldisplay.cpp +++ b/src/modules/eterm/src/eterm/terminal/iterminaldisplay.cpp @@ -48,6 +48,11 @@ int ITerminalDisplay::resetColor( const Uint32& /*index*/, const char* /*name*/ return 0; } +bool ITerminalDisplay::getColor( const Uint32& /*index*/, unsigned char* /*r*/, + unsigned char* /*g*/, unsigned char* /*b*/ ) { + return false; +} + void ITerminalDisplay::setMode( TerminalWinMode mode, int set ) { int m = mMode; MODBIT( ( (int&)mMode ), set, mode ); @@ -78,4 +83,4 @@ void ITerminalDisplay::onProcessExit( int /*exitCode*/ ) {} void ITerminalDisplay::onScrollPositionChange() {} -}} +}} // namespace eterm::Terminal diff --git a/src/modules/eterm/src/eterm/terminal/pseudoterminal.cpp b/src/modules/eterm/src/eterm/terminal/pseudoterminal.cpp index f5dec27d1..eb00c75df 100644 --- a/src/modules/eterm/src/eterm/terminal/pseudoterminal.cpp +++ b/src/modules/eterm/src/eterm/terminal/pseudoterminal.cpp @@ -115,11 +115,16 @@ bool PseudoTerminal::resize( int columns, int rows ) { w.ws_xpixel = 0; w.ws_ypixel = 0; - if ( ioctl( (int)mMaster, TIOCSWINSZ, &w ) < 0 ) { + bool masterResized = ioctl( (int)mMaster, TIOCSWINSZ, &w ) >= 0; + bool slaveResized = mSlave.handle() != -1 ? ioctl( mSlave.handle(), TIOCSWINSZ, &w ) >= 0 : false; + + if ( !masterResized && !slaveResized ) { perror( "PseudoTerminal::Resize" ); return false; } + mColumns = columns; + mRows = rows; return true; } diff --git a/src/modules/eterm/src/eterm/terminal/terminalcolorscheme.cpp b/src/modules/eterm/src/eterm/terminal/terminalcolorscheme.cpp index c4744511f..cee518272 100644 --- a/src/modules/eterm/src/eterm/terminal/terminalcolorscheme.cpp +++ b/src/modules/eterm/src/eterm/terminal/terminalcolorscheme.cpp @@ -113,6 +113,27 @@ size_t TerminalColorScheme::getPaletteSize() const { return mPalette.size(); } +void TerminalColorScheme::setForeground( const Color& foreground ) { + mForeground = foreground; +} + +void TerminalColorScheme::setBackground( const Color& background ) { + mBackground = background; +} + +void TerminalColorScheme::setCursor( const Color& cursor ) { + mCursor = cursor; +} + +void TerminalColorScheme::setPaletteIndex( const size_t& index, const Color& color ) { + if ( index < mPalette.size() ) { + mPalette[index] = color; + } else { + mPalette.resize( index + 1 ); + mPalette[index] = color; + } +} + void TerminalColorScheme::setName( const std::string& name ) { mName = name; } diff --git a/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp b/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp index 726bc5ed2..59f3de9c5 100644 --- a/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp +++ b/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp @@ -504,37 +504,61 @@ void TerminalDisplay::resetColors() { } int TerminalDisplay::resetColor( const Uint32& index, const char* name ) { - if ( !name && index < mColors.size() ) { - Color col = 0x000000FF; + if ( !name ) { + if ( index < mColors.size() ) { + Color col = 0x000000FF; - if ( index < 256 ) - col = colormapped[index]; + if ( index < 256 ) + col = colormapped[index]; - mColors[index] = col; - return 0; + mColors[index] = col; + mColorScheme.setPaletteIndex( index, col ); + return 0; + } + // Reset to default for 256, 257, 258, 259 is not well defined here without original + // defaults + return 1; } - if ( index < mColors.size() ) { - if ( name && String::startsWith( name, "rgb:" ) ) { - auto split = String::split( std::string( name ), ':' ); - if ( split.size() == 2 ) { - auto splitRgb = String::split( split[1], '/' ); - if ( splitRgb.size() == 3 ) { - char* pr = NULL; - char* pg = NULL; - char* pb = NULL; - long r = 0, g = 0, b = 0; - r = std::strtol( splitRgb[0].c_str(), &pr, 16 ); - g = std::strtol( splitRgb[1].c_str(), &pg, 16 ); - b = std::strtol( splitRgb[2].c_str(), &pb, 16 ); - if ( pr && pg && pb ) { - mColors[index] = Color( r, g, b ); - return 0; - } + Color col; + bool colorParsed = false; + + if ( name && String::startsWith( name, "rgb:" ) ) { + auto split = String::split( std::string( name ), ':' ); + if ( split.size() == 2 ) { + auto splitRgb = String::split( split[1], '/' ); + if ( splitRgb.size() == 3 ) { + char* pr = NULL; + char* pg = NULL; + char* pb = NULL; + long r = 0, g = 0, b = 0; + r = std::strtol( splitRgb[0].c_str(), &pr, 16 ); + g = std::strtol( splitRgb[1].c_str(), &pg, 16 ); + b = std::strtol( splitRgb[2].c_str(), &pb, 16 ); + if ( pr && pg && pb ) { + col = Color( r, g, b ); + colorParsed = true; } } - } else { - mColors[index] = Color::fromString( name ); + } + } else { + col = Color::fromString( name ); + colorParsed = true; + } + + if ( colorParsed ) { + if ( index < mColors.size() ) { + mColors[index] = col; + mColorScheme.setPaletteIndex( index, col ); + return 0; + } else if ( index == 256 || index == 257 ) { + mColorScheme.setCursor( col ); + return 0; + } else if ( index == 258 ) { + mColorScheme.setForeground( col ); + return 0; + } else if ( index == 259 ) { + mColorScheme.setBackground( col ); return 0; } } @@ -542,6 +566,32 @@ int TerminalDisplay::resetColor( const Uint32& index, const char* name ) { return 1; } +bool TerminalDisplay::getColor( const Uint32& index, unsigned char* r, unsigned char* g, + unsigned char* b ) { + if ( index < mColors.size() ) { + *r = mColors[index].r; + *g = mColors[index].g; + *b = mColors[index].b; + return true; + } else if ( index == 256 || index == 257 ) { + *r = mColorScheme.getCursor().r; + *g = mColorScheme.getCursor().g; + *b = mColorScheme.getCursor().b; + return true; + } else if ( index == 258 ) { + *r = mColorScheme.getForeground().r; + *g = mColorScheme.getForeground().g; + *b = mColorScheme.getForeground().b; + return true; + } else if ( index == 259 ) { + *r = mColorScheme.getBackground().r; + *g = mColorScheme.getBackground().g; + *b = mColorScheme.getBackground().b; + return true; + } + return false; +} + bool TerminalDisplay::isBlinkingCursor() { return mCursorMode == Terminal::BlinkingBlock || mCursorMode == Terminal::BlinkingBlockDefault || diff --git a/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp b/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp index ff9e99c5e..d309f3496 100644 --- a/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp +++ b/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp @@ -367,7 +367,7 @@ void TerminalEmulator::selinit( void ) { } int TerminalEmulator::tlinelen( int y ) const { - if ( y < 0 ) + if ( y < mTerm.scr - mTerm.histlen || y >= mTerm.scr + mTerm.row ) return 0; int i = mTerm.col; @@ -381,6 +381,15 @@ int TerminalEmulator::tlinelen( int y ) const { return i; } +int TerminalEmulator::tlinelen( Line line, int col ) const { + int i = col; + if ( line[i - 1].mode & ATTR_WRAP ) + return i; + while ( i > 0 && line[i - 1].u == ' ' ) + --i; + return i; +} + int TerminalEmulator::tiswrapped( int y ) { int len = tlinelen( y ); @@ -545,7 +554,7 @@ void TerminalEmulator::selsnap( int* x, int* y, int direction ) { char* TerminalEmulator::getsel( void ) const { char *str, *ptr; int y, bufsize, lastx, linelen; - TerminalGlyph *gp, *last; + TerminalGlyph *gp, *last, *it; if ( mSel.ob.x == -1 ) return NULL; @@ -567,15 +576,18 @@ char* TerminalEmulator::getsel( void ) const { gp = &TLINE( y )[mSel.nb.y == y ? mSel.nb.x : 0]; lastx = ( mSel.ne.y == y ) ? mSel.ne.x : mTerm.col - 1; } + bool wrapped = ( TLINE( y )[mTerm.col - 1].mode & ATTR_WRAP ); last = &TLINE( y )[MIN( lastx, linelen - 1 )]; - while ( last >= gp && last->u == ' ' ) - --last; + if ( !wrapped ) { + while ( last >= gp && last->u == ' ' ) + --last; + } - for ( ; gp <= last; ++gp ) { - if ( gp->mode & ATTR_WDUMMY ) + for ( it = gp; it <= last; ++it ) { + if ( it->mode & ATTR_WDUMMY ) continue; - ptr += utf8encode( gp->u, ptr ); + ptr += utf8encode( it->u, ptr ); } /* @@ -588,7 +600,7 @@ char* TerminalEmulator::getsel( void ) const { * FIXME: Fix the computer world. */ if ( ( y < mSel.ne.y || lastx >= linelen ) && - ( !( last->mode & ATTR_WRAP ) || mSel.type == SEL_RECTANGULAR ) ) + ( last < gp || !wrapped || mSel.type == SEL_RECTANGULAR ) ) *ptr++ = '\n'; } *ptr = 0; @@ -596,7 +608,7 @@ char* TerminalEmulator::getsel( void ) const { } bool TerminalEmulator::hasSelection() const { - return mSel.mode == SEL_READY; + return mSel.mode != SEL_EMPTY && mSel.ob.x != -1; } std::string TerminalEmulator::getSelection() const { @@ -681,18 +693,18 @@ void TerminalEmulator::kscrollup( const TerminalArg* a ) { int n = a->i; if ( n == INT_MAX ) - n = mTerm.histi - mTerm.scr; + n = mTerm.histlen - mTerm.scr; if ( n < 0 ) n = mTerm.row + n; - if ( mTerm.scr + n > mTerm.histi ) - n = mTerm.histi - mTerm.scr; + if ( mTerm.scr + n > mTerm.histlen ) + n = mTerm.histlen - mTerm.scr; if ( n == 0 ) return; - if ( mTerm.scr <= mTerm.histsize - n && mTerm.scr + n <= mTerm.histi ) { + if ( mTerm.scr <= mTerm.histsize - n && mTerm.scr + n <= mTerm.histlen ) { mTerm.scr += n; selmove( n ); tfulldirt(); @@ -703,9 +715,8 @@ void TerminalEmulator::kscrollup( const TerminalArg* a ) { void TerminalEmulator::kscrollto( const TerminalArg* a ) { int n = a->i; - if ( 0 <= n && n <= mTerm.histi ) { + if ( 0 <= n && n <= mTerm.histlen ) { mTerm.scr = n; - selscroll( 0, n ); tfulldirt(); onScrollPositionChange(); } @@ -716,7 +727,7 @@ int TerminalEmulator::tisaltscr() { } int TerminalEmulator::scrollSize() const { - return mTerm.histi; + return mTerm.histlen; } int TerminalEmulator::rowCount() const { @@ -737,6 +748,9 @@ void TerminalEmulator::clearHistory() { eeSAFE_FREE( mTerm.hist ); mTerm.histcursize = 0; mTerm.histi = 0; + mTerm.histlen = 0; + mTerm.max_width = 0; + mTerm.scr = 0; trimMemory(); } @@ -820,6 +834,9 @@ int TerminalEmulator::tattrset( int attr ) { void TerminalEmulator::tsetdirt( int top, int bot ) { int i; + if ( mTerm.row < 1 ) + return; + LIMIT( top, 0, mTerm.row - 1 ); LIMIT( bot, 0, mTerm.row - 1 ); mDirty = true; @@ -861,6 +878,7 @@ void TerminalEmulator::treset( void ) { mTerm.c = TerminalCursor{}; mTerm.c.attr = TerminalGlyph{}; + mTerm.c.attr.u = ' '; mTerm.c.attr.mode = ATTR_NULL; mTerm.c.attr.fg = mDefaultFg; mTerm.c.attr.bg = mDefaultBg; @@ -895,6 +913,7 @@ void TerminalEmulator::tnew( int col, int row, size_t historySize ) { mTerm = Term{}; mTerm.c = TerminalCursor{}; mTerm.c.attr = TerminalGlyph{}; + mTerm.c.attr.u = ' '; mTerm.c.attr.fg = mDefaultFg; mTerm.c.attr.bg = mDefaultBg; mTerm.histsize = historySize; @@ -912,35 +931,11 @@ void TerminalEmulator::tswapscreen( void ) { tfulldirt(); } -void TerminalEmulator::resizeHistory() { - size_t oriSize = mTerm.histcursize; - if ( mTerm.histi >= (int)mTerm.histcursize ) { - int newSize = eemin( mTerm.histi + mTerm.row, mTerm.histsize ); - mTerm.hist = (Line*)xrealloc( mTerm.hist, newSize * sizeof( Line ) ); - mTerm.histcursize = newSize; - - for ( int i = oriSize; i < mTerm.histcursize; i++ ) { - mTerm.hist[i] = (TerminalGlyph*)xmalloc( mTerm.col * sizeof( TerminalGlyph ) ); - for ( int j = 0; j < mTerm.col; j++ ) { - mTerm.hist[i][j] = mTerm.c.attr; - mTerm.hist[i][j].u = ' '; - } - } - } -} - -void TerminalEmulator::tscrolldown( int top, int n, int copyhist ) { +void TerminalEmulator::tscrolldown( int top, int n ) { int i; Line temp; LIMIT( n, 0, mTerm.bot - top + 1 ); - if ( copyhist && mTerm.histsize > 0 ) { - mTerm.histi = ( mTerm.histi - 1 + mTerm.histsize ) % mTerm.histsize; - resizeHistory(); - temp = mTerm.hist[mTerm.histi]; - mTerm.hist[mTerm.histi] = mTerm.line[mTerm.bot]; - mTerm.line[mTerm.bot] = temp; - } tsetdirt( top, mTerm.bot - n ); tclearregion( 0, mTerm.bot - n + 1, mTerm.col - 1, mTerm.bot ); @@ -961,18 +956,22 @@ void TerminalEmulator::tscrollup( int top, int n, int copyhist ) { LIMIT( n, 0, mTerm.bot - top + 1 ); - if ( copyhist && mTerm.histsize > 0 ) { - mTerm.histi = ( mTerm.histi + 1 ) % mTerm.histsize; - resizeHistory(); - temp = mTerm.hist[mTerm.histi]; - mTerm.hist[mTerm.histi] = mTerm.line[top]; - mTerm.line[top] = temp; + if ( copyhist && mTerm.histsize > 0 && !IS_SET( MODE_ALTSCREEN ) && top == mTerm.top ) { + bool attop = ( mTerm.histlen != 0 && mTerm.scr == mTerm.histlen ); + + if ( mTerm.scr > 0 && !attop ) + mTerm.scr += n; + + for ( i = 0; i < n; i++ ) + historyPush( mTerm.line[top + i], mTerm.col ); + + if ( attop ) + mTerm.scr = mTerm.histlen; + else if ( mTerm.scr > mTerm.histlen ) + mTerm.scr = mTerm.histlen; } - if ( mTerm.scr > 0 && mTerm.scr < mTerm.histsize ) - mTerm.scr = MIN( mTerm.scr + n, mTerm.histsize - 1 ); - - tclearregion( 0, top, mTerm.col - 1, top + n - 1 ); + tclearregion( 0, top, mTerm.col - 1, top + n - 1, copyhist != 0 ); tsetdirt( top + n, mTerm.bot ); for ( i = top; i <= mTerm.bot - n; i++ ) { @@ -987,26 +986,222 @@ void TerminalEmulator::tscrollup( int top, int n, int copyhist ) { onScrollPositionChange(); } +void TerminalEmulator::historyPush( Line line, int col ) { + if ( mTerm.histsize <= 0 ) + return; + + int width = tlinelen( line, col ); + if ( width > mTerm.max_width ) + mTerm.max_width = width; + + mTerm.histi = ( mTerm.histi + 1 ) % mTerm.histsize; + if ( mTerm.histlen < mTerm.histsize ) { + mTerm.histlen++; + if ( mTerm.histi >= (int)mTerm.histcursize ) { + int newSize = eemin( mTerm.histi + mTerm.row, mTerm.histsize ); + mTerm.hist = (Line*)xrealloc( mTerm.hist, newSize * sizeof( Line ) ); + for ( int i = mTerm.histcursize; i < newSize; i++ ) + mTerm.hist[i] = nullptr; + mTerm.histcursize = newSize; + } + } else if ( mTerm.hist[mTerm.histi] ) { + eeSAFE_FREE( mTerm.hist[mTerm.histi] ); + } + + mTerm.hist[mTerm.histi] = (Line)eeMalloc( col * sizeof( TerminalGlyph ) ); + memcpy( mTerm.hist[mTerm.histi], line, col * sizeof( TerminalGlyph ) ); +} + +void TerminalEmulator::historyReflow( int old_col, int new_col ) { + int i, j; + int new_len = 0; + int new_histi = -1; + int new_max_width = 0; + bool has_sel = mSel.ob.x != -1; + int ob_abs_y = mSel.ob.y; + int oe_abs_y = mSel.oe.y; + int ob_x = mSel.ob.x; + int oe_x = mSel.oe.x; + int ob_logical_offset = -1; + int oe_logical_offset = -1; + + if ( mTerm.histlen == 0 ) + return; + + Line* new_hist = (Line*)eeMalloc( mTerm.histsize * sizeof( Line ) ); + for ( i = 0; i < mTerm.histsize; i++ ) + new_hist[i] = nullptr; + + int logical_cap = old_col * 2; + Line logical = (Line)eeMalloc( logical_cap * sizeof( TerminalGlyph ) ); + int logical_len = 0; + + for ( i = 0; i < mTerm.histlen; i++ ) { + int idx = ( mTerm.histi - mTerm.histlen + 1 + i + mTerm.histsize ) % mTerm.histsize; + Line line = mTerm.hist[idx]; + int is_wrapped = ( line[old_col - 1].mode & ATTR_WRAP ); + + if ( logical_len + old_col > logical_cap ) { + logical_cap *= 2; + logical = (Line)eeRealloc( logical, logical_cap * sizeof( TerminalGlyph ) ); + } + + if ( has_sel ) { + if ( mSel.type == SEL_RECTANGULAR ) { + if ( i == ob_abs_y ) + mSel.ob.y = new_len; + if ( i == oe_abs_y ) + mSel.oe.y = new_len; + } else { + if ( i == ob_abs_y ) + ob_logical_offset = logical_len + ob_x; + if ( i == oe_abs_y ) + oe_logical_offset = logical_len + oe_x; + } + } + + memcpy( logical + logical_len, line, old_col * sizeof( TerminalGlyph ) ); + for ( j = 0; j < old_col; j++ ) + logical[logical_len + j].mode &= ~ATTR_WRAP; + + logical_len += old_col; + + if ( is_wrapped ) + continue; + + while ( logical_len > 0 ) { + TerminalGlyph* g = &logical[logical_len - 1]; + if ( g->u == ' ' && g->bg == mDefaultBg && ( g->mode & ATTR_BOLD ) == 0 ) + logical_len--; + else + break; + } + if ( logical_len == 0 ) + logical_len = 1; + + if ( has_sel ) { + if ( ob_logical_offset != -1 && ob_logical_offset > logical_len ) + ob_logical_offset = logical_len; + if ( oe_logical_offset != -1 && oe_logical_offset > logical_len ) + oe_logical_offset = logical_len; + } + + int cursor = 0; + while ( cursor < logical_len ) { + Line nl = (Line)eeMalloc( new_col * sizeof( TerminalGlyph ) ); + for ( j = 0; j < new_col; j++ ) { + nl[j] = mTerm.c.attr; + nl[j].u = ' '; + nl[j].mode = 0; + } + + int copy_width = logical_len - cursor; + if ( copy_width > new_col ) + copy_width = new_col; + + if ( copy_width > 1 && copy_width == new_col && copy_width < logical_len - cursor && + ( logical[cursor + copy_width - 1].mode & ATTR_WIDE ) ) { + copy_width--; + } + + if ( has_sel ) { + if ( ob_logical_offset >= cursor && ob_logical_offset < cursor + copy_width ) { + mSel.ob.y = new_len; + mSel.ob.x = ob_logical_offset - cursor; + } else if ( ob_logical_offset == logical_len && + cursor + copy_width == logical_len ) { + mSel.ob.y = new_len; + mSel.ob.x = eemin( copy_width, new_col - 1 ); + } + if ( oe_logical_offset >= cursor && oe_logical_offset < cursor + copy_width ) { + mSel.oe.y = new_len; + mSel.oe.x = oe_logical_offset - cursor; + } else if ( oe_logical_offset == logical_len && + cursor + copy_width == logical_len ) { + mSel.oe.y = new_len; + mSel.oe.x = eemin( copy_width, new_col - 1 ); + } + } + + memcpy( nl, logical + cursor, copy_width * sizeof( TerminalGlyph ) ); + for ( j = 0; j < copy_width; j++ ) + nl[j].mode &= ~ATTR_WRAP; + + if ( cursor + copy_width < logical_len ) + nl[new_col - 1].mode |= ATTR_WRAP; + else + nl[new_col - 1].mode &= ~ATTR_WRAP; + + new_histi = ( new_histi + 1 ) % mTerm.histsize; + if ( new_len < mTerm.histsize ) { + new_len++; + } else { + eeSAFE_FREE( new_hist[new_histi] ); + } + new_hist[new_histi] = nl; + + int current_width = ( cursor + copy_width < logical_len ) ? new_col : copy_width; + if ( current_width > new_max_width ) + new_max_width = current_width; + + cursor += copy_width; + } + logical_len = 0; + ob_logical_offset = -1; + oe_logical_offset = -1; + } + eeSAFE_FREE( logical ); + + for ( i = 0; i < mTerm.histcursize; i++ ) + eeSAFE_FREE( mTerm.hist[i] ); + eeSAFE_FREE( mTerm.hist ); + + mTerm.hist = new_hist; + mTerm.histcursize = mTerm.histsize; + mTerm.histlen = new_len; + mTerm.histi = ( new_histi == -1 ) ? 0 : new_histi; + mTerm.max_width = new_max_width; +} + +void TerminalEmulator::historyPopToScreen( int loaded, int col ) { + int i; + int start_logical = mTerm.histlen - loaded; + for ( i = 0; i < loaded; i++ ) { + int idx = ( mTerm.histi - mTerm.histlen + 1 + start_logical + i + mTerm.histsize ) % + mTerm.histsize; + Line line = mTerm.hist[idx]; + memcpy( mTerm.line[i], line, col * sizeof( TerminalGlyph ) ); + eeSAFE_FREE( mTerm.hist[idx] ); + mTerm.hist[idx] = nullptr; + } + mTerm.histi = ( mTerm.histi - loaded + mTerm.histsize ) % mTerm.histsize; + mTerm.histlen -= loaded; +} + void TerminalEmulator::selmove( int n ) { mSel.ob.y += n, mSel.nb.y += n; mSel.oe.y += n, mSel.ne.y += n; } void TerminalEmulator::selscroll( int top, int n ) { + if ( mTerm.scr != 0 ) + return; if ( mSel.ob.x == -1 || mSel.alt != IS_SET( MODE_ALTSCREEN ) ) return; - if ( BETWEEN( mSel.nb.y, top, mTerm.bot ) != BETWEEN( mSel.ne.y, top, mTerm.bot ) ) { - selclear(); - } else if ( BETWEEN( mSel.nb.y, top, mTerm.bot ) ) { + if ( top == 0 || + ( BETWEEN( mSel.nb.y, top, mTerm.bot ) && BETWEEN( mSel.ne.y, top, mTerm.bot ) ) ) { mSel.ob.y += n; mSel.oe.y += n; - if ( mSel.ob.y < mTerm.top || mSel.ob.y > mTerm.bot || mSel.oe.y < mTerm.top || + int miny = ( mTerm.histsize > 0 && !IS_SET( MODE_ALTSCREEN ) ) ? -mTerm.histsize : 0; + if ( mSel.ob.y < miny || mSel.ob.y > mTerm.bot || mSel.oe.y < miny || mSel.oe.y > mTerm.bot ) { selclear(); } else { selnormalize(); } + } else if ( BETWEEN( mSel.nb.y, top, mTerm.bot ) || BETWEEN( mSel.ne.y, top, mTerm.bot ) ) { + selclear(); } } @@ -1111,7 +1306,7 @@ void TerminalEmulator::tsetchar( Rune u, TerminalGlyph* attr, int x, int y ) { TLINE( y )[x].mode |= ATTR_BOXDRAW; } -void TerminalEmulator::tclearregion( int x1, int y1, int x2, int y2 ) { +void TerminalEmulator::tclearregion( int x1, int y1, int x2, int y2, bool skip_clear ) { int x, y, temp; TerminalGlyph* gp; @@ -1130,7 +1325,7 @@ void TerminalEmulator::tclearregion( int x1, int y1, int x2, int y2 ) { mDirty = true; for ( x = x1; x <= x2; x++ ) { gp = &TLINE( y )[x]; - if ( selected( x, y ) ) + if ( !skip_clear && selected( x, y ) ) selclear(); gp->fg = mTerm.c.attr.fg; gp->bg = mTerm.c.attr.bg; @@ -1172,7 +1367,7 @@ void TerminalEmulator::tinsertblank( int n ) { void TerminalEmulator::tinsertblankline( int n ) { if ( BETWEEN( mTerm.c.y, mTerm.top, mTerm.bot ) ) - tscrolldown( mTerm.c.y, n, 0 ); + tscrolldown( mTerm.c.y, n ); } void TerminalEmulator::tdeleteline( int n ) { @@ -1439,9 +1634,16 @@ void TerminalEmulator::tsetmode( int priv, int set, int* args, int narg ) { codes. */ case 1039: /* ESC to Meta (not implemented) */ break; - case 2026: // IGNORE DECSET/DECRST 2026 for sync updates - // (https://codeberg.org/dnkl/foot/pulls/461/files) + case 2026: { + // IGNORE DECSET/DECRST 2026 for sync updates? + // (https://codeberg.org/dnkl/foot/pulls/461/files) + // mTerm.is_syncing = ( set == 1 ); + /* if ( !mTerm.is_syncing ) { + // When syncing ends, we must perform the deferred draw + draw(); + } */ break; + } default: #ifdef EE_DEBUG fprintf( stderr, "erresc: unknown private set/reset mode %d\n", *args ); @@ -1646,7 +1848,7 @@ void TerminalEmulator::csihandle( void ) { break; case 'T': /* SD -- Scroll line down */ DEFAULT( mCsiescseq.arg[0], 1 ); - tscrolldown( mTerm.top, mCsiescseq.arg[0], 1 ); + tscrolldown( mTerm.top, mCsiescseq.arg[0] ); break; case 'L': /* IL -- Insert blank lines */ DEFAULT( mCsiescseq.arg[0], 1 ); @@ -2290,7 +2492,7 @@ int TerminalEmulator::eschandle( uchar ascii ) { break; case 'M': /* RI -- Reverse index */ if ( mTerm.c.y == mTerm.top ) { - tscrolldown( mTerm.top, 1, 1 ); + tscrolldown( mTerm.top, 1 ); } else { tmoveto( mTerm.c.x, mTerm.c.y - 1 ); } @@ -2478,6 +2680,7 @@ check_control_code: memmove( gp + width, gp, ( mTerm.col - mTerm.c.x - width ) * sizeof( TerminalGlyph ) ); if ( mTerm.c.x + width > mTerm.col ) { + mTerm.line[mTerm.c.y][mTerm.col - 1].mode |= ATTR_WRAP; tnewline( 1 ); gp = &mTerm.line[mTerm.c.y][mTerm.c.x]; } @@ -2535,93 +2738,168 @@ int TerminalEmulator::twrite( const char* buf, int buflen, int show_ctrl ) { void TerminalEmulator::tresize( int col, int row ) { int i, j; - int minrow = MIN( row, mTerm.row ); - int mincol = MIN( col, mTerm.col ); - int* bp; - TerminalCursor c; + int old_row = mTerm.row; + int old_col = mTerm.col; + int save_end = 0; + int loaded = 0; + bool is_alt = IS_SET( MODE_ALTSCREEN ); if ( col < 1 || row < 1 ) { fprintf( stderr, "tresize: error resizing to %dx%d\n", col, row ); return; } - /* - * slide screen to keep cursor where we expect it - - * tscrollup would work here, but we can optimize to - * memmove because we're freeing the earlier lines - */ - for ( i = 0; i <= mTerm.c.y - row; i++ ) { - xfree( mTerm.line[i] ); - xfree( mTerm.alt[i] ); - } - /* ensure that both src and dst are not NULL */ - if ( i > 0 ) { - memmove( mTerm.line, mTerm.line + i, row * sizeof( Line ) ); - memmove( mTerm.alt, mTerm.alt + i, row * sizeof( Line ) ); - } - for ( i += row; i < mTerm.row; i++ ) { - xfree( mTerm.line[i] ); - xfree( mTerm.alt[i] ); + bool has_sel = mSel.ob.x != -1; + // Alt-screen selections are not reflowed. + if ( has_sel && ( mSel.alt || is_alt ) ) { + selclear(); + has_sel = false; } - /* resize to new height */ - mTerm.line = (Line*)xrealloc( mTerm.line, row * sizeof( Line ) ); - mTerm.alt = (Line*)xrealloc( mTerm.alt, row * sizeof( Line ) ); - mTerm.dirty = (int*)xrealloc( mTerm.dirty, row * sizeof( *mTerm.dirty ) ); - mTerm.tabs = (int*)xrealloc( mTerm.tabs, col * sizeof( *mTerm.tabs ) ); - - /* resize each row to new width, zero-pad if needed */ - for ( i = 0; i < minrow; i++ ) { - mTerm.line[i] = (Line)xrealloc( mTerm.line[i], col * sizeof( TerminalGlyph ) ); - mTerm.alt[i] = (Line)xrealloc( mTerm.alt[i], col * sizeof( TerminalGlyph ) ); - } - - /* allocate any new rows */ - for ( /* i = minrow */; i < row; i++ ) { - mTerm.line[i] = (Line)xmalloc( col * sizeof( TerminalGlyph ) ); - mTerm.alt[i] = (Line)xmalloc( col * sizeof( TerminalGlyph ) ); - } - - /* add new columns to history */ - for ( int i = 0; i < mTerm.histcursize; i++ ) { - mTerm.hist[i] = (TerminalGlyph*)xrealloc( mTerm.hist[i], col * sizeof( TerminalGlyph ) ); - for ( j = mincol; j < col; j++ ) { - mTerm.hist[i][j] = mTerm.c.attr; - mTerm.hist[i][j].u = ' '; + int old_histlen = mTerm.histlen; + if ( has_sel ) { + if ( mTerm.histsize < mTerm.row ) { + // Back-conversion breaks when histsize < row because + // loaded < save_end; safer to drop the selection. + selclear(); + has_sel = false; + } else { + mSel.ob.y += old_histlen - mTerm.scr; + mSel.oe.y += old_histlen - mTerm.scr; } } - if ( col > mTerm.col ) { - bp = mTerm.tabs + mTerm.col; + if ( is_alt ) + tswapscreen(); - memset( bp, 0, sizeof( *mTerm.tabs ) * ( col - mTerm.col ) ); - while ( --bp > mTerm.tabs && !*bp ) - /* nothing */; - for ( bp += tabspaces; bp < mTerm.tabs + col; bp += tabspaces ) - *bp = 1; + save_end = mTerm.row; + if ( mTerm.row != 0 && mTerm.col != 0 ) { + if ( !is_alt ) { + tclearregion( mTerm.c.x, mTerm.c.y, mTerm.col - 1, mTerm.c.y, true ); + } + + if ( !is_alt && mTerm.c.y > 0 && mTerm.c.y < mTerm.row ) + mTerm.line[mTerm.c.y - 1][mTerm.col - 1].mode &= ~ATTR_WRAP; + + for ( i = mTerm.row - 1; i >= 0; i-- ) { + if ( tlinelen( mTerm.line[i], mTerm.col ) > 0 ) + break; + } + save_end = i + 1; + if ( !is_alt && save_end < mTerm.c.y + 1 ) + save_end = mTerm.c.y + 1; + if ( has_sel ) + save_end = mTerm.row; + + for ( i = 0; i < save_end; i++ ) + historyPush( mTerm.line[i], mTerm.col ); + + if ( has_sel && old_histlen + save_end > mTerm.histsize ) { + int dropped = ( old_histlen + save_end ) - mTerm.histsize; + if ( eemax( mSel.ob.y, mSel.oe.y ) < dropped ) { + selclear(); + has_sel = false; + } else { + mSel.ob.y = eemax( 0, mSel.ob.y - dropped ); + mSel.oe.y = eemax( 0, mSel.oe.y - dropped ); + } + } + + bool needs_reflow = false; + if ( col > mTerm.col ) { + needs_reflow = mTerm.max_width >= mTerm.col; + } else if ( col < mTerm.col ) { + if ( mTerm.max_width > col ) + needs_reflow = true; + } + + if ( needs_reflow ) { + historyReflow( mTerm.col, col ); + } else { + for ( i = 0; i < mTerm.histcursize; i++ ) { + if ( mTerm.hist[i] ) { + mTerm.hist[i] = (Line)eeRealloc( mTerm.hist[i], col * sizeof( TerminalGlyph ) ); + if ( col > old_col ) { + for ( j = old_col; j < col; j++ ) { + mTerm.hist[i][j] = mTerm.c.attr; + mTerm.hist[i][j].u = ' '; + mTerm.hist[i][j].mode = 0; + } + } + } + } + if ( has_sel ) { + if ( mSel.ob.x >= col ) + mSel.ob.x = col - 1; + if ( mSel.oe.x >= col ) + mSel.oe.x = col - 1; + } + } } - /* update terminal size */ + for ( i = 0; i < mTerm.row; i++ ) { + eeSAFE_FREE( mTerm.line[i] ); + eeSAFE_FREE( mTerm.alt[i] ); + } + eeSAFE_FREE( mTerm.line ); + eeSAFE_FREE( mTerm.alt ); + eeSAFE_FREE( mTerm.dirty ); + eeSAFE_FREE( mTerm.tabs ); + mTerm.col = col; mTerm.row = row; - /* reset scrolling region */ - tsetscroll( 0, row - 1 ); - /* make use of the LIMIT in tmoveto */ - tmoveto( mTerm.c.x, mTerm.c.y ); - /* Clearing both screens (it makes dirty all lines) */ - c = mTerm.c; - for ( i = 0; i < 2; i++ ) { - if ( mincol < col && 0 < minrow ) { - tclearregion( mincol, 0, col - 1, minrow - 1 ); + mTerm.line = (Line*)eeMalloc( mTerm.row * sizeof( Line ) ); + mTerm.alt = (Line*)eeMalloc( mTerm.row * sizeof( Line ) ); + mTerm.dirty = (int*)eeMalloc( mTerm.row * sizeof( int ) ); + mTerm.tabs = (int*)eeMalloc( mTerm.col * sizeof( int ) ); + + for ( i = 0; i < mTerm.row; i++ ) { + mTerm.line[i] = (Line)eeMalloc( mTerm.col * sizeof( TerminalGlyph ) ); + mTerm.alt[i] = (Line)eeMalloc( mTerm.col * sizeof( TerminalGlyph ) ); + mTerm.dirty[i] = 1; + for ( j = 0; j < mTerm.col; j++ ) { + mTerm.line[i][j] = mTerm.c.attr; + mTerm.line[i][j].u = ' '; + mTerm.line[i][j].mode = 0; + mTerm.alt[i][j] = mTerm.c.attr; + mTerm.alt[i][j].u = ' '; + mTerm.alt[i][j].mode = 0; } - if ( 0 < col && minrow < row ) { - tclearregion( 0, minrow, col - 1, row - 1 ); - } - tswapscreen(); - tcursor( CURSOR_LOAD ); } - mTerm.c = c; + + memset( mTerm.tabs, 0, mTerm.col * sizeof( int ) ); + for ( i = tabspaces; i < mTerm.col; i += tabspaces ) + mTerm.tabs[i] = 1; + + tsetscroll( 0, mTerm.row - 1 ); + + if ( old_row > 0 ) { + loaded = MIN( mTerm.histlen, mTerm.row ); + historyPopToScreen( loaded, col ); + if ( !is_alt ) + mTerm.c.y += ( loaded - save_end ); + } + + if ( is_alt ) + tswapscreen(); + + LIMIT( mTerm.scr, 0, mTerm.histlen ); + LIMIT( mTerm.c.y, 0, mTerm.row - 1 ); + LIMIT( mTerm.c.x, 0, mTerm.col - 1 ); + + if ( has_sel ) { + mSel.ob.y = mSel.ob.y - mTerm.histlen + mTerm.scr; + mSel.oe.y = mSel.oe.y - mTerm.histlen + mTerm.scr; + // Clamp to the full available range (history + screen) + mSel.ob.y = + eemax( mTerm.scr - mTerm.histlen, eemin( mTerm.scr + mTerm.row - 1, mSel.ob.y ) ); + mSel.oe.y = + eemax( mTerm.scr - mTerm.histlen, eemin( mTerm.scr + mTerm.row - 1, mSel.oe.y ) ); + selnormalize(); + } + mDirty = true; + onScrollPositionChange(); } void TerminalEmulator::resettitle( void ) { @@ -2648,6 +2926,10 @@ void TerminalEmulator::drawregion( ITerminalDisplay& dpy, int x1, int y1, int x2 } void TerminalEmulator::draw() { + // If a synchronized update is in progress, skip the physical render + // if ( mTerm.is_syncing ) + // return; + int cx = mTerm.c.x /*, ocx = term.ocx, ocy = term.ocy*/; { @@ -2704,16 +2986,11 @@ bool TerminalEmulator::xgetmode( const TerminalWinMode& mode ) { } int TerminalEmulator::xgetcolor( int x, unsigned char* r, unsigned char* g, unsigned char* b ) { - // if ( !BETWEEN( x, 0, dc.collen - 1 ) ) - // return 1; + auto dpy = mDpy.lock(); + if ( !dpy ) + return 1; - // *r = dc.col[x].color.red >> 8; - // *g = dc.col[x].color.green >> 8; - // *b = dc.col[x].color.blue >> 8; - - // return 0; - - return 1; + return dpy->getColor( x, r, g, b ) ? 0 : 1; } void TerminalEmulator::osc_color_response( int num, int index, int is_osc4 ) { @@ -2899,13 +3176,13 @@ TerminalEmulator::TerminalEmulator( PtyPtr&& pty, ProcPtr&& process, int col = mPty->getNumColumns(); int row = mPty->getNumRows(); + selinit(); tnew( col, row, historySize ); if ( display ) { display->setCursorMode( TerminalCursorMode::SteadyUnderline ); display->attach( this ); loadColors(); } - selinit(); resettitle(); } @@ -2997,17 +3274,49 @@ int TerminalEmulator::write( const char* buf, size_t buflen ) { } void TerminalEmulator::resize( int columns, int rows ) { - if ( !mPty->resize( columns, rows ) ) { - _die( "Failed to resize pty!" ); + bool is_alt = IS_SET( MODE_ALTSCREEN ); + + // Alt doesn't need reflow, we can resize and redraw instantly which looks and feels better + if ( is_alt ) { + if ( !mPty->resize( columns, rows ) ) { + _die( "Failed to resize pty!" ); + return; + } + tresize( columns, rows ); + redraw(); return; } + + // 1. Manually set sync mode to avoid flickering during internal restructuring + mTerm.is_syncing = true; tresize( columns, rows ); + + // 2. Nudge the shell to redraw the prompt immediately + // Sending DSR (Cursor Position) forces the shell to refresh the line + ttywrite( "\033[6n", 4, 0 ); + redraw(); + mPendingPtyColumns = columns; + mPendingPtyRows = rows; + mPendingPtyResize = true; + mPendingPtyResizeClock.restart(); } #define MAX_TTY_READS ( 1024 ) bool TerminalEmulator::update() { + if ( mPendingPtyResize && mPendingPtyResizeClock.getElapsedTime() >= Milliseconds( 100 ) ) { + mPendingPtyResize = false; + + if ( !mPty->resize( mPendingPtyColumns, mPendingPtyRows ) ) { + _die( "Failed to resize pty!" ); + } + + // 3. End sync mode and trigger the final draw + mTerm.is_syncing = false; + redraw(); + } + if ( mStatus == TerminalEmulator::STARTING ) { mStatus = TerminalEmulator::RUNNING; } else if ( mStatus != TerminalEmulator::RUNNING ) { diff --git a/src/tests/unit_tests/eterm_test.cpp b/src/tests/unit_tests/eterm_test.cpp new file mode 100644 index 000000000..0ad0ef3a6 --- /dev/null +++ b/src/tests/unit_tests/eterm_test.cpp @@ -0,0 +1,599 @@ +#include +#include +#include +#include +#include "utest.hpp" + +using namespace eterm::Terminal; +using namespace eterm::System; + +class MockPty : public IPseudoTerminal { +public: + std::string mBuffer; + int mCols = 80; + int mRows = 24; + int getNumColumns() const override { return mCols; } + int getNumRows() const override { return mRows; } + bool resize(int columns, int rows) override { mCols = columns; mRows = rows; return true; } + bool isTTY() const override { return true; } + int write(const char* s, size_t n) override { + mBuffer.append(s, n); + return n; + } + int read(char* buf, size_t n, bool) override { + if (mBuffer.empty()) return 0; + size_t toRead = std::min(n, mBuffer.size()); + memcpy(buf, mBuffer.data(), toRead); + mBuffer.erase(0, toRead); + return toRead; + } +}; + +class MockProcess : public IProcess { +public: + void checkExitStatus() override {} + bool hasExited() const override { return false; } + int getExitCode() const override { return 0; } + void terminate() override {} + void waitForExit() override {} + int pid() override { return 123; } +}; + +class MockDisplay : public ITerminalDisplay { +public: + bool drawBegin(Uint32, Uint32) override { return true; } + void drawLine(Line, int, int, int) override {} + void drawCursor(int, int, TerminalGlyph, int, int, TerminalGlyph) override {} + void drawEnd() override {} +}; + +UTEST(eterm, basic_write) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + term->write("ABC", 3); + term->update(); + + term->selstart(0, 0, 0); + term->selextend(2, 0, 1, 0); + EXPECT_TRUE(term->hasSelection()); + EXPECT_STDSTREQ("ABC", term->getSelection()); +} + +UTEST(eterm, selection_reflow) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // 80x24. Write 80 'A's then 80 'B's. + std::string row0(80, 'A'); + std::string row1(80, 'B'); + term->write(row0.c_str(), row0.size()); + term->write(row1.c_str(), row1.size()); + term->write(" ", 1); // Trigger wrap on row 1 to move cursor to row 2 and preserve row 0 wrap + term->update(); + + // Selection from index 70 of row 0 to index 10 of row 1. + term->selstart(70, 0, 0); + term->selextend(10, 1, 1, 0); + + // ATTR_WRAP is set on row 0, so no newline should be added between A and B. + std::string expected = std::string(10, 'A') + std::string(11, 'B'); + std::string sel = term->getSelection(); + EXPECT_STDSTREQ(expected, sel); + + // Resize to 40 columns + term->resize(40, 24); + + EXPECT_TRUE(term->hasSelection()); + EXPECT_STDSTREQ(expected, term->getSelection()); +} + +UTEST(eterm, selection_reflow_history) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // Fill history with unique lines, each 40 chars to ensure they fit in 80. + for (int i = 0; i < 40; ++i) { + std::string line = "H" + std::to_string(i) + " " + std::string(30, 'x') + "\n"; + term->write(line.c_str(), line.size()); + term->update(); + } + + // 40 lines total. 24 on screen. 16 in history. + // Let's select Line 30 (which is on screen) + // Row 0 is Line 16. Row 14 is Line 30. + term->selstart(0, 14, 0); + term->selextend(1, 14, 1, 0); + + std::string sel = term->getSelection(); + EXPECT_FALSE(sel.empty()); + + term->resize(40, 24); + + EXPECT_TRUE(term->hasSelection()); + EXPECT_STDSTREQ(sel, term->getSelection()); +} + +UTEST(eterm, selection_rectangular) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + term->write("Line 1: ABCDEFG\r\n", 17); + term->write("Line 2: HIJKLMN\r\n", 17); + term->write("Line 3: OPQRSTU\r\n", 17); + term->update(); + + // Select "ABC", "HIJ", "OPQ" area + // "Line 1: " is 8 chars. A is at col 8. + term->selstart(8, 0, 0); + term->selextend(10, 2, 2, 0); // Type 2 = SEL_RECTANGULAR + + std::string sel = term->getSelection(); + EXPECT_STDSTREQ("ABC\nHIJ\nOPQ", sel); +} + +UTEST(eterm, selection_reverse) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + term->write("Line 1\r\nLine 2\r\nLine 3", 22); + term->update(); + + // Select from Line 3 to Line 1 + term->selstart(5, 2, 0); + term->selextend(0, 0, 1, 0); + + EXPECT_STDSTREQ("Line 1\nLine 2\nLine 3", term->getSelection()); +} + +UTEST(eterm, selection_wrap) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // Terminal is 80x24. + std::string longLine(80, 'A'); + longLine += "BBBB"; + term->write(longLine.c_str(), longLine.size()); + term->update(); + + // Selection should not have a newline at the wrap point + term->selstart(78, 0, 0); + term->selextend(2, 1, 1, 0); + + std::string sel = term->getSelection(); + EXPECT_STDSTREQ("AABBB", sel); +} + +UTEST(eterm, selection_snap_word) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + term->write("Hello World Test", 16); + term->update(); + + // Snap to "World" + // World starts at index 6 + term->selstart(7, 0, 1); // Type 1 = SNAP_WORD + term->selextend(7, 0, 1, 0); + + EXPECT_STDSTREQ("World", term->getSelection()); +} + +UTEST(eterm, selection_snap_line) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + term->write("Line 1\r\nLine 2\r\nLine 3", 22); + term->update(); + + // Snap to Line 2 + term->selstart(2, 1, 2); // Type 2 = SNAP_LINE + term->selextend(2, 1, 1, 0); + + EXPECT_STDSTREQ("Line 2\n", term->getSelection()); +} + +UTEST(eterm, selection_alt_screen) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + term->write("Main Screen", 11); + term->update(); + + // Switch to alt screen and reset cursor position to (0,0) + term->write("\033[?1049h\033[H", 11); + term->update(); + + term->write("Alt Screen", 10); + term->update(); + + term->selstart(0, 0, 0); + term->selextend(2, 0, 1, 0); + EXPECT_STDSTREQ("Alt", term->getSelection()); + + // Switch back to main + term->write("\033[?1049l", 8); + term->update(); + + // Selection should be cleared or at least not "Alt" + EXPECT_FALSE(term->hasSelection()); +} + +UTEST(eterm, selection_scrolling) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + term->write("Target Line\r\n", 13); + term->update(); + + // Select "Target" + term->selstart(0, 0, 0); + term->selextend(5, 0, 1, 0); + EXPECT_STDSTREQ("Target", term->getSelection()); + + // Push it into history by writing 30 lines + for (int i = 0; i < 30; ++i) { + term->write("New Line\r\n", 10); + } + term->update(); + + // Selection should have moved with the text + EXPECT_TRUE(term->hasSelection()); + EXPECT_STDSTREQ("Target", term->getSelection()); +} + +UTEST(eterm, selection_tabs) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // Default tab stop is 4 + term->write("A\tB", 3); + term->update(); + + // Select A[tab]B + // A is at 0, tab is at 1,2,3, B is at 4 + term->selstart(0, 0, 0); + term->selextend(4, 0, 1, 0); + + std::string sel = term->getSelection(); + EXPECT_STDSTREQ("A B", sel); +} + +UTEST(eterm, selection_unicode) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // Write some UTF-8 text: "Héllo Wörld" + // é is C3 A9, ö is C3 B6 + term->write("H\xC3\xA9llo W\xC3\xB6rld", 13); + term->update(); + + term->selstart(0, 0, 0); + term->selextend(10, 0, 1, 0); // Select "Héllo Wörld" + + EXPECT_STDSTREQ("Héllo Wörld", term->getSelection()); +} + +UTEST(eterm, selection_wide_chars) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // Unicode Emoji is often wide (2 columns) + // Rocket 🚀 is F0 9F 9A 80 + term->write("A\xF0\x9F\x9A\x80Z", 6); + term->update(); + + // A is at 0, 🚀 is at 1-2, Z is at 3 + term->selstart(0, 0, 0); + term->selextend(3, 0, 1, 0); + + EXPECT_STDSTREQ("A🚀Z", term->getSelection()); + + // Test selection starting/ending in the middle of a wide char + term->selstart(1, 0, 0); // Start at first half of rocket + term->selextend(2, 0, 1, 0); // End at second half + EXPECT_STDSTREQ("🚀", term->getSelection()); + + term->selstart(1, 0, 0); + term->selextend(1, 0, 1, 0); + EXPECT_STDSTREQ("🚀", term->getSelection()); +} + +UTEST(eterm, selection_reflow_extreme) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // Initial 80x24. Write a long line. + std::string text = "A VERY LONG LINE THAT WILL BE REFLOWED TO A NARROW TERMINAL"; + term->write(text.c_str(), text.size()); + term->update(); + + // Select "REFLOWED" + // text[30] to text[37] + term->selstart(30, 0, 0); + term->selextend(37, 0, 1, 0); + EXPECT_STDSTREQ("REFLOWED", term->getSelection()); + + // Shrink to 5 columns + term->resize(5, 24); + + // REFLOWED should still be selected + EXPECT_TRUE(term->hasSelection()); + EXPECT_STDSTREQ("REFLOWED", term->getSelection()); + + // Expand back to 80 columns + term->resize(80, 24); + EXPECT_STDSTREQ("REFLOWED", term->getSelection()); +} + +UTEST(eterm, selection_clear_screen) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + term->write("Test text", 9); + term->update(); + + term->selstart(0, 0, 0); + term->selextend(3, 0, 1, 0); + EXPECT_TRUE(term->hasSelection()); + + // CSI 2 J - Clear Screen + term->write("\033[2J", 4); + term->update(); + + EXPECT_FALSE(term->hasSelection()); +} + +UTEST(eterm, selection_scroll_region) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // Initial 80x24. Fill with some text. + for (int i = 0; i < 10; ++i) { + std::string line = "Line " + std::to_string(i) + "\r\n"; + term->write(line.c_str(), line.size()); + } + term->update(); + + // Select "Line 5" at Row 5. + term->selstart(0, 5, 0); + term->selextend(5, 5, 1, 0); + EXPECT_STDSTREQ("Line 5", term->getSelection()); + + // Set scrolling region: 3rd row to 8th row. (1-indexed CSI r) + term->write("\033[3;8r", 6); + // Move cursor to 8th row (bottom of scroll region) + term->write("\033[8;1H", 6); + // Write 2 more lines to push Row 5 up by 2 within the region. + term->write("Push 1\nPush 2\n", 14); + term->update(); + + // Line 5 was at Row 5. Within [3,8], it should move to Row 3. + // However, if it moves out of the region or something weird happens? + // Let's check where it is. + // Actually, tscrollup(top, n, copyhist) is used. + // In our case top=2, bot=7. n=2. + // Row 5 should move to 5-2 = 3. + EXPECT_STDSTREQ("Line 5", term->getSelection()); +} + +UTEST(eterm, selection_trailing_spaces) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // Terminal is 80 columns. + // Write "Hello " (3 spaces) then newline. + term->write("Hello \r\nWorld", 15); + term->update(); + + // Select both lines. + term->selstart(0, 0, 0); + term->selextend(4, 1, 1, 0); // "Hello" to "World" + + // Trailing spaces on the first line should be stripped because it's not wrapped. + EXPECT_STDSTREQ("Hello\nWorld", term->getSelection()); + + // Now test with WRAPPED line. + // Row 1 has "World" (5 chars). + // Write 73 'A's and 2 spaces to reach 80 chars. + std::string fill(73, 'A'); + term->write(fill.c_str(), fill.size()); + term->write(" ", 2); // Row 1 is now 80 chars: "World" + fill + " " + term->write("BB", 2); // This forces a wrap. Row 2 will be "BB". + term->update(); + + // Select Row 1 and Row 2. + // Row 1 starts at Col 0, Row 1. Row 2 starts at Col 0, Row 2. + term->selstart(0, 1, 0); + term->selextend(1, 2, 1, 0); // From "World" to "BB" + + // The spaces at the end of Row 1 should be preserved because it wrapped. + // "World" + fill + " " + "BB" + std::string expected = "World" + fill + " BB"; + EXPECT_STDSTREQ(expected, term->getSelection()); +} + +UTEST(eterm, selection_word_snap_unicode) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // Write "Héllo-Wörld" + // Word delimiters are only space ' ' and null 0 in current implementation. + // So "Héllo-Wörld" should be one word. + term->write("H\xC3\xA9llo-W\xC3\xB6rld", 14); + term->update(); + + // Snap to word starting at "ll" + term->selstart(2, 0, 1); // Index 2 is 'l' + term->selextend(2, 0, 1, 0); + + EXPECT_STDSTREQ("Héllo-Wörld", term->getSelection()); + + // Write "Test Wörld" + term->write("\r\nTest W\xC3\xB6rld", 13); + term->update(); + + // Snap to "Wörld" + term->selstart(6, 1, 1); // index 6 is 'W' + term->selextend(6, 1, 1, 0); + EXPECT_STDSTREQ("Wörld", term->getSelection()); +} + +UTEST(eterm, selection_history_screen_boundary) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + term->resize(80, 5); // 5 rows terminal + + // Write 5 lines. + for (int i = 0; i < 5; ++i) { + std::string line = "Line" + std::to_string(i) + "\r\n"; + term->write(line.c_str(), line.size()); + } + term->update(); + + // Now screen has Row 0 empty, Row 1 empty... Row 4 empty? + // Let's check. 5 rows: 0, 1, 2, 3, 4. + // Line0\r\n -> cursor at Row 1. + // Line1\r\n -> cursor at Row 2. + // Line2\r\n -> cursor at Row 3. + // Line3\r\n -> cursor at Row 4. + // Line4\r\n -> cursor at Row 5 -> scroll up. + // Row 0 has Line1, Row 1 has Line2, Row 2 has Line3, Row 3 has Line4. + // Row 4 is empty. History has Line0. + + // Let's select Line1 (Row 0) to Line4 (Row 3). + term->selstart(0, 0, 0); + term->selextend(4, 3, 1, 0); + EXPECT_TRUE(term->hasSelection()); + + // Scroll down 2 more lines. + term->write("New1\r\nNew2\r\n", 12); + term->update(); + + // Selection should have moved to history. + // Line1 was at Row 0, moved up by 2 -> Row -2. + // Line4 was at Row 3, moved up by 2 -> Row 1. + EXPECT_TRUE(term->hasSelection()); + std::string sel = term->getSelection(); + EXPECT_TRUE(sel.find("Line1") != std::string::npos); + EXPECT_TRUE(sel.find("Line4") != std::string::npos); +} + +UTEST(eterm, selection_basic_history) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + term->resize(80, 2); // 2 rows terminal: Row 0 and Row 1. + + term->write("Line0\r\n", 7); + term->write("Line1\r\n", 7); + term->write("Line2", 5); + term->update(); + + // Line0 is at Row -1 (history) + // Line1 is at Row 0 (screen) + // Line2 is at Row 1 (screen) + + // Select Line0 (history) and Line1 (screen). + term->selstart(0, -1, 0); + term->selextend(4, 0, 1, 0); + + EXPECT_TRUE(term->hasSelection()); + std::string sel = term->getSelection(); + EXPECT_TRUE(sel.find("Line0") != std::string::npos); + EXPECT_TRUE(sel.find("Line1") != std::string::npos); + EXPECT_TRUE(sel.find("Line2") == std::string::npos); +} + +UTEST(eterm, selection_rectangular_reflow) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // Initial 80x24. + term->write("ABCDE\r\n", 7); + term->write("FGHIJ\r\n", 7); + term->update(); + + // Select BC and GH (Rectangular) + // BC is at (1,0) to (2,0) + // GH is at (1,1) to (2,1) + term->selstart(1, 0, 0); + term->selextend(2, 1, 2, 0); // type 2 = Rectangular + + EXPECT_STDSTREQ("BC\nGH", term->getSelection()); + + // Resize to 5 columns. + term->resize(5, 24); + + // Rectangular selections should be preserved. + EXPECT_TRUE(term->hasSelection()); + EXPECT_STDSTREQ("BC\nGH", term->getSelection()); +} + +UTEST(eterm, selection_rectangular_resize_no_reflow) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // Initial 80x24. + term->write("ABCDE\r\n", 7); + term->write("FGHIJ\r\n", 7); + term->update(); + + // Select BC and GH (Rectangular) + term->selstart(1, 0, 0); + term->selextend(2, 1, 2, 0); // type 2 = Rectangular + + EXPECT_STDSTREQ("BC\nGH", term->getSelection()); + + // Resize to 90 columns (wider, no reflow needed) + term->resize(90, 24); + + // It should still be selected + EXPECT_TRUE(term->hasSelection()); + EXPECT_STDSTREQ("BC\nGH", term->getSelection()); +} diff --git a/src/tools/eterm/eterm.cpp b/src/tools/eterm/eterm.cpp index 223f28409..b4c5565f0 100644 --- a/src/tools/eterm/eterm.cpp +++ b/src/tools/eterm/eterm.cpp @@ -265,7 +265,7 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { WindowBackend::Default, 32, resPath + "icon/eterm.png", pixelDensityConf ? pixelDensityConf.Get() : currentDisplay->getPixelDensity() ), - ContextSettings( vsync.Get() ) ); + ContextSettings( vsync.Get(), benchmarkModeFlag.Get() ? 0 : maxFPS.Get() ) ); if ( win->isOpen() ) { win->setClearColor( RGB( 0, 0, 0 ) );