mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-09-30 02:00:27 +03:00
* Add fast paths for printable ASCII, row clearing, scrolling, history recycling, dirty tracking, and history width detection. * Use runtime-dispatched AVX2 and NEON scanning for trailing blank cells, increase the PTY read buffer, and bound PTY processing and presentation to keep sustained output responsive. * Preserve selection redraws, wide glyph cleanup, VT100 graphics handling, history reflow, and final buffered PTY output after process exit. * Add regression coverage for deferred redraws, saturated reads, process termination, history scrolling, wide glyphs, and graphics character sets.
887 lines
28 KiB
C++
887 lines
28 KiB
C++
#include "utest.hpp"
|
|
#include <eterm/system/iprocess.hpp>
|
|
#include <eterm/terminal/ipseudoterminal.hpp>
|
|
#include <eterm/terminal/iterminaldisplay.hpp>
|
|
#include <eterm/terminal/terminalemulator.hpp>
|
|
#include <limits>
|
|
|
|
using namespace eterm::Terminal;
|
|
using namespace eterm::System;
|
|
|
|
class MockPty : public IPseudoTerminal {
|
|
public:
|
|
std::string mBuffer;
|
|
size_t mMaxRead{ std::numeric_limits<size_t>::max() };
|
|
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(), mMaxRead } );
|
|
memcpy( buf, mBuffer.data(), toRead );
|
|
mBuffer.erase( 0, toRead );
|
|
return toRead;
|
|
}
|
|
};
|
|
|
|
class MockProcess : public IProcess {
|
|
public:
|
|
bool mExited{ false };
|
|
void checkExitStatus() override {}
|
|
bool hasExited() const override { return mExited; }
|
|
int getExitCode() const override { return 0; }
|
|
void terminate() override {}
|
|
void waitForExit() override {}
|
|
int pid() override { return 123; }
|
|
};
|
|
|
|
class MockDisplay : public ITerminalDisplay {
|
|
public:
|
|
int mDrawLines{ 0 };
|
|
uint32_t mFirstMode{ 0 };
|
|
uint32_t mSecondMode{ 0 };
|
|
bool drawBegin( Uint32, Uint32 ) override { return true; }
|
|
void drawLine( Line line, int, int, int ) override {
|
|
++mDrawLines;
|
|
mFirstMode = line[0].mode;
|
|
mSecondMode = line[1].mode;
|
|
}
|
|
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_redraw_while_idle ) {
|
|
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();
|
|
display->mDrawLines = 0;
|
|
|
|
term->selstart( 0, 0, 0 );
|
|
term->selextend( 2, 0, 1, 0 );
|
|
term->update();
|
|
|
|
EXPECT_TRUE( display->mDrawLines > 0 );
|
|
}
|
|
|
|
UTEST( eterm, saturated_read_defers_only_intermediate_draw ) {
|
|
auto pty = std::make_unique<MockPty>();
|
|
pty->mMaxRead = 1;
|
|
auto process = std::make_unique<MockProcess>();
|
|
auto display = std::make_shared<MockDisplay>();
|
|
auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 );
|
|
|
|
std::string output( 1024, 'A' );
|
|
term->write( output.data(), output.size() );
|
|
display->mDrawLines = 0;
|
|
|
|
EXPECT_FALSE( term->update() );
|
|
EXPECT_EQ( 0, display->mDrawLines );
|
|
EXPECT_TRUE( term->update() );
|
|
EXPECT_TRUE( display->mDrawLines > 0 );
|
|
}
|
|
|
|
UTEST( eterm, sustained_saturated_reads_present_periodically ) {
|
|
auto pty = std::make_unique<MockPty>();
|
|
pty->mMaxRead = 1;
|
|
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->update();
|
|
display->mDrawLines = 0;
|
|
std::string output( 33 * 1024, 'A' );
|
|
term->write( output.data(), output.size() );
|
|
|
|
for ( int batch = 0; batch < 31; ++batch ) {
|
|
EXPECT_FALSE( term->update() );
|
|
EXPECT_EQ( 0, display->mDrawLines );
|
|
}
|
|
EXPECT_FALSE( term->update() );
|
|
EXPECT_TRUE( display->mDrawLines > 0 );
|
|
}
|
|
|
|
UTEST( eterm, process_exit_drains_buffered_pty_output ) {
|
|
auto pty = std::make_unique<MockPty>();
|
|
pty->mMaxRead = 1;
|
|
auto process = std::make_unique<MockProcess>();
|
|
process->mExited = true;
|
|
auto display = std::make_shared<MockDisplay>();
|
|
auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 );
|
|
|
|
size_t bytesRead = 0;
|
|
term->setDataCb( [&bytesRead]( const char*, size_t size ) { bytesRead += size; } );
|
|
std::string output( 3 * 1024, 'A' );
|
|
term->write( output.data(), output.size() );
|
|
display->mDrawLines = 0;
|
|
|
|
EXPECT_FALSE( term->update() );
|
|
EXPECT_EQ( static_cast<size_t>( 1024 ), bytesRead );
|
|
while ( !term->update() ) {
|
|
}
|
|
|
|
EXPECT_EQ( output.size(), bytesRead );
|
|
EXPECT_TRUE( display->mDrawLines > 0 );
|
|
}
|
|
|
|
UTEST( eterm, repeated_deferred_scrolls_redraw_every_row ) {
|
|
auto pty = std::make_unique<MockPty>();
|
|
MockPty* mockPty = pty.get();
|
|
pty->mMaxRead = 1;
|
|
auto process = std::make_unique<MockProcess>();
|
|
auto display = std::make_shared<MockDisplay>();
|
|
auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 );
|
|
|
|
// Consume the initial full redraw so this burst starts with clean dirty flags.
|
|
term->update();
|
|
display->mDrawLines = 0;
|
|
|
|
for ( int line = 0; line < 200; ++line )
|
|
mockPty->mBuffer += "scrolling line " + std::to_string( line ) + "\r\n";
|
|
|
|
EXPECT_FALSE( term->update() );
|
|
EXPECT_EQ( 0, display->mDrawLines );
|
|
while ( !term->update() ) {
|
|
}
|
|
EXPECT_TRUE( display->mDrawLines >= 24 );
|
|
EXPECT_EQ( 0, display->mDrawLines % 24 );
|
|
}
|
|
|
|
UTEST( eterm, ascii_overwrite_clears_wide_glyph_state ) {
|
|
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( "\xF0\x9F\x9A\x80\rA", 6 );
|
|
term->update();
|
|
|
|
EXPECT_EQ( 0u, display->mFirstMode & ATTR_WIDE );
|
|
EXPECT_EQ( 0u, display->mSecondMode & ATTR_WDUMMY );
|
|
}
|
|
|
|
UTEST( eterm, ascii_respects_vt100_graphics_charset ) {
|
|
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( "\033(0q", 4 );
|
|
term->update();
|
|
term->selstart( 0, 0, 0 );
|
|
term->selextend( 0, 0, 1, 0 );
|
|
|
|
EXPECT_STDSTREQ( "─", 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() );
|
|
}
|
|
|
|
UTEST( eterm, scroll_position_after_resize ) {
|
|
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 history with enough lines to allow scrolling.
|
|
for ( int i = 0; i < 40; ++i ) {
|
|
std::string line = "Line " + std::to_string( i ) + "\r\n";
|
|
term->write( line.c_str(), line.size() );
|
|
term->update();
|
|
}
|
|
|
|
// Scroll up by 5 lines.
|
|
TerminalArg arg;
|
|
arg.i = 5;
|
|
term->kscrollup( &arg );
|
|
|
|
int expected_scroll = term->scrollPos();
|
|
EXPECT_EQ( 5, expected_scroll );
|
|
|
|
// Resize to something wider, meaning no reflow height change.
|
|
term->resize( 90, 24 );
|
|
|
|
// Scroll position should be preserved.
|
|
EXPECT_EQ( 5, term->scrollPos() );
|
|
}
|
|
|
|
UTEST( eterm, scroll_position_after_ttyread ) {
|
|
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 history with enough lines to allow scrolling.
|
|
for ( int i = 0; i < 40; ++i ) {
|
|
std::string line = "Line " + std::to_string( i ) + "\r\n";
|
|
term->write( line.c_str(), line.size() );
|
|
term->update();
|
|
}
|
|
|
|
// Check initial state
|
|
EXPECT_EQ( 0, term->scrollPos() );
|
|
|
|
// Scroll up by 5 lines.
|
|
TerminalArg arg;
|
|
arg.i = 5;
|
|
term->kscrollup( &arg );
|
|
EXPECT_EQ( 5, term->scrollPos() );
|
|
|
|
// Write a carriage return and new line, which should add to history and push screen up.
|
|
term->write( "New output\r\n", 12 );
|
|
term->update();
|
|
|
|
// The scroll position should be updated to maintain the viewport, so it should be 6.
|
|
EXPECT_EQ( 6, term->scrollPos() );
|
|
|
|
// Let's verify that the history content hasn't been corrupted.
|
|
// If mTerm.scr was > 0 during twrite, and it wasn't handled correctly,
|
|
// "New output" might have overwritten a history line instead of the screen bottom.
|
|
|
|
// We added 40 lines (Line 0 to Line 39). Plus "New output". Total 41 lines.
|
|
// Screen holds 24 lines.
|
|
// Line 39 is on row 22. Row 23 is empty.
|
|
// We scroll up 5 lines. So we are looking at 5 lines back.
|
|
// Visual row 23 was "Line 35".
|
|
// After adding "New output\r\n", 1 line is pushed to history.
|
|
// The scroll position increases to 6, maintaining the viewport.
|
|
// So visual row 23 should still be "Line 35".
|
|
term->selstart( 0, 23, 0 );
|
|
term->selextend( 6, 23, 1, 0 );
|
|
EXPECT_STDSTREQ( "Line 35", term->getSelection() );
|
|
|
|
// Scroll down to 0 to verify "New output" is indeed at the bottom.
|
|
TerminalArg arg_down;
|
|
arg_down.i = 6;
|
|
term->kscrolldown( &arg_down );
|
|
EXPECT_EQ( 0, term->scrollPos() );
|
|
|
|
term->selstart( 0, 22, 0 ); // "New output" is at row 22 because of \r\n
|
|
term->selextend( 9, 22, 1, 0 );
|
|
EXPECT_STDSTREQ( "New output", term->getSelection() );
|
|
}
|
|
|
|
UTEST( eterm, history_corruption_on_resize ) {
|
|
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, 1000 );
|
|
|
|
std::vector<std::string> expected_lines;
|
|
|
|
// Initial 80x24. Fill history with enough lines to allow scrolling.
|
|
for ( int i = 0; i < 500; ++i ) {
|
|
std::string line = "Line " + std::to_string( i );
|
|
expected_lines.push_back( line );
|
|
std::string write_str = line + "\r\n";
|
|
term->write( write_str.c_str(), write_str.size() );
|
|
term->update();
|
|
}
|
|
|
|
// Check initial state
|
|
EXPECT_EQ( 0, term->scrollPos() );
|
|
|
|
// Scroll up to center. (500 lines total. 24 on screen. history is 476. scroll 250)
|
|
TerminalArg arg;
|
|
arg.i = 250;
|
|
term->kscrollup( &arg );
|
|
EXPECT_EQ( 250, term->scrollPos() );
|
|
|
|
// Expand the terminal quite a bit (from 24 rows to 50 rows)
|
|
term->resize( 80, 50 );
|
|
|
|
// Let's verify EVERY SINGLE LINE in the buffer.
|
|
arg.i = 1000;
|
|
term->kscrolldown( &arg ); // Scroll to bottom
|
|
EXPECT_EQ( 0, term->scrollPos() );
|
|
|
|
int history_len = term->scrollSize();
|
|
int screen_len = term->rowCount();
|
|
int total_len = history_len + screen_len;
|
|
|
|
int start_idx = 500 - total_len;
|
|
if ( start_idx < 0 )
|
|
start_idx = 0; // If some lines are empty
|
|
|
|
for ( int y = 0; y < total_len; ++y ) {
|
|
int expected_idx = start_idx + y;
|
|
if ( expected_idx >= 500 )
|
|
continue; // Don't check empty lines at the very bottom
|
|
|
|
term->selstart( 0, y - history_len, 0 );
|
|
term->selextend( 80, y - history_len, 1, 0 ); // Need to select entire row up to length
|
|
|
|
std::string sel = term->getSelection();
|
|
// Remove trailing newlines/spaces
|
|
while ( !sel.empty() &&
|
|
( sel.back() == '\n' || sel.back() == '\r' || sel.back() == ' ' ) ) {
|
|
sel.pop_back();
|
|
}
|
|
|
|
EXPECT_STDSTREQ( expected_lines[expected_idx], sel );
|
|
}
|
|
}
|