@uniflowed/tui 0.0.0-alpha.18 → 0.15.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/capability.js +15 -12
- package/cells.js +1 -1
- package/clipboard.js +61 -0
- package/components.js +766 -57
- package/index.js +41 -22
- package/internal/hits.js +2 -2
- package/internal/host.js +63 -22
- package/internal/paint.js +43 -2
- package/internal/tree.js +168 -38
- package/internal/widgets.js +771 -0
- package/keys.js +214 -15
- package/layout.js +653 -132
- package/package.json +5 -4
- package/terminal.js +49 -19
- package/widths.js +4 -1
package/keys.js
CHANGED
|
@@ -52,8 +52,11 @@ import { LEGACY_REPORT_LENGTH, decodeMouse, legacyReportLength } from "./mouse.j
|
|
|
52
52
|
/** Which of the two parsers produced an event. */
|
|
53
53
|
export type KeySource = "raw" | "escape";
|
|
54
54
|
|
|
55
|
+
/** What the terminal says happened to the key. */
|
|
56
|
+
export type KeyEventType = "press" | "repeat" | "release";
|
|
57
|
+
|
|
55
58
|
/**
|
|
56
|
-
* One key
|
|
59
|
+
* One key event.
|
|
57
60
|
*
|
|
58
61
|
* `sequence` is the text the key stands for and `raw` is the bytes it arrived
|
|
59
62
|
* as; they differ for every key that is not a printable character, and a
|
|
@@ -88,8 +91,8 @@ export type KeyEvent = {
|
|
|
88
91
|
readonly ctrl: boolean,
|
|
89
92
|
readonly shift: boolean,
|
|
90
93
|
readonly meta: boolean,
|
|
91
|
-
/**
|
|
92
|
-
readonly eventType:
|
|
94
|
+
/** Whether this was a press, terminal repeat, or release event. */
|
|
95
|
+
readonly eventType: KeyEventType,
|
|
93
96
|
/** Skip the focused node's handler, without silencing later global ones. */
|
|
94
97
|
preventDefault(): void,
|
|
95
98
|
/** Silence later global handlers, and the focused node's. */
|
|
@@ -159,6 +162,99 @@ const CSI_TILDE: { [string]: string } = {
|
|
|
159
162
|
"24": "f12",
|
|
160
163
|
};
|
|
161
164
|
|
|
165
|
+
/** Non-Unicode Kitty `CSI u` key codes that do not already have legacy names. */
|
|
166
|
+
const CSI_U_FUNCTION: { [string]: string } = {
|
|
167
|
+
"9": "tab",
|
|
168
|
+
"13": "return",
|
|
169
|
+
"27": "escape",
|
|
170
|
+
"127": "backspace",
|
|
171
|
+
"57358": "caps-lock",
|
|
172
|
+
"57359": "scroll-lock",
|
|
173
|
+
"57360": "num-lock",
|
|
174
|
+
"57361": "print-screen",
|
|
175
|
+
"57362": "pause",
|
|
176
|
+
"57363": "menu",
|
|
177
|
+
"57376": "f13",
|
|
178
|
+
"57377": "f14",
|
|
179
|
+
"57378": "f15",
|
|
180
|
+
"57379": "f16",
|
|
181
|
+
"57380": "f17",
|
|
182
|
+
"57381": "f18",
|
|
183
|
+
"57382": "f19",
|
|
184
|
+
"57383": "f20",
|
|
185
|
+
"57384": "f21",
|
|
186
|
+
"57385": "f22",
|
|
187
|
+
"57386": "f23",
|
|
188
|
+
"57387": "f24",
|
|
189
|
+
"57388": "f25",
|
|
190
|
+
"57389": "f26",
|
|
191
|
+
"57390": "f27",
|
|
192
|
+
"57391": "f28",
|
|
193
|
+
"57392": "f29",
|
|
194
|
+
"57393": "f30",
|
|
195
|
+
"57394": "f31",
|
|
196
|
+
"57395": "f32",
|
|
197
|
+
"57396": "f33",
|
|
198
|
+
"57397": "f34",
|
|
199
|
+
"57398": "f35",
|
|
200
|
+
"57399": "kp-0",
|
|
201
|
+
"57400": "kp-1",
|
|
202
|
+
"57401": "kp-2",
|
|
203
|
+
"57402": "kp-3",
|
|
204
|
+
"57403": "kp-4",
|
|
205
|
+
"57404": "kp-5",
|
|
206
|
+
"57405": "kp-6",
|
|
207
|
+
"57406": "kp-7",
|
|
208
|
+
"57407": "kp-8",
|
|
209
|
+
"57408": "kp-9",
|
|
210
|
+
"57409": "kp-decimal",
|
|
211
|
+
"57410": "kp-divide",
|
|
212
|
+
"57411": "kp-multiply",
|
|
213
|
+
"57412": "kp-subtract",
|
|
214
|
+
"57413": "kp-add",
|
|
215
|
+
"57414": "kp-enter",
|
|
216
|
+
"57415": "kp-equal",
|
|
217
|
+
"57416": "kp-separator",
|
|
218
|
+
"57417": "kp-left",
|
|
219
|
+
"57418": "kp-right",
|
|
220
|
+
"57419": "kp-up",
|
|
221
|
+
"57420": "kp-down",
|
|
222
|
+
"57421": "kp-pageup",
|
|
223
|
+
"57422": "kp-pagedown",
|
|
224
|
+
"57423": "kp-home",
|
|
225
|
+
"57424": "kp-end",
|
|
226
|
+
"57425": "kp-insert",
|
|
227
|
+
"57426": "kp-delete",
|
|
228
|
+
"57427": "kp-begin",
|
|
229
|
+
"57428": "media-play",
|
|
230
|
+
"57429": "media-pause",
|
|
231
|
+
"57430": "media-play-pause",
|
|
232
|
+
"57431": "media-reverse",
|
|
233
|
+
"57432": "media-stop",
|
|
234
|
+
"57433": "media-fast-forward",
|
|
235
|
+
"57434": "media-rewind",
|
|
236
|
+
"57435": "media-track-next",
|
|
237
|
+
"57436": "media-track-previous",
|
|
238
|
+
"57437": "media-record",
|
|
239
|
+
"57438": "volume-down",
|
|
240
|
+
"57439": "volume-up",
|
|
241
|
+
"57440": "volume-mute",
|
|
242
|
+
"57441": "left-shift",
|
|
243
|
+
"57442": "left-control",
|
|
244
|
+
"57443": "left-alt",
|
|
245
|
+
"57444": "left-super",
|
|
246
|
+
"57445": "left-hyper",
|
|
247
|
+
"57446": "left-meta",
|
|
248
|
+
"57447": "right-shift",
|
|
249
|
+
"57448": "right-control",
|
|
250
|
+
"57449": "right-alt",
|
|
251
|
+
"57450": "right-super",
|
|
252
|
+
"57451": "right-hyper",
|
|
253
|
+
"57452": "right-meta",
|
|
254
|
+
"57453": "iso-level3-shift",
|
|
255
|
+
"57454": "iso-level5-shift",
|
|
256
|
+
};
|
|
257
|
+
|
|
162
258
|
/** Build an event with its two propagation flags wired up. */
|
|
163
259
|
function event(fields: {
|
|
164
260
|
name: string,
|
|
@@ -168,6 +264,7 @@ function event(fields: {
|
|
|
168
264
|
ctrl?: boolean,
|
|
169
265
|
shift?: boolean,
|
|
170
266
|
meta?: boolean,
|
|
267
|
+
eventType?: KeyEventType,
|
|
171
268
|
}): KeyEvent {
|
|
172
269
|
const key: KeyEvent = {
|
|
173
270
|
kind: "key",
|
|
@@ -178,7 +275,7 @@ function event(fields: {
|
|
|
178
275
|
ctrl: fields.ctrl === true,
|
|
179
276
|
shift: fields.shift === true,
|
|
180
277
|
meta: fields.meta === true,
|
|
181
|
-
eventType: "press",
|
|
278
|
+
eventType: fields.eventType ?? "press",
|
|
182
279
|
defaultPrevented: false,
|
|
183
280
|
propagationStopped: false,
|
|
184
281
|
preventDefault() {
|
|
@@ -205,6 +302,49 @@ function modifiers(parameter: string | void): { ctrl: boolean, shift: boolean, m
|
|
|
205
302
|
return { shift: (bits & 1) !== 0, meta: (bits & 2) !== 0, ctrl: (bits & 4) !== 0 };
|
|
206
303
|
}
|
|
207
304
|
|
|
305
|
+
/** The Kitty keyboard protocol encodes press/repeat/release as modifier sub-fields. */
|
|
306
|
+
function keyEventType(parameter: string | void): KeyEventType | null {
|
|
307
|
+
if (parameter == null || parameter === "" || parameter === "1") {
|
|
308
|
+
return "press";
|
|
309
|
+
}
|
|
310
|
+
if (parameter === "2") {
|
|
311
|
+
return "repeat";
|
|
312
|
+
}
|
|
313
|
+
if (parameter === "3") {
|
|
314
|
+
return "release";
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function codePoint(parameter: string | void): number | null {
|
|
320
|
+
if (parameter == null || parameter === "") {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
const value = Number.parseInt(parameter, 10);
|
|
324
|
+
if (!Number.isFinite(value) || value < 0 || value > 0x10ffff) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
if (value >= 0xd800 && value <= 0xdfff) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
return value;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function codePointText(parameter: string | void): string {
|
|
334
|
+
if (parameter == null || parameter === "") {
|
|
335
|
+
return "";
|
|
336
|
+
}
|
|
337
|
+
const points: Array<number> = [];
|
|
338
|
+
for (const field of parameter.split(":")) {
|
|
339
|
+
const point = codePoint(field);
|
|
340
|
+
if (point == null || point < 0x20 || (point >= 0x7f && point <= 0x9f)) {
|
|
341
|
+
return "";
|
|
342
|
+
}
|
|
343
|
+
points.push(point);
|
|
344
|
+
}
|
|
345
|
+
return String.fromCodePoint(...points);
|
|
346
|
+
}
|
|
347
|
+
|
|
208
348
|
/**
|
|
209
349
|
* One pasted block, as the event a handler sees.
|
|
210
350
|
*
|
|
@@ -260,18 +400,18 @@ export type InputDecoder = {
|
|
|
260
400
|
* The longest run of bytes this decoder will hold waiting for the rest of it.
|
|
261
401
|
*
|
|
262
402
|
* 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
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* bytes are not the sequence they looked like
|
|
267
|
-
* are.
|
|
403
|
+
* six bytes, an old-style mouse report is six, and an SGR report is `ESC [ <`
|
|
404
|
+
* plus three decimal parameters, two semicolons and a final byte. Kitty's
|
|
405
|
+
* `CSI u` key reports can be longer because their associated text is encoded
|
|
406
|
+
* as code points. Past this, the bytes are not the sequence they looked like
|
|
407
|
+
* and are decoded as what they are.
|
|
268
408
|
*
|
|
269
409
|
* The bound is what makes the hold safe without a timer. `flush` is the
|
|
270
410
|
* ordinary way out and a driver calls it when its stream ends; this is for the
|
|
271
411
|
* case where nothing ever calls it and the terminal has sent `ESC [ <` and
|
|
272
412
|
* then a thousand digits.
|
|
273
413
|
*/
|
|
274
|
-
const HOLD_LIMIT =
|
|
414
|
+
const HOLD_LIMIT = 96;
|
|
275
415
|
|
|
276
416
|
/**
|
|
277
417
|
* Whether the bytes from `start` begin a sequence whose rest has not arrived.
|
|
@@ -287,7 +427,9 @@ const HOLD_LIMIT = 32;
|
|
|
287
427
|
* `?1003h` produces is a report per cell crossed, which is the traffic
|
|
288
428
|
* most likely to be split;
|
|
289
429
|
* * `ESC [ M` …, the old-style report, whose three payload bytes are
|
|
290
|
-
* arbitrary and become arbitrary keys
|
|
430
|
+
* arbitrary and become arbitrary keys;
|
|
431
|
+
* * Kitty `CSI u` key reports, where splitting before the final `u` would
|
|
432
|
+
* turn `CSI 97 ; 1 : 3 u` from "release A" into ordinary characters.
|
|
291
433
|
*
|
|
292
434
|
* Two bytes at least, so a lone `ESC` is still the Escape key: holding that
|
|
293
435
|
* back would mean Escape never fires until the next keystroke, which is worse
|
|
@@ -319,6 +461,9 @@ function incomplete(input: string, start: number): boolean {
|
|
|
319
461
|
if (rest.startsWith(MOUSE_LEGACY)) {
|
|
320
462
|
return rest.length < LEGACY_REPORT_LENGTH;
|
|
321
463
|
}
|
|
464
|
+
if (rest.startsWith(ESC + "[")) {
|
|
465
|
+
return /^[0-9:;]*$/.test(rest.slice(2));
|
|
466
|
+
}
|
|
322
467
|
return false;
|
|
323
468
|
}
|
|
324
469
|
|
|
@@ -333,13 +478,14 @@ export function createInputDecoder(): InputDecoder {
|
|
|
333
478
|
const events: Array<InputEvent> = [];
|
|
334
479
|
let input = waiting + chunk;
|
|
335
480
|
waiting = "";
|
|
336
|
-
|
|
481
|
+
const pasting = pending;
|
|
482
|
+
if (pasting != null) {
|
|
337
483
|
const end = input.indexOf(PASTE_END);
|
|
338
484
|
if (end < 0) {
|
|
339
|
-
pending
|
|
485
|
+
pending = pasting + input;
|
|
340
486
|
return events;
|
|
341
487
|
}
|
|
342
|
-
const text =
|
|
488
|
+
const text = pasting + input.slice(0, end);
|
|
343
489
|
pending = null;
|
|
344
490
|
events.push(pasteEvent(text, PASTE_START + text + PASTE_END));
|
|
345
491
|
input = input.slice(end + PASTE_END.length);
|
|
@@ -512,7 +658,7 @@ function decodeSequence(input: string, start: number): { key: KeyEvent, length:
|
|
|
512
658
|
|
|
513
659
|
let cursor = start + 2;
|
|
514
660
|
let parameters = "";
|
|
515
|
-
while (cursor < input.length && /[0-9
|
|
661
|
+
while (cursor < input.length && /[0-9:;]/.test(input[cursor])) {
|
|
516
662
|
parameters += input[cursor];
|
|
517
663
|
cursor += 1;
|
|
518
664
|
}
|
|
@@ -523,6 +669,14 @@ function decodeSequence(input: string, start: number): { key: KeyEvent, length:
|
|
|
523
669
|
const raw = input.slice(start, cursor + 1);
|
|
524
670
|
const [first, second] = parameters.split(";");
|
|
525
671
|
|
|
672
|
+
if (final === "u") {
|
|
673
|
+
const key = decodeKittyKey(parameters, raw);
|
|
674
|
+
if (key == null) {
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
return { key, length: raw.length };
|
|
678
|
+
}
|
|
679
|
+
|
|
526
680
|
if (final === "~") {
|
|
527
681
|
const name = CSI_TILDE[first];
|
|
528
682
|
if (name == null) {
|
|
@@ -544,6 +698,51 @@ function decodeSequence(input: string, start: number): { key: KeyEvent, length:
|
|
|
544
698
|
return { key: event({ name, sequence: "", raw, source: "escape", ...mods }), length: raw.length };
|
|
545
699
|
}
|
|
546
700
|
|
|
701
|
+
function decodeKittyKey(parameters: string, raw: string): KeyEvent | null {
|
|
702
|
+
const [keyParameter, modifierParameter, textParameter] = parameters.split(";");
|
|
703
|
+
const key = codePoint(keyParameter.split(":")[0]);
|
|
704
|
+
if (key == null) {
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
const modifierFields = modifierParameter?.split(":") ?? [];
|
|
708
|
+
const mods = modifiers(modifierFields[0]);
|
|
709
|
+
const eventType = keyEventType(modifierFields[1]);
|
|
710
|
+
if (eventType == null) {
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
const associatedText = codePointText(textParameter);
|
|
714
|
+
const functionName = CSI_U_FUNCTION[String(key)];
|
|
715
|
+
if (functionName != null) {
|
|
716
|
+
return event({
|
|
717
|
+
name: functionName,
|
|
718
|
+
sequence: "",
|
|
719
|
+
raw,
|
|
720
|
+
source: "escape",
|
|
721
|
+
...mods,
|
|
722
|
+
eventType,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
if (key === 0) {
|
|
726
|
+
return event({
|
|
727
|
+
name: "text",
|
|
728
|
+
sequence: eventType === "release" ? "" : associatedText,
|
|
729
|
+
raw,
|
|
730
|
+
source: "escape",
|
|
731
|
+
...mods,
|
|
732
|
+
eventType,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
const character = String.fromCodePoint(key);
|
|
736
|
+
const name = character === " " ? "space" : character.toLowerCase();
|
|
737
|
+
const sequence =
|
|
738
|
+
associatedText !== ""
|
|
739
|
+
? associatedText
|
|
740
|
+
: eventType === "release" || mods.ctrl || mods.meta
|
|
741
|
+
? ""
|
|
742
|
+
: character;
|
|
743
|
+
return event({ name, sequence, raw, source: "escape", ...mods, eventType });
|
|
744
|
+
}
|
|
745
|
+
|
|
547
746
|
/** One byte that is not part of an escape sequence. */
|
|
548
747
|
function decodePlain(character: string): KeyEvent {
|
|
549
748
|
const code = character.charCodeAt(0);
|