test sweep: cover core/input/ keytab + handlers (T-91)
Adds test/terminal/input/input_test.dart — 58 unit tests covering the keytab tokenizer, parser, unescape helper, KeytabRecord toString shapes, Keytab.find modifier-matching rules, and the four TerminalInputHandler implementations (CascadeInputHandler, KeytabInputHandler, CtrlInputHandler, AltInputHandler). Highlights: - keytabUnescape: every documented backslash escape + \xHH hex. - LineReader: peek/take/done, whitespace skip, readString (alphanumeric/underscore), readUntil (both exclusive and inclusive variants). - tokenize: keyboard-name and key-define lines, comment + blank stripping, shortcut vs string actions, error paths on malformed input. - KeytabParser: full mode-flag matrix, error paths on every defensive throw reachable through the public addTokens API (stray non-keyboard token, missing colon, modeStatus value other than '+'/'-', non-mode token after modeStatus, action token of wrong type, second token of wrong kind for both _parseName and _parseKeyDefine). - KeytabRecord.toString covers every supported flag (Alt, Control, Shift, AnyMod, Ansi, AppScreen, KeyPad, AppCuKeys, AppKeyPad, NewLine, Mac). - Keytab.find: -Shift / +AnyMod / -AnyMod gating, mode-flag filters (newLine, appKeyPad, appScreen, macos, appCursorKeys, keyPad), -Ansi (VT52) skip, fallthrough to fallback record, null when no key matches. - KeytabInputHandler: every modifier combination's `*` placeholder expansion (1..8 inclusive), default-keytab fallback, no-match null, no-* passthrough. - CtrlInputHandler: A..Z → 0x01..0x1A; null without ctrl, with shift / alt, or on non-letter keys. - AltInputHandler: A..Z → ESC + uppercase; null without alt, with shift / ctrl, on macOS, or on non-letter keys. - defaultInputHandler integration: keytab routing, fallthrough to CtrlInputHandler. Coverage delta: - core/input/handler.dart: 4/54 → 54/54. - keytab.dart: 0/29 → 29/29. - keytab_record.dart: 0/44 → 44/44. - keytab_token.dart: 0/82 → 80/82 (the two remaining lines are defensive throws inside `_parseKeyboardNameDefine` / `_parseKeyDefine` that are unreachable from tokenize() — the callers only enter those functions after the `_isKeyboardNameDefine` / `_isKeyDefine` guards in the same file, so the inner readString always matches). - keytab_parse.dart: 0/65 → 63/65 (the two remaining lines mirror the same shape — _parseName and _parseKeyDefine both check the first token's type, but addTokens only delegates to them after matching that type, so the throws are dead defensive code). - keytab_default.dart: 0/4 unchanged — that's the file's own `void main()` debug entrypoint that prints the parsed default keytab; not part of the runtime contract. - keytab_escape.dart: 0/14 → 14/14. - Total project: 49.31% → 52.53%; coverage_floor bumped 49 → 52. The 4 dead defensive throws are flagged but not removed in this commit — they're a code-style call (defensive paranoia vs. dead- code cleanup) that belongs in a separate review, not folded into a test sweep. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+107
-107
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"exported_at": "2026-05-06T21:25:59Z",
|
||||
"exported_at": "2026-05-06T21:33:57Z",
|
||||
"decisions": [
|
||||
{
|
||||
"id": "D-1",
|
||||
@@ -9,7 +9,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-20",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-3",
|
||||
@@ -19,7 +19,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-20",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-4",
|
||||
@@ -29,7 +29,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-20",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-5",
|
||||
@@ -39,7 +39,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-20",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-6",
|
||||
@@ -49,7 +49,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-20",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-7",
|
||||
@@ -59,7 +59,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-8",
|
||||
@@ -69,7 +69,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-9",
|
||||
@@ -79,7 +79,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-10",
|
||||
@@ -89,7 +89,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-11",
|
||||
@@ -99,7 +99,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-12",
|
||||
@@ -109,7 +109,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-13",
|
||||
@@ -119,7 +119,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-14",
|
||||
@@ -129,7 +129,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-15",
|
||||
@@ -139,7 +139,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/extensions.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-16",
|
||||
@@ -149,7 +149,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/extensions.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-17",
|
||||
@@ -159,7 +159,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/extensions.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-18",
|
||||
@@ -169,7 +169,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/extensions.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-19",
|
||||
@@ -179,7 +179,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/extensions.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-20",
|
||||
@@ -189,7 +189,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/accessibility.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-21",
|
||||
@@ -199,7 +199,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/accessibility.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-22",
|
||||
@@ -209,7 +209,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/accessibility.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-23",
|
||||
@@ -219,7 +219,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-24",
|
||||
@@ -229,7 +229,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-25",
|
||||
@@ -239,7 +239,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-26",
|
||||
@@ -249,7 +249,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-27",
|
||||
@@ -259,7 +259,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-28",
|
||||
@@ -269,7 +269,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-29",
|
||||
@@ -279,7 +279,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-30",
|
||||
@@ -289,7 +289,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-31",
|
||||
@@ -299,7 +299,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-32",
|
||||
@@ -309,7 +309,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-33",
|
||||
@@ -319,7 +319,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-34",
|
||||
@@ -329,7 +329,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/process.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-35",
|
||||
@@ -339,7 +339,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/process.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-36",
|
||||
@@ -349,7 +349,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/process.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-37",
|
||||
@@ -359,7 +359,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/process.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-38",
|
||||
@@ -369,7 +369,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/process.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-39",
|
||||
@@ -379,7 +379,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/process.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-40",
|
||||
@@ -389,7 +389,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/process.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-41",
|
||||
@@ -399,7 +399,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-42",
|
||||
@@ -409,7 +409,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-43",
|
||||
@@ -419,7 +419,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-44",
|
||||
@@ -429,7 +429,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-45",
|
||||
@@ -439,7 +439,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-46",
|
||||
@@ -449,7 +449,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/extensions.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-47",
|
||||
@@ -459,7 +459,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-48",
|
||||
@@ -469,7 +469,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-49",
|
||||
@@ -479,7 +479,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-50",
|
||||
@@ -489,7 +489,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-51",
|
||||
@@ -499,7 +499,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-52",
|
||||
@@ -509,7 +509,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-53",
|
||||
@@ -519,7 +519,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-54",
|
||||
@@ -529,7 +529,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-55",
|
||||
@@ -539,7 +539,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-23",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-56",
|
||||
@@ -549,7 +549,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-23",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-57",
|
||||
@@ -559,7 +559,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-23",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-58",
|
||||
@@ -569,7 +569,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-23",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-59",
|
||||
@@ -579,7 +579,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-25",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-60",
|
||||
@@ -589,7 +589,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-26",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-61",
|
||||
@@ -599,7 +599,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-26",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-62",
|
||||
@@ -609,7 +609,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-26",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-63",
|
||||
@@ -619,7 +619,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-26",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-64",
|
||||
@@ -629,7 +629,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-05-03",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-65",
|
||||
@@ -639,7 +639,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-26",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "D-66",
|
||||
@@ -649,7 +649,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-05-06",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"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-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"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-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "Q-3",
|
||||
@@ -676,7 +676,7 @@
|
||||
"title": "Event persistence + audit/undo",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"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-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"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-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"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-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"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-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"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-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "Q-9",
|
||||
@@ -730,7 +730,7 @@
|
||||
"title": "Lua runtime vendoring",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-extensions.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "Q-10",
|
||||
@@ -739,7 +739,7 @@
|
||||
"title": "Extension manifest `schema_version:`",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-extensions.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"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-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"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-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "Q-13",
|
||||
@@ -766,7 +766,7 @@
|
||||
"title": "Web production-mode a11y",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-accessibility.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"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-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"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-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"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-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "Q-17",
|
||||
@@ -802,7 +802,7 @@
|
||||
"title": "Icon set growth",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-process.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"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-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "Q-19",
|
||||
@@ -820,7 +820,7 @@
|
||||
"title": "(withdrawn)",
|
||||
"status": "resolved",
|
||||
"file_path": "decisions/questions-process.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"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-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"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-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "Q-22",
|
||||
@@ -847,7 +847,7 @@
|
||||
"title": "Ticket persistence strategy",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"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-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"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-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"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-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "Q-27",
|
||||
@@ -883,7 +883,7 @@
|
||||
"title": "Two-editor split",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"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-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "Q-29",
|
||||
@@ -901,7 +901,7 @@
|
||||
"title": "Branch picker location",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"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-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"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-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "R-2",
|
||||
@@ -929,7 +929,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-20",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "R-3",
|
||||
@@ -939,7 +939,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "R-4",
|
||||
@@ -949,7 +949,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "R-5",
|
||||
@@ -959,7 +959,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "R-6",
|
||||
@@ -969,7 +969,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "R-7",
|
||||
@@ -979,7 +979,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "R-8",
|
||||
@@ -989,7 +989,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "R-9",
|
||||
@@ -999,7 +999,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "R-10",
|
||||
@@ -1009,7 +1009,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "R-11",
|
||||
@@ -1019,7 +1019,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
},
|
||||
{
|
||||
"id": "R-12",
|
||||
@@ -1029,7 +1029,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-06 21:25:40"
|
||||
"synced_at": "2026-05-06 21:32:45"
|
||||
}
|
||||
],
|
||||
"decision_refs": [
|
||||
|
||||
+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: 49
|
||||
coverage_floor: 52
|
||||
|
||||
# Project metadata (was project.yaml, folded in per D-056).
|
||||
# version: above is the single source of truth. The Makefile reads
|
||||
|
||||
@@ -0,0 +1,702 @@
|
||||
/// Pure-Dart tests for `lib/src/terminal/src/core/input/`.
|
||||
///
|
||||
/// Covers the keytab tokenizer + parser, the unescape helper, the
|
||||
/// KeytabRecord toString shapes, the `Keytab.find` modifier-matching
|
||||
/// rules, and the four `TerminalInputHandler` implementations.
|
||||
library;
|
||||
|
||||
import 'package:clide/src/terminal/src/core/input/handler.dart';
|
||||
import 'package:clide/src/terminal/src/core/input/keys.dart';
|
||||
import 'package:clide/src/terminal/src/core/input/keytab/keytab.dart';
|
||||
import 'package:clide/src/terminal/src/core/input/keytab/keytab_escape.dart';
|
||||
import 'package:clide/src/terminal/src/core/input/keytab/keytab_parse.dart';
|
||||
import 'package:clide/src/terminal/src/core/input/keytab/keytab_record.dart';
|
||||
import 'package:clide/src/terminal/src/core/input/keytab/keytab_token.dart';
|
||||
import 'package:clide/src/terminal/src/core/mouse/mode.dart';
|
||||
import 'package:clide/src/terminal/src/core/cursor.dart';
|
||||
import 'package:clide/src/terminal/src/core/platform.dart';
|
||||
import 'package:clide/src/terminal/src/core/state.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class _State implements TerminalState {
|
||||
_State({
|
||||
this.lineFeedMode = false,
|
||||
this.appKeypadMode = false,
|
||||
});
|
||||
|
||||
@override
|
||||
bool lineFeedMode;
|
||||
@override
|
||||
bool appKeypadMode;
|
||||
|
||||
@override
|
||||
int get viewWidth => 80;
|
||||
@override
|
||||
int get viewHeight => 24;
|
||||
@override
|
||||
CursorStyle get cursor => CursorStyle();
|
||||
@override
|
||||
bool get reflowEnabled => false;
|
||||
@override
|
||||
bool get insertMode => false;
|
||||
@override
|
||||
bool get cursorKeysMode => false;
|
||||
@override
|
||||
bool get reverseDisplayMode => false;
|
||||
@override
|
||||
bool get originMode => false;
|
||||
@override
|
||||
bool get autoWrapMode => true;
|
||||
@override
|
||||
MouseMode get mouseMode => MouseMode.none;
|
||||
@override
|
||||
MouseReportMode get mouseReportMode => MouseReportMode.normal;
|
||||
@override
|
||||
bool get cursorBlinkMode => true;
|
||||
@override
|
||||
bool get cursorVisibleMode => true;
|
||||
@override
|
||||
bool get reportFocusMode => false;
|
||||
@override
|
||||
bool get altBufferMouseScrollMode => false;
|
||||
@override
|
||||
bool get bracketedPasteMode => false;
|
||||
}
|
||||
|
||||
TerminalKeyboardEvent _evt(
|
||||
TerminalKey key, {
|
||||
bool ctrl = false,
|
||||
bool alt = false,
|
||||
bool shift = false,
|
||||
bool altBuffer = false,
|
||||
bool lineFeedMode = false,
|
||||
bool appKeypadMode = false,
|
||||
TerminalTargetPlatform platform = TerminalTargetPlatform.linux,
|
||||
}) {
|
||||
return TerminalKeyboardEvent(
|
||||
key: key,
|
||||
shift: shift,
|
||||
ctrl: ctrl,
|
||||
alt: alt,
|
||||
state: _State(
|
||||
lineFeedMode: lineFeedMode,
|
||||
appKeypadMode: appKeypadMode,
|
||||
),
|
||||
altBuffer: altBuffer,
|
||||
platform: platform,
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('keytabUnescape', () {
|
||||
test(r'\E maps to ESC (0x1b)', () {
|
||||
expect(keytabUnescape(r'\E'), '\x1b');
|
||||
expect(keytabUnescape(r'\E[A'), '\x1b[A');
|
||||
});
|
||||
|
||||
test('classic backslash escapes round-trip', () {
|
||||
expect(keytabUnescape(r'\\'), r'\');
|
||||
expect(keytabUnescape(r'\"'), '"');
|
||||
expect(keytabUnescape(r'\t'), '\t');
|
||||
expect(keytabUnescape(r'\r'), '\r');
|
||||
expect(keytabUnescape(r'\n'), '\n');
|
||||
expect(keytabUnescape(r'\b'), '\b');
|
||||
});
|
||||
|
||||
test(r'\xHH parses two hex digits to a single char', () {
|
||||
expect(keytabUnescape(r'\x00'), '\x00');
|
||||
expect(keytabUnescape(r'\x7f'), '\x7f');
|
||||
expect(keytabUnescape(r'\xFF'), 'ÿ');
|
||||
});
|
||||
|
||||
test(r'leaves unrecognised text alone', () {
|
||||
expect(keytabUnescape('hello'), 'hello');
|
||||
expect(keytabUnescape(''), '');
|
||||
});
|
||||
});
|
||||
|
||||
group('LineReader', () {
|
||||
test('peek/take advance + done detection', () {
|
||||
final r = LineReader('abc');
|
||||
expect(r.peek(), 'a');
|
||||
expect(r.take(), 'a');
|
||||
expect(r.peek(2), 'bc');
|
||||
expect(r.take(2), 'bc');
|
||||
expect(r.done, isTrue);
|
||||
expect(r.peek(), isNull);
|
||||
expect(r.take(), isNull);
|
||||
});
|
||||
|
||||
test('peek clamps when count exceeds remaining length', () {
|
||||
final r = LineReader('ab');
|
||||
expect(r.peek(99), 'ab');
|
||||
});
|
||||
|
||||
test('skipWhitespace eats spaces and tabs but stops on other chars', () {
|
||||
final r = LineReader(' \t hello');
|
||||
r.skipWhitespace();
|
||||
expect(r.peek(5), 'hello');
|
||||
});
|
||||
|
||||
test('readString takes alphanumeric / underscore until break', () {
|
||||
final r = LineReader('abc_123 next');
|
||||
expect(r.readString(), 'abc_123');
|
||||
// Position should now be at the space.
|
||||
expect(r.peek(), ' ');
|
||||
});
|
||||
|
||||
test('readUntil takes everything up to the pattern (exclusive by default)', () {
|
||||
final r = LineReader('foo"bar');
|
||||
expect(r.readUntil('"'), 'foo');
|
||||
expect(r.peek(), '"');
|
||||
});
|
||||
|
||||
test('readUntil with inclusive=true consumes the boundary char', () {
|
||||
final r = LineReader('foo"bar');
|
||||
expect(r.readUntil('"', inclusive: true), 'foo"');
|
||||
expect(r.peek(), 'b');
|
||||
});
|
||||
});
|
||||
|
||||
group('tokenize', () {
|
||||
test('parses a keyboard-name line', () {
|
||||
final tokens = tokenize('keyboard "Default"').toList();
|
||||
expect(tokens.map((t) => t.type).toList(), [
|
||||
KeytabTokenType.keyboard,
|
||||
KeytabTokenType.input,
|
||||
]);
|
||||
expect(tokens.last.value, 'Default');
|
||||
});
|
||||
|
||||
test('parses a key-define line with modes and string action', () {
|
||||
final tokens = tokenize('key Up -Shift+Ansi : "\\EOA"').toList();
|
||||
expect(tokens.map((t) => t.type).toList(), [
|
||||
KeytabTokenType.keyDefine,
|
||||
KeytabTokenType.keyName,
|
||||
KeytabTokenType.modeStatus,
|
||||
KeytabTokenType.mode,
|
||||
KeytabTokenType.modeStatus,
|
||||
KeytabTokenType.mode,
|
||||
KeytabTokenType.colon,
|
||||
KeytabTokenType.input,
|
||||
]);
|
||||
expect(tokens[1].value, 'Up');
|
||||
expect(tokens.last.value, r'\EOA');
|
||||
});
|
||||
|
||||
test('parses a shortcut action (no quotes)', () {
|
||||
final tokens = tokenize('key Up +Shift : scrollLineUp').toList();
|
||||
expect(tokens.last.type, KeytabTokenType.shortcut);
|
||||
expect(tokens.last.value, 'scrollLineUp');
|
||||
});
|
||||
|
||||
test('skips comments and blank lines', () {
|
||||
final source = '''
|
||||
# top comment
|
||||
keyboard "X"
|
||||
|
||||
# inner
|
||||
key Tab : "\\t" # trailing comment
|
||||
''';
|
||||
final tokens = tokenize(source).toList();
|
||||
// Trailing-comment stripping leaves no broken tokens.
|
||||
expect(
|
||||
tokens.map((t) => t.type).toList(),
|
||||
contains(KeytabTokenType.colon),
|
||||
);
|
||||
});
|
||||
|
||||
test('tokenize throws TokenizeError on a malformed key line missing colon', () {
|
||||
expect(
|
||||
() => tokenize('key Tab "\\t"').toList(),
|
||||
throwsA(isA<TokenizeError>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('tokenize throws TokenizeError on unterminated keyboard line', () {
|
||||
expect(
|
||||
() => tokenize('keyboard X').toList(),
|
||||
throwsA(isA<TokenizeError>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('KeytabToken toString reflects type + value', () {
|
||||
final tok = KeytabToken(KeytabTokenType.input, 'hello');
|
||||
expect(tok.toString(), 'KeytabTokenType.input<hello>');
|
||||
});
|
||||
});
|
||||
|
||||
group('KeytabParser', () {
|
||||
test('parses a keyboard name + a key-define line into a Keytab', () {
|
||||
const src = '''
|
||||
keyboard "Test"
|
||||
key Up -Shift+Ansi : "\\EOA"
|
||||
''';
|
||||
final t = Keytab.parse(src);
|
||||
expect(t.name, 'Test');
|
||||
expect(t.records, hasLength(1));
|
||||
expect(t.records.first.qtKeyName, 'Up');
|
||||
expect(t.records.first.shift, isFalse);
|
||||
expect(t.records.first.ansi, isTrue);
|
||||
});
|
||||
|
||||
test('every supported mode label maps to its KeytabRecord field', () {
|
||||
const src = '''
|
||||
keyboard "Modes"
|
||||
key Up +Alt+Control+Shift+AnyMod+Ansi+AppScreen+KeyPad+AppCuKeys+AppKeyPad+NewLine+Mac : "x"
|
||||
''';
|
||||
final t = Keytab.parse(src);
|
||||
final r = t.records.single;
|
||||
expect(r.alt, isTrue);
|
||||
expect(r.ctrl, isTrue);
|
||||
expect(r.shift, isTrue);
|
||||
expect(r.anyModifier, isTrue);
|
||||
expect(r.ansi, isTrue);
|
||||
expect(r.appScreen, isTrue);
|
||||
expect(r.keyPad, isTrue);
|
||||
expect(r.appCursorKeys, isTrue);
|
||||
expect(r.appKeyPad, isTrue);
|
||||
expect(r.newLine, isTrue);
|
||||
expect(r.macos, isTrue);
|
||||
});
|
||||
|
||||
test('parser throws ParseError on an unknown qt key name', () {
|
||||
const src = '''
|
||||
keyboard "X"
|
||||
key NotARealKey : "x"
|
||||
''';
|
||||
expect(() => Keytab.parse(src), throwsA(isA<ParseError>()));
|
||||
});
|
||||
|
||||
test('parser throws ParseError on an unknown mode label', () {
|
||||
const src = '''
|
||||
keyboard "X"
|
||||
key Up +Bogus : "x"
|
||||
''';
|
||||
expect(() => Keytab.parse(src), throwsA(isA<ParseError>()));
|
||||
});
|
||||
|
||||
test('TokensReader peek/take semantics', () {
|
||||
final a = KeytabToken(KeytabTokenType.colon, ':');
|
||||
final b = KeytabToken(KeytabTokenType.colon, ':');
|
||||
final r = TokensReader([a, b]);
|
||||
expect(r.peek(), a);
|
||||
expect(r.take(), a);
|
||||
expect(r.peek(), b);
|
||||
expect(r.take(), b);
|
||||
expect(r.done, isTrue);
|
||||
expect(r.peek(), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('KeytabRecord / KeytabAction toString', () {
|
||||
test('action toString quotes input and bare-prints shortcut', () {
|
||||
expect(
|
||||
KeytabAction(KeytabActionType.input, r'\E[A').toString(),
|
||||
'"\\E[A"',
|
||||
);
|
||||
expect(
|
||||
KeytabAction(KeytabActionType.shortcut, 'scrollUp').toString(),
|
||||
'scrollUp',
|
||||
);
|
||||
});
|
||||
|
||||
test('record toString writes +Mode for true and -Mode for false flags', () {
|
||||
final r = KeytabRecord(
|
||||
qtKeyName: 'Up',
|
||||
key: TerminalKey.arrowUp,
|
||||
action: KeytabAction(KeytabActionType.shortcut, 'scrollLineUp'),
|
||||
alt: true,
|
||||
ctrl: false,
|
||||
shift: null,
|
||||
anyModifier: null,
|
||||
ansi: true,
|
||||
appScreen: false,
|
||||
keyPad: null,
|
||||
appCursorKeys: null,
|
||||
appKeyPad: null,
|
||||
newLine: null,
|
||||
macos: null,
|
||||
);
|
||||
final s = r.toString();
|
||||
expect(s, contains('+Alt'));
|
||||
expect(s, contains('-Control'));
|
||||
expect(s, contains('+Ansi'));
|
||||
expect(s, contains('-AppScreen'));
|
||||
expect(s, isNot(contains('Shift')));
|
||||
expect(s, endsWith(': scrollLineUp'));
|
||||
});
|
||||
|
||||
test('Keytab toString lists name + records', () {
|
||||
const src = '''
|
||||
keyboard "Listed"
|
||||
key Up +Shift : scrollLineUp
|
||||
''';
|
||||
final t = Keytab.parse(src);
|
||||
final s = t.toString();
|
||||
expect(s, contains('keyboard "Listed"'));
|
||||
expect(s, contains('Up'));
|
||||
expect(s, contains('scrollLineUp'));
|
||||
});
|
||||
|
||||
test('action unescapedValue returns the raw value for shortcut actions', () {
|
||||
// Input actions go through keytabUnescape; shortcut actions don't.
|
||||
expect(
|
||||
KeytabAction(KeytabActionType.shortcut, r'\Ehello').unescapedValue(),
|
||||
r'\Ehello',
|
||||
);
|
||||
});
|
||||
|
||||
test('record toString covers every supported mode flag when set', () {
|
||||
// Sets every nullable flag so each `if (foo != null)` branch fires.
|
||||
final r = KeytabRecord(
|
||||
qtKeyName: 'Up',
|
||||
key: TerminalKey.arrowUp,
|
||||
action: KeytabAction(KeytabActionType.input, 'x'),
|
||||
alt: true,
|
||||
ctrl: true,
|
||||
shift: false,
|
||||
anyModifier: true,
|
||||
ansi: true,
|
||||
appScreen: false,
|
||||
keyPad: true,
|
||||
appCursorKeys: false,
|
||||
appKeyPad: true,
|
||||
newLine: false,
|
||||
macos: true,
|
||||
);
|
||||
final s = r.toString();
|
||||
expect(s, contains('+Alt'));
|
||||
expect(s, contains('+Control'));
|
||||
expect(s, contains('-Shift'));
|
||||
expect(s, contains('+AnyMod'));
|
||||
expect(s, contains('+Ansi'));
|
||||
expect(s, contains('-AppScreen'));
|
||||
expect(s, contains('+KeyPad'));
|
||||
expect(s, contains('-AppCuKeys'));
|
||||
expect(s, contains('+AppKeyPad'));
|
||||
expect(s, contains('-NewLine'));
|
||||
expect(s, contains('+Mac'));
|
||||
expect(s, endsWith(': "x"'));
|
||||
});
|
||||
});
|
||||
|
||||
group('KeytabParser — defensive error paths through hand-crafted tokens', () {
|
||||
test('addTokens throws ParseError on a stray non-keyboard / non-keyDefine token', () {
|
||||
final parser = KeytabParser();
|
||||
expect(
|
||||
() => parser.addTokens([
|
||||
KeytabToken(KeytabTokenType.colon, ':'),
|
||||
]),
|
||||
throwsA(isA<ParseError>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('_parseName throws when the second token is not an input token', () {
|
||||
final parser = KeytabParser();
|
||||
expect(
|
||||
() => parser.addTokens([
|
||||
KeytabToken(KeytabTokenType.keyboard, 'keyboard'),
|
||||
KeytabToken(KeytabTokenType.keyName, 'Up'),
|
||||
]),
|
||||
throwsA(isA<ParseError>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('_parseKeyDefine throws when the second token is not a keyName', () {
|
||||
final parser = KeytabParser();
|
||||
expect(
|
||||
() => parser.addTokens([
|
||||
KeytabToken(KeytabTokenType.keyDefine, 'key'),
|
||||
KeytabToken(KeytabTokenType.colon, ':'),
|
||||
]),
|
||||
throwsA(isA<ParseError>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('_parseKeyDefine throws on an unrecognised modeStatus value', () {
|
||||
final parser = KeytabParser();
|
||||
expect(
|
||||
() => parser.addTokens([
|
||||
KeytabToken(KeytabTokenType.keyDefine, 'key'),
|
||||
KeytabToken(KeytabTokenType.keyName, 'Up'),
|
||||
KeytabToken(KeytabTokenType.modeStatus, 'X'), // not '+' / '-'
|
||||
]),
|
||||
throwsA(isA<ParseError>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('_parseKeyDefine throws when the token after modeStatus is not a mode', () {
|
||||
final parser = KeytabParser();
|
||||
expect(
|
||||
() => parser.addTokens([
|
||||
KeytabToken(KeytabTokenType.keyDefine, 'key'),
|
||||
KeytabToken(KeytabTokenType.keyName, 'Up'),
|
||||
KeytabToken(KeytabTokenType.modeStatus, '+'),
|
||||
KeytabToken(KeytabTokenType.colon, ':'), // not a mode token
|
||||
]),
|
||||
throwsA(isA<ParseError>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('_parseKeyDefine throws when the colon is missing', () {
|
||||
final parser = KeytabParser();
|
||||
expect(
|
||||
() => parser.addTokens([
|
||||
KeytabToken(KeytabTokenType.keyDefine, 'key'),
|
||||
KeytabToken(KeytabTokenType.keyName, 'Up'),
|
||||
KeytabToken(KeytabTokenType.input, 'x'), // should be colon here
|
||||
]),
|
||||
throwsA(isA<ParseError>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('_parseKeyDefine throws when the action token is neither input nor shortcut', () {
|
||||
final parser = KeytabParser();
|
||||
expect(
|
||||
() => parser.addTokens([
|
||||
KeytabToken(KeytabTokenType.keyDefine, 'key'),
|
||||
KeytabToken(KeytabTokenType.keyName, 'Up'),
|
||||
KeytabToken(KeytabTokenType.colon, ':'),
|
||||
KeytabToken(KeytabTokenType.mode, 'Alt'), // not a valid action
|
||||
]),
|
||||
throwsA(isA<ParseError>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('Keytab.find — modifier matching', () {
|
||||
Keytab build(String src) => Keytab.parse('keyboard "X"\n$src\n');
|
||||
|
||||
test('exact match: -Shift on a record with shift=null and no anyModifier', () {
|
||||
final t = build('key Up -Shift : "\\E[A"');
|
||||
expect(
|
||||
t.find(TerminalKey.arrowUp, shift: false)?.action.value,
|
||||
r'\E[A',
|
||||
);
|
||||
expect(t.find(TerminalKey.arrowUp, shift: true), isNull);
|
||||
});
|
||||
|
||||
test('+AnyMod requires at least one modifier; rejects no-modifier press', () {
|
||||
final t = build('key Up +AnyMod : "\\E[A"');
|
||||
expect(t.find(TerminalKey.arrowUp), isNull);
|
||||
expect(t.find(TerminalKey.arrowUp, ctrl: true), isNotNull);
|
||||
});
|
||||
|
||||
test('-AnyMod requires zero modifiers; rejects any-modifier press', () {
|
||||
final t = build('key Up -AnyMod : "\\E[A"');
|
||||
expect(t.find(TerminalKey.arrowUp), isNotNull);
|
||||
expect(t.find(TerminalKey.arrowUp, alt: true), isNull);
|
||||
});
|
||||
|
||||
test('mode flags filter records (newLine + appKeyPad + appScreen + macos)', () {
|
||||
final t = build('''
|
||||
key Up +NewLine+AppKeyPad+AppScreen+Mac : "match"
|
||||
key Up : "fallback"
|
||||
''');
|
||||
// Wrong newLine mode falls through to fallback.
|
||||
expect(
|
||||
t.find(TerminalKey.arrowUp, newLineMode: false, appKeyPad: true, appScreen: true, macos: true)?.action.value,
|
||||
'fallback',
|
||||
);
|
||||
// All matching → primary record.
|
||||
expect(
|
||||
t.find(TerminalKey.arrowUp, newLineMode: true, appKeyPad: true, appScreen: true, macos: true)?.action.value,
|
||||
'match',
|
||||
);
|
||||
});
|
||||
|
||||
test('-Ansi records are skipped (VT52 not supported yet)', () {
|
||||
final t = build('key Up -Ansi : "vt52"');
|
||||
expect(t.find(TerminalKey.arrowUp), isNull);
|
||||
});
|
||||
|
||||
test('returns null when no record key matches', () {
|
||||
final t = build('key Up : "\\EA"');
|
||||
expect(t.find(TerminalKey.arrowDown), isNull);
|
||||
});
|
||||
|
||||
test('appCursorKeys + keyPad gates also filter', () {
|
||||
final t = build('key Up +AppCuKeys+KeyPad : "\\EOA"\nkey Up : "fallback"');
|
||||
expect(t.find(TerminalKey.arrowUp)?.action.value, 'fallback');
|
||||
expect(
|
||||
t.find(TerminalKey.arrowUp, appCursorKeys: true, keyPad: true)?.action.value,
|
||||
r'\EOA',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('Default keytab is parsable + nontrivial', () {
|
||||
test('Keytab.defaultKeytab name + at least one record', () {
|
||||
expect(Keytab.defaultKeytab.name, isNotEmpty);
|
||||
expect(Keytab.defaultKeytab.records, isNotEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('TerminalKeyboardEvent', () {
|
||||
test('copyWith overrides only specified fields', () {
|
||||
final base = _evt(TerminalKey.arrowUp);
|
||||
final shifted = base.copyWith(shift: true);
|
||||
expect(shifted.shift, isTrue);
|
||||
expect(shifted.alt, base.alt);
|
||||
expect(shifted.ctrl, base.ctrl);
|
||||
expect(shifted.key, base.key);
|
||||
expect(shifted.platform, base.platform);
|
||||
});
|
||||
|
||||
test('copyWith() with no args is equivalent to the original', () {
|
||||
final base = _evt(TerminalKey.arrowDown, alt: true);
|
||||
final clone = base.copyWith();
|
||||
expect(clone.key, base.key);
|
||||
expect(clone.alt, base.alt);
|
||||
});
|
||||
});
|
||||
|
||||
group('CascadeInputHandler', () {
|
||||
test('returns the first non-null result; null otherwise', () {
|
||||
const cascade = CascadeInputHandler([
|
||||
_NullHandler(),
|
||||
_ConstHandler('first'),
|
||||
_ConstHandler('second'), // should never be reached
|
||||
]);
|
||||
expect(cascade(_evt(TerminalKey.arrowUp)), 'first');
|
||||
|
||||
const allNull = CascadeInputHandler([_NullHandler(), _NullHandler()]);
|
||||
expect(allNull(_evt(TerminalKey.arrowUp)), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('KeytabInputHandler', () {
|
||||
test('falls back to Keytab.defaultKeytab when none is supplied', () {
|
||||
// Up arrow with no modifiers → default keytab match (\E[A or similar
|
||||
// depending on mode flags). Just assert non-null.
|
||||
const h = KeytabInputHandler();
|
||||
expect(h(_evt(TerminalKey.arrowUp)), isNotNull);
|
||||
});
|
||||
|
||||
test('returns null when the keytab has no matching record', () {
|
||||
final empty = Keytab(name: 'empty', records: const []);
|
||||
expect(KeytabInputHandler(empty)(_evt(TerminalKey.arrowUp)), isNull);
|
||||
});
|
||||
|
||||
test('inserts a modifier code into actions containing *', () {
|
||||
// Build a keytab whose Up record uses the * placeholder; the handler
|
||||
// replaces * with a code based on the active modifiers.
|
||||
final t = Keytab.parse('keyboard "X"\nkey Up +AnyMod : "\\E[1;*A"\n');
|
||||
final h = KeytabInputHandler(t);
|
||||
|
||||
final cases = <(TerminalKeyboardEvent, String)>[
|
||||
// Single-modifier codes.
|
||||
(_evt(TerminalKey.arrowUp, shift: true), '\x1b[1;2A'), // shift → 2
|
||||
(_evt(TerminalKey.arrowUp, alt: true), '\x1b[1;3A'), // alt → 3
|
||||
(_evt(TerminalKey.arrowUp, ctrl: true), '\x1b[1;5A'), // ctrl → 5
|
||||
// Pair codes.
|
||||
(
|
||||
_evt(TerminalKey.arrowUp, shift: true, alt: true),
|
||||
'\x1b[1;4A',
|
||||
),
|
||||
(
|
||||
_evt(TerminalKey.arrowUp, shift: true, ctrl: true),
|
||||
'\x1b[1;6A',
|
||||
),
|
||||
(
|
||||
_evt(TerminalKey.arrowUp, ctrl: true, alt: true),
|
||||
'\x1b[1;7A',
|
||||
),
|
||||
// Triple.
|
||||
(
|
||||
_evt(TerminalKey.arrowUp, shift: true, alt: true, ctrl: true),
|
||||
'\x1b[1;8A',
|
||||
),
|
||||
];
|
||||
for (final c in cases) {
|
||||
expect(h(c.$1), c.$2, reason: '$c');
|
||||
}
|
||||
});
|
||||
|
||||
test('leaves the action alone when no * placeholder is present', () {
|
||||
final t = Keytab.parse('keyboard "X"\nkey Up : "\\E[A"\n');
|
||||
final h = KeytabInputHandler(t);
|
||||
expect(h(_evt(TerminalKey.arrowUp)), '\x1b[A');
|
||||
});
|
||||
});
|
||||
|
||||
group('CtrlInputHandler', () {
|
||||
const h = CtrlInputHandler();
|
||||
|
||||
test('Ctrl+A through Ctrl+Z map to control bytes 0x01..0x1A', () {
|
||||
for (var i = 0; i < 26; i++) {
|
||||
final key = TerminalKey.values[TerminalKey.keyA.index + i];
|
||||
final result = h(_evt(key, ctrl: true));
|
||||
expect(result, String.fromCharCode(i + 1), reason: 'TerminalKey.${key.name} → ${i + 1}');
|
||||
}
|
||||
});
|
||||
|
||||
test('returns null without ctrl, or when shift / alt are also pressed', () {
|
||||
expect(h(_evt(TerminalKey.keyA)), isNull);
|
||||
expect(h(_evt(TerminalKey.keyA, ctrl: true, shift: true)), isNull);
|
||||
expect(h(_evt(TerminalKey.keyA, ctrl: true, alt: true)), isNull);
|
||||
});
|
||||
|
||||
test('returns null for non-letter keys', () {
|
||||
expect(h(_evt(TerminalKey.arrowUp, ctrl: true)), isNull);
|
||||
expect(h(_evt(TerminalKey.f1, ctrl: true)), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('AltInputHandler', () {
|
||||
const h = AltInputHandler();
|
||||
|
||||
test('Alt+A through Alt+Z emit ESC + uppercase ASCII byte', () {
|
||||
for (var i = 0; i < 26; i++) {
|
||||
final key = TerminalKey.values[TerminalKey.keyA.index + i];
|
||||
final result = h(_evt(key, alt: true));
|
||||
expect(result, '\x1b${String.fromCharCode(0x41 + i)}');
|
||||
}
|
||||
});
|
||||
|
||||
test('returns null without alt, or when shift / ctrl are also pressed', () {
|
||||
expect(h(_evt(TerminalKey.keyA)), isNull);
|
||||
expect(h(_evt(TerminalKey.keyA, alt: true, ctrl: true)), isNull);
|
||||
expect(h(_evt(TerminalKey.keyA, alt: true, shift: true)), isNull);
|
||||
});
|
||||
|
||||
test('returns null on macOS (Alt is reserved for char composition)', () {
|
||||
expect(
|
||||
h(_evt(TerminalKey.keyA, alt: true, platform: TerminalTargetPlatform.macos)),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('returns null for non-letter keys', () {
|
||||
expect(h(_evt(TerminalKey.arrowUp, alt: true)), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('defaultInputHandler', () {
|
||||
test('routes a plain Up arrow through the keytab', () {
|
||||
expect(defaultInputHandler(_evt(TerminalKey.arrowUp)), isNotNull);
|
||||
});
|
||||
|
||||
test('routes Ctrl+C to 0x03 via CtrlInputHandler when keytab misses', () {
|
||||
// Default keytab has no entry for Ctrl+keyC, so CtrlInputHandler runs.
|
||||
expect(
|
||||
defaultInputHandler(_evt(TerminalKey.keyC, ctrl: true)),
|
||||
'\x03',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _NullHandler implements TerminalInputHandler {
|
||||
const _NullHandler();
|
||||
@override
|
||||
String? call(TerminalKeyboardEvent event) => null;
|
||||
}
|
||||
|
||||
class _ConstHandler implements TerminalInputHandler {
|
||||
const _ConstHandler(this._value);
|
||||
final String _value;
|
||||
@override
|
||||
String? call(TerminalKeyboardEvent event) => _value;
|
||||
}
|
||||
Reference in New Issue
Block a user