@gajae-code/tui 0.12.21 → 0.13.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/CHANGELOG.md +9 -0
- package/dist/types/components/editor.d.ts +11 -0
- package/dist/types/keys.d.ts +5 -4
- package/package.json +3 -3
- package/src/components/editor.ts +50 -0
- package/src/keys.ts +21 -9
- package/src/stdin-buffer.ts +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Added atomic same-line deletion APIs and an `Editor` undo callback for keeping application state synchronized with editor history.
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- macOS Terminal.app Option+Arrow input is now buffered and decoded as a single Meta-wrapped escape sequence, so Option+Up/Down can open and navigate queued-message selectors.
|
|
11
|
+
- Terminal.app Meta-prefix decoding now covers legacy Option shortcuts for printable symbols, digits, spaces, and Ctrl+Option symbol chords while preserving enhanced Kitty and modifyOtherKeys Super/Command matching.
|
|
12
|
+
- Kitty and modifyOtherKeys function-key sequences now match consistently for F1–F12, including unmodified CSI forms.
|
|
13
|
+
|
|
5
14
|
## [0.12.21] - 2026-08-09
|
|
6
15
|
|
|
7
16
|
## [0.12.20] - 2026-08-09
|
|
@@ -44,6 +44,7 @@ export declare class Editor implements Component, Focusable {
|
|
|
44
44
|
onSubmit?: (text: string) => void;
|
|
45
45
|
onAltEnter?: (text: string) => void;
|
|
46
46
|
onChange?: (text: string) => void;
|
|
47
|
+
onUndo?: (text: string) => void;
|
|
47
48
|
onAutocompleteCancel?: () => void;
|
|
48
49
|
onTabDeclined?: (text: string) => void;
|
|
49
50
|
/**
|
|
@@ -122,6 +123,16 @@ export declare class Editor implements Component, Focusable {
|
|
|
122
123
|
setText(text: string): void;
|
|
123
124
|
/** Insert text at the current cursor position */
|
|
124
125
|
insertText(text: string): void;
|
|
126
|
+
/**
|
|
127
|
+
* Delete an exact text run immediately before the cursor as one edit.
|
|
128
|
+
* Returns false without changing the document when the text is not present.
|
|
129
|
+
*/
|
|
130
|
+
deleteTextBeforeCursor(text: string): boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Delete a same-line text range containing the cursor as one edit.
|
|
133
|
+
* Returns false without changing the document when the range is invalid.
|
|
134
|
+
*/
|
|
135
|
+
deleteTextRangeAroundCursor(startCol: number, endCol: number): boolean;
|
|
125
136
|
isShowingAutocomplete(): boolean;
|
|
126
137
|
}
|
|
127
138
|
export {};
|
package/dist/types/keys.d.ts
CHANGED
|
@@ -62,6 +62,7 @@ export interface ParsedKeyId {
|
|
|
62
62
|
* Parse a case-insensitive key identifier into normalized dispatch parts.
|
|
63
63
|
* The legacy `plus` base-key alias normalizes to `+`; the trailing `+` in
|
|
64
64
|
* values such as `ctrl++` is the literal plus base.
|
|
65
|
+
* Modifier aliases `option`/`meta` and `command`/`cmd` normalize to `alt` and `super`.
|
|
65
66
|
*/
|
|
66
67
|
export declare function parseKeyId(value: string): ParsedKeyId | undefined;
|
|
67
68
|
/** Whether a value is a valid canonical key identifier. */
|
|
@@ -73,10 +74,7 @@ export declare function isKeyId(value: string): value is KeyId;
|
|
|
73
74
|
* is literally `"enter"`); the value of `Key` over a bag of magic strings is
|
|
74
75
|
* that each property is typed to the exact `KeyId` literal it produces and the
|
|
75
76
|
* modifier methods return precisely-typed concatenations (e.g. `Key.ctrl("c")`
|
|
76
|
-
*
|
|
77
|
-
* `@mariozechner/pi-tui` `Key` export verbatim so plugins built against any
|
|
78
|
-
* scope alias (`@mariozechner`, `@earendil-works`, `@gajae-code`) keep working
|
|
79
|
-
* once the specifier shim remaps them to this package.
|
|
77
|
+
* The canonical modifier helpers mirror the upstream `@mariozechner/pi-tui` `Key` export so plugins built against any scope alias (`@mariozechner`, `@earendil-works`, `@gajae-code`) keep working once the specifier shim remaps them to this package. The additional `option`/`command` helpers normalize macOS naming to the portable `alt`/`super` identifiers.
|
|
80
78
|
*/
|
|
81
79
|
export declare const Key: {
|
|
82
80
|
readonly escape: "escape";
|
|
@@ -143,7 +141,10 @@ export declare const Key: {
|
|
|
143
141
|
readonly ctrl: <K extends BaseKey>(key: K) => `ctrl+${K}`;
|
|
144
142
|
readonly shift: <K extends BaseKey>(key: K) => `shift+${K}`;
|
|
145
143
|
readonly alt: <K extends BaseKey>(key: K) => `alt+${K}`;
|
|
144
|
+
readonly option: <K extends BaseKey>(key: K) => `alt+${K}`;
|
|
146
145
|
readonly super: <K extends BaseKey>(key: K) => `super+${K}`;
|
|
146
|
+
readonly cmd: <K extends BaseKey>(key: K) => `super+${K}`;
|
|
147
|
+
readonly command: <K extends BaseKey>(key: K) => `super+${K}`;
|
|
147
148
|
readonly ctrlShift: <K extends BaseKey>(key: K) => `ctrl+shift+${K}`;
|
|
148
149
|
readonly shiftCtrl: <K extends BaseKey>(key: K) => `shift+ctrl+${K}`;
|
|
149
150
|
readonly ctrlAlt: <K extends BaseKey>(key: K) => `ctrl+alt+${K}`;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/tui",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.13.0",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://gajae-code.com",
|
|
7
7
|
"author": "Yeachan-Heo and Gajae Code Contributors",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"fmt": "biome format --write ."
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@gajae-code/natives": "0.
|
|
40
|
-
"@gajae-code/utils": "0.
|
|
39
|
+
"@gajae-code/natives": "0.13.0",
|
|
40
|
+
"@gajae-code/utils": "0.13.0",
|
|
41
41
|
"lru-cache": "11.3.6",
|
|
42
42
|
"marked": "18.0.6"
|
|
43
43
|
},
|
package/src/components/editor.ts
CHANGED
|
@@ -490,6 +490,7 @@ export class Editor implements Component, Focusable {
|
|
|
490
490
|
onSubmit?: (text: string) => void;
|
|
491
491
|
onAltEnter?: (text: string) => void;
|
|
492
492
|
onChange?: (text: string) => void;
|
|
493
|
+
onUndo?: (text: string) => void;
|
|
493
494
|
onAutocompleteCancel?: () => void;
|
|
494
495
|
onTabDeclined?: (text: string) => void;
|
|
495
496
|
/**
|
|
@@ -1878,6 +1879,50 @@ export class Editor implements Component, Focusable {
|
|
|
1878
1879
|
this.#exitHistoryForEditing();
|
|
1879
1880
|
this.#insertTextAtCursor(text);
|
|
1880
1881
|
}
|
|
1882
|
+
/**
|
|
1883
|
+
* Delete an exact text run immediately before the cursor as one edit.
|
|
1884
|
+
* Returns false without changing the document when the text is not present.
|
|
1885
|
+
*/
|
|
1886
|
+
deleteTextBeforeCursor(text: string): boolean {
|
|
1887
|
+
if (text.length === 0 || this.#state.cursorCol < text.length) return false;
|
|
1888
|
+
|
|
1889
|
+
const line = this.#state.lines[this.#state.cursorLine] || "";
|
|
1890
|
+
const startCol = this.#state.cursorCol - text.length;
|
|
1891
|
+
if (line.slice(startCol, this.#state.cursorCol) !== text) return false;
|
|
1892
|
+
|
|
1893
|
+
this.#exitHistoryForEditing();
|
|
1894
|
+
this.#resetKillSequence();
|
|
1895
|
+
this.#recordUndoState();
|
|
1896
|
+
this.#state.lines[this.#state.cursorLine] = line.slice(0, startCol) + line.slice(this.#state.cursorCol);
|
|
1897
|
+
this.#setCursorCol(startCol);
|
|
1898
|
+
this.#notifyBackwardDelete();
|
|
1899
|
+
return true;
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* Delete a same-line text range containing the cursor as one edit.
|
|
1903
|
+
* Returns false without changing the document when the range is invalid.
|
|
1904
|
+
*/
|
|
1905
|
+
deleteTextRangeAroundCursor(startCol: number, endCol: number): boolean {
|
|
1906
|
+
const line = this.#state.lines[this.#state.cursorLine] || "";
|
|
1907
|
+
if (
|
|
1908
|
+
!Number.isInteger(startCol) ||
|
|
1909
|
+
!Number.isInteger(endCol) ||
|
|
1910
|
+
startCol < 0 ||
|
|
1911
|
+
startCol >= endCol ||
|
|
1912
|
+
endCol > line.length ||
|
|
1913
|
+
startCol > this.#state.cursorCol ||
|
|
1914
|
+
endCol < this.#state.cursorCol
|
|
1915
|
+
)
|
|
1916
|
+
return false;
|
|
1917
|
+
|
|
1918
|
+
this.#exitHistoryForEditing();
|
|
1919
|
+
this.#resetKillSequence();
|
|
1920
|
+
this.#recordUndoState();
|
|
1921
|
+
this.#state.lines[this.#state.cursorLine] = line.slice(0, startCol) + line.slice(endCol);
|
|
1922
|
+
this.#setCursorCol(startCol);
|
|
1923
|
+
this.#notifyBackwardDelete();
|
|
1924
|
+
return true;
|
|
1925
|
+
}
|
|
1881
1926
|
|
|
1882
1927
|
// All the editor methods from before...
|
|
1883
1928
|
#insertCharacter(char: string): void {
|
|
@@ -2130,6 +2175,10 @@ export class Editor implements Component, Focusable {
|
|
|
2130
2175
|
this.#setCursorCol(previousLine.length);
|
|
2131
2176
|
}
|
|
2132
2177
|
|
|
2178
|
+
this.#notifyBackwardDelete();
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
#notifyBackwardDelete(): void {
|
|
2133
2182
|
if (this.onChange) {
|
|
2134
2183
|
this.onChange(this.getText());
|
|
2135
2184
|
}
|
|
@@ -2290,6 +2339,7 @@ export class Editor implements Component, Focusable {
|
|
|
2290
2339
|
this.#preferredVisualCol = null;
|
|
2291
2340
|
Object.assign(this.#state, snapshot);
|
|
2292
2341
|
this.#bumpDocumentVersion();
|
|
2342
|
+
this.onUndo?.(this.getText());
|
|
2293
2343
|
|
|
2294
2344
|
if (this.onChange) {
|
|
2295
2345
|
this.onChange(this.getText());
|
package/src/keys.ts
CHANGED
|
@@ -263,12 +263,22 @@ const BASE_KEYS = new Set<string>([
|
|
|
263
263
|
"f12",
|
|
264
264
|
]);
|
|
265
265
|
|
|
266
|
-
const
|
|
266
|
+
const KEY_MODIFIER_ALIASES: Readonly<Partial<Record<string, KeyModifier>>> = {
|
|
267
|
+
ctrl: "ctrl",
|
|
268
|
+
alt: "alt",
|
|
269
|
+
shift: "shift",
|
|
270
|
+
super: "super",
|
|
271
|
+
option: "alt",
|
|
272
|
+
meta: "alt",
|
|
273
|
+
cmd: "super",
|
|
274
|
+
command: "super",
|
|
275
|
+
};
|
|
267
276
|
|
|
268
277
|
/**
|
|
269
278
|
* Parse a case-insensitive key identifier into normalized dispatch parts.
|
|
270
279
|
* The legacy `plus` base-key alias normalizes to `+`; the trailing `+` in
|
|
271
280
|
* values such as `ctrl++` is the literal plus base.
|
|
281
|
+
* Modifier aliases `option`/`meta` and `command`/`cmd` normalize to `alt` and `super`.
|
|
272
282
|
*/
|
|
273
283
|
export function parseKeyId(value: string): ParsedKeyId | undefined {
|
|
274
284
|
if (hasControlChars(value) || value.length === 0) return undefined;
|
|
@@ -289,10 +299,12 @@ export function parseKeyId(value: string): ParsedKeyId | undefined {
|
|
|
289
299
|
.slice(0, -1)
|
|
290
300
|
.split("+")
|
|
291
301
|
.map(part => part.trim());
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
302
|
+
const modifiers: KeyModifier[] = [];
|
|
303
|
+
for (const part of modifierParts) {
|
|
304
|
+
const modifier = KEY_MODIFIER_ALIASES[part];
|
|
305
|
+
if (!modifier || modifiers.includes(modifier)) return undefined;
|
|
306
|
+
modifiers.push(modifier);
|
|
307
|
+
}
|
|
296
308
|
|
|
297
309
|
const canonicalBase = baseKey === "pageup" ? "pageUp" : baseKey === "pagedown" ? "pageDown" : baseKey;
|
|
298
310
|
return {
|
|
@@ -314,10 +326,7 @@ export function isKeyId(value: string): value is KeyId {
|
|
|
314
326
|
* is literally `"enter"`); the value of `Key` over a bag of magic strings is
|
|
315
327
|
* that each property is typed to the exact `KeyId` literal it produces and the
|
|
316
328
|
* modifier methods return precisely-typed concatenations (e.g. `Key.ctrl("c")`
|
|
317
|
-
*
|
|
318
|
-
* `@mariozechner/pi-tui` `Key` export verbatim so plugins built against any
|
|
319
|
-
* scope alias (`@mariozechner`, `@earendil-works`, `@gajae-code`) keep working
|
|
320
|
-
* once the specifier shim remaps them to this package.
|
|
329
|
+
* The canonical modifier helpers mirror the upstream `@mariozechner/pi-tui` `Key` export so plugins built against any scope alias (`@mariozechner`, `@earendil-works`, `@gajae-code`) keep working once the specifier shim remaps them to this package. The additional `option`/`command` helpers normalize macOS naming to the portable `alt`/`super` identifiers.
|
|
321
330
|
*/
|
|
322
331
|
export const Key = {
|
|
323
332
|
escape: "escape",
|
|
@@ -384,7 +393,10 @@ export const Key = {
|
|
|
384
393
|
ctrl: <K extends BaseKey>(key: K) => `ctrl+${key}` as const,
|
|
385
394
|
shift: <K extends BaseKey>(key: K) => `shift+${key}` as const,
|
|
386
395
|
alt: <K extends BaseKey>(key: K) => `alt+${key}` as const,
|
|
396
|
+
option: <K extends BaseKey>(key: K) => `alt+${key}` as const,
|
|
387
397
|
super: <K extends BaseKey>(key: K) => `super+${key}` as const,
|
|
398
|
+
cmd: <K extends BaseKey>(key: K) => `super+${key}` as const,
|
|
399
|
+
command: <K extends BaseKey>(key: K) => `super+${key}` as const,
|
|
388
400
|
ctrlShift: <K extends BaseKey>(key: K) => `ctrl+shift+${key}` as const,
|
|
389
401
|
shiftCtrl: <K extends BaseKey>(key: K) => `shift+ctrl+${key}` as const,
|
|
390
402
|
ctrlAlt: <K extends BaseKey>(key: K) => `ctrl+alt+${key}` as const,
|
package/src/stdin-buffer.ts
CHANGED
|
@@ -114,6 +114,14 @@ function isCompleteSequence(data: string): "complete" | "incomplete" | "not-esca
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
const afterEsc = data.slice(1);
|
|
117
|
+
// macOS Terminal.app with "Use Option as Meta key" applies to both
|
|
118
|
+
// physical Option keys and wraps an escape sequence with a second ESC
|
|
119
|
+
// (for example Option+Up: ESC ESC [ A).
|
|
120
|
+
// Delegate completeness to the inner sequence so the pair is emitted
|
|
121
|
+
// atomically instead of being split into ESC ESC and the CSI suffix.
|
|
122
|
+
if (afterEsc.startsWith(ESC)) {
|
|
123
|
+
return isCompleteSequence(afterEsc);
|
|
124
|
+
}
|
|
117
125
|
|
|
118
126
|
// CSI sequences: ESC [
|
|
119
127
|
if (afterEsc.startsWith("[")) {
|