@cplieger/web-terminal-engine 5.2.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -1,14 +1,32 @@
1
1
  export * from "./types.js";
2
2
  export { LineStore } from "./store.js";
3
3
  export type { WindowState, StoreChanges, StoreSnapshot } from "./store.js";
4
- export * as render from "./render.js";
5
- export type { ViewMemory } from "./render.js";
4
+ export { createTerminalEngine } from "./terminal.js";
5
+ export type { TerminalEngine, TerminalEngineOptions } from "./terminal.js";
6
+ export { createRenderer } from "./render.js";
7
+ export type { Renderer, RendererOptions, ViewMemory } from "./render.js";
8
+ export { createScrollController } from "./scroll.js";
9
+ export type { ScrollController, ScrollControllerOptions } from "./scroll.js";
10
+ export { createConnection, MAX_REPLAY_LINES, MAX_OUTBOX_BYTES } from "./connection.js";
11
+ export type { Connection, ConnectionOptions, ConnectionCallbacks } from "./connection.js";
12
+ export { createMouseController, encodeSGR } from "./mouse.js";
13
+ export type { MouseController, MouseControllerOptions, MouseInputHandler } from "./mouse.js";
14
+ export { createModeState, POWER_ON_MODES } from "./modes.js";
15
+ export type { ModeState, ModeSnapshot } from "./modes.js";
16
+ /**
17
+ * Key encoding, and nothing else: `mapKeyboardEvent` turns a `KeyboardEvent`
18
+ * into the bytes a PTY expects or a local action, and the paste helpers do the
19
+ * same for text. It touches no DOM and sends nothing; mode state arrives as an
20
+ * explicit argument (an engine's `modes` satisfies it).
21
+ */
6
22
  export * as keyboard from "./keyboard.js";
23
+ /**
24
+ * The on-screen mobile key toolbar, the one DOM widget in the engine:
25
+ * `bindMobileToolbar` wires an existing container's buttons to a send sink and
26
+ * a mode state and returns a controller owning sticky-Ctrl state and tear-down.
27
+ * A missing button is skipped rather than fatal.
28
+ */
7
29
  export * as toolbar from "./toolbar.js";
8
- export * as scroll from "./scroll.js";
9
- export * as modes from "./modes.js";
10
- export * as mouse from "./mouse.js";
11
- export * as connection from "./connection.js";
12
30
  export { decodeWireBinary } from "./wire-binary.js";
13
31
  export {
14
32
  MIN_SUPPORTED_SERVER_WIRE_VERSION,
package/src/keyboard.ts CHANGED
@@ -1,20 +1,9 @@
1
- // Keyboard event → terminal byte sequence mapping.
2
- //
3
- // Mirrors xterm.js's evaluateKeyboardEvent (src/common/input/Keyboard.ts):
4
- // every browser KeyboardEvent maps to either a sequence to send to the
5
- // PTY or a local action (page-up/down for local scrollback nav). The
6
- // mapping is exhaustive over xterm.js's coverage so vim/readline/Ink/
7
- // the host application get the keys they expect.
8
- //
9
- // Modifier encoding follows xterm.js / VT520 convention:
10
- // 1=Shift, 2=Alt, 4=Ctrl, 8=Meta. Sum then +1 for CSI 1;{n}{letter}.
11
- //
12
- // Application cursor mode (DECCKM, CSI ?1h/l) is tracked client-side
13
- // via modes.ts (announced by server in wireMsgModes). Server-side, the
14
- // vt screen tracks both bracketed paste (?2004) and DECCKM (?1) and
15
- // emits a modes frame whenever they change.
16
-
17
- import { getKeyboardFlags, isBracketedPaste } from "./modes.js";
1
+ // Keyboard event → terminal byte sequence mapping, mirroring xterm.js's
2
+ // evaluateKeyboardEvent (src/common/input/Keyboard.ts) so vim, readline, Ink
3
+ // and the host application get the keys they expect. Modifier digit per the
4
+ // xterm/VT520 convention: 1=Shift, 2=Alt, 4=Ctrl, 8=Meta, summed then +1 for
5
+ // CSI 1;{n}{letter}. DECCKM and DECKPAM come from the injected mode state,
6
+ // which the server's modes frame keeps current.
18
7
 
19
8
  /** Result of mapping a keyboard event. */
20
9
  export type KeyboardResult =
@@ -26,10 +15,8 @@ export type KeyboardResult =
26
15
  /**
27
16
  * KeyboardModes is the mode state `mapKeyboardEvent` reads: DECCKM (application
28
17
  * cursor keys), DECKPAM (application keypad), and the kitty keyboard
29
- * progressive-enhancement flags. Passed explicitly so the shared input maps
30
- * against the active tab's modes and so the mapping is testable without mutating
31
- * global state. The `modes` module namespace satisfies this structurally; a
32
- * tabbed shell passes its active session's modes.
18
+ * progressive-enhancement flags. A `ModeState` satisfies it; a consumer passes
19
+ * the state of the terminal the key belongs to.
33
20
  */
34
21
  export interface KeyboardModes {
35
22
  isApplicationCursor: () => boolean;
@@ -59,17 +46,10 @@ type CursorKeyLetter = "A" | "B" | "C" | "D" | "H" | "F";
59
46
 
60
47
  /**
61
48
  * plainCursorKeySeq is THE encoding of an unmodified logical cursor-key press
62
- * (arrows, Home, End) — the one home for the mode decision, shared by the
63
- * physical-key path (csiLetter's modifier-less branch) and the mobile
64
- * toolbar's arrow buttons so the two encoders cannot drift:
65
- * - kitty disambiguate active: the CSI form regardless of DECCKM (the
66
- * protocol supersedes cursor-key mode);
67
- * - DECCKM (application cursor mode): the SS3 form (ESC O letter);
68
- * - otherwise: the bare CSI form.
69
- *
70
- * Both mode bits are PARAMETERS (not read from the module-global mode state):
71
- * mapKeyboardEvent honors an injected KeyboardModes, and a helper silently
72
- * consulting the global would diverge from it in a tabbed shell.
49
+ * (arrows, Home, End), shared by the physical-key path and the mobile toolbar's
50
+ * arrow buttons so the two encoders cannot drift: the CSI form under kitty
51
+ * disambiguate (the protocol supersedes cursor-key mode), the SS3 form
52
+ * (ESC O letter) under DECCKM, the bare CSI form otherwise.
73
53
  */
74
54
  export function plainCursorKeySeq(
75
55
  letter: CursorKeyLetter,
@@ -94,40 +74,22 @@ export function plainEscapeSeq(kittyActive: boolean): string {
94
74
  return kittyActive ? `${ESC}[27u` : ESC;
95
75
  }
96
76
 
97
- /** kittyActiveIn reads the disambiguate flag from an INJECTED KeyboardModes
98
- * (the seam mapKeyboardEvent honors), as opposed to kittyDisambiguateActive,
99
- * which reads the module-global active-session facade used by the toolbar.
100
- * connection.setSession restores that facade from the target session's
101
- * snapshot synchronously before switch-window input can be encoded. */
102
- function kittyActiveIn(modes: KeyboardModes): boolean {
77
+ /** True when the kitty disambiguate flag is active in `modes`. */
78
+ export function kittyDisambiguateActive(modes: KeyboardModes): boolean {
103
79
  return (modes.getKeyboardFlags() & KITTY_DISAMBIGUATE) !== 0;
104
80
  }
105
81
 
106
- // Letter is the xterm trailing letter for the keys that honor DECCKM: the four
107
- // arrows plus Home (H) and End (F). Without modifiers we send the bare CSI form;
108
- // with modifiers we send CSI 1;{mod}{letter}. xterm.js Keyboard.ts pattern.
109
- // (F1-F4's SS3/CSI pair is encoded inline at its own call site, not here.)
110
- //
111
- // The parameter type is `CursorKeyLetter` rather than `string`, so "every
112
- // letter reaching here honors DECCKM" is a compiler guarantee instead of a
113
- // runtime net: all three call sites pass A/B/C/D from ARROW_LETTER, or the H/F
114
- // literals.
115
- //
116
- // Application cursor mode (DECCKM, CSI ?1) is plumbed via modes.ts.
117
- // When the application has set DECCKM, the modifier-less form switches
118
- // from CSI to SS3 (ESC O letter) via plainCursorKeySeq; modifier-bearing
119
- // forms stay on CSI because they have no SS3 equivalent.
82
+ // The keys that honor DECCKM (arrows, Home, End): bare CSI or SS3 unmodified,
83
+ // CSI 1;{mod}{letter} with modifiers, which have no SS3 equivalent. The
84
+ // `CursorKeyLetter` parameter type makes "every letter here honors DECCKM" a
85
+ // compiler guarantee; F1-F4's SS3/CSI pair is encoded at its own call site.
120
86
  function csiLetter(letter: CursorKeyLetter, ev: KeyboardEvent, modes: KeyboardModes): string {
121
87
  const m = modifiersDigit(ev);
122
88
  if (m === 1) {
123
- // `kittyActiveIn(modes)` is always FALSE as reached from today's only
124
- // caller: mapKeyboardEvent returns through encodeKittyDisambiguate before
125
- // any cursor key gets here. It is passed anyway rather than hard-coded,
126
- // because plainCursorKeySeq is the one home for the mode decision and a
127
- // literal `false` would make this function structurally unable to express
128
- // the kitty form — a later change to that early return would then
129
- // mis-encode silently instead of failing to compile or reading wrong.
130
- return plainCursorKeySeq(letter, kittyActiveIn(modes), modes.isApplicationCursor());
89
+ // Read from `modes` rather than hard-coded false (mapKeyboardEvent returns
90
+ // through encodeKittyDisambiguate first) so a change to that early return
91
+ // cannot mis-encode silently.
92
+ return plainCursorKeySeq(letter, kittyDisambiguateActive(modes), modes.isApplicationCursor());
131
93
  }
132
94
  return `${ESC}[1;${m}${letter}`;
133
95
  }
@@ -199,18 +161,12 @@ const KEYPAD_SS3: Record<string, string | undefined> = {
199
161
  };
200
162
 
201
163
  // -- Kitty keyboard protocol (progressive enhancement) ----------------------
202
- // When the server reports an active kitty flag (modes.getKeyboardFlags), keys
203
- // that do NOT produce text are encoded per the kitty protocol instead of the
204
- // legacy encodings, so an app that enabled the protocol (e.g. Codex via
205
- // crossterm) gets unambiguous key events. We honor the disambiguate flag (0x1)
206
- // — report-event-types (0x2) / report-alternate-keys (0x4) / report-all (0x8) /
207
- // text (0x10) are masked off server-side, so the CSI ?u query reports only 0x1
208
- // and the encoder never has to emit them.
209
- //
210
- // Under disambiguate, text-producing keys still flow through the hidden
211
- // textarea as text (this encoder returns "ignore" for them), matching both the
212
- // spec (text keys stay text under 0x1) and our IME/composition model. Only
213
- // Escape, ctrl/alt/meta combinations, and functional keys are re-encoded.
164
+ // Under an active kitty flag, keys that do NOT produce text are encoded per the
165
+ // kitty protocol so an app that enabled it (Codex via crossterm) gets
166
+ // unambiguous key events. Only the disambiguate flag (0x1) is honored; the
167
+ // server masks off 0x2/0x4/0x8/0x10, so the CSI ?u query reports only 0x1.
168
+ // Text-producing keys still flow through the hidden textarea as text (this
169
+ // encoder returns "ignore" for them), per the spec and the IME model.
214
170
 
215
171
  /** Kitty progressive-enhancement flag bits (mirror vt/kitty.go). */
216
172
  const KITTY_DISAMBIGUATE = 1;
@@ -339,17 +295,13 @@ export function kittyCtrlCharSeq(ch: string): string | null {
339
295
  return `${ESC}[${cp};${shifted ? 6 : 5}u`; // 5 = ctrl, 6 = ctrl+shift
340
296
  }
341
297
 
342
- // Keypad NON-TEXT keys under disambiguate, keyed by ev.key (the key's current
343
- // function — NumLock flips a numpad key between its text digit, which stays text
344
- // under 0x1, and these navigation functions). ev.code identifies it as a keypad
345
- // key. Per the spec these get dedicated KP_* codes (57414-57427) so an app can
346
- // tell them apart from the main navigation keys. NumpadEnter is included as
347
- // KP_ENTER (57414) — it is a distinct key, NOT the legacy-byte main Enter.
348
- // KP_BEGIN (Numpad5, NumLock off -> "Clear") is emitted as 57427 in the `u`
349
- // form: both kitty and crossterm decode `CSI 57427 u`, whereas the spec's
350
- // alternate `CSI E` letter form is kitty-only (crossterm/ratatui can't parse it).
351
- // The KP_ DIGIT/operator codes (57399-57416) are deliberately absent — they are
352
- // a report-all (0x8) feature; under 0x1 a NumLock-on numpad digit is plain text.
298
+ // Keypad NON-TEXT keys under disambiguate, keyed by ev.key (NumLock flips a
299
+ // numpad key between its text digit, which stays text under 0x1, and these
300
+ // functions), with the spec's dedicated KP_* codes (57414-57427). NumpadEnter
301
+ // is KP_ENTER, a distinct key from the legacy-byte main Enter. KP_BEGIN is
302
+ // emitted in the `u` form because the spec's alternate `CSI E` letter form is
303
+ // kitty-only (crossterm/ratatui cannot parse it). The KP_ digit/operator codes
304
+ // (57399-57416) are a report-all (0x8) feature and are deliberately absent.
353
305
  const KITTY_KEYPAD: Record<string, number | undefined> = {
354
306
  ArrowLeft: 57417, // KP_LEFT
355
307
  ArrowRight: 57418, // KP_RIGHT
@@ -490,28 +442,12 @@ function encodeKittyDisambiguate(ev: KeyboardEvent): KeyboardResult {
490
442
  return { kind: "ignore" };
491
443
  }
492
444
 
493
- /** True when the kitty disambiguate flag is active in the GLOBAL
494
- * active-session mode facade. Exported for the toolbar widget (toolbar.ts);
495
- * connection.setSession synchronously restores the target session's cached
496
- * snapshot before input resumes. Encoder paths in this module use
497
- * kittyActiveIn (the injected-modes read) instead. */
498
- export function kittyDisambiguateActive(): boolean {
499
- return (getKeyboardFlags() & KITTY_DISAMBIGUATE) !== 0;
500
- }
501
-
502
445
  /**
503
- * mapKeyboardEvent converts a KeyboardEvent into the terminal action
504
- * to take. Returns "ignore" when the event is purely a modifier press
505
- * or when the browser should be allowed to handle it (e.g. browser
506
- * shortcuts like Cmd+R).
507
- *
508
- * Caller is responsible for ev.preventDefault() when the result is
509
- * "send" or "scroll-*"; we don't call it here so the function stays
510
- * pure and testable.
511
- *
512
- * `modes` supplies the active session's DECCKM/DECKPAM state; pass the `modes`
513
- * module namespace for the single-terminal case or the active tab's modes in a
514
- * tabbed shell.
446
+ * mapKeyboardEvent converts a KeyboardEvent into the terminal action to take,
447
+ * read against `modes`, the mode state of the terminal the key belongs to.
448
+ * Returns "ignore" for a bare modifier press or a key the browser should keep
449
+ * (Cmd+R). The caller calls `ev.preventDefault()` on "send" and "scroll-*";
450
+ * this function stays pure.
515
451
  */
516
452
  export function mapKeyboardEvent(ev: KeyboardEvent, modes: KeyboardModes): KeyboardResult {
517
453
  // Modifier-only presses (Shift, Ctrl, Alt, Meta) — no-op.
@@ -683,22 +619,10 @@ export function mapKeyboardEvent(ev: KeyboardEvent, modes: KeyboardModes): Keybo
683
619
  }
684
620
 
685
621
  /**
686
- * ctrlByteFor returns the C0 control byte produced by Ctrl+`ch`, or
687
- * `null` when the character has no Ctrl mapping. The full table:
688
- *
689
- * a-z (case-folded) → \x01..\x1a (Ctrl+A=SOH .. Ctrl+Z=SUB)
690
- * ' ' (space) → \x00 (NUL — same as Ctrl+@)
691
- * '@' → \x00 (NUL)
692
- * '[' → \x1b (ESC)
693
- * '\\' → \x1c (FS)
694
- * ']' → \x1d (GS)
695
- * '^' → \x1e (RS)
696
- * '_' → \x1f (US)
697
- * '?' → \x7f (DEL — also Ctrl+8 on US layouts via Shift+/)
698
- *
699
- * Anything else (multi-char strings, unmapped single chars) returns
700
- * `null`. Used by `mapKeyboardEvent` for Ctrl+printable handling and
701
- * by `bindMobileToolbar`'s sticky-Ctrl applier.
622
+ * ctrlByteFor returns the C0 control byte produced by Ctrl+`ch`, or `null`
623
+ * when the character has no Ctrl mapping: a-z (case-folded) give \x01..\x1a,
624
+ * space and '@' give NUL, '[' '\\' ']' '^' '_' give \x1b..\x1f, '?' gives DEL.
625
+ * A multi-character string or an unmapped character returns `null`.
702
626
  */
703
627
  export function ctrlByteFor(ch: string): string | null {
704
628
  if (ch.length !== 1) {
@@ -774,21 +698,20 @@ function numpadCodeToKey(code: string): string | undefined {
774
698
 
775
699
  // -- Bracketed paste --------------------------------------------------------
776
700
 
701
+ /** The mode bit `bracketTextForPaste` reads; a `ModeState` satisfies it. */
702
+ export interface PasteModes {
703
+ isBracketedPaste(): boolean;
704
+ }
705
+
777
706
  /**
778
- * bracketTextForPaste wraps text in DEC 2004 bracketed-paste sentinels
779
- * after sanitising any embedded ESC bytes to U+241B (visible escape
780
- * symbol), but only when the application has currently enabled
781
- * bracketed-paste mode (CSI ?2004h). When disabled, returns the text
782
- * unchanged. The current mode state is owned by modes.ts, kept in
783
- * sync by the server's wireMsgModes wire frame.
784
- *
785
- * The ESC sanitisation defends against an attacker-controlled paste
786
- * containing \x1b[201~ that would prematurely close the paste region
787
- * and let the rest be interpreted as a command — only relevant when
788
- * we are bracketing.
707
+ * bracketTextForPaste wraps text in DEC 2004 bracketed-paste sentinels when
708
+ * `modes` reports bracketed-paste mode (CSI ?2004h) enabled, and returns it
709
+ * unchanged otherwise. Embedded ESC bytes are replaced with U+241B first: an
710
+ * attacker-controlled paste containing \x1b[201~ would otherwise close the
711
+ * paste region early and have the rest interpreted as a command.
789
712
  */
790
- export function bracketTextForPaste(text: string): string {
791
- if (!isBracketedPaste()) {
713
+ export function bracketTextForPaste(text: string, modes: PasteModes): string {
714
+ if (!modes.isBracketedPaste()) {
792
715
  return text;
793
716
  }
794
717
  // eslint-disable-next-line no-control-regex -- intentional: sanitising ESC bytes in pasted text
package/src/modes.ts CHANGED
@@ -1,31 +1,4 @@
1
- // DEC private mode state — THE ACTIVE SESSION'S VIEW (P3). This singleton
2
- // mirrors the modes of whichever session the live socket serves: the
3
- // connection module is the single writer (it applies every inbound modes
4
- // frame via applySnapshot AND, in a tabbed shell, synchronously restores the
5
- // target session's cached snapshot inside setSession — power-on defaults for
6
- // a session never seen). Readers therefore never observe another session's
7
- // modes after setSession returns; the only staleness left is the inherent
8
- // one-frame lag against the session's own server state. Used by:
9
- // - keyboard.ts arrow-key encoder: emits SS3 (ESC O letter) when
10
- // applicationCursor is true, CSI (ESC [ letter) otherwise.
11
- // - paste handling: wraps in \e[200~..\e[201~ only when
12
- // bracketedPaste is on; otherwise sends raw text.
13
- // - mouse.ts: encodes mouse events only when mouseMode > 0.
14
- //
15
- // Defaults: bracketedPaste starts true because most modern shells
16
- // (bash 4.4+, zsh, fish) enable it immediately on startup via
17
- // CSI ?2004h. Starting false would cause the first paste before the
18
- // server's modes frame arrives to be sent un-bracketed, which shells
19
- // interpret as typed input (potentially executing pasted commands
20
- // character-by-character). applicationCursor starts false (normal
21
- // mode) because that's the VT100 power-on default.
22
-
23
- /**
24
- * One session's complete DEC-mode mirror, as a typed value (P3). The
25
- * connection module caches one per session and applies it via applySnapshot;
26
- * the named-field shape (vs setModes' nine positional parameters) makes
27
- * field-order drift impossible for production writers.
28
- */
1
+ /** One session's complete DEC-mode mirror, as a typed value. */
29
2
  export interface ModeSnapshot {
30
3
  bracketedPaste: boolean;
31
4
  applicationCursor: boolean;
@@ -39,11 +12,12 @@ export interface ModeSnapshot {
39
12
  }
40
13
 
41
14
  /**
42
- * The VT power-on mode state — what a session that has never announced modes
43
- * is in. Matches this module's initial values (see the bracketedPaste
44
- * default rationale above).
15
+ * The VT power-on mode state, frozen: copy it (`{ ...POWER_ON_MODES }`) before
16
+ * changing a field. `bracketedPaste` starts true because modern shells enable
17
+ * DEC 2004 at startup, and a paste delivered un-bracketed before the server's
18
+ * first modes frame would be executed as typed input.
45
19
  */
46
- export const POWER_ON_MODES: Readonly<ModeSnapshot> = {
20
+ export const POWER_ON_MODES: Readonly<ModeSnapshot> = Object.freeze({
47
21
  bracketedPaste: true,
48
22
  applicationCursor: false,
49
23
  mouseSGR: false,
@@ -53,144 +27,96 @@ export const POWER_ON_MODES: Readonly<ModeSnapshot> = {
53
27
  reverseVideo: false,
54
28
  mousePixels: false,
55
29
  keyboardFlags: 0,
56
- };
30
+ });
57
31
 
58
- let bracketedPaste = true;
59
- let applicationCursor = false;
60
- let applicationKeypad = false;
61
- let mouseMode = 0; // 0=off, 1000=normal, 1002=button-event, 1003=any-event
62
- let mouseSGR = false;
63
- let mousePixels = false; // DEC 1016: report pixel coords instead of cell coords
64
- let focusReporting = false;
65
- let reverseVideo = false;
66
- // Kitty keyboard progressive-enhancement flags (bit0 disambiguate, bit1
67
- // event-types, bit2 alternate-keys). 0 = protocol disabled (legacy encoding).
68
- let keyboardFlags = 0;
69
-
70
- /**
71
- * Update the cached mode state. Called by the consumer whenever the server
72
- * sends a `ModesMessage`. Pass-through arguments are optional so older
73
- * server builds that don't include all fields don't reset them to defaults.
74
- */
75
- export function setModes(
76
- bracketed: boolean,
77
- appCursor: boolean,
78
- mSGR?: boolean,
79
- focus?: boolean,
80
- mMode?: number,
81
- appKeypad?: boolean,
82
- revVideo?: boolean,
83
- mPixels?: boolean,
84
- kbdFlags?: number,
85
- ): void {
86
- bracketedPaste = bracketed;
87
- applicationCursor = appCursor;
88
- if (mSGR !== undefined) {
89
- mouseSGR = mSGR;
90
- }
91
- if (focus !== undefined) {
92
- focusReporting = focus;
93
- }
94
- if (mMode !== undefined) {
95
- mouseMode = mMode;
96
- }
97
- if (appKeypad !== undefined) {
98
- applicationKeypad = appKeypad;
99
- }
100
- if (revVideo !== undefined) {
101
- reverseVideo = revVideo;
102
- }
103
- if (mPixels !== undefined) {
104
- mousePixels = mPixels;
105
- }
106
- if (kbdFlags !== undefined) {
107
- keyboardFlags = kbdFlags;
108
- }
32
+ /** The DEC private-mode state of one terminal instance. */
33
+ export interface ModeState {
34
+ /** True when the server has DEC 2004 (bracketed paste) enabled. */
35
+ isBracketedPaste(): boolean;
36
+ /** True when the server has DECCKM (application cursor keys) enabled. */
37
+ isApplicationCursor(): boolean;
38
+ /** Active mouse tracking mode (xterm DECSET): 0 = off, 1000 = normal, 1002 = button-event, 1003 = any-event. */
39
+ getMouseMode(): number;
40
+ /** True when the server has DEC 1006 (SGR mouse encoding) enabled. */
41
+ isMouseSGR(): boolean;
42
+ /** True when the server has DEC 1016 (SGR-pixels mouse) enabled: reports carry pixel coordinates. */
43
+ isMousePixels(): boolean;
44
+ /** True when the server has DEC 1004 (focus event reporting) enabled. */
45
+ isFocusReporting(): boolean;
46
+ /** True when the server has DECKPAM (application keypad) enabled. */
47
+ isApplicationKeypad(): boolean;
48
+ /** True when the server has DEC 5 (reverse video / DECSCNM) enabled. */
49
+ isReverseVideo(): boolean;
50
+ /**
51
+ * Kitty keyboard progressive-enhancement flags in effect (bit0 disambiguate,
52
+ * bit1 report-event-types, bit2 report-alternate-keys); 0 means legacy encoding.
53
+ */
54
+ getKeyboardFlags(): number;
55
+ /** Replaces every mode bit with the snapshot's; no field is left at its previous value. */
56
+ applySnapshot(s: Readonly<ModeSnapshot>): void;
57
+ /** The current mode state as a snapshot value (a copy; safe to retain). */
58
+ snapshot(): ModeSnapshot;
59
+ /** Resets every field to `POWER_ON_MODES` and makes `applySnapshot` a no-op. */
60
+ dispose(): void;
109
61
  }
110
62
 
111
- /**
112
- * Apply a complete mode snapshot — every field, no optional-leaves-unchanged
113
- * semantics (that is setModes' positional back-compat contract). The
114
- * connection module uses this for both inbound modes frames and the
115
- * synchronous per-session restore in setSession.
116
- */
117
- export function applySnapshot(s: Readonly<ModeSnapshot>): void {
118
- bracketedPaste = s.bracketedPaste;
119
- applicationCursor = s.applicationCursor;
120
- mouseSGR = s.mouseSGR;
121
- focusReporting = s.focusReporting;
122
- mouseMode = s.mouseMode;
123
- applicationKeypad = s.applicationKeypad;
124
- reverseVideo = s.reverseVideo;
125
- mousePixels = s.mousePixels;
126
- keyboardFlags = s.keyboardFlags;
127
- }
63
+ /** Creates independent mode state initialized from a copy of `initial` (default `POWER_ON_MODES`). */
64
+ export function createModeState(initial: Readonly<ModeSnapshot> = POWER_ON_MODES): ModeState {
65
+ let bracketedPaste = initial.bracketedPaste;
66
+ let applicationCursor = initial.applicationCursor;
67
+ let mouseSGR = initial.mouseSGR;
68
+ let focusReporting = initial.focusReporting;
69
+ let mouseMode = initial.mouseMode;
70
+ let applicationKeypad = initial.applicationKeypad;
71
+ let reverseVideo = initial.reverseVideo;
72
+ let mousePixels = initial.mousePixels;
73
+ let keyboardFlags = initial.keyboardFlags;
74
+ let disposed = false;
75
+
76
+ function assign(s: Readonly<ModeSnapshot>): void {
77
+ bracketedPaste = s.bracketedPaste;
78
+ applicationCursor = s.applicationCursor;
79
+ mouseSGR = s.mouseSGR;
80
+ focusReporting = s.focusReporting;
81
+ mouseMode = s.mouseMode;
82
+ applicationKeypad = s.applicationKeypad;
83
+ reverseVideo = s.reverseVideo;
84
+ mousePixels = s.mousePixels;
85
+ keyboardFlags = s.keyboardFlags;
86
+ }
128
87
 
129
- /** The current mode state as a snapshot value (a copy; safe to retain). */
130
- export function snapshot(): ModeSnapshot {
131
88
  return {
132
- bracketedPaste,
133
- applicationCursor,
134
- mouseSGR,
135
- focusReporting,
136
- mouseMode,
137
- applicationKeypad,
138
- reverseVideo,
139
- mousePixels,
140
- keyboardFlags,
89
+ isBracketedPaste: () => bracketedPaste,
90
+ isApplicationCursor: () => applicationCursor,
91
+ getMouseMode: () => mouseMode,
92
+ isMouseSGR: () => mouseSGR,
93
+ isMousePixels: () => mousePixels,
94
+ isFocusReporting: () => focusReporting,
95
+ isApplicationKeypad: () => applicationKeypad,
96
+ isReverseVideo: () => reverseVideo,
97
+ getKeyboardFlags: () => keyboardFlags,
98
+ applySnapshot(s: Readonly<ModeSnapshot>): void {
99
+ if (disposed) {
100
+ return;
101
+ }
102
+ assign(s);
103
+ },
104
+ snapshot(): ModeSnapshot {
105
+ return {
106
+ bracketedPaste,
107
+ applicationCursor,
108
+ mouseSGR,
109
+ focusReporting,
110
+ mouseMode,
111
+ applicationKeypad,
112
+ reverseVideo,
113
+ mousePixels,
114
+ keyboardFlags,
115
+ };
116
+ },
117
+ dispose(): void {
118
+ disposed = true;
119
+ assign(POWER_ON_MODES);
120
+ },
141
121
  };
142
122
  }
143
-
144
- /** True when the server has DEC 2004 (bracketed paste) enabled. */
145
- export function isBracketedPaste(): boolean {
146
- return bracketedPaste;
147
- }
148
-
149
- /** True when the server has DECCKM (application cursor keys) enabled. */
150
- export function isApplicationCursor(): boolean {
151
- return applicationCursor;
152
- }
153
-
154
- /**
155
- * Active mouse tracking mode (xterm DECSET): 0 = off, 1000 = normal,
156
- * 1002 = button-event, 1003 = any-event.
157
- */
158
- export function getMouseMode(): number {
159
- return mouseMode;
160
- }
161
-
162
- /** True when the server has DEC 1006 (SGR mouse encoding) enabled. */
163
- export function isMouseSGR(): boolean {
164
- return mouseSGR;
165
- }
166
-
167
- /** True when the server has DEC 1016 (SGR-pixels mouse) enabled: mouse reports
168
- * carry pixel coordinates instead of cell coordinates. */
169
- export function isMousePixels(): boolean {
170
- return mousePixels;
171
- }
172
-
173
- /** True when the server has DEC 1004 (focus event reporting) enabled. */
174
- export function isFocusReporting(): boolean {
175
- return focusReporting;
176
- }
177
-
178
- /** True when the server has DECKPAM (application keypad) enabled. */
179
- export function isApplicationKeypad(): boolean {
180
- return applicationKeypad;
181
- }
182
-
183
- /** True when the server has DEC 5 (reverse video / DECSCNM) enabled. */
184
- export function isReverseVideo(): boolean {
185
- return reverseVideo;
186
- }
187
-
188
- /**
189
- * Kitty keyboard progressive-enhancement flags currently in effect (bit0
190
- * disambiguate, bit1 report-event-types, bit2 report-alternate-keys). 0 means
191
- * the protocol is disabled and keys use legacy encoding. Read by keyboard.ts to
192
- * choose between legacy and kitty CSI-u encoding.
193
- */
194
- export function getKeyboardFlags(): number {
195
- return keyboardFlags;
196
- }