@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 +3 -3
- package/src/autocomplete.ts +1 -0
- package/src/components/editor.ts +40 -70
- package/src/components/input.ts +5 -8
- package/src/keys.ts +96 -1
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.
|
|
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.
|
|
37
|
-
"@oh-my-pi/pi-utils": "13.9.
|
|
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": {
|
package/src/autocomplete.ts
CHANGED
package/src/components/editor.ts
CHANGED
|
@@ -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
|
-
|
|
761
|
-
|
|
704
|
+
const printableText = extractPrintableText(data);
|
|
705
|
+
if (printableText) {
|
|
762
706
|
const direction = this.#jumpMode;
|
|
763
707
|
this.#jumpMode = null;
|
|
764
|
-
this.#jumpToChar(
|
|
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
|
|
1018
|
+
// Printable keystrokes, including Kitty CSI-u text-producing sequences.
|
|
1070
1019
|
else {
|
|
1071
|
-
const
|
|
1072
|
-
if (
|
|
1073
|
-
this
|
|
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
|
-
//
|
|
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
|
|
package/src/components/input.ts
CHANGED
|
@@ -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
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
*
|