@gajae-code/tui 0.5.0 → 0.5.2

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,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.5.1] - 2026-06-14
6
+
7
+ ### Fixed
8
+
9
+ - Fixed Korean/Hangul input composition breaking in Android Termius, where typing `안녕하세요` produced duplicated jamo/syllable residue (`ㅇ아안ㄴ녀녕…`). GJC's startup keyboard reprogramming (Kitty keyboard protocol query `CSI ? u` / push `CSI > 7 u`, and the xterm modifyOtherKeys fallback `CSI > 4 ; 2 m`) disrupts the Android IME's syllable composition. Added a `GJC_TUI_KEYBOARD_PROTOCOL` opt-out (enabled by default): set `GJC_TUI_KEYBOARD_PROTOCOL=0` to leave the keyboard in its default mode so IME composition works, matching terminals/TUIs that never enable these enhanced input modes.
10
+
5
11
  ## [0.5.0] - 2026-06-13
6
12
 
7
13
  ### Added
@@ -22,7 +22,7 @@ interface HistoryEntry {
22
22
  }
23
23
  interface HistoryStorage {
24
24
  add(prompt: string, cwd?: string): Promise<void>;
25
- getRecent(limit: number): HistoryEntry[];
25
+ getRecent(limit: number, cwd?: string): HistoryEntry[];
26
26
  }
27
27
  export declare class Editor implements Component, Focusable {
28
28
  #private;
@@ -41,6 +41,9 @@ export declare class Editor implements Component, Focusable {
41
41
  disableSubmit: boolean;
42
42
  constructor(theme: EditorTheme);
43
43
  setAutocompleteProvider(provider: AutocompleteProvider): void;
44
+ getAutocompleteProvider(): AutocompleteProvider | undefined;
45
+ /** Whether the autocomplete dropdown is currently open. */
46
+ isAutocompleteOpen(): boolean;
44
47
  /**
45
48
  * Set custom content for the top border (e.g., status line).
46
49
  * Pass undefined to use the default plain border.
@@ -1,3 +1,16 @@
1
+ /**
2
+ * Whether GJC may reprogram the keyboard with enhanced input protocols
3
+ * (the Kitty keyboard protocol and the xterm modifyOtherKeys fallback).
4
+ *
5
+ * Enabled by default. Set `GJC_TUI_KEYBOARD_PROTOCOL=0` to leave the keyboard in
6
+ * its default mode. Some terminals — notably Android Termius — break IME
7
+ * composition (e.g. Korean/Hangul syllable composition) while these enhanced
8
+ * modes are active, committing every intermediate composing jamo/syllable
9
+ * instead of only the final character. Disabling the protocol restores normal
10
+ * IME behavior, matching how other TUIs that leave the keyboard untouched render
11
+ * Korean correctly.
12
+ */
13
+ export declare function keyboardEnhancementEnabled(): boolean;
1
14
  /**
2
15
  * Emergency terminal restore - call this from signal/crash handlers
3
16
  * Resets terminal state without requiring access to the ProcessTerminal instance
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/tui",
4
- "version": "0.5.0",
4
+ "version": "0.5.2",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://gaebal-gajae.dev",
7
7
  "author": "Yeachan-Heo",
@@ -38,8 +38,8 @@
38
38
  "fmt": "biome format --write ."
39
39
  },
40
40
  "dependencies": {
41
- "@gajae-code/natives": "0.5.0",
42
- "@gajae-code/utils": "0.5.0",
41
+ "@gajae-code/natives": "0.5.2",
42
+ "@gajae-code/utils": "0.5.2",
43
43
  "lru-cache": "11.3.6",
44
44
  "marked": "^18.0.3"
45
45
  },
@@ -368,7 +368,7 @@ interface HistoryEntry {
368
368
 
369
369
  interface HistoryStorage {
370
370
  add(prompt: string, cwd?: string): Promise<void>;
371
- getRecent(limit: number): HistoryEntry[];
371
+ getRecent(limit: number, cwd?: string): HistoryEntry[];
372
372
  }
373
373
 
374
374
  type HistoryCursorAnchor = "start" | "end";
@@ -464,6 +464,15 @@ export class Editor implements Component, Focusable {
464
464
  this.#autocompleteProvider = provider;
465
465
  }
466
466
 
467
+ getAutocompleteProvider(): AutocompleteProvider | undefined {
468
+ return this.#autocompleteProvider;
469
+ }
470
+
471
+ /** Whether the autocomplete dropdown is currently open. */
472
+ isAutocompleteOpen(): boolean {
473
+ return this.#autocompleteState !== null && this.#autocompleteList !== undefined;
474
+ }
475
+
467
476
  /**
468
477
  * Set custom content for the top border (e.g., status line).
469
478
  * Pass undefined to use the default plain border.
@@ -545,7 +554,7 @@ export class Editor implements Component, Focusable {
545
554
 
546
555
  setHistoryStorage(storage: HistoryStorage): void {
547
556
  this.#historyStorage = storage;
548
- const recent = storage.getRecent(100);
557
+ const recent = storage.getRecent(100, getProjectDir());
549
558
  this.#history = recent.map(entry => entry.prompt);
550
559
  this.#historyIndex = -1;
551
560
  }
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 } from "@gajae-code/utils";
3
+ import { $env, $flag } from "@gajae-code/utils";
4
4
  import { setKittyProtocolActive } from "./keys";
5
5
  import { StdinBuffer } from "./stdin-buffer";
6
6
 
@@ -8,6 +8,22 @@ const TERMINAL_PROGRESS_KEEPALIVE_MS = 1000;
8
8
  const TERMINAL_PROGRESS_ACTIVE_SEQUENCE = "\x1b]9;4;3\x07";
9
9
  const TERMINAL_PROGRESS_CLEAR_SEQUENCE = "\x1b]9;4;0;\x07";
10
10
 
11
+ /**
12
+ * Whether GJC may reprogram the keyboard with enhanced input protocols
13
+ * (the Kitty keyboard protocol and the xterm modifyOtherKeys fallback).
14
+ *
15
+ * Enabled by default. Set `GJC_TUI_KEYBOARD_PROTOCOL=0` to leave the keyboard in
16
+ * its default mode. Some terminals — notably Android Termius — break IME
17
+ * composition (e.g. Korean/Hangul syllable composition) while these enhanced
18
+ * modes are active, committing every intermediate composing jamo/syllable
19
+ * instead of only the final character. Disabling the protocol restores normal
20
+ * IME behavior, matching how other TUIs that leave the keyboard untouched render
21
+ * Korean correctly.
22
+ */
23
+ export function keyboardEnhancementEnabled(): boolean {
24
+ return $flag("GJC_TUI_KEYBOARD_PROTOCOL", true);
25
+ }
26
+
11
27
  /**
12
28
  * Minimal terminal interface for TUI
13
29
  */
@@ -513,6 +529,14 @@ export class ProcessTerminal implements Terminal {
513
529
  #queryAndEnableKittyProtocol(): void {
514
530
  this.#setupStdinBuffer();
515
531
  process.stdin.on("data", this.#stdinDataHandler!);
532
+ // Leave the keyboard in its default mode when enhanced input protocols are
533
+ // disabled. Android Termius (and similar terminals) break IME/Hangul
534
+ // composition when the Kitty keyboard protocol or modifyOtherKeys is active,
535
+ // committing every intermediate composing jamo/syllable. Skipping the query
536
+ // and the modifyOtherKeys fallback restores normal IME composition.
537
+ if (!keyboardEnhancementEnabled()) {
538
+ return;
539
+ }
516
540
  this.#safeWrite("\x1b[?u");
517
541
  this.#modifyOtherKeysTimeout = setTimeout(() => {
518
542
  this.#modifyOtherKeysTimeout = undefined;
package/src/tui.ts CHANGED
@@ -254,6 +254,11 @@ export class TUI extends Container {
254
254
  #renderTimer: NodeJS.Timeout | undefined;
255
255
  #lastRenderAt = 0;
256
256
  static readonly #MIN_RENDER_INTERVAL_MS = 16;
257
+ // Input-priority scheduling: an input keystroke must never be starved behind a
258
+ // pending normal (frame-budget) render timer. When set, an input-priority render
259
+ // is queued for the next tick and supersedes any pending normal timer.
260
+ #inputRenderPending = false;
261
+
257
262
  #cursorRow = 0; // Logical cursor row (end of rendered content)
258
263
  #hardwareCursorRow = 0; // Actual terminal cursor row (may differ due to IME positioning)
259
264
  #viewportTopRow = 0; // Content row currently mapped to screen row 0
@@ -671,6 +676,8 @@ export class TUI extends Container {
671
676
  }
672
677
  if (renderMetrics.enabled) renderMetrics.recordRequest(source);
673
678
  if (force) {
679
+ // A forced full redraw supersedes any queued input-priority render.
680
+ this.#inputRenderPending = false;
674
681
  this.#previousLines = [];
675
682
  this.#lineNormalizationCache.clear();
676
683
  this.#lineTruncationCache.clear();
@@ -700,6 +707,19 @@ export class TUI extends Container {
700
707
  });
701
708
  return;
702
709
  }
710
+ // Input-priority path: expedite so the keystroke echoes within the next tick
711
+ // instead of waiting for (or behind) the frame-budget timer. Re-entrant input
712
+ // requests in the same turn coalesce via #inputRenderPending, so at most one
713
+ // expedited render commits per event-loop turn (no repaint storms). This only
714
+ // changes WHEN #doRender runs; the render output path is unchanged.
715
+ if (source === "input" || source === "editor.input") {
716
+ if (!this.#inputRenderPending) {
717
+ this.#inputRenderPending = true;
718
+ this.#renderRequested = true;
719
+ process.nextTick(() => this.#commitExpeditedRender());
720
+ }
721
+ return;
722
+ }
703
723
  if (this.#renderRequested) return;
704
724
  this.#renderRequested = true;
705
725
  process.nextTick(() => this.#scheduleRender());
@@ -729,6 +749,27 @@ export class TUI extends Container {
729
749
  if (renderMetrics.enabled) renderMetrics.setTimerGauge("tui.renderTimer", 1);
730
750
  }
731
751
 
752
+ // Commit a single input-priority render on the next tick, cancelling any normal
753
+ // frame-budget timer scheduled in the same turn. nextTick always precedes a
754
+ // pending setTimeout, so the keystroke is never starved behind streaming renders.
755
+ #commitExpeditedRender(): void {
756
+ if (!this.#inputRenderPending) return; // cancelled (e.g., by a forced render)
757
+ this.#inputRenderPending = false;
758
+ if (this.#stopped || !this.#renderRequested) {
759
+ return;
760
+ }
761
+ if (this.#renderTimer) {
762
+ clearTimeout(this.#renderTimer);
763
+ this.#renderTimer = undefined;
764
+ if (renderMetrics.enabled) renderMetrics.setTimerGauge("tui.renderTimer", 0);
765
+ }
766
+ this.#renderRequested = false;
767
+ this.#lastRenderAt = performance.now();
768
+ const t0 = renderMetrics.now();
769
+ this.#doRender();
770
+ if (renderMetrics.enabled) renderMetrics.recordRender(renderMetrics.now() - t0);
771
+ }
772
+
732
773
  #handleInput(data: string): void {
733
774
  if (this.#inputListeners.size > 0) {
734
775
  let current = data;
@@ -780,7 +821,7 @@ export class TUI extends Container {
780
821
  return;
781
822
  }
782
823
  this.#focusedComponent.handleInput(data);
783
- this.requestRender();
824
+ this.requestRender(false, "input");
784
825
  }
785
826
  }
786
827