Terminal core (src/core, QtCore only, headless testable): - VT100/xterm parser: CSI/OSC/DCS, SGR 256/truecolor, scroll regions, insert/delete, DEC special graphics, alternate screen - cell grid with 5000 line scrollback, CJK wide chars, combining marks - xterm 256 color palette Terminal widget (src/term): - QPainter renderer with run batching, wide char clipping, cursor blink - mouse selection (char/word/line), auto copy, middle click paste - full key encoding: arrows + DECCKM, F1-F12, modifiers, ctrl/alt - IME support via inputMethodEvent Transports (src/session): - SSH through the system OpenSSH client on a forkpty pseudo terminal, so agent, ProxyJump, known_hosts and 2FA keep working - Telnet with IAC negotiation (NAWS, ECHO, SGA) - Serial via QSerialPort UI (src/ui): - tabbed main window, session dialog with saved profiles - dark violet theme, CLI targets (user@host:port, telnet://, serial:) Tests: - tests/test_core: escape sequence and layout assertions - tests/test_widget: offscreen render checks and key encoding
232 lines
5.7 KiB
C++
232 lines
5.7 KiB
C++
// Minimal self checks for the terminal core (no GUI required).
|
|
#include <QByteArray>
|
|
#include <QString>
|
|
#include <cstdio>
|
|
|
|
#include "core/Screen.h"
|
|
#include "core/VtParser.h"
|
|
|
|
using namespace vx;
|
|
|
|
static int g_failures = 0;
|
|
|
|
#define CHECK(cond) \
|
|
do { \
|
|
if (!(cond)) { \
|
|
std::printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); \
|
|
++g_failures; \
|
|
} \
|
|
} while (0)
|
|
|
|
static void checkEq(const QString &got, const QString &want, const char *what, int line)
|
|
{
|
|
if (got != want) {
|
|
std::printf("FAIL %s:%d %s\n got: [%s]\n exp: [%s]\n",
|
|
__FILE__, line, what, qPrintable(got), qPrintable(want));
|
|
++g_failures;
|
|
}
|
|
}
|
|
#define CHECK_EQ(got, want) checkEq((got), (want), #got, __LINE__)
|
|
|
|
struct Fixture {
|
|
Screen screen;
|
|
VtParser parser;
|
|
|
|
explicit Fixture(int cols = 20, int rows = 5)
|
|
: parser(&screen)
|
|
{
|
|
screen.initialize(cols, rows);
|
|
}
|
|
|
|
void feed(const char *text) { parser.feed(QByteArray(text)); }
|
|
|
|
const Cell *row(int r) const { return screen.lineData(screen.historyCount() + r); }
|
|
|
|
QString dump() const
|
|
{
|
|
QString out;
|
|
for (int r = 0; r < screen.rows(); ++r) {
|
|
const Cell *cells = row(r);
|
|
QString line;
|
|
for (int c = 0; c < screen.columns(); ++c) {
|
|
if (cells[c].isWideTail())
|
|
continue;
|
|
line += codePointToString(cells[c].ch);
|
|
line += screen.graphemeText(cells[c].extra);
|
|
}
|
|
while (line.endsWith(QLatin1Char(' ')))
|
|
line.chop(1);
|
|
out += line;
|
|
if (r + 1 < screen.rows())
|
|
out += QLatin1Char('\n');
|
|
}
|
|
while (out.endsWith(QLatin1Char('\n')))
|
|
out.chop(1);
|
|
return out;
|
|
}
|
|
};
|
|
|
|
static void testPlainText()
|
|
{
|
|
Fixture f;
|
|
f.feed("hello");
|
|
CHECK_EQ(f.dump(), QStringLiteral("hello"));
|
|
CHECK(f.screen.cursorCol() == 5);
|
|
CHECK(f.screen.cursorRow() == 0);
|
|
}
|
|
|
|
static void testNewline()
|
|
{
|
|
Fixture f;
|
|
f.feed("abc\r\ndef");
|
|
CHECK_EQ(f.dump(), QStringLiteral("abc\ndef"));
|
|
CHECK(f.screen.cursorRow() == 1);
|
|
CHECK(f.screen.cursorCol() == 3);
|
|
}
|
|
|
|
static void testCursorPosition()
|
|
{
|
|
Fixture f;
|
|
f.feed("\x1b[2;3HX");
|
|
CHECK_EQ(f.dump(), QStringLiteral("\n X"));
|
|
CHECK(f.screen.cursorRow() == 1);
|
|
CHECK(f.screen.cursorCol() == 3);
|
|
}
|
|
|
|
static void testSgrColors()
|
|
{
|
|
Fixture f;
|
|
f.feed("\x1b[1;31mR\x1b[0mN");
|
|
const Cell *cells = f.row(0);
|
|
CHECK(cells[0].fg == paletteColor(1));
|
|
CHECK((cells[0].attr & AttrBold) != 0);
|
|
CHECK(cells[1].fg == kColorDefault);
|
|
CHECK(cells[1].attr == 0);
|
|
}
|
|
|
|
static void testTrueColor()
|
|
{
|
|
Fixture f;
|
|
f.feed("\x1b[38;2;10;20;30mX");
|
|
CHECK(f.row(0)[0].fg == rgbColor(10, 20, 30));
|
|
}
|
|
|
|
static void testErase()
|
|
{
|
|
Fixture f;
|
|
f.feed("abc\r\ndef");
|
|
f.feed("\x1b[2J");
|
|
CHECK_EQ(f.dump(), QString());
|
|
}
|
|
|
|
static void testWideChars()
|
|
{
|
|
Fixture f;
|
|
f.feed("\xe4\xb8\xad\xe6\x96\x87");
|
|
const Cell *cells = f.row(0);
|
|
CHECK(cells[0].ch == 0x4E2D);
|
|
CHECK(cells[0].isWideHead());
|
|
CHECK(cells[1].isWideTail());
|
|
CHECK(cells[2].ch == 0x6587);
|
|
CHECK(f.screen.cursorCol() == 4);
|
|
CHECK(cells[0].attr & AttrWide);
|
|
}
|
|
|
|
static void testWrap()
|
|
{
|
|
Fixture f(20, 5);
|
|
QString text;
|
|
for (int i = 0; i < 25; ++i)
|
|
text += QLatin1Char('x');
|
|
f.feed(text.toUtf8().constData());
|
|
CHECK(f.screen.cursorRow() == 1);
|
|
CHECK(f.screen.cursorCol() == 5);
|
|
const Cell *cells = f.row(0);
|
|
CHECK(cells[19].ch == U'x');
|
|
CHECK(f.row(1)[4].ch == U'x');
|
|
}
|
|
|
|
static void testScrollback()
|
|
{
|
|
Fixture f(20, 5);
|
|
for (int i = 0; i < 10; ++i)
|
|
f.feed(QByteArray::number(i).append("\r\n").constData());
|
|
CHECK(f.screen.historyCount() == 6);
|
|
CHECK(f.screen.totalLines() == 11);
|
|
const Cell *first = f.screen.lineData(0);
|
|
CHECK(first[0].ch == U'0');
|
|
}
|
|
|
|
static void testAltScreen()
|
|
{
|
|
Fixture f;
|
|
f.feed("main");
|
|
f.feed("\x1b[?1049h");
|
|
f.feed("alt");
|
|
CHECK_EQ(f.dump(), QStringLiteral("alt"));
|
|
CHECK(f.screen.alternateScreen());
|
|
f.feed("\x1b[?1049l");
|
|
CHECK_EQ(f.dump(), QStringLiteral("main"));
|
|
CHECK(!f.screen.alternateScreen());
|
|
}
|
|
|
|
static void testDecGraphics()
|
|
{
|
|
Fixture f;
|
|
f.feed("\x1b(0q\x1b(Bq");
|
|
CHECK(f.row(0)[0].ch == 0x2500);
|
|
CHECK(f.row(0)[1].ch == U'q');
|
|
}
|
|
|
|
static void testInsertDeleteChars()
|
|
{
|
|
Fixture f;
|
|
f.feed("abcdef\x1b[1;3H");
|
|
f.feed("\x1b[2@");
|
|
CHECK_EQ(f.dump(), QStringLiteral("ab cdef"));
|
|
f.feed("\x1b[2P");
|
|
CHECK_EQ(f.dump(), QStringLiteral("abcdef"));
|
|
}
|
|
|
|
static void testScrollRegion()
|
|
{
|
|
Fixture f(10, 5);
|
|
f.feed("a\r\nb\r\nc\r\nd\r\ne");
|
|
f.feed("\x1b[2;4r");
|
|
f.feed("\x1b[4;1H\r\n");
|
|
// line 1 ("b") scrolled out of the region
|
|
CHECK(f.row(1)[0].ch == U'c');
|
|
CHECK(f.row(0)[0].ch == U'a');
|
|
}
|
|
|
|
static void testReverseIndex()
|
|
{
|
|
Fixture f(10, 4);
|
|
f.feed("\x1b[1;1H\x1bM");
|
|
CHECK_EQ(f.dump(), QString());
|
|
}
|
|
|
|
int main()
|
|
{
|
|
testPlainText();
|
|
testNewline();
|
|
testCursorPosition();
|
|
testSgrColors();
|
|
testTrueColor();
|
|
testErase();
|
|
testWideChars();
|
|
testWrap();
|
|
testScrollback();
|
|
testAltScreen();
|
|
testDecGraphics();
|
|
testInsertDeleteChars();
|
|
testScrollRegion();
|
|
testReverseIndex();
|
|
|
|
if (g_failures == 0)
|
|
std::printf("all core tests passed\n");
|
|
else
|
|
std::printf("%d core test(s) failed\n", g_failures);
|
|
return g_failures == 0 ? 0 : 1;
|
|
}
|