@oh-my-pi/pi-tui 13.9.6 → 13.9.12

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "13.9.6",
4
+ "version": "13.9.12",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -33,8 +33,8 @@
33
33
  "test": "bun test test/*.test.ts"
34
34
  },
35
35
  "dependencies": {
36
- "@oh-my-pi/pi-natives": "13.9.6",
37
- "@oh-my-pi/pi-utils": "13.9.6",
36
+ "@oh-my-pi/pi-natives": "13.9.12",
37
+ "@oh-my-pi/pi-utils": "13.9.12",
38
38
  "marked": "^17.0"
39
39
  },
40
40
  "devDependencies": {
@@ -188,6 +188,7 @@ export interface AutocompleteProvider {
188
188
  lines: string[];
189
189
  cursorLine: number;
190
190
  cursorCol: number;
191
+ onApplied?: () => void;
191
192
  };
192
193
 
193
194
  /** Get inline hint text to show as dim ghost text after the cursor */
@@ -2,7 +2,7 @@ import { getProjectDir } from "@oh-my-pi/pi-utils";
2
2
  import type { AutocompleteProvider, CombinedAutocompleteProvider } from "../autocomplete";
3
3
  import { BracketedPasteHandler } from "../bracketed-paste";
4
4
  import { type EditorKeybindingsManager, getEditorKeybindings } from "../keybindings";
5
- import { matchesKey } from "../keys";
5
+ import { extractPrintableText, matchesKey } from "../keys";
6
6
  import { KillRing } from "../kill-ring";
7
7
  import type { SymbolTheme } from "../symbols";
8
8
  import { type Component, CURSOR_MARKER, type Focusable } from "../tui";
@@ -255,62 +255,6 @@ function wordWrapLine(line: string, maxWidth: number): TextChunk[] {
255
255
  return chunks.length > 0 ? chunks : [{ text: "", startIndex: 0, endIndex: 0 }];
256
256
  }
257
257
 
258
- // Kitty CSI-u sequences for printable keys, including optional shifted/base codepoints and text field.
259
- const KITTY_CSI_U_REGEX = /^\x1b\[(\d+)(?::(\d*))?(?::(\d+))?(?:;(\d+))?(?::(\d+))?(?:;([\d:]*))?u$/;
260
- const KITTY_MOD_SHIFT = 1;
261
- const KITTY_MOD_ALT = 2;
262
- const KITTY_MOD_CTRL = 4;
263
-
264
- // Decode a printable CSI-u sequence, preferring the shifted key when present.
265
- function decodeKittyPrintable(data: string): string | undefined {
266
- const match = data.match(KITTY_CSI_U_REGEX);
267
- if (!match) return undefined;
268
-
269
- // CSI-u groups: <codepoint>[:<shifted>[:<base>]];<mod>u
270
- const codepoint = Number.parseInt(match[1] ?? "", 10);
271
- if (!Number.isFinite(codepoint)) return undefined;
272
-
273
- const shiftedKey = match[2] && match[2].length > 0 ? Number.parseInt(match[2], 10) : undefined;
274
- const modValue = match[4] ? Number.parseInt(match[4], 10) : 1;
275
- // Modifiers are 1-indexed in CSI-u; normalize to our bitmask.
276
- const modifier = Number.isFinite(modValue) ? modValue - 1 : 0;
277
-
278
- // Ignore CSI-u sequences used for Alt/Ctrl shortcuts.
279
- if (modifier & (KITTY_MOD_ALT | KITTY_MOD_CTRL)) return undefined;
280
-
281
- const textField = match[6];
282
- if (textField && textField.length > 0) {
283
- const codepoints = textField
284
- .split(":")
285
- .filter(Boolean)
286
- .map(value => Number.parseInt(value, 10))
287
- .filter(value => Number.isFinite(value) && value >= 32);
288
- if (codepoints.length > 0) {
289
- try {
290
- return String.fromCodePoint(...codepoints);
291
- } catch {
292
- return undefined;
293
- }
294
- }
295
- }
296
-
297
- // Prefer the shifted keycode when Shift is held.
298
- let effectiveCodepoint = codepoint;
299
- if (modifier & KITTY_MOD_SHIFT && typeof shiftedKey === "number") {
300
- effectiveCodepoint = shiftedKey;
301
- }
302
- if (effectiveCodepoint >= 0xe000 && effectiveCodepoint <= 0xf8ff) {
303
- return undefined;
304
- }
305
- // Drop control characters or invalid codepoints.
306
- if (!Number.isFinite(effectiveCodepoint) || effectiveCodepoint < 32) return undefined;
307
-
308
- try {
309
- return String.fromCodePoint(effectiveCodepoint);
310
- } catch {
311
- return undefined;
312
- }
313
- }
314
258
  const DEFAULT_PAGE_SCROLL_LINES = 10;
315
259
 
316
260
  interface EditorState {
@@ -757,11 +701,11 @@ export class Editor implements Component, Focusable {
757
701
  return;
758
702
  }
759
703
 
760
- if (data.charCodeAt(0) >= 32) {
761
- // Printable character - perform the jump
704
+ const printableText = extractPrintableText(data);
705
+ if (printableText) {
762
706
  const direction = this.#jumpMode;
763
707
  this.#jumpMode = null;
764
- this.#jumpToChar(data, direction);
708
+ this.#jumpToChar(printableText, direction);
765
709
  return;
766
710
  }
767
711
 
@@ -845,6 +789,8 @@ export class Editor implements Component, Focusable {
845
789
  if (this.onChange) {
846
790
  this.onChange(this.getText());
847
791
  }
792
+
793
+ result.onApplied?.();
848
794
  }
849
795
  return;
850
796
  }
@@ -874,6 +820,7 @@ export class Editor implements Component, Focusable {
874
820
  this.#state.lines = result.lines;
875
821
  this.#state.cursorLine = result.cursorLine;
876
822
  this.#setCursorCol(result.cursorCol);
823
+ result.onApplied?.();
877
824
  }
878
825
  this.#cancelAutocomplete();
879
826
  }
@@ -900,6 +847,8 @@ export class Editor implements Component, Focusable {
900
847
  if (this.onChange) {
901
848
  this.onChange(this.getText());
902
849
  }
850
+
851
+ result.onApplied?.();
903
852
  }
904
853
  return;
905
854
  }
@@ -1066,16 +1015,11 @@ export class Editor implements Component, Focusable {
1066
1015
  } else if (kb.matches(data, "jumpBackward")) {
1067
1016
  this.#jumpMode = "backward";
1068
1017
  }
1069
- // Kitty CSI-u printable characters (shifted symbols like @, ?, {, })
1018
+ // Printable keystrokes, including Kitty CSI-u text-producing sequences.
1070
1019
  else {
1071
- const kittyChar = decodeKittyPrintable(data);
1072
- if (kittyChar) {
1073
- this.insertText(kittyChar);
1074
- return;
1075
- }
1076
- // Regular characters (printable characters and unicode, but not control characters)
1077
- if (data.charCodeAt(0) >= 32) {
1078
- this.#insertCharacter(data);
1020
+ const printableText = extractPrintableText(data);
1021
+ if (printableText) {
1022
+ this.#insertCharacter(printableText);
1079
1023
  }
1080
1024
  }
1081
1025
  }
@@ -1193,6 +1137,14 @@ export class Editor implements Component, Focusable {
1193
1137
  return { line: this.#state.cursorLine, col: this.#state.cursorCol };
1194
1138
  }
1195
1139
 
1140
+ moveToLineStart(): void {
1141
+ this.#moveToLineStart();
1142
+ }
1143
+
1144
+ moveToLineEnd(): void {
1145
+ this.#moveToLineEnd();
1146
+ }
1147
+
1196
1148
  setText(text: string): void {
1197
1149
  this.#historyIndex = -1; // Exit history browsing mode
1198
1150
  this.#resetKillSequence();
@@ -1261,7 +1213,11 @@ export class Editor implements Component, Focusable {
1261
1213
  this.#tryTriggerAutocomplete();
1262
1214
  }
1263
1215
  }
1264
- // Also auto-trigger when typing letters/path chars in a slash command context
1216
+ // Auto-trigger for "#" prompt actions anywhere in the current token
1217
+ else if (char === "#") {
1218
+ this.#tryTriggerAutocomplete();
1219
+ }
1220
+ // Also auto-trigger when typing letters/path chars in a completable context
1265
1221
  else if (/[a-zA-Z0-9.\-_/]/.test(char)) {
1266
1222
  const currentLine = this.#state.lines[this.#state.cursorLine] || "";
1267
1223
  const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
@@ -1273,6 +1229,10 @@ export class Editor implements Component, Focusable {
1273
1229
  else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
1274
1230
  this.#tryTriggerAutocomplete();
1275
1231
  }
1232
+ // Check if we're in a # prompt action context
1233
+ else if (textBeforeCursor.match(/#[^\s#]*$/)) {
1234
+ this.#tryTriggerAutocomplete();
1235
+ }
1276
1236
  }
1277
1237
  } else {
1278
1238
  this.#debouncedUpdateAutocomplete();
@@ -1446,6 +1406,10 @@ export class Editor implements Component, Focusable {
1446
1406
  else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
1447
1407
  this.#tryTriggerAutocomplete();
1448
1408
  }
1409
+ // # prompt action context
1410
+ else if (textBeforeCursor.match(/#[^\s#]*$/)) {
1411
+ this.#tryTriggerAutocomplete();
1412
+ }
1449
1413
  }
1450
1414
  }
1451
1415
 
@@ -1582,6 +1546,8 @@ export class Editor implements Component, Focusable {
1582
1546
  this.#tryTriggerAutocomplete();
1583
1547
  } else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
1584
1548
  this.#tryTriggerAutocomplete();
1549
+ } else if (textBeforeCursor.match(/#[^\s#]*$/)) {
1550
+ this.#tryTriggerAutocomplete();
1585
1551
  }
1586
1552
  }
1587
1553
  }
@@ -1873,6 +1839,10 @@ export class Editor implements Component, Focusable {
1873
1839
  else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
1874
1840
  this.#tryTriggerAutocomplete();
1875
1841
  }
1842
+ // # prompt action context
1843
+ else if (textBeforeCursor.match(/#[^\s#]*$/)) {
1844
+ this.#tryTriggerAutocomplete();
1845
+ }
1876
1846
  }
1877
1847
  }
1878
1848
 
@@ -1,5 +1,6 @@
1
1
  import { BracketedPasteHandler } from "../bracketed-paste";
2
2
  import { getEditorKeybindings } from "../keybindings";
3
+ import { extractPrintableText } from "../keys";
3
4
  import { KillRing } from "../kill-ring";
4
5
  import { type Component, CURSOR_MARKER, type Focusable } from "../tui";
5
6
  import {
@@ -169,14 +170,10 @@ export class Input implements Component, Focusable {
169
170
  return;
170
171
  }
171
172
 
172
- // Regular character input - accept printable characters including Unicode,
173
- // but reject control characters (C0: 0x00-0x1F, DEL: 0x7F, C1: 0x80-0x9F)
174
- const hasControlChars = [...data].some(ch => {
175
- const code = ch.charCodeAt(0);
176
- return code < 32 || code === 0x7f || (code >= 0x80 && code <= 0x9f);
177
- });
178
- if (!hasControlChars) {
179
- this.#insertCharacter(data);
173
+ // Regular character input, including Kitty CSI-u text-producing sequences.
174
+ const printableText = extractPrintableText(data);
175
+ if (printableText) {
176
+ this.#insertCharacter(printableText);
180
177
  }
181
178
  }
182
179
 
package/src/keys.ts CHANGED
@@ -78,6 +78,8 @@ type Letter =
78
78
  | "y"
79
79
  | "z";
80
80
 
81
+ type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
82
+
81
83
  type SymbolKey =
82
84
  | "`"
83
85
  | "-"
@@ -143,7 +145,7 @@ type SpecialKey =
143
145
  | "f11"
144
146
  | "f12";
145
147
 
146
- type BaseKey = Letter | SymbolKey | SpecialKey;
148
+ type BaseKey = Letter | Digit | SymbolKey | SpecialKey;
147
149
 
148
150
  /**
149
151
  * Union type of all valid key identifiers.
@@ -184,6 +186,24 @@ interface ParsedKittySequence {
184
186
  // Format: \x1b[...;modifier:event_type<terminator> where terminator is u, ~, or A-F/H
185
187
  const KITTY_RELEASE_PATTERN = /^\x1b\[[\d:;]*:3[u~ABCDHF]$/;
186
188
  const KITTY_REPEAT_PATTERN = /^\x1b\[[\d:;]*:2[u~ABCDHF]$/;
189
+ const KITTY_CSI_U_PATTERN = /^\x1b\[(\d+)(?::(\d*))?(?::(\d+))?(?:;(\d+))?(?::(\d+))?(?:;([\d:]*))?u$/;
190
+ const KITTY_MOD_SHIFT = 1;
191
+ const KITTY_MOD_ALT = 2;
192
+ const KITTY_MOD_CTRL = 4;
193
+ const KITTY_MOD_NUM_LOCK = 128;
194
+ const KITTY_NUMPAD_TEXT: Record<number, string> = {
195
+ 57399: "0",
196
+ 57400: "1",
197
+ 57401: "2",
198
+ 57402: "3",
199
+ 57403: "4",
200
+ 57404: "5",
201
+ 57405: "6",
202
+ 57406: "7",
203
+ 57407: "8",
204
+ 57408: "9",
205
+ 57409: ".",
206
+ };
187
207
 
188
208
  /**
189
209
  * Check if the input is a key release event.
@@ -237,6 +257,81 @@ export function parseKittySequence(data: string): ParsedKittySequence | null {
237
257
  };
238
258
  }
239
259
 
260
+ function hasControlChars(data: string): boolean {
261
+ return [...data].some(ch => {
262
+ const code = ch.charCodeAt(0);
263
+ return code < 32 || code === 0x7f || (code >= 0x80 && code <= 0x9f);
264
+ });
265
+ }
266
+
267
+ function decodeKittyPrintable(data: string): string | undefined {
268
+ const match = data.match(KITTY_CSI_U_PATTERN);
269
+ if (!match) return undefined;
270
+
271
+ const codepoint = Number.parseInt(match[1] ?? "", 10);
272
+ if (!Number.isFinite(codepoint)) return undefined;
273
+
274
+ if (match[5] === "3") return undefined;
275
+
276
+ const shiftedKey = match[2] && match[2].length > 0 ? Number.parseInt(match[2], 10) : undefined;
277
+ const modValue = match[4] ? Number.parseInt(match[4], 10) : 1;
278
+ const modifier = Number.isFinite(modValue) ? modValue - 1 : 0;
279
+ const effectiveMod = modifier & ~(64 + 128);
280
+
281
+ if (effectiveMod & (KITTY_MOD_ALT | KITTY_MOD_CTRL)) return undefined;
282
+
283
+ const textField = match[6];
284
+ if (textField && textField.length > 0) {
285
+ const codepoints = textField
286
+ .split(":")
287
+ .filter(Boolean)
288
+ .map(value => Number.parseInt(value, 10))
289
+ .filter(value => Number.isFinite(value) && value >= 32);
290
+ if (codepoints.length > 0) {
291
+ try {
292
+ return String.fromCodePoint(...codepoints);
293
+ } catch {
294
+ return undefined;
295
+ }
296
+ }
297
+ }
298
+
299
+ if (effectiveMod === 0 && modifier & KITTY_MOD_NUM_LOCK) {
300
+ const numpadText = KITTY_NUMPAD_TEXT[codepoint];
301
+ if (numpadText) return numpadText;
302
+ }
303
+
304
+ let effectiveCodepoint = codepoint;
305
+ if (effectiveMod & KITTY_MOD_SHIFT && typeof shiftedKey === "number") {
306
+ effectiveCodepoint = shiftedKey;
307
+ }
308
+
309
+ if (effectiveCodepoint >= 0xe000 && effectiveCodepoint <= 0xf8ff) {
310
+ return undefined;
311
+ }
312
+
313
+ if (!Number.isFinite(effectiveCodepoint) || effectiveCodepoint < 32) return undefined;
314
+
315
+ try {
316
+ return String.fromCodePoint(effectiveCodepoint);
317
+ } catch {
318
+ return undefined;
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Extract printable text from raw terminal input.
324
+ *
325
+ * Handles Kitty CSI-u text-producing keys so text-entry components can treat
326
+ * keypad digits and shifted symbols the same as direct character input.
327
+ */
328
+ export function extractPrintableText(data: string): string | undefined {
329
+ const kittyText = decodeKittyPrintable(data);
330
+ if (kittyText) return kittyText;
331
+ if (data.length === 0 || hasControlChars(data)) return undefined;
332
+ return data;
333
+ }
334
+
240
335
  /**
241
336
  * Match input data against a key identifier string.
242
337
  *