@gajae-code/tui 0.11.7 → 0.11.9

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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.11.9] - 2026-07-24
6
+
5
7
  ## [0.11.7] - 2026-07-22
6
8
  ### Fixed
7
9
 
@@ -41,11 +41,11 @@ export declare function setKittyProtocolActive(active: boolean): void;
41
41
  export declare function isKittyProtocolActive(): boolean;
42
42
  type Letter = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z";
43
43
  type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
44
- type SymbolKey = "`" | "-" | "=" | "[" | "]" | "\\" | ";" | "'" | "," | "." | "/" | "!" | "@" | "#" | "$" | "%" | "^" | "&" | "*" | "(" | ")" | "_" | "+" | "|" | "~" | "{" | "}" | ":" | "<" | ">" | "?";
44
+ type SymbolKey = "`" | "-" | "=" | "[" | "]" | "\\" | ";" | "'" | '"' | "," | "." | "/" | "!" | "@" | "#" | "$" | "%" | "^" | "&" | "*" | "(" | ")" | "_" | "+" | "|" | "~" | "{" | "}" | ":" | "<" | ">" | "?";
45
45
  type SpecialKey = "escape" | "esc" | "enter" | "return" | "tab" | "space" | "backspace" | "delete" | "insert" | "clear" | "home" | "end" | "pageUp" | "pageDown" | "up" | "down" | "left" | "right" | "f1" | "f2" | "f3" | "f4" | "f5" | "f6" | "f7" | "f8" | "f9" | "f10" | "f11" | "f12";
46
- type BaseKey = Letter | Digit | SymbolKey | SpecialKey;
47
- type ModifierName = "ctrl" | "shift" | "alt" | "super";
48
- type ModifiedKeyId<Key extends string, RemainingModifiers extends ModifierName = ModifierName> = {
46
+ export type BaseKey = Letter | Digit | SymbolKey | SpecialKey;
47
+ export type KeyModifier = "ctrl" | "shift" | "alt" | "super";
48
+ type ModifiedKeyId<Key extends string, RemainingModifiers extends KeyModifier = KeyModifier> = {
49
49
  [M in RemainingModifiers]: `${M}+${Key}` | `${M}+${ModifiedKeyId<Key, Exclude<RemainingModifiers, M>>}`;
50
50
  }[RemainingModifiers];
51
51
  /**
@@ -53,6 +53,19 @@ type ModifiedKeyId<Key extends string, RemainingModifiers extends ModifierName =
53
53
  * Provides autocomplete and catches typos at compile time.
54
54
  */
55
55
  export type KeyId = BaseKey | ModifiedKeyId<BaseKey>;
56
+ export interface ParsedKeyId {
57
+ keyId: KeyId;
58
+ modifiers: KeyModifier[];
59
+ baseKey: BaseKey;
60
+ }
61
+ /**
62
+ * Parse a case-insensitive key identifier into normalized dispatch parts.
63
+ * The legacy `plus` base-key alias normalizes to `+`; the trailing `+` in
64
+ * values such as `ctrl++` is the literal plus base.
65
+ */
66
+ export declare function parseKeyId(value: string): ParsedKeyId | undefined;
67
+ /** Whether a value is a valid canonical key identifier. */
68
+ export declare function isKeyId(value: string): value is KeyId;
56
69
  /**
57
70
  * Typed helper for constructing key identifiers with autocomplete.
58
71
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/tui",
4
- "version": "0.11.7",
4
+ "version": "0.11.9",
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.11.7",
40
- "@gajae-code/utils": "0.11.7",
39
+ "@gajae-code/natives": "0.11.9",
40
+ "@gajae-code/utils": "0.11.9",
41
41
  "lru-cache": "11.3.6",
42
42
  "marked": "18.0.6"
43
43
  },
@@ -170,7 +170,16 @@ const SHIFTED_SYMBOL_KEYS = new Set<string>([
170
170
  "~",
171
171
  ]);
172
172
 
173
- const normalizeKeyId = (key: KeyId): KeyId => key.toLowerCase() as KeyId;
173
+ const normalizeKeyId = (key: KeyId): KeyId => {
174
+ const normalized = key.toLowerCase();
175
+ if (normalized.endsWith("pageup")) {
176
+ return `${normalized.slice(0, -6)}pageUp` as KeyId;
177
+ }
178
+ if (normalized.endsWith("pagedown")) {
179
+ return `${normalized.slice(0, -8)}pageDown` as KeyId;
180
+ }
181
+ return normalized as KeyId;
182
+ };
174
183
 
175
184
  function normalizeKeys(keys: KeyId | KeyId[] | undefined): KeyId[] {
176
185
  if (keys === undefined) return [];
package/src/keys.ts CHANGED
@@ -117,6 +117,7 @@ type SymbolKey =
117
117
  | "\\"
118
118
  | ";"
119
119
  | "'"
120
+ | '"'
120
121
  | ","
121
122
  | "."
122
123
  | "/"
@@ -173,10 +174,10 @@ type SpecialKey =
173
174
  | "f11"
174
175
  | "f12";
175
176
 
176
- type BaseKey = Letter | Digit | SymbolKey | SpecialKey;
177
- type ModifierName = "ctrl" | "shift" | "alt" | "super";
177
+ export type BaseKey = Letter | Digit | SymbolKey | SpecialKey;
178
+ export type KeyModifier = "ctrl" | "shift" | "alt" | "super";
178
179
 
179
- type ModifiedKeyId<Key extends string, RemainingModifiers extends ModifierName = ModifierName> = {
180
+ type ModifiedKeyId<Key extends string, RemainingModifiers extends KeyModifier = KeyModifier> = {
180
181
  [M in RemainingModifiers]: `${M}+${Key}` | `${M}+${ModifiedKeyId<Key, Exclude<RemainingModifiers, M>>}`;
181
182
  }[RemainingModifiers];
182
183
 
@@ -186,6 +187,123 @@ type ModifiedKeyId<Key extends string, RemainingModifiers extends ModifierName =
186
187
  */
187
188
  export type KeyId = BaseKey | ModifiedKeyId<BaseKey>;
188
189
 
190
+ export interface ParsedKeyId {
191
+ keyId: KeyId;
192
+ modifiers: KeyModifier[];
193
+ baseKey: BaseKey;
194
+ }
195
+
196
+ const BASE_KEYS = new Set<string>([
197
+ ..."abcdefghijklmnopqrstuvwxyz",
198
+ ..."0123456789",
199
+ "`",
200
+ "-",
201
+ "=",
202
+ "[",
203
+ "]",
204
+ "\\",
205
+ ";",
206
+ "'",
207
+ '"',
208
+ ",",
209
+ ".",
210
+ "/",
211
+ "!",
212
+ "@",
213
+ "#",
214
+ "$",
215
+ "%",
216
+ "^",
217
+ "&",
218
+ "*",
219
+ "(",
220
+ ")",
221
+ "_",
222
+ "+",
223
+ "|",
224
+ "~",
225
+ "{",
226
+ "}",
227
+ ":",
228
+ "<",
229
+ ">",
230
+ "?",
231
+ "escape",
232
+ "esc",
233
+ "enter",
234
+ "return",
235
+ "tab",
236
+ "space",
237
+ "backspace",
238
+ "delete",
239
+ "insert",
240
+ "clear",
241
+ "home",
242
+ "end",
243
+ "pageup",
244
+ "pagedown",
245
+ "up",
246
+ "down",
247
+ "left",
248
+ "right",
249
+ "f1",
250
+ "f2",
251
+ "f3",
252
+ "f4",
253
+ "f5",
254
+ "f6",
255
+ "f7",
256
+ "f8",
257
+ "f9",
258
+ "f10",
259
+ "f11",
260
+ "f12",
261
+ ]);
262
+
263
+ const KEY_MODIFIERS: readonly KeyModifier[] = ["ctrl", "alt", "shift", "super"];
264
+
265
+ /**
266
+ * Parse a case-insensitive key identifier into normalized dispatch parts.
267
+ * The legacy `plus` base-key alias normalizes to `+`; the trailing `+` in
268
+ * values such as `ctrl++` is the literal plus base.
269
+ */
270
+ export function parseKeyId(value: string): ParsedKeyId | undefined {
271
+ if (hasControlChars(value) || value.length === 0) return undefined;
272
+
273
+ const lowerCaseValue = value
274
+ .trim()
275
+ .toLowerCase()
276
+ .replace(/\s*\+\s*/g, "+");
277
+ const normalized = lowerCaseValue === "plus" ? "+" : lowerCaseValue.replace(/\+plus$/, "++");
278
+ const baseKey = normalized.endsWith("+") ? "+" : normalized.split("+").pop();
279
+ if (!baseKey || !BASE_KEYS.has(baseKey)) return undefined;
280
+
281
+ const modifierSource = normalized.slice(0, normalized.length - baseKey.length);
282
+ const modifierParts =
283
+ modifierSource.length === 0
284
+ ? []
285
+ : modifierSource
286
+ .slice(0, -1)
287
+ .split("+")
288
+ .map(part => part.trim());
289
+ if (modifierParts.some(part => !KEY_MODIFIERS.includes(part as KeyModifier))) return undefined;
290
+
291
+ const modifiers = modifierParts as KeyModifier[];
292
+ if (new Set(modifiers).size !== modifiers.length) return undefined;
293
+
294
+ const canonicalBase = baseKey === "pageup" ? "pageUp" : baseKey === "pagedown" ? "pageDown" : baseKey;
295
+ return {
296
+ keyId: [...modifiers, canonicalBase].join("+") as KeyId,
297
+ modifiers,
298
+ baseKey: canonicalBase as BaseKey,
299
+ };
300
+ }
301
+
302
+ /** Whether a value is a valid canonical key identifier. */
303
+ export function isKeyId(value: string): value is KeyId {
304
+ return parseKeyId(value)?.keyId === value;
305
+ }
306
+
189
307
  /**
190
308
  * Typed helper for constructing key identifiers with autocomplete.
191
309
  *
@@ -1,5 +1,5 @@
1
1
  import { encodeSixel } from "@gajae-code/natives";
2
- import { $env } from "@gajae-code/utils";
2
+ import { $env, $pickenv } from "@gajae-code/utils";
3
3
 
4
4
  export enum ImageProtocol {
5
5
  Kitty = "\x1b_G",
@@ -125,7 +125,7 @@ export function isCursorNeutralImagePermittedInFallback(): boolean {
125
125
  }
126
126
 
127
127
  function getForcedImageProtocol(): ImageProtocol | null | undefined {
128
- const raw = $env.PI_FORCE_IMAGE_PROTOCOL?.trim().toLowerCase();
128
+ const raw = $pickenv("GJC_FORCE_IMAGE_PROTOCOL", "PI_FORCE_IMAGE_PROTOCOL")?.trim().toLowerCase();
129
129
  if (!raw) return undefined;
130
130
  if (raw === "kitty") return ImageProtocol.Kitty;
131
131
  if (raw === "iterm2" || raw === "iterm") return ImageProtocol.Iterm2;
package/src/terminal.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { dlopen, FFIType, ptr } from "bun:ffi";
2
2
  import * as fs from "node:fs";
3
- import { $env, $flag } from "@gajae-code/utils";
3
+ import { $env, $flag, $pickenv } from "@gajae-code/utils";
4
4
  import { setKittyProtocolActive } from "./keys";
5
5
  import { StdinBuffer } from "./stdin-buffer";
6
6
  import { isUnderTerminalMultiplexer } from "./terminal-capabilities";
@@ -209,7 +209,7 @@ export class ProcessTerminal implements Terminal {
209
209
  #stdinBuffer?: StdinBuffer;
210
210
  #stdinDataHandler?: (data: string | Buffer) => void;
211
211
  #dead = false;
212
- #writeLogPath = $env.PI_TUI_WRITE_LOG || "";
212
+ #writeLogPath = $pickenv("GJC_TUI_WRITE_LOG", "PI_TUI_WRITE_LOG") || "";
213
213
  #detachLogPath = $env.PI_TUI_TERMINAL_DETACH_LOG || "";
214
214
  #windowsVTInputRestore?: () => void;
215
215
  #stdoutErrorHandler?: (err: Error) => void;
package/src/tui.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  import * as fs from "node:fs";
5
5
  import * as path from "node:path";
6
6
  import { performance } from "node:perf_hooks";
7
- import { $flag, getDebugLogPath, logger, onDefaultTabWidthChange } from "@gajae-code/utils";
7
+ import { $flag, $pickflag, getDebugLogPath, logger, onDefaultTabWidthChange } from "@gajae-code/utils";
8
8
  import { getKeybindings } from "./keybindings";
9
9
  import { isKeyRelease } from "./keys";
10
10
  import { renderMetrics } from "./metrics";
@@ -654,13 +654,13 @@ export class TUI extends Container {
654
654
  #sixelProbeBuffer = "";
655
655
  #sixelProbeTimeout?: NodeJS.Timeout;
656
656
  #sixelProbeUnsubscribe?: () => void;
657
- #showHardwareCursor = $flag("PI_HARDWARE_CURSOR");
657
+ #showHardwareCursor = $pickflag("GJC_HARDWARE_CURSOR", "PI_HARDWARE_CURSOR");
658
658
  #debugRedraw = TUI.#readDebugRedrawFlag();
659
659
  // macOS: steady-block cursor anchors CJK IME overlays; disable with GJC_TUI_IME_CURSOR=0.
660
660
  readonly #useImeBlockCursor = $flag("GJC_TUI_IME_CURSOR", process.platform === "darwin");
661
661
  // showHardwareCursor=false but cursor is shown for IME anchoring (macOS).
662
662
  #imeCursorActive = false;
663
- #clearOnShrink = $flag("PI_CLEAR_ON_SHRINK"); // Clear empty rows when content shrinks (default: off)
663
+ #clearOnShrink = $pickflag("GJC_CLEAR_ON_SHRINK", "PI_CLEAR_ON_SHRINK"); // Clear empty rows when content shrinks (default: off)
664
664
  // Default-on: reuse the previous normalized off-screen prefix and only normalize/diff the
665
665
  // visible window, bounding per-frame work on huge transcripts. Output stays byte-identical;
666
666
  // set PI_TUI_VIRTUAL_VIEWPORT=0 to restore legacy full-transcript normalization.
@@ -693,7 +693,7 @@ export class TUI extends Container {
693
693
 
694
694
  static #readDebugRedrawFlag(): boolean {
695
695
  TUI.#renderCounters.debugRedrawEnvReads += 1;
696
- return $flag("PI_DEBUG_REDRAW");
696
+ return $pickflag("GJC_DEBUG_REDRAW", "PI_DEBUG_REDRAW");
697
697
  }
698
698
 
699
699
  #appendDebugRedrawLog(message: string): void {
@@ -2451,7 +2451,7 @@ export class TUI extends Container {
2451
2451
 
2452
2452
  // Content shrunk below the previous render and no overlays - re-render to clear empty rows
2453
2453
  // (overlays need the padding, so only do this when no overlays are active)
2454
- // Configurable via setClearOnShrink() or PI_CLEAR_ON_SHRINK=0 env var
2454
+ // Configurable via setClearOnShrink() or GJC_CLEAR_ON_SHRINK=0 env var
2455
2455
  if (this.#clearOnShrink && newLines.length < this.#previousLines.length && this.overlayStack.length === 0) {
2456
2456
  logRedraw(`clearOnShrink (prev=${this.#previousLines.length}, new=${newLines.length})`);
2457
2457
  if (
@@ -2662,7 +2662,7 @@ export class TUI extends Container {
2662
2662
  buffer += seq;
2663
2663
  buffer += "\x1b[?2026l"; // End synchronized output
2664
2664
 
2665
- if ($flag("PI_TUI_DEBUG")) {
2665
+ if ($pickflag("GJC_TUI_DEBUG", "PI_TUI_DEBUG")) {
2666
2666
  const debugDir = "/tmp/tui";
2667
2667
  fs.mkdirSync(debugDir, { recursive: true });
2668
2668
  const debugPath = path.join(debugDir, `render-${Date.now()}-${Math.random().toString(36).slice(2)}.log`);