@oh-my-pi/pi-tui 17.0.9 → 17.1.1

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,22 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.1.1] - 2026-07-24
6
+
7
+ ### Fixed
8
+
9
+ - Fixed WarpTerminal wrapping streamed Compatibility Jamo at the platform-default width instead of its rendered one-cell width ([#6461](https://github.com/can1357/oh-my-pi/issues/6461)).
10
+
11
+ ## [17.1.0] - 2026-07-24
12
+
13
+ ### Added
14
+
15
+ - Added Text.setStyleFn() to apply foreground stylers at render time, allowing components to dynamically re-resolve colors after invalidation instead of baking in the palette active at construction.
16
+
17
+ ### Fixed
18
+
19
+ - Fixed an issue where exiting or tearing down the TUI left the terminal in cursor-key/keypad application mode (DECCKM), which broke arrow keys in the parent shell. Both stop() and emergencyTerminalRestore() now correctly emit standard rmkx resets.
20
+
5
21
  ## [17.0.9] - 2026-07-23
6
22
 
7
23
  ### Added
@@ -1,6 +1,12 @@
1
1
  import type { Component } from "../tui.js";
2
2
  /**
3
- * Text component - displays multi-line text with word wrapping
3
+ * Text component - displays multi-line text with word wrapping.
4
+ *
5
+ * Foreground colors may be supplied lazily via {@link setStyleFn} instead of
6
+ * baked into `text`: the styler runs at render time, so a caller that
7
+ * invalidates the component on a theme change (see the coding-agent's
8
+ * `onThemeChange` handler) re-resolves the color against the now-active theme
9
+ * rather than replaying the palette active when the component was constructed.
4
10
  */
5
11
  export declare class Text implements Component {
6
12
  #private;
@@ -9,6 +15,13 @@ export declare class Text implements Component {
9
15
  getText(): string;
10
16
  setText(text: string): boolean;
11
17
  setCustomBgFn(customBgFn?: (text: string) => string): void;
18
+ /**
19
+ * Supply a foreground styler applied to the text at render time (e.g. a
20
+ * theme color resolver). Unlike baking the color into `text`, the styler
21
+ * re-runs on every render, so invalidating the component after a theme
22
+ * change re-resolves the color against the active theme.
23
+ */
24
+ setStyleFn(styleFn?: (text: string) => string): this;
12
25
  invalidate(): void;
13
26
  render(width: number): readonly string[];
14
27
  }
@@ -1,3 +1,4 @@
1
+ import type { HangulCompatibilityJamoWidth } from "./utils.js";
1
2
  export { isInsideTmux, wrapTmuxPassthrough } from "./tmux.js";
2
3
  export declare enum ImageProtocol {
3
4
  Kitty = "\u001B_G",
@@ -21,9 +22,21 @@ export declare class TerminalInfo {
21
22
  readonly supportsScreenToScrollback: boolean;
22
23
  /** Renders the Kitty OSC 66 text-sizing protocol (scaled spans). Kitty only. */
23
24
  readonly textSizing: boolean;
25
+ /**
26
+ * Hangul Compatibility Jamo (U+3131..=U+318E) cell width. Ghostty follows
27
+ * UAX#11 (2 cells); Warp paints 1; "platform" keeps the OS default
28
+ * (macOS narrow, otherwise UAX#11).
29
+ */
30
+ readonly hangulJamoWidth: HangulCompatibilityJamoWidth;
24
31
  constructor(id: TerminalId, imageProtocol: ImageProtocol | null, trueColor: boolean, hyperlinks: boolean, notifyProtocol?: NotifyProtocol, deccara?: boolean, supportsScreenToScrollback?: boolean,
25
32
  /** Renders the Kitty OSC 66 text-sizing protocol (scaled spans). Kitty only. */
26
- textSizing?: boolean);
33
+ textSizing?: boolean,
34
+ /**
35
+ * Hangul Compatibility Jamo (U+3131..=U+318E) cell width. Ghostty follows
36
+ * UAX#11 (2 cells); Warp paints 1; "platform" keeps the OS default
37
+ * (macOS narrow, otherwise UAX#11).
38
+ */
39
+ hangulJamoWidth?: HangulCompatibilityJamoWidth);
27
40
  /**
28
41
  * Mutable clone for the {@link TERMINAL} singleton: copies every field and
29
42
  * keeps the prototype methods, so the builder and runtime setters flip
@@ -1,5 +1,3 @@
1
- import { type HangulCompatibilityJamoWidth } from "./utils.js";
2
- export declare function resolveHangulCompatibilityJamoWidthFromTerminalIdentity(env?: NodeJS.ProcessEnv): HangulCompatibilityJamoWidth;
3
1
  /**
4
2
  * Split `data` into chunks whose encoded UTF-8 byte length is no greater than
5
3
  * `maxChunkBytes`, preferring a line boundary (`\n`) as the cut point so
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "17.0.9",
4
+ "version": "17.1.1",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -37,8 +37,8 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/pi-natives": "17.0.9",
41
- "@oh-my-pi/pi-utils": "17.0.9",
40
+ "@oh-my-pi/pi-natives": "17.1.1",
41
+ "@oh-my-pi/pi-utils": "17.1.1",
42
42
  "lru-cache": "11.5.2",
43
43
  "marked": "^18.0.6"
44
44
  },
@@ -11,13 +11,20 @@ import {
11
11
  } from "../utils";
12
12
 
13
13
  /**
14
- * Text component - displays multi-line text with word wrapping
14
+ * Text component - displays multi-line text with word wrapping.
15
+ *
16
+ * Foreground colors may be supplied lazily via {@link setStyleFn} instead of
17
+ * baked into `text`: the styler runs at render time, so a caller that
18
+ * invalidates the component on a theme change (see the coding-agent's
19
+ * `onThemeChange` handler) re-resolves the color against the now-active theme
20
+ * rather than replaying the palette active when the component was constructed.
15
21
  */
16
22
  export class Text implements Component {
17
23
  #text: string;
18
24
  #paddingX: number; // Left/right padding
19
25
  #paddingY: number; // Top/bottom padding
20
26
  #customBgFn?: (text: string) => string;
27
+ #styleFn?: (text: string) => string;
21
28
 
22
29
  #ignoreTight = false;
23
30
 
@@ -64,6 +71,21 @@ export class Text implements Component {
64
71
  this.#cachedLines = undefined;
65
72
  }
66
73
 
74
+ /**
75
+ * Supply a foreground styler applied to the text at render time (e.g. a
76
+ * theme color resolver). Unlike baking the color into `text`, the styler
77
+ * re-runs on every render, so invalidating the component after a theme
78
+ * change re-resolves the color against the active theme.
79
+ */
80
+ setStyleFn(styleFn?: (text: string) => string): this {
81
+ this.#styleFn = styleFn;
82
+ this.#cachedText = undefined;
83
+ this.#cachedWidth = undefined;
84
+ this.#cachedWidthEpoch = undefined;
85
+ this.#cachedLines = undefined;
86
+ return this;
87
+ }
88
+
67
89
  invalidate(): void {
68
90
  this.#cachedText = undefined;
69
91
  this.#cachedWidth = undefined;
@@ -93,7 +115,7 @@ export class Text implements Component {
93
115
  }
94
116
 
95
117
  // Replace tabs with 3 spaces
96
- const normalizedText = replaceTabs(this.#text);
118
+ const normalizedText = replaceTabs(this.#styleFn ? this.#styleFn(this.#text) : this.#text);
97
119
 
98
120
  // Calculate content width (subtract left/right margins)
99
121
  const paddingX = this.#ignoreTight ? this.#paddingX : getPaddingX(this.#paddingX);
@@ -10,6 +10,7 @@ import {
10
10
  setKittyGraphics,
11
11
  } from "./kitty-graphics";
12
12
  import { isInsideTmux, wrapTmuxPassthrough, wrapTmuxPassthroughIfNeeded } from "./tmux";
13
+ import type { HangulCompatibilityJamoWidth } from "./utils";
13
14
 
14
15
  export { isInsideTmux, wrapTmuxPassthrough } from "./tmux";
15
16
 
@@ -105,6 +106,12 @@ export class TerminalInfo {
105
106
  readonly supportsScreenToScrollback: boolean = false,
106
107
  /** Renders the Kitty OSC 66 text-sizing protocol (scaled spans). Kitty only. */
107
108
  public readonly textSizing: boolean = false,
109
+ /**
110
+ * Hangul Compatibility Jamo (U+3131..=U+318E) cell width. Ghostty follows
111
+ * UAX#11 (2 cells); Warp paints 1; "platform" keeps the OS default
112
+ * (macOS narrow, otherwise UAX#11).
113
+ */
114
+ public readonly hangulJamoWidth: HangulCompatibilityJamoWidth = "platform",
108
115
  ) {}
109
116
 
110
117
  /**
@@ -447,7 +454,17 @@ export function resolveWarpImageProtocol(
447
454
  }
448
455
 
449
456
  function getWarpTerminalInfo(platform: NodeJS.Platform, env: NodeJS.ProcessEnv = Bun.env): TerminalInfo {
450
- return new TerminalInfo("warp", resolveWarpImageProtocol(platform, env), true, false, NotifyProtocol.Osc9);
457
+ return new TerminalInfo(
458
+ "warp",
459
+ resolveWarpImageProtocol(platform, env),
460
+ true,
461
+ false,
462
+ NotifyProtocol.Osc9,
463
+ false,
464
+ false,
465
+ false,
466
+ 1,
467
+ );
451
468
  }
452
469
  const KNOWN_TERMINALS = Object.freeze({
453
470
  // Fallback terminals
@@ -455,7 +472,7 @@ const KNOWN_TERMINALS = Object.freeze({
455
472
  trueColor: new TerminalInfo("trueColor", null, true, false, NotifyProtocol.Bell),
456
473
  // Recognized terminals
457
474
  kitty: new TerminalInfo("kitty", ImageProtocol.Kitty, true, true, NotifyProtocol.Osc99, true, true, true),
458
- ghostty: new TerminalInfo("ghostty", ImageProtocol.Kitty, true, true, NotifyProtocol.Osc9),
475
+ ghostty: new TerminalInfo("ghostty", ImageProtocol.Kitty, true, true, NotifyProtocol.Osc9, false, false, false, 2),
459
476
  wezterm: new TerminalInfo("wezterm", ImageProtocol.Kitty, true, true, NotifyProtocol.Osc9),
460
477
  iterm2: new TerminalInfo("iterm2", ImageProtocol.Iterm2, true, true, NotifyProtocol.Osc9),
461
478
  vscode: new TerminalInfo("vscode", null, true, true, NotifyProtocol.Bell),
@@ -465,7 +482,7 @@ const KNOWN_TERMINALS = Object.freeze({
465
482
  // detectKittyUnicodePlaceholdersSupport correctly excludes it). It does not
466
483
  // honor OSC 8 yet (the escape renders as visible text), so hyperlinks stay off,
467
484
  // but it does support OSC 9 notifications.
468
- warp: new TerminalInfo("warp", ImageProtocol.Kitty, true, false, NotifyProtocol.Osc9),
485
+ warp: new TerminalInfo("warp", ImageProtocol.Kitty, true, false, NotifyProtocol.Osc9, false, false, false, 1),
469
486
  });
470
487
 
471
488
  /** Resolve terminal identity from environment markers used by common emulators. */
package/src/terminal.ts CHANGED
@@ -19,31 +19,12 @@ import {
19
19
  TERMINAL,
20
20
  } from "./terminal-capabilities";
21
21
  import { isInsideTmux, wrapTmuxPassthrough } from "./tmux";
22
- import { type HangulCompatibilityJamoWidth, setHangulCompatibilityJamoWidth } from "./utils";
22
+ import { setHangulCompatibilityJamoWidth } from "./utils";
23
23
 
24
24
  const TERMINAL_PROGRESS_KEEPALIVE_MS = 1000;
25
25
  const TERMINAL_PROGRESS_ACTIVE_SEQUENCE = "\x1b]9;4;3\x07";
26
26
  const TERMINAL_PROGRESS_CLEAR_SEQUENCE = "\x1b]9;4;0;\x07";
27
27
  const WINDOWS_TERMINAL_OSC11_POLL_MS = 30_000;
28
- // Hangul Compatibility Jamo (U+3131..=U+318E) render width is terminal-dependent:
29
- // Ghostty follows UAX#11 (2 cells); Terminal.app and iTerm2 render narrow (1),
30
- // matching the macOS platform default. Override only for terminals known to
31
- // disagree — the rest keep the platform default (macOS narrow, otherwise UAX#11),
32
- // so this is a no-op everywhere except Ghostty. A runtime DSR/CPR probe that
33
- // auto-detects the width on unknown terminals is tracked separately.
34
- export function resolveHangulCompatibilityJamoWidthFromTerminalIdentity(
35
- env: NodeJS.ProcessEnv = Bun.env,
36
- ): HangulCompatibilityJamoWidth {
37
- if (
38
- env.GHOSTTY_RESOURCES_DIR ||
39
- env.TERM_PROGRAM?.toLowerCase() === "ghostty" ||
40
- env.TERM?.toLowerCase().includes("ghostty")
41
- ) {
42
- return 2;
43
- }
44
- return "platform";
45
- }
46
-
47
28
  function shouldEnableModifyOtherKeysFallback(env: NodeJS.ProcessEnv = Bun.env): boolean {
48
29
  if (!env.SSH_CONNECTION && !env.SSH_TTY && !env.SSH_CLIENT) return true;
49
30
  return TERMINAL.id !== "base" && TERMINAL.id !== "trueColor";
@@ -312,6 +293,7 @@ export function emergencyTerminalRestore(): void {
312
293
  process.stdout.write(
313
294
  "\x1b[?2026l" + // End synchronized output
314
295
  "\x1b[?7h" + // Restore autowrap
296
+ "\x1b[?1l\x1b>" + // Restore normal cursor-key + keypad mode (rmkx, #6374)
315
297
  "\x1b[?2004l" + // Disable bracketed paste
316
298
  "\x1b[?2031l" + // Disable Mode 2031 appearance notifications
317
299
  "\x1b[?2048l" + // Disable in-band resize notifications
@@ -625,6 +607,15 @@ export class ProcessTerminal implements Terminal {
625
607
  // Enable bracketed paste mode - terminal will wrap pastes in \x1b[200~ ... \x1b[201~
626
608
  this.#safeWrite("\x1b[?2004h");
627
609
 
610
+ // Force normal cursor-key (DECCKM) and numeric-keypad mode (terminfo
611
+ // `rmkx` = "\x1b[?1l\x1b>"). omp decodes both CSI ("\x1b[A") and SS3
612
+ // ("\x1bOA") arrow encodings, so it never enables application mode
613
+ // itself — but a prior program that left the TTY in application-cursor-
614
+ // keys mode makes arrows arrive as SS3. Normalizing on entry keeps input
615
+ // in the predictable default state; stop() restores the same on exit.
616
+ // See #6374.
617
+ this.#safeWrite("\x1b[?1l\x1b>");
618
+
628
619
  // Set up resize handler immediately. The OS refreshes process.stdout
629
620
  // dimensions before firing `resize`, so it is authoritative for geometry:
630
621
  // reconcile any stale cached DEC 2048 report before notifying the renderer.
@@ -649,7 +640,7 @@ export class ProcessTerminal implements Terminal {
649
640
  // The query handler intercepts input temporarily, then installs the user's handler
650
641
  // See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
651
642
  this.#queryAndEnableKittyProtocol();
652
- setHangulCompatibilityJamoWidth(resolveHangulCompatibilityJamoWidthFromTerminalIdentity());
643
+ setHangulCompatibilityJamoWidth(TERMINAL.hangulJamoWidth);
653
644
 
654
645
  // Query terminal background color via OSC 11 for dark/light detection.
655
646
  // Uses DA1 (Primary Device Attributes) as a sentinel: terminals process
@@ -1376,6 +1367,13 @@ export class ProcessTerminal implements Terminal {
1376
1367
  // begin/end halves of a frame. Safe no-ops on terminals that ignored them.
1377
1368
  this.#safeWrite("\x1b[?2026l\x1b[?7h");
1378
1369
 
1370
+ // Restore normal cursor-key (DECCKM) and numeric-keypad mode (terminfo
1371
+ // `rmkx`). Symmetric with the normalize in start(): a TTY-sharing child
1372
+ // can leave the terminal in application-cursor-keys mode, and without
1373
+ // this reset the parent shell inherits SS3 arrows so Up/Down history
1374
+ // navigation stays broken after omp exits (#6374).
1375
+ this.#safeWrite("\x1b[?1l\x1b>");
1376
+
1379
1377
  // Disable bracketed paste mode
1380
1378
  this.#safeWrite("\x1b[?2004l");
1381
1379
  this.#safeWrite("\x1b[?5522l");