@oh-my-pi/pi-tui 16.1.18 → 16.1.20

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
+ ## [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
+
12
+ ## [16.1.19] - 2026-06-25
13
+
14
+ ### Fixed
15
+
16
+ - Fixed bordered `Editor` rendering 1–2 cells past the terminal width when the end-of-line cursor glyph landed past a wide trailing grapheme (CJK comma `,`, emoji, etc.), wrapping the bottom-right corner (`╯`) to its own row. The right chrome (padding + `─` + corner) now shrinks by the exact cursor overflow cell count instead of a 1-cell boolean, so the box stays inside `width` for any `paddingX` ([#3431](https://github.com/can1357/oh-my-pi/issues/3431)).
17
+
5
18
  ## [16.1.17] - 2026-06-24
6
19
 
7
20
  ### Added
@@ -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.18",
4
+ "version": "16.1.20",
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.18",
41
- "@oh-my-pi/pi-utils": "16.1.18",
40
+ "@oh-my-pi/pi-natives": "16.1.20",
41
+ "@oh-my-pi/pi-utils": "16.1.20",
42
42
  "lru-cache": "11.5.1",
43
43
  "marked": "^18.0.5"
44
44
  },
@@ -837,7 +837,7 @@ export class Editor implements Component, Focusable {
837
837
  const layoutLine = visibleLayoutLines[visibleIndex]!;
838
838
  let displayText = layoutLine.text;
839
839
  let displayWidth = visibleWidth(layoutLine.text);
840
- let cursorInPadding = false;
840
+ let cursorPaddingOverflow = 0;
841
841
  let decorated = false;
842
842
  const showPromptGutter = promptGutter !== undefined && visibleIndex === 0;
843
843
  const gutterText =
@@ -963,7 +963,7 @@ export class Editor implements Component, Focusable {
963
963
  displayWidth += cursorWidth;
964
964
  }
965
965
  if (displayWidth > lineContentWidth && paddingX > 0) {
966
- cursorInPadding = true;
966
+ cursorPaddingOverflow = displayWidth - lineContentWidth;
967
967
  }
968
968
  }
969
969
  }
@@ -982,18 +982,22 @@ export class Editor implements Component, Focusable {
982
982
  continue;
983
983
  }
984
984
 
985
- // All lines have consistent borders based on padding
985
+ // All lines have consistent borders based on padding. When the end-of-line cursor
986
+ // glyph (or a wide trailing grapheme) extends past `lineContentWidth`, shrink the
987
+ // right chrome by the exact overflow count: drop padding spaces first, then the
988
+ // trailing `─`, but never the corner/vertical bar itself.
986
989
  const isLastLine = visibleIndex === visibleLayoutLines.length - 1;
987
- const rightPaddingWidth = Math.max(0, paddingX - (cursorInPadding ? 1 : 0));
990
+ const rightChromeCells = Math.max(1, paddingX + 1 - cursorPaddingOverflow);
988
991
  if (isLastLine) {
989
- const bottomRightPadding = Math.max(0, paddingX - 1 - (cursorInPadding ? 1 : 0));
992
+ const rightPad = Math.max(0, rightChromeCells - 2);
993
+ const includeHorizontal = rightChromeCells >= 2;
990
994
  const bottomRightAdjusted = this.borderColor(
991
- `${padding(bottomRightPadding)}${box.horizontal}${box.bottomRight}`,
995
+ `${padding(rightPad)}${includeHorizontal ? box.horizontal : ""}${box.bottomRight}`,
992
996
  );
993
997
  result.push(`${bottomLeft}${displayText}${linePad}${bottomRightAdjusted}`);
994
998
  } else {
995
999
  const leftBorder = this.borderColor(`${box.vertical}${padding(paddingX)}`);
996
- const rightBorder = this.borderColor(`${padding(rightPaddingWidth)}${box.vertical}`);
1000
+ const rightBorder = this.borderColor(`${padding(Math.max(0, rightChromeCells - 1))}${box.vertical}`);
997
1001
  result.push(leftBorder + displayText + linePad + rightBorder);
998
1002
  }
999
1003
  }
@@ -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
  );