From f8958fd37256645aa53fa8fd5426ff9e58843eb4 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Fri, 12 Jun 2026 02:50:17 +0200 Subject: [PATCH] split escape parser handlers into part files (T-123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parser.dart (1139 LOC) now keeps only the core — byte queue, dispatch tables, ESC/CSI consumers — at 373 LOC. The handler groups move to mixins in part files of the same library, so every private member stays library-scoped and no public surface is added: - csi_handlers.dart — cursor/erase/scroll ops, DA/DSR, margins, tab clear, repeat, window manipulation - sgr_handlers.dart — SGR incl. the guarded 38/48 extended-color path (T-369) - mode_handlers.dart — ANSI + DEC private mode set/reset - osc_handlers.dart — OSC string parsing + dispatch An abstract _EscapeParserBase carries the shared state (handler sink, queue, token bookkeeping, the reusable _Csi scratch) the mixins are `on`. All 76 parser tests (and the rest of the terminal suite) pass unchanged. Co-Authored-By: Claude Fable 5 --- .pql/changelog/ticket_history/2026-06.sql | 2 + .pql/changelog/tickets/2026-06.sql | 39 + .../src/core/escape/csi_handlers.dart | 416 +++++++++ .../src/core/escape/mode_handlers.dart | 114 +++ .../src/core/escape/osc_handlers.dart | 89 ++ lib/src/terminal/src/core/escape/parser.dart | 875 +----------------- .../src/core/escape/sgr_handlers.dart | 249 +++++ 7 files changed, 938 insertions(+), 846 deletions(-) create mode 100644 lib/src/terminal/src/core/escape/csi_handlers.dart create mode 100644 lib/src/terminal/src/core/escape/mode_handlers.dart create mode 100644 lib/src/terminal/src/core/escape/osc_handlers.dart create mode 100644 lib/src/terminal/src/core/escape/sgr_handlers.dart diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index f344eb08..52959f46 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -4128,3 +4128,5 @@ CORRECTION to fable-ous.md: ColumnHat is NOT duplicated line-for-line in app.dar Done 2026-06-12. One deviation from the plan: the shell pieces went to lib/src/shell/ rather than lib/widgets/ + lib/builtin/hat/ — SlotHost/RootLayout know kernel + contribution types (not widget primitives), and the hat bar isn''t extension-shaped (no contributions), so neither home fit. app.dart kept the planned re-exports; zero test edits needed.', NULL, '2026-06-12 00:30:11', '2026-06-12 00:30:11', '2026-06-12 00:30:11', NULL, '871c3a1d4c3545dfa41bdafa3a39aaa8', 2) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDH8TE9MJBXZ4GQ5YT10JM', 'status', 'backlog', 'in_progress', NULL, '2026-06-12 00:30:17', '2026-06-12 00:30:17', '2026-06-12 00:30:17', NULL, '95cd4a80d4e61af091427c6d20bf701a', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDH8TE9MJBXZ4GQ5YT10JM', 'status', 'in_progress', 'done', NULL, '2026-06-12 00:39:04', '2026-06-12 00:39:04', '2026-06-12 00:39:04', NULL, '9354054c9dee37482c76674a331ebc26', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4RJ9K19P5AX14RHRR', 'status', 'backlog', 'in_progress', NULL, '2026-06-12 00:39:41', '2026-06-12 00:39:41', '2026-06-12 00:39:41', NULL, 'a5439ee868e2491127308f8028d29417', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index b1fb909e..c006bda4 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -4344,3 +4344,42 @@ Target layout under lib/builtin/claude/src/meta_sidebar/: models.dart (enums + _ Execution order (each phase independently green): 1) primitives (icon button, inject field, permission badge, models); 2) stateless tab views (activity, config, tab strip) with prop threading; 3) team tab + roster row (most orchestrator coupling: verify show/hide, mute, inject submit-and-clear, shift-click bypass confirm, fork, close, task reassign cycle, auto-front on TeamMemberJoined at line 158); 4) cleanup of moved methods from the root state. Tests: test/builtin/claude/claude_meta_sidebar_test.dart imports only the root file and public symbols — no changes needed; no goldens reference the sidebar. Caveat from the rat sweep (T-385): TeamMemberJoined currently has no emitter — coordinate before investing in the team tab plumbing.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-11 22:03:26', '2026-06-12 00:30:17', NULL, '9c58fdb1224831d870110543a53389a6', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDH8TE9MJBXZ4GQ5YT10JM', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Split lib/builtin/claude/src/claude_meta_sidebar.dart (1192 LOC)', 'lib/builtin/claude/src/claude_meta_sidebar.dart is 1192 LOC. Split plan (verified against the file 2026-06-12). Public API (ClaudeMetaSidebar widget + SidebarTab enum) stays in the root file — tests and extension.dart need zero import changes. + +Inventory: consts _labelColumnWidth/_rowPitch (43-44), SidebarTab enum (46), ClaudeMetaSidebar (48-79), _ClaudeMetaSidebarState monolith (81-638: lifecycle, stats polling, team membership streams, primary-session binding, broker subscription, config listener, inject state, accordion state, plus the three tab bodies), _ConfigSection/_ConfigPermKind enums (641/644), _MetaSection/_MetaRow models (646-657), _AgentRosterRow(+State) (680-928), _permissionModeBadge + _PermissionModeBadge T-181 (935-1009), _IconButton (1012-1039), _InjectTextField (1043-1070), _TaskRow T-171 (1077-1141), _TabStrip (1145-1192). + +Target layout under lib/builtin/claude/src/meta_sidebar/: models.dart (enums + _MetaSection/_MetaRow + layout consts), activity_tab.dart (ActivityTabView ~70 LOC, from _activityBody/_runtimeSection 272-302), team_tab.dart (TeamTabView ~120 LOC, from _teamBody/_taskSection 304-380, props-driven with callbacks), config_tab.dart (ConfigTabView ~200 LOC, from _configBody family 382-597; accordion _expanded state stays in parent, passed as prop+callback), roster_row.dart (_AgentRosterRow ~250 LOC incl. bypass-confirm state), permission_badge.dart (~80), task_row.dart (~70), tab_strip.dart (~55), icon_button.dart + inject_field.dart (~30 each — keep here initially; promote to lib/widgets/ only when a second consumer appears). Root file shrinks to ~150 LOC of lifecycle + event bindings + tab switch. + +Execution order (each phase independently green): 1) primitives (icon button, inject field, permission badge, models); 2) stateless tab views (activity, config, tab strip) with prop threading; 3) team tab + roster row (most orchestrator coupling: verify show/hide, mute, inject submit-and-clear, shift-click bypass confirm, fork, close, task reassign cycle, auto-front on TeamMemberJoined at line 158); 4) cleanup of moved methods from the root state. + +Tests: test/builtin/claude/claude_meta_sidebar_test.dart imports only the root file and public symbols — no changes needed; no goldens reference the sidebar. Caveat from the rat sweep (T-385): TeamMemberJoined currently has no emitter — coordinate before investing in the team tab plumbing.', 'done', 'medium', NULL, NULL, NULL, '2026-06-11 22:03:26', '2026-06-12 00:39:04', NULL, '130bc44e5a82ac00eb280f13cc8c19c3', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4RJ9K19P5AX14RHRR', 'task', '06FB0TNQM4HGK6KR0314P2M32M', 'split lib/src/terminal/.../escape/parser.dart (1139 LOC)', 'parser.dart is a single 1139-line file containing the full ESC/CSI/OSC/DCS handler tree for the terminal emulator. Functional but unwieldy; the consultant flagged it as ''consider splitting'' in the T-107 review. + +Suggested split (sequenced with the T-91 coverage sweep on lib/src/terminal/, so the split doesn''t fight in-flight test work): + +- parser.dart — entry point + state machine driver +- esc_handlers.dart — single-char ESC dispatch table + handlers +- csi_handlers.dart — CSI parameter parsing + handlers +- osc_handlers.dart — OSC string handlers (title, colour set, etc.) +- dcs_handlers.dart — DCS/SOS/PM/APC string handlers + +Each handler module exports a registrar that the driver wires at construction. + +Done when: +- parser.dart < 400 LOC +- All existing parser tests pass without changes +- No new public surface; everything stays library-private + +Source: T-107 / consultants.md "Code quality — Findings — [Major]". + +Split breakdown from the 2026-06-11 Fable review (epic T-359), verified against the file 2026-06-12: + +Structure today: main parser + routing (11-116), CSI core _escHandleCSI (196-209) + _consumeCsi (217-272), _csiHandlers table of 27 final bytes (274-304), cursor movement handlers (306-807), erase/scroll/line/char ops (809-945), SGR monolith (411-622, 212 LOC), mode set/reset (395-409, 946-1030), DA/DSR (334-343, 624-635), window manipulation (658-712), OSC (1034-1110), _Csi state object (1113-1131 — note the commented-out `intermediates` field at 1117/1125). + +Target layout under escape/: parser.dart keeps EscapeParser (queue, tokenization, top-level dispatch, ~400 LOC); csi_parser.dart (CsiSequence value object — prefix, params, RESTORED intermediates, finalByte — plus the consume logic from _consumeCsi); csi_handlers.dart (dispatch table, now keyed on final byte + intermediates); cursor_handlers.dart; sgr_handler.dart (the 411-622 monolith); mode_handler.dart; osc_parser.dart + osc_handlers.dart. EscapeHandler interface unchanged. Preserve the zero-allocation/reset-able design goal noted at lines 14-16. + +Bug this split must fix (verified): _consumeCsi DISCARDS intermediate bytes — lines 258-261 have `// intermediates.add(char);` commented out and `continue`, so CSI Ps SP q (DECSCUSR, cursor style) and CSI ! p / SP-intermediate forms dispatch on the bare final byte and fall to unknownCSI. Fix: restore the intermediates field on CsiSequence, capture them during consume, dispatch on (intermediates, finalByte), and add EscapeHandler.setCursorStyle for DECSCUSR. (DECSTR is CSI ! p — soft terminal reset — same intermediate mechanism.) + +Related bug with its own ticket (T-369): unguarded params[i+1] lookahead in SGR 38/48 at lines ~502/512/547/557 (RangeError on truncated sequences) + colon-form sub-parameters unhandled. The split makes the fix natural: sgr_handler.dart owns guarded lookahead helpers; if T-369 lands first, carry its tests over; if this lands first, fix it inside sgr_handler.dart and close T-369 with it. + +Tests: test/terminal/escape/parser_test.dart (786 LOC) splits along the same seams — keep parser_test.dart for top-level dispatch/SBC/rollback, add csi_parser_test.dart (intermediates capture, DECSCUSR), sgr_handler_test.dart (bounds + colon form + 256/RGB), mode_handler_test.dart, osc_parser_test.dart, window/DA splits as convenient. The _RecordingHandler fixture is reusable across all of them.', 'in_progress', 'low', NULL, NULL, NULL, '2026-05-18 10:29:02', '2026-06-12 00:39:41', NULL, 'e8810fd23adc05225565eaa3af23eb53', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/lib/src/terminal/src/core/escape/csi_handlers.dart b/lib/src/terminal/src/core/escape/csi_handlers.dart new file mode 100644 index 00000000..4be56c09 --- /dev/null +++ b/lib/src/terminal/src/core/escape/csi_handlers.dart @@ -0,0 +1,416 @@ +// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory. + +// CSI handlers: cursor movement, erase/scroll/line/char ops, device +// attributes + status reports, margins, tab clear, repeat, and window +// manipulation. Split out of parser.dart (T-123); dispatched from the +// _csiHandlers table in the EscapeParser core. + +part of 'parser.dart'; + +mixin _CsiHandlers on _EscapeParserBase { + /// `ESC [ Ps a` Cursor Horizontal Position Relative (HPR) + /// + /// https://terminalguide.namepad.de/seq/csi_sa/ + // void _csiHandleCursorHorizontalRelative() { + // if (_csi.params.isEmpty) { + // handler.cursorHorizontal(1); + // } else { + // handler.cursorHorizontal(_csi.params[0]); + // } + // } + /// `ESC [ Ps b` Repeat Previous Character (REP) + /// + /// https://terminalguide.namepad.de/seq/csi_sb/ + void _csiHandleRepeatPreviousCharacter() { + var amount = 1; + + if (_csi.params.isNotEmpty) { + amount = _csi.params[0]; + if (amount == 0) amount = 1; + } + + handler.repeatPreviousCharacter(amount); + } + + /// `ESC [ Ps c` Device Attributes (DA) + /// + /// https://terminalguide.namepad.de/seq/csi_sc/ + void _csiHandleSendDeviceAttributes() { + switch (_csi.prefix) { + case Ascii.greaterThan: + return handler.sendSecondaryDeviceAttributes(); + case Ascii.equal: + return handler.sendTertiaryDeviceAttributes(); + default: + handler.sendPrimaryDeviceAttributes(); + } + } + + /// `ESC [ Ps d` Cursor Vertical Position Absolute (VPA) + /// + /// https://terminalguide.namepad.de/seq/csi_sd/ + void _csiHandleLinePositionAbsolute() { + var y = 1; + + if (_csi.params.isNotEmpty) { + y = _csi.params[0]; + } + + handler.setCursorY(y - 1); + } + + /// `ESC [ Ps ; Ps f` Alias: Set Cursor Position + /// + /// https://terminalguide.namepad.de/seq/csi_sf/ + void _csiHandleCursorPosition() { + var row = 1; + var col = 1; + + if (_csi.params.length == 2) { + row = _csi.params[0]; + col = _csi.params[1]; + } + + handler.setCursor(col - 1, row - 1); + } + + /// `ESC [ Ps g` Tab Clear (TBC) + /// + /// https://terminalguide.namepad.de/seq/csi_sg/ + void _csiHandelClearTabStop() { + var cmd = 0; + + if (_csi.params.length == 1) { + cmd = _csi.params[0]; + } + + switch (cmd) { + case 0: + return handler.clearTabStopUnderCursor(); + default: + return handler.clearAllTabStops(); + } + } + + /// `ESC [ Ps n` Device Status Report [Dispatch] (DSR) + /// + /// https://terminalguide.namepad.de/seq/csi_sn/ + void _csiHandleDeviceStatusReport() { + if (_csi.params.isEmpty) return; + + switch (_csi.params[0]) { + case 5: + return handler.sendOperatingStatus(); + case 6: + return handler.sendCursorPosition(); + } + } + + /// `ESC [ Ps ; Ps r` Set Top and Bottom Margins (DECSTBM) + /// + /// https://terminalguide.namepad.de/seq/csi_sr/ + void _csiHandleSetMargins() { + var top = 1; + int? bottom; + + if (_csi.params.length > 2) return; + + if (_csi.params.isNotEmpty) { + top = _csi.params[0]; + + if (_csi.params.length == 2) { + bottom = _csi.params[1] - 1; + } + } + + handler.setMargins(top - 1, bottom); + } + + /// `ESC [ Ps t` Window operations [DISPATCH] + /// + /// https://terminalguide.namepad.de/seq/csi_st/ + void _csiWindowManipulation() { + // The sequence needs at least one parameter. + if (_csi.params.isEmpty) { + return; + } + // Most the commands in this group are either of the scope of this package, + // or should be disabled for security risks. + switch (_csi.params.first) { + // Window handling is currently not in the scope of the package. + case 1: // Restore Terminal Window (show window if minimized) + case 2: // Minimize Terminal Window + case 3: // Set Terminal Window Position + case 4: // Set Terminal Window Size in Pixels + case 5: // Raise Terminal Window + case 6: // Lower Terminal Window + case 7: // Refresh/Redraw Terminal Window + return; + case 8: // Set Terminal Window Size (in characters) + // This CSI contains 2 more parameters: width and height. + if (_csi.params.length != 3) { + return; + } + final rows = _csi.params[1]; + final cols = _csi.params[2]; + handler.resize(cols, rows); + return; + // Window handling is currently no in the scope of the package. + case 9: // Maximize Terminal Window + case 10: // Alias: Maximize Terminal Window + case 11: // Report Terminal Window State + case 13: // Report Terminal Window Position + case 14: // Report Terminal Window Size in Pixels + case 15: // Report Screen Size in Pixels + case 16: // Report Cell Size in Pixels + return; + case 18: // Report Terminal Size (in characters) + handler.sendSize(); + return; + // Screen handling is currently no in the scope of the package. + case 19: // Report Screen Size (in characters) + // Disabled as these can a security risk. + case 20: // Get Icon Title + case 21: // Get Terminal Title + // Not implemented. + case 22: // Push Terminal Title + case 23: // Pop Terminal Title + return; + // Unknown CSI. + default: + return; + } + } + + /// `ESC [ Ps A` Cursor Up (CUU) + /// + /// https://terminalguide.namepad.de/seq/csi_ca/ + void _csiHandleCursorUp() { + var amount = 1; + + if (_csi.params.isNotEmpty) { + amount = _csi.params[0]; + if (amount == 0) amount = 1; + } + + handler.moveCursorY(-amount); + } + + /// `ESC [ Ps B` Cursor Down (CUD) + /// + /// https://terminalguide.namepad.de/seq/csi_cb/ + void _csiHandleCursorDown() { + var amount = 1; + + if (_csi.params.isNotEmpty) { + amount = _csi.params[0]; + if (amount == 0) amount = 1; + } + + handler.moveCursorY(amount); + } + + /// `ESC [ Ps C` Cursor Right (CUF) + /// + /// Cursor Right (CUF) + void _csiHandleCursorForward() { + var amount = 1; + + if (_csi.params.isNotEmpty) { + amount = _csi.params[0]; + if (amount == 0) amount = 1; + } + + handler.moveCursorX(amount); + } + + /// `ESC [ Ps D` Cursor Left (CUB) + /// + /// https://terminalguide.namepad.de/seq/csi_cd/ + void _csiHandleCursorBackward() { + var amount = 1; + + if (_csi.params.isNotEmpty) { + amount = _csi.params[0]; + if (amount == 0) amount = 1; + } + + handler.moveCursorX(-amount); + } + + /// `ESC [ Ps E` Cursor Next Line (CNL) + /// + /// https://terminalguide.namepad.de/seq/csi_ce/ + void _csiHandleCursorNextLine() { + var amount = 1; + + if (_csi.params.isNotEmpty) { + amount = _csi.params[0]; + if (amount == 0) amount = 1; + } + + handler.cursorNextLine(amount); + } + + /// `ESC [ Ps F` Cursor Previous Line (CPL) + /// + /// https://terminalguide.namepad.de/seq/csi_cf/ + void _csiHandleCursorPrecedingLine() { + var amount = 1; + + if (_csi.params.isNotEmpty) { + amount = _csi.params[0]; + if (amount == 0) amount = 1; + } + + handler.cursorPrecedingLine(amount); + } + + void _csiHandleCursorHorizontalAbsolute() { + var x = 1; + + if (_csi.params.isNotEmpty) { + x = _csi.params[0]; + if (x == 0) x = 1; + } + + handler.setCursorX(x - 1); + } + + /// ESC [ Ps J Erase Display [Dispatch] (ED) + /// + /// https://terminalguide.namepad.de/seq/csi_cj/ + void _csiHandleEraseDisplay() { + var cmd = 0; + + if (_csi.params.length == 1) { + cmd = _csi.params[0]; + } + + switch (cmd) { + case 0: + return handler.eraseDisplayBelow(); + case 1: + return handler.eraseDisplayAbove(); + case 2: + return handler.eraseDisplay(); + case 3: + return handler.eraseScrollbackOnly(); + } + } + + /// `ESC [ Ps K` Erase Line [Dispatch] (EL) + /// + /// https://terminalguide.namepad.de/seq/csi_ck/ + void _csiHandleEraseLine() { + var cmd = 0; + + if (_csi.params.length == 1) { + cmd = _csi.params[0]; + } + + switch (cmd) { + case 0: + return handler.eraseLineRight(); + case 1: + return handler.eraseLineLeft(); + case 2: + return handler.eraseLine(); + } + } + + /// `ESC [ Ps L` Insert Line (IL) + /// + /// https://terminalguide.namepad.de/seq/csi_cl/ + void _csiHandleInsertLines() { + var amount = 1; + + if (_csi.params.isNotEmpty) { + amount = _csi.params[0]; + } + + handler.insertLines(amount); + } + + /// ESC [ Ps M Delete Line (DL) + /// + /// https://terminalguide.namepad.de/seq/csi_cm/ + void _csiHandleDeleteLines() { + var amount = 1; + + if (_csi.params.isNotEmpty) { + amount = _csi.params[0]; + } + + handler.deleteLines(amount); + } + + /// ESC [ Ps P Delete Character (DCH) + /// + /// https://terminalguide.namepad.de/seq/csi_cp/ + void _csiHandleDelete() { + var amount = 1; + + if (_csi.params.isNotEmpty) { + amount = _csi.params[0]; + } + + handler.deleteChars(amount); + } + + /// `ESC [ Ps S` Scroll Up (SU) + /// + /// https://terminalguide.namepad.de/seq/csi_cs/ + void _csiHandleScrollUp() { + var amount = 1; + + if (_csi.params.isNotEmpty) { + amount = _csi.params[0]; + } + + handler.scrollUp(amount); + } + + /// `ESC [ Ps T `Scroll Down (SD) + /// + /// https://terminalguide.namepad.de/seq/csi_ct_1param/ + void _csiHandleScrollDown() { + var amount = 1; + + if (_csi.params.isNotEmpty) { + amount = _csi.params[0]; + } + + handler.scrollDown(amount); + } + + /// `ESC [ Ps X` Erase Character (ECH) + /// + /// https://terminalguide.namepad.de/seq/csi_cx/ + void _csiHandleEraseCharacters() { + var amount = 1; + + if (_csi.params.isNotEmpty) { + amount = _csi.params[0]; + } + + handler.eraseChars(amount); + } + + /// `ESC [ Ps @` Insert Blanks (ICH) + /// + /// https://terminalguide.namepad.de/seq/csi_x40_at/ + /// + /// Inserts amount spaces at current cursor position moving existing cell + /// contents to the right. The contents of the amount right-most columns in + /// the scroll region are lost. The cursor position is not changed. + void _csiHandleInsertBlankCharacters() { + var amount = 1; + + if (_csi.params.isNotEmpty) { + amount = _csi.params[0]; + } + + handler.insertBlankChars(amount); + } +} diff --git a/lib/src/terminal/src/core/escape/mode_handlers.dart b/lib/src/terminal/src/core/escape/mode_handlers.dart new file mode 100644 index 00000000..bf54fe82 --- /dev/null +++ b/lib/src/terminal/src/core/escape/mode_handlers.dart @@ -0,0 +1,114 @@ +// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory. + +// ANSI + DEC private mode set/reset (CSI h / CSI l, with and without +// the ? prefix). Split out of parser.dart (T-123). + +part of 'parser.dart'; + +mixin _ModeHandlers on _EscapeParserBase { + /// - `ESC [ [ Pm ] h Set Mode (SM)` https://terminalguide.namepad.de/seq/csi_sm/ + /// - `ESC [ ? [ Pm ] h` Set Mode (?) (SM) https://terminalguide.namepad.de/seq/csi_sh__p/ + /// - `ESC [ [ Pm ] l` Reset Mode (RM) https://terminalguide.namepad.de/seq/csi_rm/ + /// - `ESC [ ? [ Pm ] l` Reset Mode (?) (RM) https://terminalguide.namepad.de/seq/csi_sl__p/ + void _csiHandleMode() { + final isEnabled = _csi.finalByte == Ascii.h; + + final isDecModes = _csi.prefix == Ascii.questionMark; + + if (isDecModes) { + for (var mode in _csi.params) { + _setDecMode(mode, isEnabled); + } + } else { + for (var mode in _csi.params) { + _setMode(mode, isEnabled); + } + } + } + + void _setMode(int mode, bool enabled) { + switch (mode) { + case 4: + return handler.setInsertMode(enabled); + case 20: + return handler.setLineFeedMode(enabled); + default: + return handler.setUnknownMode(mode, enabled); + } + } + + void _setDecMode(int mode, bool enabled) { + switch (mode) { + case 1: + return handler.setCursorKeysMode(enabled); + case 3: + return handler.setColumnMode(enabled); + case 5: + return handler.setReverseDisplayMode(enabled); + case 6: + return handler.setOriginMode(enabled); + case 7: + return handler.setAutoWrapMode(enabled); + case 9: + return enabled ? handler.setMouseMode(MouseMode.clickOnly) : handler.setMouseMode(MouseMode.none); + case 12: + case 13: + return handler.setCursorBlinkMode(enabled); + case 25: + return handler.setCursorVisibleMode(enabled); + case 47: + if (enabled) { + return handler.useAltBuffer(); + } else { + return handler.useMainBuffer(); + } + case 66: + return handler.setAppKeypadMode(enabled); + case 1000: + return enabled ? handler.setMouseMode(MouseMode.upDownScroll) : handler.setMouseMode(MouseMode.none); + case 1001: + return enabled ? handler.setMouseMode(MouseMode.upDownScroll) : handler.setMouseMode(MouseMode.none); + case 1002: + return enabled ? handler.setMouseMode(MouseMode.upDownScrollDrag) : handler.setMouseMode(MouseMode.none); + case 1003: + return enabled ? handler.setMouseMode(MouseMode.upDownScrollMove) : handler.setMouseMode(MouseMode.none); + case 1004: + return handler.setReportFocusMode(enabled); + case 1005: + return enabled ? handler.setMouseReportMode(MouseReportMode.utf) : handler.setMouseReportMode(MouseReportMode.normal); + case 1006: + return enabled ? handler.setMouseReportMode(MouseReportMode.sgr) : handler.setMouseReportMode(MouseReportMode.normal); + case 1007: + return handler.setAltBufferMouseScrollMode(enabled); + case 1015: + return enabled ? handler.setMouseReportMode(MouseReportMode.urxvt) : handler.setMouseReportMode(MouseReportMode.normal); + case 1047: + if (enabled) { + handler.useAltBuffer(); + } else { + handler.clearAltBuffer(); + handler.useMainBuffer(); + } + return; + case 1048: + if (enabled) { + return handler.saveCursor(); + } else { + return handler.restoreCursor(); + } + case 1049: + if (enabled) { + handler.saveCursor(); + handler.clearAltBuffer(); + handler.useAltBuffer(); + } else { + handler.useMainBuffer(); + } + return; + case 2004: + return handler.setBracketedPasteMode(enabled); + default: + return handler.setUnknownDecMode(mode, enabled); + } + } +} diff --git a/lib/src/terminal/src/core/escape/osc_handlers.dart b/lib/src/terminal/src/core/escape/osc_handlers.dart new file mode 100644 index 00000000..c2abfa40 --- /dev/null +++ b/lib/src/terminal/src/core/escape/osc_handlers.dart @@ -0,0 +1,89 @@ +// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory. + +// OSC string parsing + dispatch (title / icon name / private +// pass-through), BEL or ST terminated. Split out of parser.dart +// (T-123). + +part of 'parser.dart'; + +mixin _OscHandlers on _EscapeParserBase { + /// Parse a OSC sequence from the queue. Returns true if a sequence was + /// found and handled. + bool _escHandleOSC() { + final consumed = _consumeOsc(); + if (!consumed) { + return false; + } + + if (_osc.isEmpty) { + return true; + } + + // Common OSCs + if (_osc.length >= 2) { + final ps = _osc[0]; + final pt = _osc[1]; + + switch (ps) { + case '0': + handler.setTitle(pt); + handler.setIconName(pt); + return true; + case '1': + handler.setIconName(pt); + return true; + case '2': + handler.setTitle(pt); + return true; + } + } + + // Private extensions + handler.unknownOSC(_osc[0], _osc.sublist(1)); + + return true; + } + + final _osc = []; + + bool _consumeOsc() { + _osc.clear(); + final param = StringBuffer(); + + while (true) { + if (_queue.isEmpty) { + return false; + } + + final char = _queue.consume(); + + // OSC terminates with BEL + if (char == Ascii.BEL) { + _osc.add(param.toString()); + return true; + } + + /// OSC terminates with ST + if (char == Ascii.ESC) { + if (_queue.isEmpty) { + return false; + } + + if (_queue.consume() == Ascii.backslash) { + _osc.add(param.toString()); + } + + return true; + } + + /// Parse next parameter + if (char == Ascii.semicolon) { + _osc.add(param.toString()); + param.clear(); + continue; + } + + param.writeCharCode(char); + } + } +} diff --git a/lib/src/terminal/src/core/escape/parser.dart b/lib/src/terminal/src/core/escape/parser.dart index 71e9a3bd..7242ca91 100644 --- a/lib/src/terminal/src/core/escape/parser.dart +++ b/lib/src/terminal/src/core/escape/parser.dart @@ -8,16 +8,18 @@ import 'package:clide/src/terminal/src/utils/byte_consumer.dart'; import 'package:clide/src/terminal/src/utils/char_code.dart'; import 'package:clide/src/terminal/src/utils/lookup_table.dart'; -/// [EscapeParser] translates control characters and escape sequences into -/// function calls that the terminal can handle. -/// -/// Design goals: -/// * Zero object allocation during processing. -/// * No internal state. Same input will always produce same output. -class EscapeParser { - final EscapeHandler handler; +part 'csi_handlers.dart'; +part 'mode_handlers.dart'; +part 'osc_handlers.dart'; +part 'sgr_handlers.dart'; - EscapeParser(this.handler); +/// Shared parser state the handler mixins operate on: the escape +/// handler sink, the byte queue, token bookkeeping, and the reusable +/// CSI scratch object (zero-allocation design — see [EscapeParser]). +abstract class _EscapeParserBase { + _EscapeParserBase(this.handler); + + final EscapeHandler handler; final _queue = ByteConsumer(); @@ -27,6 +29,24 @@ class EscapeParser { /// End of sequence or character being processed. Useful for debugging. int get tokenEnd => _queue.totalConsumed; + /// The last parsed [_Csi]. This is a mutable singletion by design to reduce + /// object allocations. + final _csi = _Csi(finalByte: 0, params: []); +} + +/// [EscapeParser] translates control characters and escape sequences into +/// function calls that the terminal can handle. +/// +/// Design goals: +/// * Zero object allocation during processing. +/// * No internal state. Same input will always produce same output. +/// +/// The handler groups live as mixins in this library's part files +/// (csi/sgr/mode/osc handlers, T-123); this core owns the byte queue, +/// the dispatch tables, and the ESC/CSI consumers. +class EscapeParser extends _EscapeParserBase with _CsiHandlers, _ModeHandlers, _OscHandlers, _SgrHandlers { + EscapeParser(super.handler); + void write(String chunk) { _queue.unrefConsumedBlocks(); _queue.add(chunk); @@ -208,10 +228,6 @@ class EscapeParser { return true; } - /// The last parsed [_Csi]. This is a mutable singletion by design to reduce - /// object allocations. - final _csi = _Csi(finalByte: 0, params: []); - /// Parse a CSI from the head of the queue. Return false if the CSI isn't /// complete. After a CSI is successfully parsed, [_csi] is updated. bool _consumeCsi() { @@ -322,839 +338,6 @@ class EscapeParser { 'X'.codeUnitAt(0): _csiHandleEraseCharacters, '@'.codeUnitAt(0): _csiHandleInsertBlankCharacters, }); - - /// `ESC [ Ps a` Cursor Horizontal Position Relative (HPR) - /// - /// https://terminalguide.namepad.de/seq/csi_sa/ - // void _csiHandleCursorHorizontalRelative() { - // if (_csi.params.isEmpty) { - // handler.cursorHorizontal(1); - // } else { - // handler.cursorHorizontal(_csi.params[0]); - // } - // } - - /// `ESC [ Ps b` Repeat Previous Character (REP) - /// - /// https://terminalguide.namepad.de/seq/csi_sb/ - void _csiHandleRepeatPreviousCharacter() { - var amount = 1; - - if (_csi.params.isNotEmpty) { - amount = _csi.params[0]; - if (amount == 0) amount = 1; - } - - handler.repeatPreviousCharacter(amount); - } - - /// `ESC [ Ps c` Device Attributes (DA) - /// - /// https://terminalguide.namepad.de/seq/csi_sc/ - void _csiHandleSendDeviceAttributes() { - switch (_csi.prefix) { - case Ascii.greaterThan: - return handler.sendSecondaryDeviceAttributes(); - case Ascii.equal: - return handler.sendTertiaryDeviceAttributes(); - default: - handler.sendPrimaryDeviceAttributes(); - } - } - - /// `ESC [ Ps d` Cursor Vertical Position Absolute (VPA) - /// - /// https://terminalguide.namepad.de/seq/csi_sd/ - void _csiHandleLinePositionAbsolute() { - var y = 1; - - if (_csi.params.isNotEmpty) { - y = _csi.params[0]; - } - - handler.setCursorY(y - 1); - } - - /// `ESC [ Ps ; Ps f` Alias: Set Cursor Position - /// - /// https://terminalguide.namepad.de/seq/csi_sf/ - void _csiHandleCursorPosition() { - var row = 1; - var col = 1; - - if (_csi.params.length == 2) { - row = _csi.params[0]; - col = _csi.params[1]; - } - - handler.setCursor(col - 1, row - 1); - } - - /// `ESC [ Ps g` Tab Clear (TBC) - /// - /// https://terminalguide.namepad.de/seq/csi_sg/ - void _csiHandelClearTabStop() { - var cmd = 0; - - if (_csi.params.length == 1) { - cmd = _csi.params[0]; - } - - switch (cmd) { - case 0: - return handler.clearTabStopUnderCursor(); - default: - return handler.clearAllTabStops(); - } - } - - /// - `ESC [ [ Pm ] h Set Mode (SM)` https://terminalguide.namepad.de/seq/csi_sm/ - /// - `ESC [ ? [ Pm ] h` Set Mode (?) (SM) https://terminalguide.namepad.de/seq/csi_sh__p/ - /// - `ESC [ [ Pm ] l` Reset Mode (RM) https://terminalguide.namepad.de/seq/csi_rm/ - /// - `ESC [ ? [ Pm ] l` Reset Mode (?) (RM) https://terminalguide.namepad.de/seq/csi_sl__p/ - void _csiHandleMode() { - final isEnabled = _csi.finalByte == Ascii.h; - - final isDecModes = _csi.prefix == Ascii.questionMark; - - if (isDecModes) { - for (var mode in _csi.params) { - _setDecMode(mode, isEnabled); - } - } else { - for (var mode in _csi.params) { - _setMode(mode, isEnabled); - } - } - } - - /// `ESC [ [ Ps ] m` Select Graphic Rendition (SGR) - /// - /// https://terminalguide.namepad.de/seq/csi_sm/ - void _csiHandleSgr() { - final params = _csi.params; - - if (params.isEmpty) { - return handler.resetCursorStyle(); - } - - for (var i = 0; i < _csi.params.length; i++) { - final param = params[i]; - switch (param) { - case 0: - handler.resetCursorStyle(); - continue; - case 1: - handler.setCursorBold(); - continue; - case 2: - handler.setCursorFaint(); - continue; - case 3: - handler.setCursorItalic(); - continue; - case 4: - handler.setCursorUnderline(); - continue; - case 5: - handler.setCursorBlink(); - continue; - case 7: - handler.setCursorInverse(); - continue; - case 8: - handler.setCursorInvisible(); - continue; - case 9: - handler.setCursorStrikethrough(); - continue; - - case 21: - handler.unsetCursorBold(); - continue; - case 22: - handler.unsetCursorFaint(); - continue; - case 23: - handler.unsetCursorItalic(); - continue; - case 24: - handler.unsetCursorUnderline(); - continue; - case 25: - handler.unsetCursorBlink(); - continue; - case 27: - handler.unsetCursorInverse(); - continue; - case 28: - handler.unsetCursorInvisible(); - continue; - case 29: - handler.unsetCursorStrikethrough(); - continue; - - case 30: - handler.setForegroundColor16(NamedColor.black); - continue; - case 31: - handler.setForegroundColor16(NamedColor.red); - continue; - case 32: - handler.setForegroundColor16(NamedColor.green); - continue; - case 33: - handler.setForegroundColor16(NamedColor.yellow); - continue; - case 34: - handler.setForegroundColor16(NamedColor.blue); - continue; - case 35: - handler.setForegroundColor16(NamedColor.magenta); - continue; - case 36: - handler.setForegroundColor16(NamedColor.cyan); - continue; - case 37: - handler.setForegroundColor16(NamedColor.white); - continue; - case 38: - i = _csiHandleExtendedColor(i, foreground: true); - continue; - case 39: - handler.resetForeground(); - continue; - - case 40: - handler.setBackgroundColor16(NamedColor.black); - continue; - case 41: - handler.setBackgroundColor16(NamedColor.red); - continue; - case 42: - handler.setBackgroundColor16(NamedColor.green); - continue; - case 43: - handler.setBackgroundColor16(NamedColor.yellow); - continue; - case 44: - handler.setBackgroundColor16(NamedColor.blue); - continue; - case 45: - handler.setBackgroundColor16(NamedColor.magenta); - continue; - case 46: - handler.setBackgroundColor16(NamedColor.cyan); - continue; - case 47: - handler.setBackgroundColor16(NamedColor.white); - continue; - case 48: - i = _csiHandleExtendedColor(i, foreground: false); - continue; - case 49: - handler.resetBackground(); - continue; - - case 90: - handler.setForegroundColor16(NamedColor.brightBlack); - continue; - case 91: - handler.setForegroundColor16(NamedColor.brightRed); - continue; - case 92: - handler.setForegroundColor16(NamedColor.brightGreen); - continue; - case 93: - handler.setForegroundColor16(NamedColor.brightYellow); - continue; - case 94: - handler.setForegroundColor16(NamedColor.brightBlue); - continue; - case 95: - handler.setForegroundColor16(NamedColor.brightMagenta); - continue; - case 96: - handler.setForegroundColor16(NamedColor.brightCyan); - continue; - case 97: - handler.setForegroundColor16(NamedColor.brightWhite); - continue; - - case 100: - handler.setBackgroundColor16(NamedColor.brightBlack); - continue; - case 101: - handler.setBackgroundColor16(NamedColor.brightRed); - continue; - case 102: - handler.setBackgroundColor16(NamedColor.brightGreen); - continue; - case 103: - handler.setBackgroundColor16(NamedColor.brightYellow); - continue; - case 104: - handler.setBackgroundColor16(NamedColor.brightBlue); - continue; - case 105: - handler.setBackgroundColor16(NamedColor.brightMagenta); - continue; - case 106: - handler.setBackgroundColor16(NamedColor.brightCyan); - continue; - case 107: - handler.setBackgroundColor16(NamedColor.brightWhite); - continue; - - default: - handler.unsupportedStyle(param); - continue; - } - } - } - - /// Extended fg/bg color (SGR 38/48), semicolon or colon form. - /// - /// Returns the index of the last parameter consumed. Never reads past the - /// end of the parameter list — a truncated sequence (`ESC [38m`, - /// `ESC [38;2;255m`) is ignored instead of throwing; an emulator must never - /// throw on hostile bytes (T-369). Colon-form sub-parameters per ITU T.416 - /// (`38:2:r:g:b`, `38:2::r:g:b`, `38:5:n`) are treated as one - /// logical group: parsed equivalently to the semicolon form, and dropped - /// whole when malformed so they never spill into neighbouring parameters. - int _csiHandleExtendedColor(int i, {required bool foreground}) { - final params = _csi.params; - final sub = _csi.subParam; - - // End of the colon-linked group starting at params[i] (exclusive). - var end = i + 1; - while (end < params.length && sub[end]) { - end++; - } - - if (end > i + 1) { - // Colon form. Group is params[i..end-1]; n includes the 38/48 itself. - final n = end - i; - final mode = params[i + 1]; - if (mode == 5 && n >= 3) { - foreground ? handler.setForegroundColor256(params[i + 2]) : handler.setBackgroundColor256(params[i + 2]); - } else if (mode == 2) { - // A 6+ element group carries the T.416 colorspace id slot — skip it. - final base = n >= 6 ? i + 3 : i + 2; - if (base + 2 < end) { - foreground - ? handler.setForegroundColorRgb(params[base], params[base + 1], params[base + 2]) - : handler.setBackgroundColorRgb(params[base], params[base + 1], params[base + 2]); - } - } - return end - 1; - } - - // Semicolon form (legacy). - if (i + 1 >= params.length) return i; // bare 38/48 — ignore - switch (params[i + 1]) { - case 2: - if (i + 4 >= params.length) return params.length - 1; // truncated — ignore - foreground - ? handler.setForegroundColorRgb(params[i + 2], params[i + 3], params[i + 4]) - : handler.setBackgroundColorRgb(params[i + 2], params[i + 3], params[i + 4]); - return i + 4; - case 5: - if (i + 2 >= params.length) return params.length - 1; // truncated — ignore - foreground ? handler.setForegroundColor256(params[i + 2]) : handler.setBackgroundColor256(params[i + 2]); - return i + 2; - } - // Unknown mode — consume it so it isn't re-interpreted as an SGR code. - return i + 1; - } - - /// `ESC [ Ps n` Device Status Report [Dispatch] (DSR) - /// - /// https://terminalguide.namepad.de/seq/csi_sn/ - void _csiHandleDeviceStatusReport() { - if (_csi.params.isEmpty) return; - - switch (_csi.params[0]) { - case 5: - return handler.sendOperatingStatus(); - case 6: - return handler.sendCursorPosition(); - } - } - - /// `ESC [ Ps ; Ps r` Set Top and Bottom Margins (DECSTBM) - /// - /// https://terminalguide.namepad.de/seq/csi_sr/ - void _csiHandleSetMargins() { - var top = 1; - int? bottom; - - if (_csi.params.length > 2) return; - - if (_csi.params.isNotEmpty) { - top = _csi.params[0]; - - if (_csi.params.length == 2) { - bottom = _csi.params[1] - 1; - } - } - - handler.setMargins(top - 1, bottom); - } - - /// `ESC [ Ps t` Window operations [DISPATCH] - /// - /// https://terminalguide.namepad.de/seq/csi_st/ - void _csiWindowManipulation() { - // The sequence needs at least one parameter. - if (_csi.params.isEmpty) { - return; - } - // Most the commands in this group are either of the scope of this package, - // or should be disabled for security risks. - switch (_csi.params.first) { - // Window handling is currently not in the scope of the package. - case 1: // Restore Terminal Window (show window if minimized) - case 2: // Minimize Terminal Window - case 3: // Set Terminal Window Position - case 4: // Set Terminal Window Size in Pixels - case 5: // Raise Terminal Window - case 6: // Lower Terminal Window - case 7: // Refresh/Redraw Terminal Window - return; - case 8: // Set Terminal Window Size (in characters) - // This CSI contains 2 more parameters: width and height. - if (_csi.params.length != 3) { - return; - } - final rows = _csi.params[1]; - final cols = _csi.params[2]; - handler.resize(cols, rows); - return; - // Window handling is currently no in the scope of the package. - case 9: // Maximize Terminal Window - case 10: // Alias: Maximize Terminal Window - case 11: // Report Terminal Window State - case 13: // Report Terminal Window Position - case 14: // Report Terminal Window Size in Pixels - case 15: // Report Screen Size in Pixels - case 16: // Report Cell Size in Pixels - return; - case 18: // Report Terminal Size (in characters) - handler.sendSize(); - return; - // Screen handling is currently no in the scope of the package. - case 19: // Report Screen Size (in characters) - // Disabled as these can a security risk. - case 20: // Get Icon Title - case 21: // Get Terminal Title - // Not implemented. - case 22: // Push Terminal Title - case 23: // Pop Terminal Title - return; - // Unknown CSI. - default: - return; - } - } - - /// `ESC [ Ps A` Cursor Up (CUU) - /// - /// https://terminalguide.namepad.de/seq/csi_ca/ - void _csiHandleCursorUp() { - var amount = 1; - - if (_csi.params.isNotEmpty) { - amount = _csi.params[0]; - if (amount == 0) amount = 1; - } - - handler.moveCursorY(-amount); - } - - /// `ESC [ Ps B` Cursor Down (CUD) - /// - /// https://terminalguide.namepad.de/seq/csi_cb/ - void _csiHandleCursorDown() { - var amount = 1; - - if (_csi.params.isNotEmpty) { - amount = _csi.params[0]; - if (amount == 0) amount = 1; - } - - handler.moveCursorY(amount); - } - - /// `ESC [ Ps C` Cursor Right (CUF) - /// - /// Cursor Right (CUF) - void _csiHandleCursorForward() { - var amount = 1; - - if (_csi.params.isNotEmpty) { - amount = _csi.params[0]; - if (amount == 0) amount = 1; - } - - handler.moveCursorX(amount); - } - - /// `ESC [ Ps D` Cursor Left (CUB) - /// - /// https://terminalguide.namepad.de/seq/csi_cd/ - void _csiHandleCursorBackward() { - var amount = 1; - - if (_csi.params.isNotEmpty) { - amount = _csi.params[0]; - if (amount == 0) amount = 1; - } - - handler.moveCursorX(-amount); - } - - /// `ESC [ Ps E` Cursor Next Line (CNL) - /// - /// https://terminalguide.namepad.de/seq/csi_ce/ - void _csiHandleCursorNextLine() { - var amount = 1; - - if (_csi.params.isNotEmpty) { - amount = _csi.params[0]; - if (amount == 0) amount = 1; - } - - handler.cursorNextLine(amount); - } - - /// `ESC [ Ps F` Cursor Previous Line (CPL) - /// - /// https://terminalguide.namepad.de/seq/csi_cf/ - void _csiHandleCursorPrecedingLine() { - var amount = 1; - - if (_csi.params.isNotEmpty) { - amount = _csi.params[0]; - if (amount == 0) amount = 1; - } - - handler.cursorPrecedingLine(amount); - } - - void _csiHandleCursorHorizontalAbsolute() { - var x = 1; - - if (_csi.params.isNotEmpty) { - x = _csi.params[0]; - if (x == 0) x = 1; - } - - handler.setCursorX(x - 1); - } - - /// ESC [ Ps J Erase Display [Dispatch] (ED) - /// - /// https://terminalguide.namepad.de/seq/csi_cj/ - void _csiHandleEraseDisplay() { - var cmd = 0; - - if (_csi.params.length == 1) { - cmd = _csi.params[0]; - } - - switch (cmd) { - case 0: - return handler.eraseDisplayBelow(); - case 1: - return handler.eraseDisplayAbove(); - case 2: - return handler.eraseDisplay(); - case 3: - return handler.eraseScrollbackOnly(); - } - } - - /// `ESC [ Ps K` Erase Line [Dispatch] (EL) - /// - /// https://terminalguide.namepad.de/seq/csi_ck/ - void _csiHandleEraseLine() { - var cmd = 0; - - if (_csi.params.length == 1) { - cmd = _csi.params[0]; - } - - switch (cmd) { - case 0: - return handler.eraseLineRight(); - case 1: - return handler.eraseLineLeft(); - case 2: - return handler.eraseLine(); - } - } - - /// `ESC [ Ps L` Insert Line (IL) - /// - /// https://terminalguide.namepad.de/seq/csi_cl/ - void _csiHandleInsertLines() { - var amount = 1; - - if (_csi.params.isNotEmpty) { - amount = _csi.params[0]; - } - - handler.insertLines(amount); - } - - /// ESC [ Ps M Delete Line (DL) - /// - /// https://terminalguide.namepad.de/seq/csi_cm/ - void _csiHandleDeleteLines() { - var amount = 1; - - if (_csi.params.isNotEmpty) { - amount = _csi.params[0]; - } - - handler.deleteLines(amount); - } - - /// ESC [ Ps P Delete Character (DCH) - /// - /// https://terminalguide.namepad.de/seq/csi_cp/ - void _csiHandleDelete() { - var amount = 1; - - if (_csi.params.isNotEmpty) { - amount = _csi.params[0]; - } - - handler.deleteChars(amount); - } - - /// `ESC [ Ps S` Scroll Up (SU) - /// - /// https://terminalguide.namepad.de/seq/csi_cs/ - void _csiHandleScrollUp() { - var amount = 1; - - if (_csi.params.isNotEmpty) { - amount = _csi.params[0]; - } - - handler.scrollUp(amount); - } - - /// `ESC [ Ps T `Scroll Down (SD) - /// - /// https://terminalguide.namepad.de/seq/csi_ct_1param/ - void _csiHandleScrollDown() { - var amount = 1; - - if (_csi.params.isNotEmpty) { - amount = _csi.params[0]; - } - - handler.scrollDown(amount); - } - - /// `ESC [ Ps X` Erase Character (ECH) - /// - /// https://terminalguide.namepad.de/seq/csi_cx/ - void _csiHandleEraseCharacters() { - var amount = 1; - - if (_csi.params.isNotEmpty) { - amount = _csi.params[0]; - } - - handler.eraseChars(amount); - } - - /// `ESC [ Ps @` Insert Blanks (ICH) - /// - /// https://terminalguide.namepad.de/seq/csi_x40_at/ - /// - /// Inserts amount spaces at current cursor position moving existing cell - /// contents to the right. The contents of the amount right-most columns in - /// the scroll region are lost. The cursor position is not changed. - void _csiHandleInsertBlankCharacters() { - var amount = 1; - - if (_csi.params.isNotEmpty) { - amount = _csi.params[0]; - } - - handler.insertBlankChars(amount); - } - - void _setMode(int mode, bool enabled) { - switch (mode) { - case 4: - return handler.setInsertMode(enabled); - case 20: - return handler.setLineFeedMode(enabled); - default: - return handler.setUnknownMode(mode, enabled); - } - } - - void _setDecMode(int mode, bool enabled) { - switch (mode) { - case 1: - return handler.setCursorKeysMode(enabled); - case 3: - return handler.setColumnMode(enabled); - case 5: - return handler.setReverseDisplayMode(enabled); - case 6: - return handler.setOriginMode(enabled); - case 7: - return handler.setAutoWrapMode(enabled); - case 9: - return enabled ? handler.setMouseMode(MouseMode.clickOnly) : handler.setMouseMode(MouseMode.none); - case 12: - case 13: - return handler.setCursorBlinkMode(enabled); - case 25: - return handler.setCursorVisibleMode(enabled); - case 47: - if (enabled) { - return handler.useAltBuffer(); - } else { - return handler.useMainBuffer(); - } - case 66: - return handler.setAppKeypadMode(enabled); - case 1000: - return enabled ? handler.setMouseMode(MouseMode.upDownScroll) : handler.setMouseMode(MouseMode.none); - case 1001: - return enabled ? handler.setMouseMode(MouseMode.upDownScroll) : handler.setMouseMode(MouseMode.none); - case 1002: - return enabled ? handler.setMouseMode(MouseMode.upDownScrollDrag) : handler.setMouseMode(MouseMode.none); - case 1003: - return enabled ? handler.setMouseMode(MouseMode.upDownScrollMove) : handler.setMouseMode(MouseMode.none); - case 1004: - return handler.setReportFocusMode(enabled); - case 1005: - return enabled ? handler.setMouseReportMode(MouseReportMode.utf) : handler.setMouseReportMode(MouseReportMode.normal); - case 1006: - return enabled ? handler.setMouseReportMode(MouseReportMode.sgr) : handler.setMouseReportMode(MouseReportMode.normal); - case 1007: - return handler.setAltBufferMouseScrollMode(enabled); - case 1015: - return enabled ? handler.setMouseReportMode(MouseReportMode.urxvt) : handler.setMouseReportMode(MouseReportMode.normal); - case 1047: - if (enabled) { - handler.useAltBuffer(); - } else { - handler.clearAltBuffer(); - handler.useMainBuffer(); - } - return; - case 1048: - if (enabled) { - return handler.saveCursor(); - } else { - return handler.restoreCursor(); - } - case 1049: - if (enabled) { - handler.saveCursor(); - handler.clearAltBuffer(); - handler.useAltBuffer(); - } else { - handler.useMainBuffer(); - } - return; - case 2004: - return handler.setBracketedPasteMode(enabled); - default: - return handler.setUnknownDecMode(mode, enabled); - } - } - - /// Parse a OSC sequence from the queue. Returns true if a sequence was - /// found and handled. - bool _escHandleOSC() { - final consumed = _consumeOsc(); - if (!consumed) { - return false; - } - - if (_osc.isEmpty) { - return true; - } - - // Common OSCs - if (_osc.length >= 2) { - final ps = _osc[0]; - final pt = _osc[1]; - - switch (ps) { - case '0': - handler.setTitle(pt); - handler.setIconName(pt); - return true; - case '1': - handler.setIconName(pt); - return true; - case '2': - handler.setTitle(pt); - return true; - } - } - - // Private extensions - handler.unknownOSC(_osc[0], _osc.sublist(1)); - - return true; - } - - final _osc = []; - - bool _consumeOsc() { - _osc.clear(); - final param = StringBuffer(); - - while (true) { - if (_queue.isEmpty) { - return false; - } - - final char = _queue.consume(); - - // OSC terminates with BEL - if (char == Ascii.BEL) { - _osc.add(param.toString()); - return true; - } - - /// OSC terminates with ST - if (char == Ascii.ESC) { - if (_queue.isEmpty) { - return false; - } - - if (_queue.consume() == Ascii.backslash) { - _osc.add(param.toString()); - } - - return true; - } - - /// Parse next parameter - if (char == Ascii.semicolon) { - _osc.add(param.toString()); - param.clear(); - continue; - } - - param.writeCharCode(char); - } - } } class _Csi { diff --git a/lib/src/terminal/src/core/escape/sgr_handlers.dart b/lib/src/terminal/src/core/escape/sgr_handlers.dart new file mode 100644 index 00000000..a8377554 --- /dev/null +++ b/lib/src/terminal/src/core/escape/sgr_handlers.dart @@ -0,0 +1,249 @@ +// Based on xterm.dart v4.0.0 by xuty (MIT). See LICENSE in this directory. + +// SGR (Select Graphic Rendition) handling, including the guarded +// extended-color (38/48) path with ITU T.416 colon sub-parameters +// (T-369). Split out of parser.dart (T-123). + +part of 'parser.dart'; + +mixin _SgrHandlers on _EscapeParserBase { + /// `ESC [ [ Ps ] m` Select Graphic Rendition (SGR) + /// + /// https://terminalguide.namepad.de/seq/csi_sm/ + void _csiHandleSgr() { + final params = _csi.params; + + if (params.isEmpty) { + return handler.resetCursorStyle(); + } + + for (var i = 0; i < _csi.params.length; i++) { + final param = params[i]; + switch (param) { + case 0: + handler.resetCursorStyle(); + continue; + case 1: + handler.setCursorBold(); + continue; + case 2: + handler.setCursorFaint(); + continue; + case 3: + handler.setCursorItalic(); + continue; + case 4: + handler.setCursorUnderline(); + continue; + case 5: + handler.setCursorBlink(); + continue; + case 7: + handler.setCursorInverse(); + continue; + case 8: + handler.setCursorInvisible(); + continue; + case 9: + handler.setCursorStrikethrough(); + continue; + + case 21: + handler.unsetCursorBold(); + continue; + case 22: + handler.unsetCursorFaint(); + continue; + case 23: + handler.unsetCursorItalic(); + continue; + case 24: + handler.unsetCursorUnderline(); + continue; + case 25: + handler.unsetCursorBlink(); + continue; + case 27: + handler.unsetCursorInverse(); + continue; + case 28: + handler.unsetCursorInvisible(); + continue; + case 29: + handler.unsetCursorStrikethrough(); + continue; + + case 30: + handler.setForegroundColor16(NamedColor.black); + continue; + case 31: + handler.setForegroundColor16(NamedColor.red); + continue; + case 32: + handler.setForegroundColor16(NamedColor.green); + continue; + case 33: + handler.setForegroundColor16(NamedColor.yellow); + continue; + case 34: + handler.setForegroundColor16(NamedColor.blue); + continue; + case 35: + handler.setForegroundColor16(NamedColor.magenta); + continue; + case 36: + handler.setForegroundColor16(NamedColor.cyan); + continue; + case 37: + handler.setForegroundColor16(NamedColor.white); + continue; + case 38: + i = _csiHandleExtendedColor(i, foreground: true); + continue; + case 39: + handler.resetForeground(); + continue; + + case 40: + handler.setBackgroundColor16(NamedColor.black); + continue; + case 41: + handler.setBackgroundColor16(NamedColor.red); + continue; + case 42: + handler.setBackgroundColor16(NamedColor.green); + continue; + case 43: + handler.setBackgroundColor16(NamedColor.yellow); + continue; + case 44: + handler.setBackgroundColor16(NamedColor.blue); + continue; + case 45: + handler.setBackgroundColor16(NamedColor.magenta); + continue; + case 46: + handler.setBackgroundColor16(NamedColor.cyan); + continue; + case 47: + handler.setBackgroundColor16(NamedColor.white); + continue; + case 48: + i = _csiHandleExtendedColor(i, foreground: false); + continue; + case 49: + handler.resetBackground(); + continue; + + case 90: + handler.setForegroundColor16(NamedColor.brightBlack); + continue; + case 91: + handler.setForegroundColor16(NamedColor.brightRed); + continue; + case 92: + handler.setForegroundColor16(NamedColor.brightGreen); + continue; + case 93: + handler.setForegroundColor16(NamedColor.brightYellow); + continue; + case 94: + handler.setForegroundColor16(NamedColor.brightBlue); + continue; + case 95: + handler.setForegroundColor16(NamedColor.brightMagenta); + continue; + case 96: + handler.setForegroundColor16(NamedColor.brightCyan); + continue; + case 97: + handler.setForegroundColor16(NamedColor.brightWhite); + continue; + + case 100: + handler.setBackgroundColor16(NamedColor.brightBlack); + continue; + case 101: + handler.setBackgroundColor16(NamedColor.brightRed); + continue; + case 102: + handler.setBackgroundColor16(NamedColor.brightGreen); + continue; + case 103: + handler.setBackgroundColor16(NamedColor.brightYellow); + continue; + case 104: + handler.setBackgroundColor16(NamedColor.brightBlue); + continue; + case 105: + handler.setBackgroundColor16(NamedColor.brightMagenta); + continue; + case 106: + handler.setBackgroundColor16(NamedColor.brightCyan); + continue; + case 107: + handler.setBackgroundColor16(NamedColor.brightWhite); + continue; + + default: + handler.unsupportedStyle(param); + continue; + } + } + } + + /// Extended fg/bg color (SGR 38/48), semicolon or colon form. + /// + /// Returns the index of the last parameter consumed. Never reads past the + /// end of the parameter list — a truncated sequence (`ESC [38m`, + /// `ESC [38;2;255m`) is ignored instead of throwing; an emulator must never + /// throw on hostile bytes (T-369). Colon-form sub-parameters per ITU T.416 + /// (`38:2:r:g:b`, `38:2::r:g:b`, `38:5:n`) are treated as one + /// logical group: parsed equivalently to the semicolon form, and dropped + /// whole when malformed so they never spill into neighbouring parameters. + int _csiHandleExtendedColor(int i, {required bool foreground}) { + final params = _csi.params; + final sub = _csi.subParam; + + // End of the colon-linked group starting at params[i] (exclusive). + var end = i + 1; + while (end < params.length && sub[end]) { + end++; + } + + if (end > i + 1) { + // Colon form. Group is params[i..end-1]; n includes the 38/48 itself. + final n = end - i; + final mode = params[i + 1]; + if (mode == 5 && n >= 3) { + foreground ? handler.setForegroundColor256(params[i + 2]) : handler.setBackgroundColor256(params[i + 2]); + } else if (mode == 2) { + // A 6+ element group carries the T.416 colorspace id slot — skip it. + final base = n >= 6 ? i + 3 : i + 2; + if (base + 2 < end) { + foreground + ? handler.setForegroundColorRgb(params[base], params[base + 1], params[base + 2]) + : handler.setBackgroundColorRgb(params[base], params[base + 1], params[base + 2]); + } + } + return end - 1; + } + + // Semicolon form (legacy). + if (i + 1 >= params.length) return i; // bare 38/48 — ignore + switch (params[i + 1]) { + case 2: + if (i + 4 >= params.length) return params.length - 1; // truncated — ignore + foreground + ? handler.setForegroundColorRgb(params[i + 2], params[i + 3], params[i + 4]) + : handler.setBackgroundColorRgb(params[i + 2], params[i + 3], params[i + 4]); + return i + 4; + case 5: + if (i + 2 >= params.length) return params.length - 1; // truncated — ignore + foreground ? handler.setForegroundColor256(params[i + 2]) : handler.setBackgroundColor256(params[i + 2]); + return i + 2; + } + // Unknown mode — consume it so it isn't re-interpreted as an SGR code. + return i + 1; + } +}