@uniflowed/tui 0.0.0-alpha.18 → 0.14.1

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/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 press.
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
- /** Always `"press"`. Release reporting needs the Kitty protocol; see #314. */
92
- readonly eventType: "press",
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
- * `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.
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 = 32;
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
- if (pending != null) {
481
+ const pasting = pending;
482
+ if (pasting != null) {
337
483
  const end = input.indexOf(PASTE_END);
338
484
  if (end < 0) {
339
- pending += input;
485
+ pending = pasting + input;
340
486
  return events;
341
487
  }
342
- const text = pending + input.slice(0, end);
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;]/.test(input[cursor])) {
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);