@cplieger/web-terminal-engine 5.3.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/README.md +121 -61
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/connection.ts +1205 -1676
- package/src/index.ts +20 -100
- package/src/keyboard.ts +56 -133
- package/src/modes.ts +92 -166
- package/src/mouse.ts +380 -413
- package/src/render.ts +1894 -2355
- package/src/scroll.ts +225 -476
- package/src/store.ts +1 -1
- package/src/terminal.ts +135 -0
- package/src/toolbar.ts +29 -43
package/src/index.ts
CHANGED
|
@@ -1,112 +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 { 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";
|
|
4
16
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* (`updateFontMetrics`, `computeSize`, `getCursorPx`), and to swap stores on a
|
|
10
|
-
* tab switch (`bind`).
|
|
11
|
-
*
|
|
12
|
-
* `init` must run once against the two elements before any frame is applied,
|
|
13
|
-
* and it always installs a fresh implicit store — so a consumer that manages
|
|
14
|
-
* its own stores binds AFTER init, never before. `getHighestIndex` reports how
|
|
15
|
-
* far the store's content reaches and is NOT a resume claim; a transport asks
|
|
16
|
-
* `getReplayBoundary` for that, and wiring the former is the defect the latter
|
|
17
|
-
* exists to prevent.
|
|
18
|
-
*/
|
|
19
|
-
export * as render from "./render.js";
|
|
20
|
-
export type { ViewMemory } from "./render.js";
|
|
21
|
-
/**
|
|
22
|
-
* Key encoding, and nothing else: `mapKeyboardEvent` turns a browser
|
|
23
|
-
* `KeyboardEvent` into either the bytes a PTY expects or a local action, and
|
|
24
|
-
* the paste helpers (`bracketTextForPaste`, `prepareTextForTerminal`) do the
|
|
25
|
-
* same for text. Reach for it when your own input surface — a hidden textarea,
|
|
26
|
-
* an IME composition, a paste handler — has an event and needs the wire form.
|
|
27
|
-
*
|
|
28
|
-
* It touches no DOM and sends nothing: the caller delivers the bytes, and the
|
|
29
|
-
* `scroll-up` / `scroll-down` results carry no bytes at all because local
|
|
30
|
-
* scrollback navigation is the consumer's to perform. Mode state arrives as an
|
|
31
|
-
* explicit `KeyboardModes` argument rather than being read from a global, so a
|
|
32
|
-
* tabbed shell encodes against the active session's modes.
|
|
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).
|
|
33
21
|
*/
|
|
34
22
|
export * as keyboard from "./keyboard.js";
|
|
35
23
|
/**
|
|
36
|
-
* The on-screen mobile key toolbar
|
|
37
|
-
* `bindMobileToolbar` wires an existing container's buttons
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* physical keyboard supplies have to be painted.
|
|
41
|
-
*
|
|
42
|
-
* The consumer supplies the markup; the module only looks buttons up by id
|
|
43
|
-
* within the container, so a missing button is skipped rather than fatal.
|
|
44
|
-
* Sticky Ctrl only takes effect on text routed through
|
|
45
|
-
* `controller.applyStickyCtrl` — the toolbar cannot see keystrokes it did not
|
|
46
|
-
* emit. Its arrow and Escape bytes come from `keyboard`'s exported logical-key
|
|
47
|
-
* encodings, so they cannot drift from the physical-key path.
|
|
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.
|
|
48
28
|
*/
|
|
49
29
|
export * as toolbar from "./toolbar.js";
|
|
50
|
-
/**
|
|
51
|
-
* The scroll controller: the single owner of the scroll container's
|
|
52
|
-
* `scrollTop`, holding one piece of state — whether the viewport is following
|
|
53
|
-
* the tail or holding a reading position — derived from scroll events alone.
|
|
54
|
-
* Reach for it to `init` the container and for consumer-facing controls: a
|
|
55
|
-
* jump-to-bottom button (`scrollToBottom`), and `isUserScrolledUp` to decide
|
|
56
|
-
* whether new output deserves an affordance.
|
|
57
|
-
*
|
|
58
|
-
* Everything else here is the renderer's to call around its own DOM mutations
|
|
59
|
-
* (`stickToBottom`, `adjustForContentShift`, `noteContentShrink`). A consumer
|
|
60
|
-
* that writes `scrollTop` directly is competing with this module for the same
|
|
61
|
-
* value, which is how a held reading position gets lost.
|
|
62
|
-
*/
|
|
63
|
-
export * as scroll from "./scroll.js";
|
|
64
|
-
/**
|
|
65
|
-
* The DEC private-mode mirror for the ACTIVE session — bracketed paste,
|
|
66
|
-
* application cursor and keypad, mouse tracking and its encoding, focus
|
|
67
|
-
* reporting, reverse video, kitty keyboard flags. Reach for it to read what the
|
|
68
|
-
* application has turned on, most often to decide whether a paste needs
|
|
69
|
-
* bracketing or whether a mouse gesture belongs to the terminal at all.
|
|
70
|
-
*
|
|
71
|
-
* It is a module singleton with one writer: `connection` applies every inbound
|
|
72
|
-
* modes frame and, in a tabbed shell, restores the target session's snapshot
|
|
73
|
-
* inside `setSession`. A consumer that calls `setModes` is racing that writer.
|
|
74
|
-
* Values lag the server by the one frame in flight, and an unseen session
|
|
75
|
-
* starts at `POWER_ON_MODES`, where bracketed paste is already ON so the first
|
|
76
|
-
* paste cannot be delivered as typed input.
|
|
77
|
-
*/
|
|
78
|
-
export * as modes from "./modes.js";
|
|
79
|
-
/**
|
|
80
|
-
* Mouse input for the terminal: SGR 1006 encoding (`encodeSGR`) plus `init`,
|
|
81
|
-
* which attaches pointer and wheel listeners to an element and returns an
|
|
82
|
-
* idempotent disposer. Reach for it when the terminal element should forward
|
|
83
|
-
* clicks, drags and wheel ticks to the application rather than the page.
|
|
84
|
-
*
|
|
85
|
-
* The listeners gate on the mode state, so they are inert until the
|
|
86
|
-
* application enables tracking — which is also why selection and page
|
|
87
|
-
* scrolling keep working in a normal shell. Reports are best-effort: one
|
|
88
|
-
* describes a screen a resume may since have repainted, so a refused report is
|
|
89
|
-
* forgotten rather than retried. Focus is not mouse input and lives in
|
|
90
|
-
* `connection.setClientFocus`.
|
|
91
|
-
*/
|
|
92
|
-
export * as mouse from "./mouse.js";
|
|
93
|
-
/**
|
|
94
|
-
* The client-to-server transport. It owns the socket, the jittered
|
|
95
|
-
* exponential-backoff reconnect, and the resume/inputAck layer that makes input
|
|
96
|
-
* survive a reconnect: a bounded outbox retransmitted against the server's
|
|
97
|
-
* acked byte count, resume by absolute index, and server-restart detection via
|
|
98
|
-
* the boot epoch. Reach for it to `init` the callbacks, `connect`, send input
|
|
99
|
-
* (`sendBinary`), announce geometry (`sendResize`), and switch sessions
|
|
100
|
-
* (`setSession`).
|
|
101
|
-
*
|
|
102
|
-
* It decodes frames and applies mode changes itself, so a consumer's
|
|
103
|
-
* `onMessage` only has to route screen and scroll to the renderer. Supply as
|
|
104
|
-
* few of the resume and history callbacks as possible: an omitted one gets the
|
|
105
|
-
* renderer's own answer, read per resume rather than captured, and overriding
|
|
106
|
-
* one means deciding a local fact on the engine's behalf — `getReplayMax`, how
|
|
107
|
-
* much history the client keeps resident, is the one override that is expected.
|
|
108
|
-
*/
|
|
109
|
-
export * as connection from "./connection.js";
|
|
110
30
|
export { decodeWireBinary } from "./wire-binary.js";
|
|
111
31
|
export {
|
|
112
32
|
MIN_SUPPORTED_SERVER_WIRE_VERSION,
|
package/src/keyboard.ts
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
|
-
// Keyboard event → terminal byte sequence mapping.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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.
|
|
30
|
-
*
|
|
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)
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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
|
-
/**
|
|
98
|
-
|
|
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
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
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
|
-
// `
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
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
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
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 (
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
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
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
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
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
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
|
-
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
*
|
|
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
|