test sweep: cover Terminal orchestrator (T-91)
Adds test/terminal/terminal_test.dart — 54 unit tests covering the
`Terminal` class as a pure-Dart orchestrator: construction +
TerminalState defaults, the Observable mixin, write/writeChar,
keyInput / charInput / textInput / paste (with bracketed-paste +
ctrl/alt encodings, including macOS reservation), mouseInput
gating, resize (clamping + onResize callback + alt-buffer
scrollback clear), buffer switching (use{Alt,Main}Buffer +
clearAltBuffer), every SBC handler (bell / backspace / lineFeed /
CR / SO / SI / unknown), tab-stop manipulation (tab jump +
saturation, clearTabStopUnderCursor, clearAllTabStops, setTapStop),
every ANSI escape handler (save/restore cursor, index, nextLine,
reverseIndex, designateCharset), CSI cursor + erase + line/char
insert/delete + scroll + repeatPreviousCharacter (incl. no-op when
no preceding char), device-attribute and status reports, every
mode setter mirroring into its getter, every SGR set/unset attr +
colour setter, OSC handlers (setTitle / setIconName / unknownOSC),
and all the documented no-op fallbacks (unknownSBC, unkownEscape,
unknownCSI, setUnknownMode, setUnknownDecMode, setColumnMode,
unsupportedStyle).
Also fixes a real production bug surfaced while writing tests:
`BufferLine.eraseRange(0, 0, ...)` panicked with a `RangeError`
because the right-side wide-char guard read `_data[-1]` via
`getWidth(end - 1)` when `end == 0`. The left guard already had a
`start > 0` check; the right guard was missing the symmetric
`end > 0`. Real trigger path: `Terminal.eraseDisplayAbove`
(`ESC[1J`) with the cursor at column 0 — common after `ESC[H\x1b[1J`
home-then-erase-above sequences that many TUIs emit on redraw.
Regression test added in line_test.dart.
Coverage delta:
- terminal.dart: 0/283 → 291/291 (file grew by 8 LF for the
fix's comment lines).
- base/observable.dart: 0/7 → 7/7 (covered transitively via
Terminal's listener tests).
- Total project: 56.40% → 59.82%; coverage_floor bumped 56 → 59.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+107
-107
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"exported_at": "2026-05-07T06:13:07Z",
|
||||
"exported_at": "2026-05-07T06:21:18Z",
|
||||
"decisions": [
|
||||
{
|
||||
"id": "D-1",
|
||||
@@ -9,7 +9,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-20",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-3",
|
||||
@@ -19,7 +19,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-20",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-4",
|
||||
@@ -29,7 +29,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-20",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-5",
|
||||
@@ -39,7 +39,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-20",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-6",
|
||||
@@ -49,7 +49,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-20",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-7",
|
||||
@@ -59,7 +59,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-8",
|
||||
@@ -69,7 +69,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-9",
|
||||
@@ -79,7 +79,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-10",
|
||||
@@ -89,7 +89,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-11",
|
||||
@@ -99,7 +99,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-12",
|
||||
@@ -109,7 +109,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-13",
|
||||
@@ -119,7 +119,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-14",
|
||||
@@ -129,7 +129,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-15",
|
||||
@@ -139,7 +139,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/extensions.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-16",
|
||||
@@ -149,7 +149,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/extensions.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-17",
|
||||
@@ -159,7 +159,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/extensions.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-18",
|
||||
@@ -169,7 +169,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/extensions.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-19",
|
||||
@@ -179,7 +179,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/extensions.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-20",
|
||||
@@ -189,7 +189,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/accessibility.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-21",
|
||||
@@ -199,7 +199,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/accessibility.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-22",
|
||||
@@ -209,7 +209,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/accessibility.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-23",
|
||||
@@ -219,7 +219,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-24",
|
||||
@@ -229,7 +229,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-25",
|
||||
@@ -239,7 +239,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-26",
|
||||
@@ -249,7 +249,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-27",
|
||||
@@ -259,7 +259,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-28",
|
||||
@@ -269,7 +269,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-29",
|
||||
@@ -279,7 +279,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-30",
|
||||
@@ -289,7 +289,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-31",
|
||||
@@ -299,7 +299,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-32",
|
||||
@@ -309,7 +309,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-33",
|
||||
@@ -319,7 +319,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-34",
|
||||
@@ -329,7 +329,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/process.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-35",
|
||||
@@ -339,7 +339,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/process.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-36",
|
||||
@@ -349,7 +349,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/process.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-37",
|
||||
@@ -359,7 +359,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/process.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-38",
|
||||
@@ -369,7 +369,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/process.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-39",
|
||||
@@ -379,7 +379,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/process.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-40",
|
||||
@@ -389,7 +389,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/process.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-41",
|
||||
@@ -399,7 +399,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-42",
|
||||
@@ -409,7 +409,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-43",
|
||||
@@ -419,7 +419,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-44",
|
||||
@@ -429,7 +429,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-45",
|
||||
@@ -439,7 +439,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-46",
|
||||
@@ -449,7 +449,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/extensions.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-47",
|
||||
@@ -459,7 +459,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-48",
|
||||
@@ -469,7 +469,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-49",
|
||||
@@ -479,7 +479,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-50",
|
||||
@@ -489,7 +489,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-51",
|
||||
@@ -499,7 +499,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-52",
|
||||
@@ -509,7 +509,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-53",
|
||||
@@ -519,7 +519,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-54",
|
||||
@@ -529,7 +529,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-55",
|
||||
@@ -539,7 +539,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-23",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-56",
|
||||
@@ -549,7 +549,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-23",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-57",
|
||||
@@ -559,7 +559,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-23",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-58",
|
||||
@@ -569,7 +569,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-23",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-59",
|
||||
@@ -579,7 +579,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-25",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-60",
|
||||
@@ -589,7 +589,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-26",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-61",
|
||||
@@ -599,7 +599,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-26",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-62",
|
||||
@@ -609,7 +609,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-26",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-63",
|
||||
@@ -619,7 +619,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-26",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-64",
|
||||
@@ -629,7 +629,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-05-03",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-65",
|
||||
@@ -639,7 +639,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-26",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "D-66",
|
||||
@@ -649,7 +649,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-05-06",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-1",
|
||||
@@ -658,7 +658,7 @@
|
||||
"title": "Authorisation granularity on the IPC socket",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-2",
|
||||
@@ -667,7 +667,7 @@
|
||||
"title": "Back-pressure on event streams",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-3",
|
||||
@@ -676,7 +676,7 @@
|
||||
"title": "Event persistence + audit/undo",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-4",
|
||||
@@ -685,7 +685,7 @@
|
||||
"title": "`.canvas` schema compatibility with Obsidian",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-5",
|
||||
@@ -694,7 +694,7 @@
|
||||
"title": "IPC wire-format stability + `schema_version:`",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-6",
|
||||
@@ -703,7 +703,7 @@
|
||||
"title": "Window chrome — native frame vs frameless custom",
|
||||
"status": "resolved",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-7",
|
||||
@@ -712,7 +712,7 @@
|
||||
"title": "macOS app bundle signing / notarisation",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-8",
|
||||
@@ -721,7 +721,7 @@
|
||||
"title": "Extension API shape — widgets, subcommands, both?",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-extensions.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-9",
|
||||
@@ -730,7 +730,7 @@
|
||||
"title": "Lua runtime vendoring",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-extensions.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-10",
|
||||
@@ -739,7 +739,7 @@
|
||||
"title": "Extension manifest `schema_version:`",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-extensions.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-11",
|
||||
@@ -748,7 +748,7 @@
|
||||
"title": "Coverage gates — hard thresholds vs soft reporting",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-testing.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-12",
|
||||
@@ -757,7 +757,7 @@
|
||||
"title": "Screen-reader automation (axe-core via Playwright)",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-testing.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-13",
|
||||
@@ -766,7 +766,7 @@
|
||||
"title": "Web production-mode a11y",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-accessibility.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-14",
|
||||
@@ -775,7 +775,7 @@
|
||||
"title": "i18n plurals / gender / date-format tooling",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-accessibility.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-15",
|
||||
@@ -784,7 +784,7 @@
|
||||
"title": "Editor tab — full LSP vs tree-sitter-only highlight",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-process.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-16",
|
||||
@@ -793,7 +793,7 @@
|
||||
"title": "`tree-sitter-dart` grammar maintenance",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-process.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-17",
|
||||
@@ -802,7 +802,7 @@
|
||||
"title": "Icon set growth",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-process.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-18",
|
||||
@@ -811,7 +811,7 @@
|
||||
"title": "Theme hot-reload in release builds",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-process.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-19",
|
||||
@@ -820,7 +820,7 @@
|
||||
"title": "(withdrawn)",
|
||||
"status": "resolved",
|
||||
"file_path": "decisions/questions-process.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-20",
|
||||
@@ -829,7 +829,7 @@
|
||||
"title": "Kernel DB service — namespaced SQL access?",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-process.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-21",
|
||||
@@ -838,7 +838,7 @@
|
||||
"title": "Pql absorbs planning vs keeps separate",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-22",
|
||||
@@ -847,7 +847,7 @@
|
||||
"title": "Ticket persistence strategy",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-23",
|
||||
@@ -856,7 +856,7 @@
|
||||
"title": "SSH-remote development — run clide against a remote workspace",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-25",
|
||||
@@ -865,7 +865,7 @@
|
||||
"title": "Body text face — mono everywhere vs Josefin Sans UI + mono code",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-26",
|
||||
@@ -874,7 +874,7 @@
|
||||
"title": "Small screen layout (\u003c 1000px)",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-27",
|
||||
@@ -883,7 +883,7 @@
|
||||
"title": "Two-editor split",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-28",
|
||||
@@ -892,7 +892,7 @@
|
||||
"title": "Terminal strip scope — shell only or logs/errors/tests",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-29",
|
||||
@@ -901,7 +901,7 @@
|
||||
"title": "Branch picker location",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-30",
|
||||
@@ -910,7 +910,7 @@
|
||||
"title": "Focus behavior when editor is dirty and viewer is peeked",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "Q-31",
|
||||
@@ -919,7 +919,7 @@
|
||||
"title": "XWayland fallback for frameless — proper Wayland protocol needed",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "R-2",
|
||||
@@ -929,7 +929,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-20",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "R-3",
|
||||
@@ -939,7 +939,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "R-4",
|
||||
@@ -949,7 +949,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "R-5",
|
||||
@@ -959,7 +959,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "R-6",
|
||||
@@ -969,7 +969,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "R-7",
|
||||
@@ -979,7 +979,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "R-8",
|
||||
@@ -989,7 +989,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "R-9",
|
||||
@@ -999,7 +999,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "R-10",
|
||||
@@ -1009,7 +1009,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "R-11",
|
||||
@@ -1019,7 +1019,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
},
|
||||
{
|
||||
"id": "R-12",
|
||||
@@ -1029,7 +1029,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-07 06:10:54"
|
||||
"synced_at": "2026-05-07 06:20:17"
|
||||
}
|
||||
],
|
||||
"decision_refs": [
|
||||
|
||||
@@ -47,6 +47,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `BufferLine.eraseRange` no longer panics when called with `end == 0`.
|
||||
The right-side wide-char guard read `_data[-1]` via `getWidth(-1)`,
|
||||
which threw a `RangeError`. Real trigger path: `Terminal.eraseDisplayAbove`
|
||||
with the cursor at column 0 — common after `ESC[H\x1b[1J`
|
||||
(home + erase-above) sequences that many TUIs emit on redraw.
|
||||
- Terminal selections no longer silently disappear when the terminal is
|
||||
resized narrower. Reflow's tail-anchor handler used to reparent
|
||||
anchors past the source's trimmed-content range onto a builder line
|
||||
|
||||
@@ -142,8 +142,11 @@ class BufferLine with IndexedItem {
|
||||
eraseCell(start - 1, style);
|
||||
}
|
||||
|
||||
// reset cell one to the right if end is second cell of a wide char
|
||||
if (end < _length && getWidth(end - 1) == 2) {
|
||||
// reset cell one to the right if end is second cell of a wide char.
|
||||
// Mirror the `start > 0` guard above so eraseRange(0, 0, ...) (used
|
||||
// by eraseLineToCursor when the cursor sits at column 0) doesn't read
|
||||
// _data[-1] via getWidth(-1).
|
||||
if (end > 0 && end < _length && getWidth(end - 1) == 2) {
|
||||
eraseCell(end - 1, style);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ repository: https://github.com/postmeridiem/clide
|
||||
|
||||
# Pre-push line-coverage floor. Ratchets up only — see D-66.
|
||||
# Reading: `awk -F: '/^coverage_floor:/ {gsub(/ /,"",$2); print $2}' pubspec.yaml`.
|
||||
coverage_floor: 56
|
||||
coverage_floor: 59
|
||||
|
||||
# Project metadata (was project.yaml, folded in per D-056).
|
||||
# version: above is the single source of truth. The Makefile reads
|
||||
|
||||
@@ -177,6 +177,15 @@ void main() {
|
||||
expect(l.getContent(0), 0);
|
||||
});
|
||||
|
||||
test('eraseRange(0, 0, ...) does not panic when called at column 0', () {
|
||||
// Surfaced by Terminal.eraseDisplayAbove when the cursor sits at
|
||||
// column 0: eraseLineToCursor → eraseRange(0, _cursorX, ...) with
|
||||
// _cursorX == 0. Without the `end > 0` guard, the right-side
|
||||
// wide-char check reads _data[-1] via getWidth(-1).
|
||||
final l = BufferLine(4);
|
||||
l.eraseRange(0, 0, _styleEmpty);
|
||||
});
|
||||
|
||||
test('extends one cell right when end-1 is the second cell of a wide char', () {
|
||||
final l = BufferLine(4);
|
||||
l.setCell(0, _aChar, 1, _styleEmpty);
|
||||
|
||||
@@ -0,0 +1,687 @@
|
||||
/// Pure-Dart tests for the `Terminal` orchestrator
|
||||
/// (`lib/src/terminal/src/terminal.dart`). The `Terminal` is the
|
||||
/// EscapeHandler implementation that wires the parser into a
|
||||
/// Buffer-backed scrollback, exposes mode flags via TerminalState,
|
||||
/// and emits user output through the `onOutput` callback. No Flutter
|
||||
/// dependency.
|
||||
library;
|
||||
|
||||
import 'package:clide/src/terminal/src/core/buffer/cell_offset.dart';
|
||||
import 'package:clide/src/terminal/src/core/input/keys.dart';
|
||||
import 'package:clide/src/terminal/src/core/mouse/button.dart';
|
||||
import 'package:clide/src/terminal/src/core/mouse/button_state.dart';
|
||||
import 'package:clide/src/terminal/src/core/mouse/mode.dart';
|
||||
import 'package:clide/src/terminal/src/core/platform.dart';
|
||||
import 'package:clide/src/terminal/src/terminal.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class _Recorder {
|
||||
final outputs = <String>[];
|
||||
int bellCount = 0;
|
||||
String? lastTitle;
|
||||
String? lastIcon;
|
||||
int? resizeWidth, resizeHeight, resizePixelW, resizePixelH;
|
||||
String? lastOscPs;
|
||||
List<String>? lastOscArgs;
|
||||
|
||||
Terminal build({
|
||||
int maxLines = 100,
|
||||
TerminalTargetPlatform platform = TerminalTargetPlatform.linux,
|
||||
bool reflow = true,
|
||||
}) {
|
||||
return Terminal(
|
||||
maxLines: maxLines,
|
||||
platform: platform,
|
||||
reflowEnabled: reflow,
|
||||
onOutput: outputs.add,
|
||||
onBell: () => bellCount++,
|
||||
onTitleChange: (t) => lastTitle = t,
|
||||
onIconChange: (i) => lastIcon = i,
|
||||
onResize: (w, h, pw, ph) {
|
||||
resizeWidth = w;
|
||||
resizeHeight = h;
|
||||
resizePixelW = pw;
|
||||
resizePixelH = ph;
|
||||
},
|
||||
onPrivateOSC: (ps, args) {
|
||||
lastOscPs = ps;
|
||||
lastOscArgs = args;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('Terminal — construction + state defaults', () {
|
||||
test('default view is 80x24, mainBuffer is active, mode flags zeroed', () {
|
||||
final t = _Recorder().build();
|
||||
expect(t.viewWidth, 80);
|
||||
expect(t.viewHeight, 24);
|
||||
expect(t.isUsingAltBuffer, isFalse);
|
||||
expect(identical(t.buffer, t.mainBuffer), isTrue);
|
||||
expect(t.insertMode, isFalse);
|
||||
expect(t.lineFeedMode, isFalse);
|
||||
expect(t.cursorKeysMode, isFalse);
|
||||
expect(t.reverseDisplayMode, isFalse);
|
||||
expect(t.originMode, isFalse);
|
||||
expect(t.autoWrapMode, isTrue);
|
||||
expect(t.cursorBlinkMode, isFalse);
|
||||
expect(t.cursorVisibleMode, isTrue);
|
||||
expect(t.appKeypadMode, isFalse);
|
||||
expect(t.reportFocusMode, isFalse);
|
||||
expect(t.altBufferMouseScrollMode, isFalse);
|
||||
expect(t.bracketedPasteMode, isFalse);
|
||||
expect(t.mouseMode, MouseMode.none);
|
||||
expect(t.mouseReportMode, MouseReportMode.normal);
|
||||
});
|
||||
|
||||
test('lines getter returns the active buffer lines', () {
|
||||
final t = _Recorder().build();
|
||||
expect(t.lines, t.mainBuffer.lines);
|
||||
});
|
||||
|
||||
test('toString shape includes hashCode + dimensions + height', () {
|
||||
final t = _Recorder().build();
|
||||
expect(t.toString(), contains('${t.viewWidth} x ${t.viewHeight}'));
|
||||
expect(t.toString(), contains('${t.buffer.height} lines'));
|
||||
});
|
||||
});
|
||||
|
||||
group('Terminal — Observable mixin', () {
|
||||
test('listeners fire on write() and stop firing after removeListener', () {
|
||||
final t = _Recorder().build();
|
||||
var fired = 0;
|
||||
void cb() => fired++;
|
||||
t.addListener(cb);
|
||||
t.write('hi');
|
||||
expect(fired, 1);
|
||||
t.removeListener(cb);
|
||||
t.write('more');
|
||||
expect(fired, 1);
|
||||
});
|
||||
|
||||
test('multiple listeners all fire', () {
|
||||
final t = _Recorder().build();
|
||||
var a = 0;
|
||||
var b = 0;
|
||||
t.addListener(() => a++);
|
||||
t.addListener(() => b++);
|
||||
t.write('x');
|
||||
expect(a, 1);
|
||||
expect(b, 1);
|
||||
});
|
||||
});
|
||||
|
||||
group('Terminal — write + writeChar', () {
|
||||
test('writeChar advances the buffer cursor', () {
|
||||
final t = _Recorder().build();
|
||||
t.writeChar('a'.codeUnitAt(0));
|
||||
expect(t.buffer.cursorX, 1);
|
||||
expect(t.buffer.lines[0].getCodePoint(0), 'a'.codeUnitAt(0));
|
||||
});
|
||||
|
||||
test('write feeds the escape parser end-to-end', () {
|
||||
final t = _Recorder().build();
|
||||
t.write('\x1b[31mred'); // SGR red foreground
|
||||
expect(t.cursor.foreground, isNot(0)); // foreground was set
|
||||
});
|
||||
});
|
||||
|
||||
group('Terminal — keyInput', () {
|
||||
test('returns true and emits when the input handler produces output', () {
|
||||
final r = _Recorder();
|
||||
final t = r.build();
|
||||
// Up arrow on the default keytab produces a non-empty escape.
|
||||
expect(t.keyInput(TerminalKey.arrowUp), isTrue);
|
||||
expect(r.outputs, isNotEmpty);
|
||||
});
|
||||
|
||||
test('returns false when no handler matches', () {
|
||||
final r = _Recorder();
|
||||
final t = r.build()..inputHandler = null;
|
||||
expect(t.keyInput(TerminalKey.arrowUp), isFalse);
|
||||
expect(r.outputs, isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('Terminal — charInput', () {
|
||||
test('Ctrl + a..z → 0x01..0x1A', () {
|
||||
final r = _Recorder();
|
||||
final t = r.build();
|
||||
for (var c = 'a'.codeUnitAt(0); c <= 'z'.codeUnitAt(0); c++) {
|
||||
r.outputs.clear();
|
||||
expect(t.charInput(c, ctrl: true), isTrue);
|
||||
expect(r.outputs.single, String.fromCharCode(c - 'a'.codeUnitAt(0) + 1));
|
||||
}
|
||||
});
|
||||
|
||||
test('Ctrl + [ ..._ → 0x1B..0x1F', () {
|
||||
final r = _Recorder();
|
||||
final t = r.build();
|
||||
for (var c = '['.codeUnitAt(0); c <= '_'.codeUnitAt(0); c++) {
|
||||
r.outputs.clear();
|
||||
expect(t.charInput(c, ctrl: true), isTrue);
|
||||
expect(r.outputs.single, String.fromCharCode(c - '['.codeUnitAt(0) + 27));
|
||||
}
|
||||
});
|
||||
|
||||
test('Alt + a..z (linux) → ESC + uppercase', () {
|
||||
final r = _Recorder();
|
||||
final t = r.build();
|
||||
r.outputs.clear();
|
||||
expect(t.charInput('a'.codeUnitAt(0), alt: true), isTrue);
|
||||
expect(r.outputs.single, '\x1bA');
|
||||
});
|
||||
|
||||
test('Alt on macOS is reserved → returns false (no output)', () {
|
||||
final r = _Recorder();
|
||||
final t = r.build(platform: TerminalTargetPlatform.macos);
|
||||
expect(t.charInput('a'.codeUnitAt(0), alt: true), isFalse);
|
||||
expect(r.outputs, isEmpty);
|
||||
});
|
||||
|
||||
test('non-letter char with no modifier returns false', () {
|
||||
final r = _Recorder();
|
||||
final t = r.build();
|
||||
expect(t.charInput('1'.codeUnitAt(0)), isFalse);
|
||||
expect(r.outputs, isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('Terminal — textInput / paste', () {
|
||||
test('textInput emits the literal string', () {
|
||||
final r = _Recorder();
|
||||
final t = r.build();
|
||||
t.textInput('hello');
|
||||
expect(r.outputs.single, 'hello');
|
||||
});
|
||||
|
||||
test('paste without bracketed mode behaves like textInput', () {
|
||||
final r = _Recorder();
|
||||
final t = r.build();
|
||||
t.paste('plain');
|
||||
expect(r.outputs.single, 'plain');
|
||||
});
|
||||
|
||||
test('paste with bracketed mode wraps the text in markers', () {
|
||||
final r = _Recorder();
|
||||
final t = r.build();
|
||||
t.setBracketedPasteMode(true);
|
||||
t.paste('bracketed');
|
||||
expect(r.outputs.single, '\x1b[200~bracketed\x1b[201~');
|
||||
});
|
||||
});
|
||||
|
||||
group('Terminal — mouseInput', () {
|
||||
test('returns true and emits when the mouse handler produces output', () {
|
||||
final r = _Recorder();
|
||||
final t = r.build();
|
||||
t.setMouseMode(MouseMode.upDownScroll);
|
||||
final handled = t.mouseInput(TerminalMouseButton.left, TerminalMouseButtonState.down, const CellOffset(1, 1));
|
||||
expect(handled, isTrue);
|
||||
expect(r.outputs, isNotEmpty);
|
||||
});
|
||||
|
||||
test('returns false when mouse is in none mode', () {
|
||||
final r = _Recorder();
|
||||
final t = r.build();
|
||||
// mouseMode is none by default → ClickMouseHandler + UpDownMouseHandler
|
||||
// both decline.
|
||||
final handled = t.mouseInput(TerminalMouseButton.left, TerminalMouseButtonState.down, const CellOffset(0, 0));
|
||||
expect(handled, isFalse);
|
||||
});
|
||||
|
||||
test('returns false when mouseHandler is null', () {
|
||||
final r = _Recorder();
|
||||
final t = r.build()..mouseHandler = null;
|
||||
final handled = t.mouseInput(TerminalMouseButton.left, TerminalMouseButtonState.down, const CellOffset(0, 0));
|
||||
expect(handled, isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('Terminal — resize', () {
|
||||
test('updates viewWidth/Height, fires onResize, resets margins', () {
|
||||
final r = _Recorder();
|
||||
final t = r.build();
|
||||
t.resize(40, 12, 800, 600);
|
||||
expect(t.viewWidth, 40);
|
||||
expect(t.viewHeight, 12);
|
||||
expect(r.resizeWidth, 40);
|
||||
expect(r.resizeHeight, 12);
|
||||
expect(r.resizePixelW, 800);
|
||||
expect(r.resizePixelH, 600);
|
||||
expect(t.mainBuffer.marginTop, 0);
|
||||
expect(t.mainBuffer.marginBottom, 11);
|
||||
});
|
||||
|
||||
test('clamps newWidth/newHeight to >= 1', () {
|
||||
final r = _Recorder();
|
||||
final t = r.build();
|
||||
t.resize(0, -5);
|
||||
expect(t.viewWidth, 1);
|
||||
expect(t.viewHeight, 1);
|
||||
});
|
||||
|
||||
test('clears scrollback when active buffer is the alt buffer', () {
|
||||
final r = _Recorder();
|
||||
final t = r.build();
|
||||
t.useAltBuffer();
|
||||
t.write('a\nb\nc'); // not strictly building scrollback but exercises path
|
||||
t.resize(40, 10);
|
||||
// alt buffer has at most viewHeight lines after clearScrollback.
|
||||
expect(t.altBuffer.height, lessThanOrEqualTo(10));
|
||||
});
|
||||
});
|
||||
|
||||
group('Terminal — buffer switching', () {
|
||||
test('useAltBuffer / useMainBuffer / clearAltBuffer', () {
|
||||
final t = _Recorder().build();
|
||||
expect(t.isUsingAltBuffer, isFalse);
|
||||
t.useAltBuffer();
|
||||
expect(t.isUsingAltBuffer, isTrue);
|
||||
expect(identical(t.buffer, t.altBuffer), isTrue);
|
||||
// Write something to alt buffer, then clear it.
|
||||
t.writeChar('x'.codeUnitAt(0));
|
||||
t.useMainBuffer();
|
||||
expect(t.isUsingAltBuffer, isFalse);
|
||||
// Switch back and verify clear works.
|
||||
t.useAltBuffer();
|
||||
t.clearAltBuffer();
|
||||
// Cleared buffer has all-empty cells.
|
||||
expect(t.altBuffer.lines[0].getCodePoint(0), 0);
|
||||
});
|
||||
});
|
||||
|
||||
group('Terminal — SBC handlers', () {
|
||||
test('bell fires onBell', () {
|
||||
final r = _Recorder();
|
||||
final t = r.build();
|
||||
t.bell();
|
||||
expect(r.bellCount, 1);
|
||||
});
|
||||
|
||||
test('backspaceReturn moves cursor back by 1', () {
|
||||
final t = _Recorder().build();
|
||||
t.buffer.setCursorX(3);
|
||||
t.backspaceReturn();
|
||||
expect(t.buffer.cursorX, 2);
|
||||
});
|
||||
|
||||
test('lineFeed delegates to buffer.lineFeed', () {
|
||||
final t = _Recorder().build();
|
||||
final yBefore = t.buffer.cursorY;
|
||||
t.lineFeed();
|
||||
expect(t.buffer.cursorY, yBefore + 1);
|
||||
});
|
||||
|
||||
test('carriageReturn resets cursorX to 0', () {
|
||||
final t = _Recorder().build();
|
||||
t.buffer.setCursorX(5);
|
||||
t.carriageReturn();
|
||||
expect(t.buffer.cursorX, 0);
|
||||
});
|
||||
|
||||
test('shiftOut / shiftIn switch the active charset slot', () {
|
||||
final t = _Recorder().build();
|
||||
t.shiftOut();
|
||||
// No exception, no observable side-effect from outside.
|
||||
t.shiftIn();
|
||||
});
|
||||
|
||||
test('unknownSBC and unkownEscape are no-ops', () {
|
||||
final t = _Recorder().build();
|
||||
// Just ensure they don't throw.
|
||||
t.unknownSBC(0xFF);
|
||||
t.unkownEscape(0xFF);
|
||||
});
|
||||
});
|
||||
|
||||
group('Terminal — tab stops', () {
|
||||
test('tab jumps to the next 8-column stop', () {
|
||||
final t = _Recorder().build();
|
||||
t.buffer.setCursorX(0);
|
||||
t.tab();
|
||||
expect(t.buffer.cursorX, 8);
|
||||
});
|
||||
|
||||
test('tab past the last stop saturates the cursor at viewWidth', () {
|
||||
final t = _Recorder().build();
|
||||
t.clearAllTabStops(); // remove every stop
|
||||
t.buffer.setCursorX(70);
|
||||
t.tab();
|
||||
// cursor saturated at viewWidth (80); the public getter clamps to 79.
|
||||
expect(t.buffer.cursorX, 79);
|
||||
});
|
||||
|
||||
test('clearTabStopUnderCursor / clearAllTabStops mutate the table', () {
|
||||
final t = _Recorder().build();
|
||||
t.buffer.setCursorX(8);
|
||||
t.clearTabStopUnderCursor();
|
||||
// Subsequent tab from 0 must skip the cleared 8 stop.
|
||||
t.buffer.setCursorX(0);
|
||||
t.tab();
|
||||
expect(t.buffer.cursorX, 16);
|
||||
|
||||
t.clearAllTabStops();
|
||||
t.buffer.setCursorX(0);
|
||||
t.tab();
|
||||
expect(t.buffer.cursorX, 79); // saturated
|
||||
});
|
||||
|
||||
test('setTapStop is a query, not a write — does not throw', () {
|
||||
final t = _Recorder().build();
|
||||
t.buffer.setCursorX(5);
|
||||
t.setTapStop();
|
||||
});
|
||||
});
|
||||
|
||||
group('Terminal — ANSI escape handlers', () {
|
||||
test('saveCursor / restoreCursor round-trip through buffer', () {
|
||||
final t = _Recorder().build();
|
||||
t.buffer.setCursor(3, 2);
|
||||
t.saveCursor();
|
||||
t.buffer.setCursor(0, 0);
|
||||
t.restoreCursor();
|
||||
expect(t.buffer.cursorX, 3);
|
||||
expect(t.buffer.cursorY, 2);
|
||||
});
|
||||
|
||||
test('index moves the cursor down one line', () {
|
||||
final t = _Recorder().build();
|
||||
t.buffer.setCursorY(1);
|
||||
t.index();
|
||||
expect(t.buffer.cursorY, 2);
|
||||
});
|
||||
|
||||
test('nextLine moves down + resets x to 0', () {
|
||||
final t = _Recorder().build();
|
||||
t.buffer.setCursor(5, 1);
|
||||
t.nextLine();
|
||||
expect(t.buffer.cursorX, 0);
|
||||
});
|
||||
|
||||
test('reverseIndex moves the cursor up one line (inside margins)', () {
|
||||
final t = _Recorder().build();
|
||||
t.buffer.setCursorY(3);
|
||||
t.reverseIndex();
|
||||
expect(t.buffer.cursorY, 2);
|
||||
});
|
||||
|
||||
test('designateCharset routes to buffer.charset.designate', () {
|
||||
final t = _Recorder().build();
|
||||
t.designateCharset(0, '0'.codeUnitAt(0)); // DEC special graphics
|
||||
// After use(0), the next char goes through the DEC graphics translator.
|
||||
// Verify by checking the translator picks up the mapping (test-side).
|
||||
t.buffer.charset.use(0);
|
||||
expect(t.buffer.charset.translate(0x6a), 0x2518);
|
||||
});
|
||||
});
|
||||
|
||||
group('Terminal — CSI handlers', () {
|
||||
test('cursor motion delegates to buffer (setCursor / setCursorX/Y, moveCursorX/Y)', () {
|
||||
final t = _Recorder().build();
|
||||
t.setCursor(3, 5);
|
||||
expect(t.buffer.cursorX, 3);
|
||||
expect(t.buffer.cursorY, 5);
|
||||
t.setCursorX(7);
|
||||
expect(t.buffer.cursorX, 7);
|
||||
t.setCursorY(2);
|
||||
expect(t.buffer.cursorY, 2);
|
||||
t.moveCursorX(2);
|
||||
expect(t.buffer.cursorX, 9);
|
||||
t.moveCursorY(1);
|
||||
expect(t.buffer.cursorY, 3);
|
||||
});
|
||||
|
||||
test('cursorNextLine / cursorPrecedingLine move y + reset x', () {
|
||||
final t = _Recorder().build();
|
||||
t.buffer.setCursor(5, 2);
|
||||
t.cursorNextLine(2);
|
||||
expect(t.buffer.cursorY, 4);
|
||||
expect(t.buffer.cursorX, 0);
|
||||
|
||||
t.buffer.setCursor(5, 5);
|
||||
t.cursorPrecedingLine(2);
|
||||
expect(t.buffer.cursorY, 3);
|
||||
expect(t.buffer.cursorX, 0);
|
||||
});
|
||||
|
||||
test('setMargins forwards to buffer', () {
|
||||
final t = _Recorder().build();
|
||||
t.setMargins(2, 5);
|
||||
expect(t.buffer.marginTop, 2);
|
||||
expect(t.buffer.marginBottom, 5);
|
||||
// null bottom defaults to viewHeight - 1.
|
||||
t.setMargins(0);
|
||||
expect(t.buffer.marginBottom, t.viewHeight - 1);
|
||||
});
|
||||
|
||||
test('erase commands forward to buffer.erase*', () {
|
||||
// Helper that fills line 0 with 'abcde' from column 0 deterministically.
|
||||
Terminal fresh() {
|
||||
final t = _Recorder().build();
|
||||
t.buffer.setCursor(0, 0);
|
||||
t.write('abcde');
|
||||
t.buffer.setCursor(0, 0);
|
||||
return t;
|
||||
}
|
||||
|
||||
// eraseLine wipes the whole line.
|
||||
var t = fresh();
|
||||
t.eraseLine();
|
||||
expect(t.buffer.lines[0].getCodePoint(0), 0);
|
||||
expect(t.buffer.lines[0].getCodePoint(4), 0);
|
||||
|
||||
// eraseLineRight from column 2 keeps 0..1, wipes 2..end.
|
||||
t = fresh();
|
||||
t.buffer.setCursorX(2);
|
||||
t.eraseLineRight();
|
||||
expect(t.buffer.lines[0].getCodePoint(0), 'a'.codeUnitAt(0));
|
||||
expect(t.buffer.lines[0].getCodePoint(2), 0);
|
||||
|
||||
// eraseLineLeft from column 2 wipes 0..1, keeps 2..end.
|
||||
t = fresh();
|
||||
t.buffer.setCursorX(2);
|
||||
t.eraseLineLeft();
|
||||
expect(t.buffer.lines[0].getCodePoint(0), 0);
|
||||
expect(t.buffer.lines[0].getCodePoint(2), 'c'.codeUnitAt(0));
|
||||
|
||||
// eraseDisplay wipes everything.
|
||||
t = fresh();
|
||||
t.eraseDisplay();
|
||||
expect(t.buffer.lines[0].getCodePoint(0), 0);
|
||||
|
||||
// eraseDisplayBelow from column 2 keeps 0..1, wipes the rest.
|
||||
t = fresh();
|
||||
t.buffer.setCursor(2, 0);
|
||||
t.eraseDisplayBelow();
|
||||
expect(t.buffer.lines[0].getCodePoint(0), 'a'.codeUnitAt(0));
|
||||
expect(t.buffer.lines[0].getCodePoint(2), 0);
|
||||
|
||||
// eraseDisplayAbove + eraseScrollbackOnly — exercise without throw.
|
||||
t = fresh();
|
||||
t.buffer.setCursor(0, 0);
|
||||
t.eraseDisplayAbove();
|
||||
t.eraseScrollbackOnly();
|
||||
});
|
||||
|
||||
test('insertLines / deleteLines / deleteChars / scrollUp / scrollDown / eraseChars / insertBlankChars — delegates', () {
|
||||
final t = _Recorder().build();
|
||||
// Just ensure none throw and each side-effect is observable somewhere.
|
||||
t.setMargins(0, 5);
|
||||
t.buffer.setCursorY(2);
|
||||
t.insertLines(1);
|
||||
t.deleteLines(1);
|
||||
t.write('abc');
|
||||
t.buffer.setCursorX(1);
|
||||
t.deleteChars(1);
|
||||
t.scrollUp(1);
|
||||
t.scrollDown(1);
|
||||
t.eraseChars(1);
|
||||
t.insertBlankChars(1);
|
||||
});
|
||||
|
||||
test('repeatPreviousCharacter writes the last codepoint N times', () {
|
||||
final t = _Recorder().build();
|
||||
t.writeChar('z'.codeUnitAt(0));
|
||||
final xBefore = t.buffer.cursorX;
|
||||
t.repeatPreviousCharacter(3);
|
||||
expect(t.buffer.cursorX, xBefore + 3);
|
||||
});
|
||||
|
||||
test('repeatPreviousCharacter is a no-op with no preceding codepoint', () {
|
||||
final t = _Recorder().build();
|
||||
final xBefore = t.buffer.cursorX;
|
||||
t.repeatPreviousCharacter(3);
|
||||
expect(t.buffer.cursorX, xBefore);
|
||||
});
|
||||
|
||||
test('unknownCSI / setUnknownMode / setUnknownDecMode / setColumnMode / unsupportedStyle — no-ops, no throw', () {
|
||||
final t = _Recorder().build();
|
||||
t.unknownCSI('Z'.codeUnitAt(0));
|
||||
t.setUnknownMode(99, true);
|
||||
t.setUnknownDecMode(9999, true);
|
||||
t.setColumnMode(true);
|
||||
t.unsupportedStyle(123);
|
||||
});
|
||||
});
|
||||
|
||||
group('Terminal — device-attribute reports', () {
|
||||
test('every device-attribute / status report hits onOutput', () {
|
||||
final r = _Recorder();
|
||||
final t = r.build();
|
||||
t.sendPrimaryDeviceAttributes();
|
||||
t.sendSecondaryDeviceAttributes();
|
||||
t.sendTertiaryDeviceAttributes();
|
||||
t.sendOperatingStatus();
|
||||
t.sendCursorPosition();
|
||||
t.sendSize();
|
||||
expect(r.outputs.length, 6);
|
||||
expect(r.outputs[0], '\x1b[?1;2c');
|
||||
expect(r.outputs[3], '\x1b[0n');
|
||||
});
|
||||
});
|
||||
|
||||
group('Terminal — mode setters', () {
|
||||
test('every mode setter mirrors into its getter', () {
|
||||
final t = _Recorder().build();
|
||||
t.setInsertMode(true);
|
||||
expect(t.insertMode, isTrue);
|
||||
t.setLineFeedMode(true);
|
||||
expect(t.lineFeedMode, isTrue);
|
||||
t.setCursorKeysMode(true);
|
||||
expect(t.cursorKeysMode, isTrue);
|
||||
t.setReverseDisplayMode(true);
|
||||
expect(t.reverseDisplayMode, isTrue);
|
||||
t.setOriginMode(true);
|
||||
expect(t.originMode, isTrue);
|
||||
t.setAutoWrapMode(false);
|
||||
expect(t.autoWrapMode, isFalse);
|
||||
t.setMouseMode(MouseMode.upDownScroll);
|
||||
expect(t.mouseMode, MouseMode.upDownScroll);
|
||||
t.setCursorBlinkMode(true);
|
||||
expect(t.cursorBlinkMode, isTrue);
|
||||
t.setCursorVisibleMode(false);
|
||||
expect(t.cursorVisibleMode, isFalse);
|
||||
t.setAppKeypadMode(true);
|
||||
expect(t.appKeypadMode, isTrue);
|
||||
t.setReportFocusMode(true);
|
||||
expect(t.reportFocusMode, isTrue);
|
||||
t.setMouseReportMode(MouseReportMode.sgr);
|
||||
expect(t.mouseReportMode, MouseReportMode.sgr);
|
||||
t.setAltBufferMouseScrollMode(true);
|
||||
expect(t.altBufferMouseScrollMode, isTrue);
|
||||
t.setBracketedPasteMode(true);
|
||||
expect(t.bracketedPasteMode, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('Terminal — SGR forwarding', () {
|
||||
test('every set/unset attr toggles the cursor flag', () {
|
||||
final t = _Recorder().build();
|
||||
t.setCursorBold();
|
||||
expect(t.cursor.isBold, isTrue);
|
||||
t.setCursorFaint();
|
||||
expect(t.cursor.isFaint, isTrue);
|
||||
t.setCursorItalic();
|
||||
expect(t.cursor.isItalic, isTrue);
|
||||
t.setCursorUnderline();
|
||||
expect(t.cursor.isUnderline, isTrue);
|
||||
t.setCursorBlink();
|
||||
expect(t.cursor.isBlink, isTrue);
|
||||
t.setCursorInverse();
|
||||
expect(t.cursor.isInverse, isTrue);
|
||||
t.setCursorInvisible();
|
||||
expect(t.cursor.isInvisible, isTrue);
|
||||
t.setCursorStrikethrough();
|
||||
|
||||
// Unset each in turn and check the matching getter goes back to false
|
||||
// (where one is exposed).
|
||||
t.unsetCursorBold();
|
||||
expect(t.cursor.isBold, isFalse);
|
||||
t.unsetCursorFaint();
|
||||
expect(t.cursor.isFaint, isFalse);
|
||||
t.unsetCursorItalic();
|
||||
expect(t.cursor.isItalic, isFalse);
|
||||
t.unsetCursorUnderline();
|
||||
expect(t.cursor.isUnderline, isFalse);
|
||||
t.unsetCursorBlink();
|
||||
expect(t.cursor.isBlink, isFalse);
|
||||
t.unsetCursorInverse();
|
||||
expect(t.cursor.isInverse, isFalse);
|
||||
t.unsetCursorInvisible();
|
||||
expect(t.cursor.isInvisible, isFalse);
|
||||
t.unsetCursorStrikethrough(); // no getter, just exercise
|
||||
});
|
||||
|
||||
test('foreground / background colour setters + reset', () {
|
||||
final t = _Recorder().build();
|
||||
t.setForegroundColor16(2);
|
||||
t.setForegroundColor256(123);
|
||||
t.setForegroundColorRgb(10, 20, 30);
|
||||
t.resetForeground();
|
||||
expect(t.cursor.foreground, 0);
|
||||
|
||||
t.setBackgroundColor16(4);
|
||||
t.setBackgroundColor256(99);
|
||||
t.setBackgroundColorRgb(1, 2, 3);
|
||||
t.resetBackground();
|
||||
expect(t.cursor.background, 0);
|
||||
});
|
||||
|
||||
test('resetCursorStyle wipes attrs + foreground + background', () {
|
||||
final t = _Recorder().build();
|
||||
t.setCursorBold();
|
||||
t.setForegroundColor16(2);
|
||||
t.setBackgroundColor16(4);
|
||||
t.resetCursorStyle();
|
||||
expect(t.cursor.attrs, 0);
|
||||
expect(t.cursor.foreground, 0);
|
||||
expect(t.cursor.background, 0);
|
||||
});
|
||||
});
|
||||
|
||||
group('Terminal — OSC handlers', () {
|
||||
test('setTitle fires onTitleChange with the supplied name', () {
|
||||
final r = _Recorder();
|
||||
final t = r.build();
|
||||
t.setTitle('hello world');
|
||||
expect(r.lastTitle, 'hello world');
|
||||
});
|
||||
|
||||
test('setIconName fires onIconChange', () {
|
||||
final r = _Recorder();
|
||||
final t = r.build();
|
||||
t.setIconName('icon-id');
|
||||
expect(r.lastIcon, 'icon-id');
|
||||
});
|
||||
|
||||
test('unknownOSC routes to onPrivateOSC', () {
|
||||
final r = _Recorder();
|
||||
final t = r.build();
|
||||
t.unknownOSC('99', ['arg1', 'arg2']);
|
||||
expect(r.lastOscPs, '99');
|
||||
expect(r.lastOscArgs, ['arg1', 'arg2']);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user