mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-05-28 17:16:29 +03:00
Merge branch 'feature/terminal-reflow' into develop
This commit is contained in:
@@ -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 )
|
||||
|
||||
@@ -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 )
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
#include <eepp/math/vector2.hpp>
|
||||
#include <eepp/system/clock.hpp>
|
||||
#include <eepp/window/keycodes.hpp>
|
||||
#include <eterm/system/iprocess.hpp>
|
||||
#include <eterm/terminal/ipseudoterminal.hpp>
|
||||
@@ -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<std::string> 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 );
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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 <n> 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 <n> 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 ) {
|
||||
|
||||
599
src/tests/unit_tests/eterm_test.cpp
Normal file
599
src/tests/unit_tests/eterm_test.cpp
Normal file
@@ -0,0 +1,599 @@
|
||||
#include <eterm/terminal/terminalemulator.hpp>
|
||||
#include <eterm/terminal/iterminaldisplay.hpp>
|
||||
#include <eterm/terminal/ipseudoterminal.hpp>
|
||||
#include <eterm/system/iprocess.hpp>
|
||||
#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<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
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<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
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<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
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<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
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<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
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<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
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<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
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<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
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<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
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<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
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<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
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<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
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<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
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<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
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<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
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<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
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<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
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<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
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<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
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<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
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<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
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<MockPty>();
|
||||
auto process = std::make_unique<MockProcess>();
|
||||
auto display = std::make_shared<MockDisplay>();
|
||||
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());
|
||||
}
|
||||
@@ -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 ) );
|
||||
|
||||
Reference in New Issue
Block a user