@uniflowed/tui 0.0.0-alpha.18
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/capability.js +366 -0
- package/cells.js +332 -0
- package/components.js +602 -0
- package/diff.js +286 -0
- package/index.js +206 -0
- package/internal/hits.js +156 -0
- package/internal/host.js +928 -0
- package/internal/paint.js +664 -0
- package/internal/tree.js +373 -0
- package/keys.js +585 -0
- package/layout.js +833 -0
- package/mouse.js +333 -0
- package/package.json +36 -0
- package/selection.js +101 -0
- package/terminal.js +457 -0
- package/widths.js +201 -0
package/keys.js
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// Terminal bytes in, key events out.
|
|
4
|
+
//
|
|
5
|
+
// A terminal does not deliver keys. It delivers a byte stream in which most
|
|
6
|
+
// keys are one byte, some are two, and the interesting ones are escape
|
|
7
|
+
// sequences of four to nine bytes whose grammar predates every convention a
|
|
8
|
+
// reader would guess from. `Escape` and `Alt+A` begin with the same byte.
|
|
9
|
+
// `Enter` is `\r` under raw mode and `\n` when it is not. The up arrow is
|
|
10
|
+
// `ESC [ A` on one terminal and `ESC O A` on another, depending on a mode the
|
|
11
|
+
// application itself may have set.
|
|
12
|
+
//
|
|
13
|
+
// This module is the whole of that grammar, and it is a pure function from a
|
|
14
|
+
// string to events so that a test can press a key without a terminal. Every
|
|
15
|
+
// input test in `tui.test.js` goes through here, which is what makes them
|
|
16
|
+
// tests of the real path rather than tests of a fake one.
|
|
17
|
+
//
|
|
18
|
+
// # One stream, two kinds of event
|
|
19
|
+
//
|
|
20
|
+
// A terminal with mouse reporting on writes its reports into the same stream,
|
|
21
|
+
// as escape sequences that are not keys. So the decoder's output is a union:
|
|
22
|
+
// {@link InputEvent} is a key or a mouse report, told apart by `kind`, and
|
|
23
|
+
// `mouse.js` holds the half this module does not. Splitting them at the byte
|
|
24
|
+
// level rather than after the fact is not a preference — a mouse report and
|
|
25
|
+
// `Alt+[` begin with the same two bytes, and a decoder that guessed later
|
|
26
|
+
// would have had to un-decode a key it had already emitted.
|
|
27
|
+
//
|
|
28
|
+
// # The names are OpenTUI's
|
|
29
|
+
//
|
|
30
|
+
// `"return"`, not `"enter"`. `"escape"`, not `"esc"`. Those are the canonical
|
|
31
|
+
// names OpenTUI's `KeyEvent.name` uses, and a component written against its
|
|
32
|
+
// documentation compares against them — so a handler that reads
|
|
33
|
+
// `key.name === "return"` behaves identically under either library. The
|
|
34
|
+
// aliases people actually type (`"enter"`, `"esc"`) are accepted where uf
|
|
35
|
+
// takes a key *binding* from a caller, and normalised there rather than here,
|
|
36
|
+
// so there is exactly one spelling in an event.
|
|
37
|
+
//
|
|
38
|
+
// # Propagation is OpenTUI's too, including the part that looks wrong
|
|
39
|
+
//
|
|
40
|
+
// `stopPropagation()` stops later global listeners *and* prevents the focused
|
|
41
|
+
// node from seeing the event. `preventDefault()` does the opposite half: later
|
|
42
|
+
// global listeners still run, but the focused node is skipped. They are not a
|
|
43
|
+
// pair of ordered severities, they are two independent answers to two
|
|
44
|
+
// questions — "does anyone else get to see this" and "does the thing that has
|
|
45
|
+
// focus act on it" — and conflating them is what makes a global `Ctrl+C`
|
|
46
|
+
// handler either unreachable or unable to stop a text input from inserting a
|
|
47
|
+
// character.
|
|
48
|
+
|
|
49
|
+
import type { MouseEvent } from "./mouse.js";
|
|
50
|
+
import { LEGACY_REPORT_LENGTH, decodeMouse, legacyReportLength } from "./mouse.js";
|
|
51
|
+
|
|
52
|
+
/** Which of the two parsers produced an event. */
|
|
53
|
+
export type KeySource = "raw" | "escape";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* One key press.
|
|
57
|
+
*
|
|
58
|
+
* `sequence` is the text the key stands for and `raw` is the bytes it arrived
|
|
59
|
+
* as; they differ for every key that is not a printable character, and a
|
|
60
|
+
* handler that inserts `sequence` into a buffer rather than `raw` is the
|
|
61
|
+
* difference between typing `a` and typing `^[[A`.
|
|
62
|
+
*/
|
|
63
|
+
export type KeyEvent = {
|
|
64
|
+
/**
|
|
65
|
+
* Which of the two things a terminal's byte stream carries.
|
|
66
|
+
*
|
|
67
|
+
* A stream holds keys and, when mouse reporting is on, mouse reports. This
|
|
68
|
+
* is what tells them apart, and it is on the event rather than inferred from
|
|
69
|
+
* the presence of a field so that a `switch` over it is exhaustive.
|
|
70
|
+
*/
|
|
71
|
+
readonly kind: "key",
|
|
72
|
+
/**
|
|
73
|
+
* The canonical name: `"a"`, `"space"`, `"return"`, `"escape"`, `"up"`.
|
|
74
|
+
*
|
|
75
|
+
* `"paste"` is the one name that is not a key. A terminal in bracketed
|
|
76
|
+
* paste mode wraps pasted text in `ESC[200~` and `ESC[201~` so that an
|
|
77
|
+
* application can tell it from typing, and the whole point of knowing is to
|
|
78
|
+
* treat it as *text* — so it arrives as one event carrying all of it rather
|
|
79
|
+
* than as the burst of key presses it would otherwise look like.
|
|
80
|
+
*/
|
|
81
|
+
readonly name: string,
|
|
82
|
+
/** The text this key stands for, empty for keys that stand for none. */
|
|
83
|
+
readonly sequence: string,
|
|
84
|
+
/** The bytes as they arrived. */
|
|
85
|
+
readonly raw: string,
|
|
86
|
+
/** Which parser produced it. */
|
|
87
|
+
readonly source: KeySource,
|
|
88
|
+
readonly ctrl: boolean,
|
|
89
|
+
readonly shift: boolean,
|
|
90
|
+
readonly meta: boolean,
|
|
91
|
+
/** Always `"press"`. Release reporting needs the Kitty protocol; see #314. */
|
|
92
|
+
readonly eventType: "press",
|
|
93
|
+
/** Skip the focused node's handler, without silencing later global ones. */
|
|
94
|
+
preventDefault(): void,
|
|
95
|
+
/** Silence later global handlers, and the focused node's. */
|
|
96
|
+
stopPropagation(): void,
|
|
97
|
+
/** Whether `preventDefault()` was called. */
|
|
98
|
+
defaultPrevented: boolean,
|
|
99
|
+
/** Whether `stopPropagation()` was called. */
|
|
100
|
+
propagationStopped: boolean,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* One thing that arrived from a terminal.
|
|
105
|
+
*
|
|
106
|
+
* Everything a driver reads is one of these two, and `kind` is how a caller
|
|
107
|
+
* tells them apart without a type test on a field that might one day exist on
|
|
108
|
+
* both.
|
|
109
|
+
*/
|
|
110
|
+
export type InputEvent = KeyEvent | MouseEvent;
|
|
111
|
+
|
|
112
|
+
const ESC = "\u001b";
|
|
113
|
+
|
|
114
|
+
/** What a terminal in bracketed paste mode puts around pasted text. */
|
|
115
|
+
const PASTE_START = "\u001b[200~";
|
|
116
|
+
const PASTE_END = "\u001b[201~";
|
|
117
|
+
|
|
118
|
+
/** What an SGR-1006 mouse report begins with; `mouse.js` has the rest. */
|
|
119
|
+
const MOUSE_SGR = "\u001b[<";
|
|
120
|
+
|
|
121
|
+
/** What a terminal that ignored `?1006h` begins one with instead. */
|
|
122
|
+
const MOUSE_LEGACY = "\u001b[M";
|
|
123
|
+
|
|
124
|
+
/** The `CSI …` final bytes that name a key on their own. */
|
|
125
|
+
const CSI_FINAL: { [string]: string } = {
|
|
126
|
+
A: "up",
|
|
127
|
+
B: "down",
|
|
128
|
+
C: "right",
|
|
129
|
+
D: "left",
|
|
130
|
+
H: "home",
|
|
131
|
+
F: "end",
|
|
132
|
+
E: "clear",
|
|
133
|
+
P: "f1",
|
|
134
|
+
Q: "f2",
|
|
135
|
+
R: "f3",
|
|
136
|
+
S: "f4",
|
|
137
|
+
Z: "tab",
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/** The `CSI n ~` numbers, which is the other half of the same vocabulary. */
|
|
141
|
+
const CSI_TILDE: { [string]: string } = {
|
|
142
|
+
"1": "home",
|
|
143
|
+
"2": "insert",
|
|
144
|
+
"3": "delete",
|
|
145
|
+
"4": "end",
|
|
146
|
+
"5": "pageup",
|
|
147
|
+
"6": "pagedown",
|
|
148
|
+
"11": "f1",
|
|
149
|
+
"12": "f2",
|
|
150
|
+
"13": "f3",
|
|
151
|
+
"14": "f4",
|
|
152
|
+
"15": "f5",
|
|
153
|
+
"17": "f6",
|
|
154
|
+
"18": "f7",
|
|
155
|
+
"19": "f8",
|
|
156
|
+
"20": "f9",
|
|
157
|
+
"21": "f10",
|
|
158
|
+
"23": "f11",
|
|
159
|
+
"24": "f12",
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/** Build an event with its two propagation flags wired up. */
|
|
163
|
+
function event(fields: {
|
|
164
|
+
name: string,
|
|
165
|
+
sequence: string,
|
|
166
|
+
raw: string,
|
|
167
|
+
source: KeySource,
|
|
168
|
+
ctrl?: boolean,
|
|
169
|
+
shift?: boolean,
|
|
170
|
+
meta?: boolean,
|
|
171
|
+
}): KeyEvent {
|
|
172
|
+
const key: KeyEvent = {
|
|
173
|
+
kind: "key",
|
|
174
|
+
name: fields.name,
|
|
175
|
+
sequence: fields.sequence,
|
|
176
|
+
raw: fields.raw,
|
|
177
|
+
source: fields.source,
|
|
178
|
+
ctrl: fields.ctrl === true,
|
|
179
|
+
shift: fields.shift === true,
|
|
180
|
+
meta: fields.meta === true,
|
|
181
|
+
eventType: "press",
|
|
182
|
+
defaultPrevented: false,
|
|
183
|
+
propagationStopped: false,
|
|
184
|
+
preventDefault() {
|
|
185
|
+
key.defaultPrevented = true;
|
|
186
|
+
},
|
|
187
|
+
stopPropagation() {
|
|
188
|
+
key.propagationStopped = true;
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
return key;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Decode the `1 + shift + 2·alt + 4·ctrl` parameter terminals encode
|
|
196
|
+
* modifiers in.
|
|
197
|
+
*
|
|
198
|
+
* The offset of one is not decoration: a parameter of zero means "absent" in
|
|
199
|
+
* the CSI grammar, so the unmodified case has to be one and every modifier
|
|
200
|
+
* combination is that plus a bit mask.
|
|
201
|
+
*/
|
|
202
|
+
function modifiers(parameter: string | void): { ctrl: boolean, shift: boolean, meta: boolean } {
|
|
203
|
+
const value = Number.parseInt(parameter ?? "1", 10);
|
|
204
|
+
const bits = Number.isFinite(value) && value > 0 ? value - 1 : 0;
|
|
205
|
+
return { shift: (bits & 1) !== 0, meta: (bits & 2) !== 0, ctrl: (bits & 4) !== 0 };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* One pasted block, as the event a handler sees.
|
|
210
|
+
*
|
|
211
|
+
* The payload is whatever was on the clipboard, given back verbatim and
|
|
212
|
+
* therefore **untrusted**: it can hold newlines, control bytes and escape
|
|
213
|
+
* sequences of its own. Handing it over as text rather than as keys is the
|
|
214
|
+
* entire purpose of bracketed paste — a terminal without it delivers a pasted
|
|
215
|
+
* `\r` as Enter, which is how pasting a two-line command into a prompt runs
|
|
216
|
+
* the first line. What a component does with the text is its own decision;
|
|
217
|
+
* `Input` takes the first line and drops the control characters, because it is
|
|
218
|
+
* one line of text and cannot hold either.
|
|
219
|
+
*/
|
|
220
|
+
function pasteEvent(text: string, raw: string): KeyEvent {
|
|
221
|
+
return event({ name: "paste", sequence: text, raw, source: "escape" });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* A decoder that survives a sequence arriving in pieces.
|
|
226
|
+
*
|
|
227
|
+
* {@link decodeInput} is a pure function of one chunk, which is right for
|
|
228
|
+
* every key: a terminal delivers a key's escape sequence in a single read, and
|
|
229
|
+
* `ESC` at the end of a chunk is the Escape key. Two things a terminal sends
|
|
230
|
+
* are not keys and do not keep that promise — a paste, which is as long as the
|
|
231
|
+
* clipboard, and a mouse report, which a terminal in any-motion mode sends one
|
|
232
|
+
* of per cell the pointer crosses. The operating system splits either wherever
|
|
233
|
+
* it likes. So a driver reading a real stream holds one of these across
|
|
234
|
+
* chunks.
|
|
235
|
+
*
|
|
236
|
+
* # The one rule, and what bounds it
|
|
237
|
+
*
|
|
238
|
+
* `push` holds back a trailing run of bytes that **cannot be anything but the
|
|
239
|
+
* beginning of a sequence this decoder must see whole** — see `incomplete`,
|
|
240
|
+
* which is the whole of that judgement and the only place it is made. Nothing
|
|
241
|
+
* else is buffered: a chunk that ends anywhere else is decoded completely,
|
|
242
|
+
* because every other sequence a terminal sends either fits in a read or is
|
|
243
|
+
* ambiguous with a key that must fire now.
|
|
244
|
+
*
|
|
245
|
+
* A held run is released by exactly two things. The next chunk completes it,
|
|
246
|
+
* or {@link InputDecoder.flush} says no next chunk is coming and the bytes are
|
|
247
|
+
* decoded as they stand. And a run that grows past {@link HOLD_LIMIT} without
|
|
248
|
+
* completing is not one of these sequences however it began, so it is decoded
|
|
249
|
+
* rather than held — which is what keeps a terminal emitting nonsense from
|
|
250
|
+
* wedging the decoder even where nobody calls `flush`.
|
|
251
|
+
*/
|
|
252
|
+
export type InputDecoder = {
|
|
253
|
+
/** Decode one chunk, holding back a sequence that has not ended yet. */
|
|
254
|
+
push(chunk: string): Array<InputEvent>,
|
|
255
|
+
/** Give up on an unfinished sequence and emit what arrived. */
|
|
256
|
+
flush(): Array<InputEvent>,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The longest run of bytes this decoder will hold waiting for the rest of it.
|
|
261
|
+
*
|
|
262
|
+
* Every sequence `incomplete` waits for is shorter: the paste introducer is
|
|
263
|
+
* six bytes, an old-style mouse report is six, and an SGR report is
|
|
264
|
+
* `ESC [ <` plus three decimal parameters, two semicolons and a final byte —
|
|
265
|
+
* nineteen bytes for coordinates larger than any terminal has. Past this, the
|
|
266
|
+
* bytes are not the sequence they looked like and are decoded as what they
|
|
267
|
+
* are.
|
|
268
|
+
*
|
|
269
|
+
* The bound is what makes the hold safe without a timer. `flush` is the
|
|
270
|
+
* ordinary way out and a driver calls it when its stream ends; this is for the
|
|
271
|
+
* case where nothing ever calls it and the terminal has sent `ESC [ <` and
|
|
272
|
+
* then a thousand digits.
|
|
273
|
+
*/
|
|
274
|
+
const HOLD_LIMIT = 32;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Whether the bytes from `start` begin a sequence whose rest has not arrived.
|
|
278
|
+
*
|
|
279
|
+
* The one buffering rule. It answers for the three sequences a read boundary
|
|
280
|
+
* can fall inside and this decoder would otherwise mis-decode:
|
|
281
|
+
*
|
|
282
|
+
* * `ESC [ 2 0 0 ~`, the paste introducer, where a split turns the marker
|
|
283
|
+
* into Alt-and-a-bracket followed by three digits and the paste's first
|
|
284
|
+
* line then runs as typing;
|
|
285
|
+
* * `ESC [ <` …, an SGR mouse report, where a split turns a click into the
|
|
286
|
+
* characters of its coordinates — ubugeeei-prod/uf#612, and the traffic
|
|
287
|
+
* `?1003h` produces is a report per cell crossed, which is the traffic
|
|
288
|
+
* most likely to be split;
|
|
289
|
+
* * `ESC [ M` …, the old-style report, whose three payload bytes are
|
|
290
|
+
* arbitrary and become arbitrary keys.
|
|
291
|
+
*
|
|
292
|
+
* Two bytes at least, so a lone `ESC` is still the Escape key: holding that
|
|
293
|
+
* back would mean Escape never fires until the next keystroke, which is worse
|
|
294
|
+
* than the ambiguity it would solve. From `ESC[` on there is nothing else the
|
|
295
|
+
* bytes could be that this decoder would get right anyway — an unfinished CSI
|
|
296
|
+
* at the end of a chunk decodes as Alt and a bracket.
|
|
297
|
+
*
|
|
298
|
+
* It is deliberately *not* asked about a complete sequence with more input
|
|
299
|
+
* after it. A run is held only when it reaches the end of the chunk, which is
|
|
300
|
+
* what "the rest has not arrived" means; a report followed by a keystroke has
|
|
301
|
+
* a final byte and is decoded where it stands.
|
|
302
|
+
*/
|
|
303
|
+
function incomplete(input: string, start: number): boolean {
|
|
304
|
+
const rest = input.slice(start);
|
|
305
|
+
if (rest.length < 2 || rest.length > HOLD_LIMIT || !rest.startsWith(ESC)) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
if (rest.length < PASTE_START.length && PASTE_START.startsWith(rest)) {
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
if (rest.startsWith(MOUSE_SGR)) {
|
|
312
|
+
// Complete when the final byte has arrived, and no longer a report at all
|
|
313
|
+
// once something that is not a parameter byte has: `decodeMouse` answers
|
|
314
|
+
// `null` for that and the bytes fall through to the key grammar, which is
|
|
315
|
+
// where they should fall through rather than being waited on for ever.
|
|
316
|
+
const parameters = rest.slice(MOUSE_SGR.length);
|
|
317
|
+
return /^[0-9;]*$/.test(parameters);
|
|
318
|
+
}
|
|
319
|
+
if (rest.startsWith(MOUSE_LEGACY)) {
|
|
320
|
+
return rest.length < LEGACY_REPORT_LENGTH;
|
|
321
|
+
}
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** A decoder with somewhere to keep a half-arrived sequence. */
|
|
326
|
+
export function createInputDecoder(): InputDecoder {
|
|
327
|
+
let pending: string | null = null;
|
|
328
|
+
/** A chunk that ended part-way through a sequence; see `incomplete`. */
|
|
329
|
+
let waiting = "";
|
|
330
|
+
|
|
331
|
+
const decoder: InputDecoder = {
|
|
332
|
+
push(chunk: string): Array<InputEvent> {
|
|
333
|
+
const events: Array<InputEvent> = [];
|
|
334
|
+
let input = waiting + chunk;
|
|
335
|
+
waiting = "";
|
|
336
|
+
if (pending != null) {
|
|
337
|
+
const end = input.indexOf(PASTE_END);
|
|
338
|
+
if (end < 0) {
|
|
339
|
+
pending += input;
|
|
340
|
+
return events;
|
|
341
|
+
}
|
|
342
|
+
const text = pending + input.slice(0, end);
|
|
343
|
+
pending = null;
|
|
344
|
+
events.push(pasteEvent(text, PASTE_START + text + PASTE_END));
|
|
345
|
+
input = input.slice(end + PASTE_END.length);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
let index = 0;
|
|
349
|
+
while (index < input.length) {
|
|
350
|
+
if (incomplete(input, index)) {
|
|
351
|
+
waiting = input.slice(index);
|
|
352
|
+
return events;
|
|
353
|
+
}
|
|
354
|
+
if (input.startsWith(PASTE_START, index)) {
|
|
355
|
+
const from = index + PASTE_START.length;
|
|
356
|
+
const end = input.indexOf(PASTE_END, from);
|
|
357
|
+
if (end < 0) {
|
|
358
|
+
pending = input.slice(from);
|
|
359
|
+
return events;
|
|
360
|
+
}
|
|
361
|
+
const text = input.slice(from, end);
|
|
362
|
+
events.push(pasteEvent(text, PASTE_START + text + PASTE_END));
|
|
363
|
+
index = end + PASTE_END.length;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
index += decodeOne(input, index, events);
|
|
367
|
+
}
|
|
368
|
+
return events;
|
|
369
|
+
},
|
|
370
|
+
flush(): Array<InputEvent> {
|
|
371
|
+
if (waiting !== "") {
|
|
372
|
+
// Not the sequence it looked like after all: no more input is coming,
|
|
373
|
+
// so the bytes are whatever they decode to on their own.
|
|
374
|
+
const held = waiting;
|
|
375
|
+
waiting = "";
|
|
376
|
+
const events: Array<InputEvent> = [];
|
|
377
|
+
let index = 0;
|
|
378
|
+
while (index < held.length) {
|
|
379
|
+
index += decodeOne(held, index, events);
|
|
380
|
+
}
|
|
381
|
+
return events;
|
|
382
|
+
}
|
|
383
|
+
if (pending == null) {
|
|
384
|
+
return [];
|
|
385
|
+
}
|
|
386
|
+
const text = pending;
|
|
387
|
+
pending = null;
|
|
388
|
+
return [pasteEvent(text, PASTE_START + text)];
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
return decoder;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Everything in a chunk of terminal input: keys, and mouse reports.
|
|
396
|
+
*
|
|
397
|
+
* A chunk is not a key. Holding a key down, pasting, or simply typing fast
|
|
398
|
+
* delivers several at once, and a decoder that returns the first and drops the
|
|
399
|
+
* rest loses characters under exactly the conditions — fast typing — where
|
|
400
|
+
* losing them is most obvious.
|
|
401
|
+
*
|
|
402
|
+
* One chunk, decoded completely. A sequence this chunk begins and does not end
|
|
403
|
+
* is not held, because there is no later chunk for a pure function to wait for:
|
|
404
|
+
* an unfinished paste is emitted as the paste it was becoming, and an
|
|
405
|
+
* unfinished mouse report is decoded as the bytes it is. A driver reading a
|
|
406
|
+
* stream wants {@link createInputDecoder} instead, which holds them.
|
|
407
|
+
*/
|
|
408
|
+
export function decodeInput(input: string): Array<InputEvent> {
|
|
409
|
+
const decoder = createInputDecoder();
|
|
410
|
+
return [...decoder.push(input), ...decoder.flush()];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* The key events in a chunk, with any mouse reports left out.
|
|
415
|
+
*
|
|
416
|
+
* The narrow view, for a caller that has not turned mouse reporting on and
|
|
417
|
+
* therefore cannot receive one — which is every caller of this function until
|
|
418
|
+
* an application asks `render` for the mouse. A caller that has wants
|
|
419
|
+
* {@link decodeInput}, because dropping half of what a terminal said is a
|
|
420
|
+
* poor way to find out it was said.
|
|
421
|
+
*/
|
|
422
|
+
export function decodeKeys(input: string): Array<KeyEvent> {
|
|
423
|
+
const keys: Array<KeyEvent> = [];
|
|
424
|
+
for (const event of decodeInput(input)) {
|
|
425
|
+
if (event.kind === "key") {
|
|
426
|
+
keys.push(event);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return keys;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function decodeOne(input: string, start: number, events: Array<InputEvent>): number {
|
|
433
|
+
const character = input[start];
|
|
434
|
+
|
|
435
|
+
if (character !== ESC) {
|
|
436
|
+
events.push(decodePlain(character));
|
|
437
|
+
return 1;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// `ESC` with nothing after it is the Escape key. This is the ambiguity at
|
|
441
|
+
// the centre of terminal input: the same byte begins `Alt+A` and every
|
|
442
|
+
// arrow key, and the only thing that distinguishes them is what follows in
|
|
443
|
+
// the *same read*. A terminal delivers a real escape sequence as one chunk,
|
|
444
|
+
// so "nothing follows in this chunk" is the signal — imperfect, and the
|
|
445
|
+
// reason `Escape` is felt as slightly laggy in every terminal program ever
|
|
446
|
+
// written.
|
|
447
|
+
const next = input[start + 1];
|
|
448
|
+
if (next === undefined) {
|
|
449
|
+
events.push(event({ name: "escape", sequence: "", raw: ESC, source: "raw" }));
|
|
450
|
+
return 1;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// A mouse report, before the key grammar gets a look at it. `ESC[<` is not
|
|
454
|
+
// reachable as a key — the CSI parameter bytes are digits and semicolons —
|
|
455
|
+
// so this branch takes nothing away from the one below it.
|
|
456
|
+
if (next === "[" && input[start + 2] === "<") {
|
|
457
|
+
const report = decodeMouse(input, start);
|
|
458
|
+
if (report != null) {
|
|
459
|
+
events.push(report.event);
|
|
460
|
+
return report.length;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// The report a terminal sends when it did not understand `?1006h`. Consumed
|
|
465
|
+
// and dropped: `mouse.js` says why decoding it would be worse, and why
|
|
466
|
+
// letting its three payload bytes through as keys would be worse still.
|
|
467
|
+
if (next === "[") {
|
|
468
|
+
const legacy = legacyReportLength(input, start);
|
|
469
|
+
if (legacy > 0) {
|
|
470
|
+
return legacy;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (next === "[" || next === "O") {
|
|
475
|
+
const parsed = decodeSequence(input, start);
|
|
476
|
+
if (parsed != null) {
|
|
477
|
+
events.push(parsed.key);
|
|
478
|
+
return parsed.length;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// `ESC` followed by anything else is that key with Alt held.
|
|
483
|
+
const inner = decodePlain(next);
|
|
484
|
+
events.push(
|
|
485
|
+
event({
|
|
486
|
+
name: inner.name,
|
|
487
|
+
sequence: inner.sequence,
|
|
488
|
+
raw: ESC + next,
|
|
489
|
+
source: "escape",
|
|
490
|
+
ctrl: inner.ctrl,
|
|
491
|
+
shift: inner.shift,
|
|
492
|
+
meta: true,
|
|
493
|
+
}),
|
|
494
|
+
);
|
|
495
|
+
return 2;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function decodeSequence(input: string, start: number): { key: KeyEvent, length: number } | null {
|
|
499
|
+
const introducer = input[start + 1];
|
|
500
|
+
// `ESC O x` — the "application cursor keys" form. Same keys, different
|
|
501
|
+
// spelling, chosen by a mode the terminal may be in for reasons that have
|
|
502
|
+
// nothing to do with this program.
|
|
503
|
+
if (introducer === "O") {
|
|
504
|
+
const final = input[start + 2];
|
|
505
|
+
const name = final != null ? CSI_FINAL[final] : undefined;
|
|
506
|
+
if (name == null) {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
const raw = input.slice(start, start + 3);
|
|
510
|
+
return { key: event({ name, sequence: "", raw, source: "escape" }), length: 3 };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
let cursor = start + 2;
|
|
514
|
+
let parameters = "";
|
|
515
|
+
while (cursor < input.length && /[0-9;]/.test(input[cursor])) {
|
|
516
|
+
parameters += input[cursor];
|
|
517
|
+
cursor += 1;
|
|
518
|
+
}
|
|
519
|
+
const final = input[cursor];
|
|
520
|
+
if (final === undefined) {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
const raw = input.slice(start, cursor + 1);
|
|
524
|
+
const [first, second] = parameters.split(";");
|
|
525
|
+
|
|
526
|
+
if (final === "~") {
|
|
527
|
+
const name = CSI_TILDE[first];
|
|
528
|
+
if (name == null) {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
const mods = modifiers(second);
|
|
532
|
+
return {
|
|
533
|
+
key: event({ name, sequence: "", raw, source: "escape", ...mods }),
|
|
534
|
+
length: raw.length,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const name = CSI_FINAL[final];
|
|
539
|
+
if (name == null) {
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
// `CSI Z` is Shift+Tab, and it carries no modifier parameter to say so.
|
|
543
|
+
const mods = final === "Z" ? { ctrl: false, shift: true, meta: false } : modifiers(second);
|
|
544
|
+
return { key: event({ name, sequence: "", raw, source: "escape", ...mods }), length: raw.length };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** One byte that is not part of an escape sequence. */
|
|
548
|
+
function decodePlain(character: string): KeyEvent {
|
|
549
|
+
const code = character.charCodeAt(0);
|
|
550
|
+
|
|
551
|
+
if (character === "\r" || character === "\n") {
|
|
552
|
+
return event({ name: "return", sequence: "\r", raw: character, source: "raw" });
|
|
553
|
+
}
|
|
554
|
+
if (character === "\t") {
|
|
555
|
+
return event({ name: "tab", sequence: "\t", raw: character, source: "raw" });
|
|
556
|
+
}
|
|
557
|
+
if (character === " ") {
|
|
558
|
+
return event({ name: "space", sequence: " ", raw: character, source: "raw" });
|
|
559
|
+
}
|
|
560
|
+
// Both spellings of Backspace. Which one arrives depends on the terminal's
|
|
561
|
+
// `erase` setting, and a program that handles only `\x7f` is a program whose
|
|
562
|
+
// backspace key does nothing on somebody else's machine.
|
|
563
|
+
if (code === 0x7f || code === 0x08) {
|
|
564
|
+
return event({ name: "backspace", sequence: "", raw: character, source: "raw" });
|
|
565
|
+
}
|
|
566
|
+
if (code === 0) {
|
|
567
|
+
return event({ name: "space", sequence: "", raw: character, source: "raw", ctrl: true });
|
|
568
|
+
}
|
|
569
|
+
if (code < 0x20) {
|
|
570
|
+
// A C0 control is Ctrl plus the letter at that position in the alphabet.
|
|
571
|
+
const letter = String.fromCharCode(code + 0x60);
|
|
572
|
+
return event({ name: letter, sequence: "", raw: character, source: "raw", ctrl: true });
|
|
573
|
+
}
|
|
574
|
+
// A printable character. `shift` is reported for an uppercase letter because
|
|
575
|
+
// that is the only evidence a terminal gives: there is no separate shift
|
|
576
|
+
// report outside the Kitty protocol, and `A` is what Shift+A means.
|
|
577
|
+
const lower = character.toLowerCase();
|
|
578
|
+
return event({
|
|
579
|
+
name: lower,
|
|
580
|
+
sequence: character,
|
|
581
|
+
raw: character,
|
|
582
|
+
source: "raw",
|
|
583
|
+
shift: character !== lower,
|
|
584
|
+
});
|
|
585
|
+
}
|