diff --git a/premake4.lua b/premake4.lua index 4f307bf7d..20c93fd8f 100644 --- a/premake4.lua +++ b/premake4.lua @@ -1795,6 +1795,8 @@ solution "eepp" project "eepp-unit_tests" kind "ConsoleApp" targetdir("./bin/unit_tests") + links { "eterm-static", "languages-syntax-highlighting-static" } + includedirs { "src/modules/eterm/include/" } language "C++" files { "src/tests/unit_tests/*.cpp" } build_link_configuration( "eepp-unit_tests", true ) diff --git a/premake5.lua b/premake5.lua index 827aed538..124efc44e 100644 --- a/premake5.lua +++ b/premake5.lua @@ -1663,6 +1663,8 @@ workspace "eepp" project "eepp-unit_tests" kind "ConsoleApp" targetdir(_MAIN_SCRIPT_DIR .. "/bin/unit_tests") + links { "eterm-static", "languages-syntax-highlighting-static" } + incdirs { "src/modules/eterm/include/" } language "C++" files { "src/tests/unit_tests/*.cpp" } build_link_configuration( "eepp-unit_tests", true ) diff --git a/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp b/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp index 2e340f292..cfa0b00dc 100644 --- a/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp +++ b/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp @@ -554,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; @@ -576,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 ); } /* @@ -597,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; @@ -605,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 { @@ -875,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; @@ -909,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; @@ -966,7 +971,7 @@ void TerminalEmulator::tscrollup( int top, int n, int copyhist ) { mTerm.scr = mTerm.histlen; } - 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++ ) { @@ -1093,20 +1098,22 @@ void TerminalEmulator::historyReflow( int old_col, int new_col ) { } if ( has_sel ) { - if ( ob_logical_offset >= cursor && - ob_logical_offset < cursor + copy_width + - ( ( cursor + copy_width == logical_len ) ? 1 : 0 ) ) { + 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 + - ( ( cursor + copy_width == logical_len ) ? 1 : 0 ) ) { + 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 ); } - mSel.ob.x = eemin( ob_logical_offset - cursor, new_col - 1 ); - mSel.oe.x = eemin( oe_logical_offset - cursor, new_col - 1 ); } memcpy( nl, logical + cursor, copy_width * sizeof( TerminalGlyph ) ); @@ -1175,17 +1182,17 @@ void TerminalEmulator::selscroll( int top, int n ) { 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 || - mSel.oe.y > mTerm.bot ) { + 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(); } } diff --git a/src/tests/unit_tests/eterm_test.cpp b/src/tests/unit_tests/eterm_test.cpp new file mode 100644 index 000000000..9be10171d --- /dev/null +++ b/src/tests/unit_tests/eterm_test.cpp @@ -0,0 +1,574 @@ +#include +#include +#include +#include +#include "utest.hpp" + +using namespace eterm::Terminal; +using namespace eterm::System; + +class MockPty : public IPseudoTerminal { +public: + std::string mBuffer; + int mCols = 80; + int mRows = 24; + int getNumColumns() const override { return mCols; } + int getNumRows() const override { return mRows; } + bool resize(int columns, int rows) override { mCols = columns; mRows = rows; return true; } + bool isTTY() const override { return true; } + int write(const char* s, size_t n) override { + mBuffer.append(s, n); + return n; + } + int read(char* buf, size_t n, bool) override { + if (mBuffer.empty()) return 0; + size_t toRead = std::min(n, mBuffer.size()); + memcpy(buf, mBuffer.data(), toRead); + mBuffer.erase(0, toRead); + return toRead; + } +}; + +class MockProcess : public IProcess { +public: + void checkExitStatus() override {} + bool hasExited() const override { return false; } + int getExitCode() const override { return 0; } + void terminate() override {} + void waitForExit() override {} + int pid() override { return 123; } +}; + +class MockDisplay : public ITerminalDisplay { +public: + bool drawBegin(Uint32, Uint32) override { return true; } + void drawLine(Line, int, int, int) override {} + void drawCursor(int, int, TerminalGlyph, int, int, TerminalGlyph) override {} + void drawEnd() override {} +}; + +UTEST(eterm, basic_write) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + term->write("ABC", 3); + term->update(); + + term->selstart(0, 0, 0); + term->selextend(2, 0, 1, 0); + EXPECT_TRUE(term->hasSelection()); + EXPECT_STDSTREQ("ABC", term->getSelection()); +} + +UTEST(eterm, selection_reflow) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // 80x24. Write 80 'A's then 80 'B's. + std::string row0(80, 'A'); + std::string row1(80, 'B'); + term->write(row0.c_str(), row0.size()); + term->write(row1.c_str(), row1.size()); + term->write(" ", 1); // Trigger wrap on row 1 to move cursor to row 2 and preserve row 0 wrap + term->update(); + + // Selection from index 70 of row 0 to index 10 of row 1. + term->selstart(70, 0, 0); + term->selextend(10, 1, 1, 0); + + // ATTR_WRAP is set on row 0, so no newline should be added between A and B. + std::string expected = std::string(10, 'A') + std::string(11, 'B'); + std::string sel = term->getSelection(); + EXPECT_STDSTREQ(expected, sel); + + // Resize to 40 columns + term->resize(40, 24); + + EXPECT_TRUE(term->hasSelection()); + EXPECT_STDSTREQ(expected, term->getSelection()); +} + +UTEST(eterm, selection_reflow_history) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // Fill history with unique lines, each 40 chars to ensure they fit in 80. + for (int i = 0; i < 40; ++i) { + std::string line = "H" + std::to_string(i) + " " + std::string(30, 'x') + "\n"; + term->write(line.c_str(), line.size()); + term->update(); + } + + // 40 lines total. 24 on screen. 16 in history. + // Let's select Line 30 (which is on screen) + // Row 0 is Line 16. Row 14 is Line 30. + term->selstart(0, 14, 0); + term->selextend(1, 14, 1, 0); + + std::string sel = term->getSelection(); + EXPECT_FALSE(sel.empty()); + + term->resize(40, 24); + + EXPECT_TRUE(term->hasSelection()); + EXPECT_STDSTREQ(sel, term->getSelection()); +} + +UTEST(eterm, selection_rectangular) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + term->write("Line 1: ABCDEFG\r\n", 17); + term->write("Line 2: HIJKLMN\r\n", 17); + term->write("Line 3: OPQRSTU\r\n", 17); + term->update(); + + // Select "ABC", "HIJ", "OPQ" area + // "Line 1: " is 8 chars. A is at col 8. + term->selstart(8, 0, 0); + term->selextend(10, 2, 2, 0); // Type 2 = SEL_RECTANGULAR + + std::string sel = term->getSelection(); + EXPECT_STDSTREQ("ABC\nHIJ\nOPQ", sel); +} + +UTEST(eterm, selection_reverse) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + term->write("Line 1\r\nLine 2\r\nLine 3", 22); + term->update(); + + // Select from Line 3 to Line 1 + term->selstart(5, 2, 0); + term->selextend(0, 0, 1, 0); + + EXPECT_STDSTREQ("Line 1\nLine 2\nLine 3", term->getSelection()); +} + +UTEST(eterm, selection_wrap) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // Terminal is 80x24. + std::string longLine(80, 'A'); + longLine += "BBBB"; + term->write(longLine.c_str(), longLine.size()); + term->update(); + + // Selection should not have a newline at the wrap point + term->selstart(78, 0, 0); + term->selextend(2, 1, 1, 0); + + std::string sel = term->getSelection(); + EXPECT_STDSTREQ("AABBB", sel); +} + +UTEST(eterm, selection_snap_word) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + term->write("Hello World Test", 16); + term->update(); + + // Snap to "World" + // World starts at index 6 + term->selstart(7, 0, 1); // Type 1 = SNAP_WORD + term->selextend(7, 0, 1, 0); + + EXPECT_STDSTREQ("World", term->getSelection()); +} + +UTEST(eterm, selection_snap_line) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + term->write("Line 1\r\nLine 2\r\nLine 3", 22); + term->update(); + + // Snap to Line 2 + term->selstart(2, 1, 2); // Type 2 = SNAP_LINE + term->selextend(2, 1, 1, 0); + + EXPECT_STDSTREQ("Line 2\n", term->getSelection()); +} + +UTEST(eterm, selection_alt_screen) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + term->write("Main Screen", 11); + term->update(); + + // Switch to alt screen and reset cursor position to (0,0) + term->write("\033[?1049h\033[H", 11); + term->update(); + + term->write("Alt Screen", 10); + term->update(); + + term->selstart(0, 0, 0); + term->selextend(2, 0, 1, 0); + EXPECT_STDSTREQ("Alt", term->getSelection()); + + // Switch back to main + term->write("\033[?1049l", 8); + term->update(); + + // Selection should be cleared or at least not "Alt" + EXPECT_FALSE(term->hasSelection()); +} + +UTEST(eterm, selection_scrolling) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + term->write("Target Line\r\n", 13); + term->update(); + + // Select "Target" + term->selstart(0, 0, 0); + term->selextend(5, 0, 1, 0); + EXPECT_STDSTREQ("Target", term->getSelection()); + + // Push it into history by writing 30 lines + for (int i = 0; i < 30; ++i) { + term->write("New Line\r\n", 10); + } + term->update(); + + // Selection should have moved with the text + EXPECT_TRUE(term->hasSelection()); + EXPECT_STDSTREQ("Target", term->getSelection()); +} + +UTEST(eterm, selection_tabs) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // Default tab stop is 4 + term->write("A\tB", 3); + term->update(); + + // Select A[tab]B + // A is at 0, tab is at 1,2,3, B is at 4 + term->selstart(0, 0, 0); + term->selextend(4, 0, 1, 0); + + std::string sel = term->getSelection(); + EXPECT_STDSTREQ("A B", sel); +} + +UTEST(eterm, selection_unicode) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // Write some UTF-8 text: "Héllo Wörld" + // é is C3 A9, ö is C3 B6 + term->write("H\xC3\xA9llo W\xC3\xB6rld", 13); + term->update(); + + term->selstart(0, 0, 0); + term->selextend(10, 0, 1, 0); // Select "Héllo Wörld" + + EXPECT_STDSTREQ("Héllo Wörld", term->getSelection()); +} + +UTEST(eterm, selection_wide_chars) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // Unicode Emoji is often wide (2 columns) + // Rocket 🚀 is F0 9F 9A 80 + term->write("A\xF0\x9F\x9A\x80Z", 6); + term->update(); + + // A is at 0, 🚀 is at 1-2, Z is at 3 + term->selstart(0, 0, 0); + term->selextend(3, 0, 1, 0); + + EXPECT_STDSTREQ("A🚀Z", term->getSelection()); + + // Test selection starting/ending in the middle of a wide char + term->selstart(1, 0, 0); // Start at first half of rocket + term->selextend(2, 0, 1, 0); // End at second half + EXPECT_STDSTREQ("🚀", term->getSelection()); + + term->selstart(1, 0, 0); + term->selextend(1, 0, 1, 0); + EXPECT_STDSTREQ("🚀", term->getSelection()); +} + +UTEST(eterm, selection_reflow_extreme) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // Initial 80x24. Write a long line. + std::string text = "A VERY LONG LINE THAT WILL BE REFLOWED TO A NARROW TERMINAL"; + term->write(text.c_str(), text.size()); + term->update(); + + // Select "REFLOWED" + // text[30] to text[37] + term->selstart(30, 0, 0); + term->selextend(37, 0, 1, 0); + EXPECT_STDSTREQ("REFLOWED", term->getSelection()); + + // Shrink to 5 columns + term->resize(5, 24); + + // REFLOWED should still be selected + EXPECT_TRUE(term->hasSelection()); + EXPECT_STDSTREQ("REFLOWED", term->getSelection()); + + // Expand back to 80 columns + term->resize(80, 24); + EXPECT_STDSTREQ("REFLOWED", term->getSelection()); +} + +UTEST(eterm, selection_clear_screen) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + term->write("Test text", 9); + term->update(); + + term->selstart(0, 0, 0); + term->selextend(3, 0, 1, 0); + EXPECT_TRUE(term->hasSelection()); + + // CSI 2 J - Clear Screen + term->write("\033[2J", 4); + term->update(); + + EXPECT_FALSE(term->hasSelection()); +} + +UTEST(eterm, selection_scroll_region) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // Initial 80x24. Fill with some text. + for (int i = 0; i < 10; ++i) { + std::string line = "Line " + std::to_string(i) + "\r\n"; + term->write(line.c_str(), line.size()); + } + term->update(); + + // Select "Line 5" at Row 5. + term->selstart(0, 5, 0); + term->selextend(5, 5, 1, 0); + EXPECT_STDSTREQ("Line 5", term->getSelection()); + + // Set scrolling region: 3rd row to 8th row. (1-indexed CSI r) + term->write("\033[3;8r", 6); + // Move cursor to 8th row (bottom of scroll region) + term->write("\033[8;1H", 6); + // Write 2 more lines to push Row 5 up by 2 within the region. + term->write("Push 1\nPush 2\n", 14); + term->update(); + + // Line 5 was at Row 5. Within [3,8], it should move to Row 3. + // However, if it moves out of the region or something weird happens? + // Let's check where it is. + // Actually, tscrollup(top, n, copyhist) is used. + // In our case top=2, bot=7. n=2. + // Row 5 should move to 5-2 = 3. + EXPECT_STDSTREQ("Line 5", term->getSelection()); +} + +UTEST(eterm, selection_trailing_spaces) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // Terminal is 80 columns. + // Write "Hello " (3 spaces) then newline. + term->write("Hello \r\nWorld", 15); + term->update(); + + // Select both lines. + term->selstart(0, 0, 0); + term->selextend(4, 1, 1, 0); // "Hello" to "World" + + // Trailing spaces on the first line should be stripped because it's not wrapped. + EXPECT_STDSTREQ("Hello\nWorld", term->getSelection()); + + // Now test with WRAPPED line. + // Row 1 has "World" (5 chars). + // Write 73 'A's and 2 spaces to reach 80 chars. + std::string fill(73, 'A'); + term->write(fill.c_str(), fill.size()); + term->write(" ", 2); // Row 1 is now 80 chars: "World" + fill + " " + term->write("BB", 2); // This forces a wrap. Row 2 will be "BB". + term->update(); + + // Select Row 1 and Row 2. + // Row 1 starts at Col 0, Row 1. Row 2 starts at Col 0, Row 2. + term->selstart(0, 1, 0); + term->selextend(1, 2, 1, 0); // From "World" to "BB" + + // The spaces at the end of Row 1 should be preserved because it wrapped. + // "World" + fill + " " + "BB" + std::string expected = "World" + fill + " BB"; + EXPECT_STDSTREQ(expected, term->getSelection()); +} + +UTEST(eterm, selection_word_snap_unicode) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // Write "Héllo-Wörld" + // Word delimiters are only space ' ' and null 0 in current implementation. + // So "Héllo-Wörld" should be one word. + term->write("H\xC3\xA9llo-W\xC3\xB6rld", 14); + term->update(); + + // Snap to word starting at "ll" + term->selstart(2, 0, 1); // Index 2 is 'l' + term->selextend(2, 0, 1, 0); + + EXPECT_STDSTREQ("Héllo-Wörld", term->getSelection()); + + // Write "Test Wörld" + term->write("\r\nTest W\xC3\xB6rld", 13); + term->update(); + + // Snap to "Wörld" + term->selstart(6, 1, 1); // index 6 is 'W' + term->selextend(6, 1, 1, 0); + EXPECT_STDSTREQ("Wörld", term->getSelection()); +} + +UTEST(eterm, selection_history_screen_boundary) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + term->resize(80, 5); // 5 rows terminal + + // Write 5 lines. + for (int i = 0; i < 5; ++i) { + std::string line = "Line" + std::to_string(i) + "\r\n"; + term->write(line.c_str(), line.size()); + } + term->update(); + + // Now screen has Row 0 empty, Row 1 empty... Row 4 empty? + // Let's check. 5 rows: 0, 1, 2, 3, 4. + // Line0\r\n -> cursor at Row 1. + // Line1\r\n -> cursor at Row 2. + // Line2\r\n -> cursor at Row 3. + // Line3\r\n -> cursor at Row 4. + // Line4\r\n -> cursor at Row 5 -> scroll up. + // Row 0 has Line1, Row 1 has Line2, Row 2 has Line3, Row 3 has Line4. + // Row 4 is empty. History has Line0. + + // Let's select Line1 (Row 0) to Line4 (Row 3). + term->selstart(0, 0, 0); + term->selextend(4, 3, 1, 0); + EXPECT_TRUE(term->hasSelection()); + + // Scroll down 2 more lines. + term->write("New1\r\nNew2\r\n", 12); + term->update(); + + // Selection should have moved to history. + // Line1 was at Row 0, moved up by 2 -> Row -2. + // Line4 was at Row 3, moved up by 2 -> Row 1. + EXPECT_TRUE(term->hasSelection()); + std::string sel = term->getSelection(); + EXPECT_TRUE(sel.find("Line1") != std::string::npos); + EXPECT_TRUE(sel.find("Line4") != std::string::npos); +} + +UTEST(eterm, selection_basic_history) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + term->resize(80, 2); // 2 rows terminal: Row 0 and Row 1. + + term->write("Line0\r\n", 7); + term->write("Line1\r\n", 7); + term->write("Line2", 5); + term->update(); + + // Line0 is at Row -1 (history) + // Line1 is at Row 0 (screen) + // Line2 is at Row 1 (screen) + + // Select Line0 (history) and Line1 (screen). + term->selstart(0, -1, 0); + term->selextend(4, 0, 1, 0); + + EXPECT_TRUE(term->hasSelection()); + std::string sel = term->getSelection(); + EXPECT_TRUE(sel.find("Line0") != std::string::npos); + EXPECT_TRUE(sel.find("Line1") != std::string::npos); + EXPECT_TRUE(sel.find("Line2") == std::string::npos); +} + +UTEST(eterm, selection_rectangular_reflow) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create(std::move(pty), std::move(process), display, 100); + + // Initial 80x24. + term->write("ABCDE\r\n", 7); + term->write("FGHIJ\r\n", 7); + term->update(); + + // Select BC and GH (Rectangular) + // BC is at (1,0) to (2,0) + // GH is at (1,1) to (2,1) + term->selstart(1, 0, 0); + term->selextend(2, 1, 2, 0); // type 2 = Rectangular + + EXPECT_STDSTREQ("BC\nGH", term->getSelection()); + + // Resize to 5 columns. + term->resize(5, 24); + + // Rectangular selections are currently cleared on resize. + EXPECT_FALSE(term->hasSelection()); + EXPECT_STDSTREQ("", term->getSelection()); +} diff --git a/src/tools/eterm/eterm.cpp b/src/tools/eterm/eterm.cpp index 223f28409..b4c5565f0 100644 --- a/src/tools/eterm/eterm.cpp +++ b/src/tools/eterm/eterm.cpp @@ -265,7 +265,7 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { WindowBackend::Default, 32, resPath + "icon/eterm.png", pixelDensityConf ? pixelDensityConf.Get() : currentDisplay->getPixelDensity() ), - ContextSettings( vsync.Get() ) ); + ContextSettings( vsync.Get(), benchmarkModeFlag.Get() ? 0 : maxFPS.Get() ) ); if ( win->isOpen() ) { win->setClearColor( RGB( 0, 0, 0 ) );