@gajae-code/tui 0.7.3 → 0.7.5

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,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.7.4] - 2026-06-27
6
+
7
+ ### Fixed
8
+
9
+ - Fixed Korean IME ghost character on macOS: a Hangul syllable committed by the IME immediately after `ctrl+u` (line-delete) is now discarded, preventing it from appearing in the cleared buffer (#1150).
10
+ - Fixed CJK IME composition overlay positioning on macOS: the TUI now shows a steady-block hardware cursor (`\x1b[2 q`) when the soft cursor is active, anchoring the IME overlay to the correct position. Removed the synchronized-output wrapper from standalone cursor nudges, which was flushing terminal state and dismissing the overlay on every keystroke (#1150).
11
+ - Fixed IME preedit caret anchoring so the composition caret stays aligned with the composed text (#1178).
12
+ - Preserve the composer placeholder while an IME cursor is active so the prompt hint does not flicker during composition.
13
+ - Reflow the in-progress prompt draft when the terminal is resized.
14
+ - Repaint the multiplexer viewport on resize so stale rows are not left behind (#1137).
15
+ - Fixed Alt+Enter queue key parsing (#1145).
16
+ - Fixed Windows prompt newline shortcut discoverability (#1134).
17
+
5
18
  ## [0.7.3] - 2026-06-25
6
19
 
7
20
  ### Added
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/tui",
4
- "version": "0.7.3",
4
+ "version": "0.7.5",
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.7.3",
42
- "@gajae-code/utils": "0.7.3",
41
+ "@gajae-code/natives": "0.7.5",
42
+ "@gajae-code/utils": "0.7.5",
43
43
  "lru-cache": "11.3.6",
44
44
  "marked": "^18.0.3"
45
45
  },
@@ -385,6 +385,8 @@ export class Editor implements Component, Focusable {
385
385
 
386
386
  #theme: EditorTheme;
387
387
  #useTerminalCursor = false;
388
+ // macOS: Hangul arriving within 50ms of a line-delete is an IME ghost char — discard it.
389
+ #lastLineDeleteAt = 0;
388
390
 
389
391
  /** When set, replaces the normal cursor glyph at end-of-text with this ANSI-styled string. */
390
392
  cursorOverride: string | undefined;
@@ -850,7 +852,13 @@ export class Editor implements Component, Focusable {
850
852
 
851
853
  if (showPlaceholder) {
852
854
  const hintText = hintStyle(truncateToWidth(this.#placeholder ?? "", lineContentWidth));
853
- displayText = hintText;
855
+ // Anchor the hardware cursor at the input start (terminal-cursor mode) even
856
+ // while the placeholder shows. Otherwise no cursor marker is emitted, the
857
+ // hardware cursor is left at its stale position from the previous frame,
858
+ // and a composing IME preedit renders there instead of at the prompt.
859
+ // The marker is zero-width and stripped before output, so the placeholder is unchanged.
860
+ const anchorCursor = emitCursorMarker && this.#useTerminalCursor && visibleIndex === 0;
861
+ displayText = anchorCursor ? marker + hintText : hintText;
854
862
  displayWidth = Math.min(visibleWidth(this.#placeholder ?? ""), lineContentWidth);
855
863
  hasCursor = false;
856
864
  }
@@ -1384,6 +1392,21 @@ export class Editor implements Component, Focusable {
1384
1392
  else {
1385
1393
  const printableText = extractPrintableText(data);
1386
1394
  if (printableText) {
1395
+ if (
1396
+ process.platform === "darwin" &&
1397
+ printableText.length === 1 &&
1398
+ Date.now() - this.#lastLineDeleteAt < 50
1399
+ ) {
1400
+ const code = printableText.charCodeAt(0);
1401
+ const isHangul =
1402
+ (code >= 0xac00 && code <= 0xd7a3) ||
1403
+ (code >= 0x1100 && code <= 0x11ff) ||
1404
+ (code >= 0x3130 && code <= 0x318f);
1405
+ if (isHangul) {
1406
+ this.#lastLineDeleteAt = 0;
1407
+ return;
1408
+ }
1409
+ }
1387
1410
  this.#insertCharacter(printableText);
1388
1411
  }
1389
1412
  }
@@ -2209,6 +2232,7 @@ export class Editor implements Component, Focusable {
2209
2232
  #deleteToStartOfLine(): void {
2210
2233
  this.#historyIndex = -1; // Exit history browsing mode
2211
2234
  this.#recordUndoState();
2235
+ if (process.platform === "darwin") this.#lastLineDeleteAt = Date.now();
2212
2236
 
2213
2237
  const currentLine = this.#state.lines[this.#state.cursorLine] || "";
2214
2238
  let deletedText = "";
package/src/tui.ts CHANGED
@@ -313,6 +313,10 @@ export class TUI extends Container {
313
313
  #sixelProbeTimeout?: NodeJS.Timeout;
314
314
  #sixelProbeUnsubscribe?: () => void;
315
315
  #showHardwareCursor = $flag("PI_HARDWARE_CURSOR");
316
+ // macOS: steady-block cursor anchors CJK IME overlays; disable with GJC_TUI_IME_CURSOR=0.
317
+ readonly #useImeBlockCursor = $flag("GJC_TUI_IME_CURSOR", process.platform === "darwin");
318
+ // showHardwareCursor=false but cursor is shown for IME anchoring (macOS).
319
+ #imeCursorActive = false;
316
320
  #clearOnShrink = $flag("PI_CLEAR_ON_SHRINK"); // Clear empty rows when content shrinks (default: off)
317
321
  // Opt-in: reuse the previous normalized off-screen prefix and only normalize/diff the
318
322
  // visible window, bounding per-frame work on huge transcripts. Output stays byte-identical.
@@ -337,6 +341,7 @@ export class TUI extends Container {
337
341
  if (showHardwareCursor !== undefined) {
338
342
  this.#showHardwareCursor = showHardwareCursor;
339
343
  }
344
+ this.#imeCursorActive = !this.#showHardwareCursor && this.#useImeBlockCursor;
340
345
  }
341
346
 
342
347
  get fullRedraws(): number {
@@ -350,6 +355,7 @@ export class TUI extends Container {
350
355
  setShowHardwareCursor(enabled: boolean): void {
351
356
  if (this.#showHardwareCursor === enabled) return;
352
357
  this.#showHardwareCursor = enabled;
358
+ this.#imeCursorActive = !enabled && this.#useImeBlockCursor;
353
359
  if (!enabled) {
354
360
  this.#hideCursor();
355
361
  }
@@ -484,7 +490,10 @@ export class TUI extends Container {
484
490
  this.#terminalUnavailable = false;
485
491
  this.terminal.start(
486
492
  data => this.#handleInput(data),
487
- () => this.requestRender(),
493
+ () => {
494
+ this.invalidate();
495
+ this.requestRender(!(isMultiplexerSession() && !useLegacyMultiplexerFullRender()), "resize");
496
+ },
488
497
  );
489
498
  this.#hideCursor();
490
499
  this.#querySixelSupport();
@@ -704,6 +713,9 @@ export class TUI extends Container {
704
713
  this.#writeTerminal("\r\n");
705
714
  }
706
715
 
716
+ if (this.#useImeBlockCursor) {
717
+ this.#writeTerminal("\x1b[0 q");
718
+ }
707
719
  this.#showCursor();
708
720
  try {
709
721
  this.terminal.stop();
@@ -1762,8 +1774,10 @@ export class TUI extends Container {
1762
1774
  totalLines: number,
1763
1775
  fromRow: number,
1764
1776
  ): { seq: string; toRow: number } {
1765
- // No IME target or no content — hide cursor regardless of preference
1766
- if (!cursorPos || totalLines <= 0) return { seq: "\x1b[?25l", toRow: fromRow };
1777
+ if (!cursorPos || totalLines <= 0) {
1778
+ const hide = this.#useImeBlockCursor ? "\x1b[0 q\x1b[?25l" : "\x1b[?25l";
1779
+ return { seq: hide, toRow: fromRow };
1780
+ }
1767
1781
 
1768
1782
  // Clamp cursor position to valid range
1769
1783
  const targetRow = Math.max(0, Math.min(cursorPos.row, totalLines - 1));
@@ -1779,7 +1793,11 @@ export class TUI extends Container {
1779
1793
  }
1780
1794
  // Move to absolute column (1-indexed)
1781
1795
  seq += `\x1b[${targetCol + 1}G`;
1782
- seq += this.#showHardwareCursor ? "\x1b[?25h" : "\x1b[?25l";
1796
+ if (this.#showHardwareCursor || this.#imeCursorActive) {
1797
+ seq += this.#useImeBlockCursor ? "\x1b[2 q\x1b[?25h" : "\x1b[?25h";
1798
+ } else {
1799
+ seq += "\x1b[?25l";
1800
+ }
1783
1801
 
1784
1802
  return { seq, toRow: targetRow };
1785
1803
  }
@@ -1796,6 +1814,7 @@ export class TUI extends Container {
1796
1814
  }
1797
1815
  const { seq, toRow } = this.#cursorControlSequence(cursorPos, totalLines, this.#hardwareCursorRow);
1798
1816
  this.#hardwareCursorRow = toRow;
1799
- this.#writeTerminal(`\x1b[?2026h${seq}\x1b[?2026l`);
1817
+ // No \x1b[?2026h/l wrapper: synchronized output flushes terminal state and discards macOS IME composition.
1818
+ this.#writeTerminal(seq);
1800
1819
  }
1801
1820
  }