Files
liyepan 580abbcadf feat: VoiletXterm - Qt5 terminal workspace for SSH, Telnet and serial
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
2026-10-23 17:38:16 +08:00

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;
}