@oh-my-pi/pi-tui 16.1.19 → 16.1.21

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,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.1.20] - 2026-06-25
6
+
7
+ ### Fixed
8
+
9
+ - Recognized Warp (`TERM_PROGRAM=WarpTerminal`) as a first-class terminal, enabling Kitty inline images on macOS/Linux while keeping Warp's unsafe OSC 8 hyperlinks and Windows Kitty graphics disabled ([#3471](https://github.com/can1357/oh-my-pi/issues/3471)).
10
+ - Kept queued interrupt keys ahead of ordinary repaints so a slow long-transcript frame cannot consume the Ctrl+C/Esc double-press window before the second key is handled.
11
+
5
12
  ## [16.1.19] - 2026-06-25
6
13
 
7
14
  ### Fixed
@@ -8,7 +8,7 @@ export declare enum NotifyProtocol {
8
8
  Osc99 = "\u001B]99;;",
9
9
  Osc9 = "\u001B]9;"
10
10
  }
11
- export type TerminalId = "kitty" | "ghostty" | "wezterm" | "iterm2" | "vscode" | "alacritty" | "base" | "trueColor";
11
+ export type TerminalId = "kitty" | "ghostty" | "wezterm" | "iterm2" | "vscode" | "alacritty" | "warp" | "base" | "trueColor";
12
12
  /** Terminal capability details used for rendering and protocol selection. */
13
13
  export declare class TerminalInfo {
14
14
  readonly id: TerminalId;
@@ -136,6 +136,8 @@ export declare function hyperlinksUserOverride(env?: NodeJS.ProcessEnv): boolean
136
136
  * 7. Otherwise honor the static terminal capability.
137
137
  */
138
138
  export declare function shouldEnableHyperlinksByDefault(env?: NodeJS.ProcessEnv, terminalId?: TerminalId): boolean;
139
+ /** Resolve terminal identity from environment markers used by common emulators. */
140
+ export declare function detectTerminalId(env?: NodeJS.ProcessEnv): TerminalId;
139
141
  export declare const TERMINAL_ID: TerminalId;
140
142
  /**
141
143
  * The process-wide {@link TERMINAL} singleton: a {@link TerminalInfo} whose
@@ -169,7 +171,7 @@ export declare function setTerminalScreenToScrollback(enabled: boolean): void;
169
171
  * capability); tests flip it directly to exercise the scaled-heading path.
170
172
  */
171
173
  export declare function setTerminalTextSizing(enabled: boolean): void;
172
- export declare function getTerminalInfo(terminalId: TerminalId): TerminalInfo;
174
+ export declare function getTerminalInfo(terminalId: TerminalId, platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): TerminalInfo;
173
175
  export interface CellDimensions {
174
176
  widthPx: number;
175
177
  heightPx: number;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "16.1.19",
4
+ "version": "16.1.21",
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": "16.1.19",
41
- "@oh-my-pi/pi-utils": "16.1.19",
40
+ "@oh-my-pi/pi-natives": "16.1.21",
41
+ "@oh-my-pi/pi-utils": "16.1.21",
42
42
  "lru-cache": "11.5.1",
43
43
  "marked": "^18.0.5"
44
44
  },
@@ -21,7 +21,16 @@ export enum NotifyProtocol {
21
21
  Osc9 = "\x1b]9;",
22
22
  }
23
23
 
24
- export type TerminalId = "kitty" | "ghostty" | "wezterm" | "iterm2" | "vscode" | "alacritty" | "base" | "trueColor";
24
+ export type TerminalId =
25
+ | "kitty"
26
+ | "ghostty"
27
+ | "wezterm"
28
+ | "iterm2"
29
+ | "vscode"
30
+ | "alacritty"
31
+ | "warp"
32
+ | "base"
33
+ | "trueColor";
25
34
 
26
35
  function hasNeedleBefore(line: string, needle: string, limit: number): boolean {
27
36
  const index = line.indexOf(needle);
@@ -251,9 +260,9 @@ export function shouldEnableSynchronizedOutputByDefault(
251
260
  case "vscode":
252
261
  return true;
253
262
  default:
254
- // VTE family, GNU screen, Apple Terminal, legacy native console host
255
- // (no WT_SESSION), and bare/unknown xterm profiles stay off until the
256
- // DECRQM probe proves support.
263
+ // VTE family, GNU screen, Apple Terminal, Warp, legacy native console
264
+ // host (no WT_SESSION), and bare/unknown xterm profiles stay off until
265
+ // the DECRQM probe proves support.
257
266
  return false;
258
267
  }
259
268
  }
@@ -374,6 +383,18 @@ function getFallbackImageProtocol(terminalId: TerminalId): ImageProtocol | null
374
383
  }
375
384
  return null;
376
385
  }
386
+ function getWarpTerminalInfo(platform: NodeJS.Platform, env: NodeJS.ProcessEnv = Bun.env): TerminalInfo {
387
+ // Warp for Windows still drives WSL shells from the Windows renderer, where
388
+ // the Kitty APC sequences print as visible garbage. Detect that case via the
389
+ // WSL host markers (Bun reports `process.platform === "linux"` inside WSL)
390
+ // and treat it the same as native win32.
391
+ const windowsHost =
392
+ platform === "win32" || (platform === "linux" && Boolean(env.WSL_DISTRO_NAME || env.WSL_INTEROP));
393
+ return windowsHost
394
+ ? new TerminalInfo("warp", null, true, false, NotifyProtocol.Bell)
395
+ : new TerminalInfo("warp", ImageProtocol.Kitty, true, false, NotifyProtocol.Bell);
396
+ }
397
+
377
398
  const KNOWN_TERMINALS = Object.freeze({
378
399
  // Fallback terminals
379
400
  base: new TerminalInfo("base", null, false, false, NotifyProtocol.Bell),
@@ -385,9 +406,11 @@ const KNOWN_TERMINALS = Object.freeze({
385
406
  iterm2: new TerminalInfo("iterm2", ImageProtocol.Iterm2, true, true, NotifyProtocol.Osc9),
386
407
  vscode: new TerminalInfo("vscode", null, true, true, NotifyProtocol.Bell),
387
408
  alacritty: new TerminalInfo("alacritty", null, true, true, NotifyProtocol.Bell),
409
+ warp: getWarpTerminalInfo(process.platform),
388
410
  });
389
411
 
390
- export const TERMINAL_ID: TerminalId = (() => {
412
+ /** Resolve terminal identity from environment markers used by common emulators. */
413
+ export function detectTerminalId(env: NodeJS.ProcessEnv = Bun.env): TerminalId {
391
414
  function caseEq(a: string, b: string): boolean {
392
415
  return a.toLowerCase() === b.toLowerCase(); // For compiler to pattern match
393
416
  }
@@ -402,7 +425,7 @@ export const TERMINAL_ID: TerminalId = (() => {
402
425
  TERM_PROGRAM,
403
426
  TERM,
404
427
  COLORTERM,
405
- } = Bun.env;
428
+ } = env;
406
429
 
407
430
  if (KITTY_WINDOW_ID) return "kitty";
408
431
  if (GHOSTTY_RESOURCES_DIR) return "ghostty";
@@ -418,6 +441,7 @@ export const TERMINAL_ID: TerminalId = (() => {
418
441
  if (caseEq(TERM_PROGRAM, "iterm.app")) return "iterm2";
419
442
  if (caseEq(TERM_PROGRAM, "vscode")) return "vscode";
420
443
  if (caseEq(TERM_PROGRAM, "alacritty")) return "alacritty";
444
+ if (caseEq(TERM_PROGRAM, "WarpTerminal")) return "warp";
421
445
  }
422
446
 
423
447
  if (TERM?.toLowerCase().includes("ghostty")) return "ghostty";
@@ -426,7 +450,9 @@ export const TERMINAL_ID: TerminalId = (() => {
426
450
  if (caseEq(COLORTERM, "truecolor") || caseEq(COLORTERM, "24bit")) return "trueColor";
427
451
  }
428
452
  return "base";
429
- })();
453
+ }
454
+
455
+ export const TERMINAL_ID: TerminalId = detectTerminalId(Bun.env);
430
456
 
431
457
  /**
432
458
  * The process-wide {@link TERMINAL} singleton: a {@link TerminalInfo} whose
@@ -504,8 +530,12 @@ export function setTerminalTextSizing(enabled: boolean): void {
504
530
  TERMINAL.textSizing = enabled;
505
531
  }
506
532
 
507
- export function getTerminalInfo(terminalId: TerminalId): TerminalInfo {
508
- return KNOWN_TERMINALS[terminalId];
533
+ export function getTerminalInfo(
534
+ terminalId: TerminalId,
535
+ platform: NodeJS.Platform = process.platform,
536
+ env: NodeJS.ProcessEnv = Bun.env,
537
+ ): TerminalInfo {
538
+ return terminalId === "warp" ? getWarpTerminalInfo(platform, env) : KNOWN_TERMINALS[terminalId];
509
539
  }
510
540
 
511
541
  export interface CellDimensions {
package/src/tui.ts CHANGED
@@ -924,6 +924,8 @@ export class TUI extends Container {
924
924
  #renderScheduler: RenderScheduler;
925
925
  #lastRenderAt = 0;
926
926
  static readonly #MIN_RENDER_INTERVAL_MS = 1000 / 30;
927
+ static readonly #INPUT_RENDER_GRACE_MS = TUI.#MIN_RENDER_INTERVAL_MS;
928
+ #inputRenderGraceUntilMs = 0;
927
929
  // Pane-reflow settle window for tmux/screen/zellij. The host process gets
928
930
  // SIGWINCH (and `process.stdout` already reports the new geometry) before
929
931
  // the multiplexer finishes repainting the pane at the new size, and
@@ -2109,8 +2111,11 @@ export class TUI extends Container {
2109
2111
  if (this.#multiplexerResizeTimer) {
2110
2112
  return;
2111
2113
  }
2112
- const elapsed = this.#renderScheduler.now() - this.#lastRenderAt;
2113
- const delay = Math.max(0, TUI.#MIN_RENDER_INTERVAL_MS - elapsed);
2114
+ const now = this.#renderScheduler.now();
2115
+ const elapsed = now - this.#lastRenderAt;
2116
+ const cadenceDelay = Math.max(0, TUI.#MIN_RENDER_INTERVAL_MS - elapsed);
2117
+ const inputGraceDelay = Math.max(0, this.#inputRenderGraceUntilMs - now);
2118
+ const delay = Math.max(cadenceDelay, inputGraceDelay);
2114
2119
  this.#renderTimer = this.#renderScheduler.scheduleRender(() => {
2115
2120
  this.#renderTimer = undefined;
2116
2121
  if (this.#stopped || !this.#renderRequested) {
@@ -2126,6 +2131,12 @@ export class TUI extends Container {
2126
2131
  }
2127
2132
 
2128
2133
  #handleInput(data: string): void {
2134
+ // Raw-mode Ctrl+C/Esc arrive as stdin data, not process signals. If the
2135
+ // first key in a double-key gesture schedules an immediate slow repaint,
2136
+ // the queued second key can sit behind that repaint long enough for the
2137
+ // app-level double-press window to expire. Give the input queue one frame
2138
+ // before ordinary paints; forced repaints still bypass this path.
2139
+ this.#inputRenderGraceUntilMs = this.#renderScheduler.now() + TUI.#INPUT_RENDER_GRACE_MS;
2129
2140
  if (this.#inputListeners.size > 0) {
2130
2141
  let current = data;
2131
2142
  for (const listener of this.#inputListeners) {
package/src/utils.ts CHANGED
@@ -121,7 +121,7 @@ export function truncateToWidth(
121
121
  return nativeTruncateToWidth(
122
122
  text,
123
123
  maxWidth,
124
- (ellipsisKind === "" ? Ellipsis.Omit : ellipsisKind) ?? Ellipsis.Unicode,
124
+ (typeof ellipsisKind === "string" ? Ellipsis.Omit : ellipsisKind) ?? Ellipsis.Unicode,
125
125
  pad ?? false,
126
126
  DEFAULT_TAB_WIDTH,
127
127
  );