mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-08-11 17:17:42 +03:00
Terminal selection reflow fixes.
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 )
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
574
src/tests/unit_tests/eterm_test.cpp
Normal file
574
src/tests/unit_tests/eterm_test.cpp
Normal file
@@ -0,0 +1,574 @@
|
||||
#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 are currently cleared on resize.
|
||||
EXPECT_FALSE(term->hasSelection());
|
||||
EXPECT_STDSTREQ("", 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