@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 +13 -0
- package/package.json +3 -3
- package/src/components/editor.ts +25 -1
- package/src/tui.ts +24 -5
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.
|
|
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.
|
|
42
|
-
"@gajae-code/utils": "0.7.
|
|
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
|
},
|
package/src/components/editor.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
() =>
|
|
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
|
-
|
|
1766
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1817
|
+
// No \x1b[?2026h/l wrapper: synchronized output flushes terminal state and discards macOS IME composition.
|
|
1818
|
+
this.#writeTerminal(seq);
|
|
1800
1819
|
}
|
|
1801
1820
|
}
|