@oh-my-pi/pi-tui 9.0.0 → 9.1.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-tui",
3
- "version": "9.0.0",
3
+ "version": "9.1.0",
4
4
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -47,7 +47,7 @@
47
47
  "bun": ">=1.3.7"
48
48
  },
49
49
  "dependencies": {
50
- "@oh-my-pi/pi-natives": "9.0.0",
50
+ "@oh-my-pi/pi-natives": "9.1.0",
51
51
  "@types/mime-types": "^3.0.1",
52
52
  "chalk": "^5.6.2",
53
53
  "marked": "^17.0.1",
@@ -1,10 +1,4 @@
1
- import {
2
- getCapabilities,
3
- getImageDimensions,
4
- type ImageDimensions,
5
- imageFallback,
6
- renderImage,
7
- } from "../terminal-image";
1
+ import { getImageDimensions, type ImageDimensions, imageFallback, renderImage, TERMINAL_INFO } from "../terminal-image";
8
2
  import type { Component } from "../tui";
9
3
 
10
4
  export interface ImageTheme {
@@ -53,10 +47,9 @@ export class Image implements Component {
53
47
 
54
48
  const maxWidth = Math.min(width - 2, this.options.maxWidthCells ?? 60);
55
49
 
56
- const caps = getCapabilities();
57
50
  let lines: string[];
58
51
 
59
- if (caps.images) {
52
+ if (TERMINAL_INFO.imageProtocol) {
60
53
  const result = renderImage(this.base64Data, this.dimensions, { maxWidthCells: maxWidth });
61
54
 
62
55
  if (result) {
@@ -1,7 +1,7 @@
1
1
  import { marked, type Token } from "marked";
2
2
  import type { MermaidImage } from "../mermaid";
3
3
  import type { SymbolTheme } from "../symbols";
4
- import { encodeITerm2, encodeKitty, getCapabilities, getCellDimensions } from "../terminal-image";
4
+ import { encodeITerm2, encodeKitty, getCellDimensions, ImageProtocol, TERMINAL_INFO } from "../terminal-image";
5
5
  import type { Component } from "../tui";
6
6
  import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils";
7
7
 
@@ -290,9 +290,8 @@ export class Markdown implements Component {
290
290
  if (token.lang === "mermaid" && this.theme.getMermaidImage) {
291
291
  const hash = Bun.hash(token.text.trim()).toString(16);
292
292
  const image = this.theme.getMermaidImage(hash);
293
- const caps = getCapabilities();
294
293
 
295
- if (image && caps.images) {
294
+ if (image && TERMINAL_INFO.imageProtocol) {
296
295
  const imageLines = this.renderMermaidImage(image, width);
297
296
  if (imageLines) {
298
297
  lines.push(...imageLines);
@@ -771,8 +770,7 @@ export class Markdown implements Component {
771
770
  * Returns array of lines (image placeholder rows) or null if rendering fails.
772
771
  */
773
772
  private renderMermaidImage(image: MermaidImage, availableWidth: number): string[] | null {
774
- const caps = getCapabilities();
775
- if (!caps.images) return null;
773
+ if (!TERMINAL_INFO.imageProtocol) return null;
776
774
 
777
775
  const cellDims = getCellDimensions();
778
776
  const scale = 0.5; // Render at 50% of natural size
@@ -796,16 +794,19 @@ export class Markdown implements Component {
796
794
  }
797
795
 
798
796
  let sequence: string;
799
- if (caps.images === "kitty") {
800
- sequence = encodeKitty(image.base64, { columns, rows });
801
- } else if (caps.images === "iterm2") {
802
- sequence = encodeITerm2(image.base64, {
803
- width: columns,
804
- height: "auto",
805
- preserveAspectRatio: true,
806
- });
807
- } else {
808
- return null;
797
+ switch (TERMINAL_INFO.imageProtocol) {
798
+ case ImageProtocol.Kitty:
799
+ sequence = encodeKitty(image.base64, { columns, rows });
800
+ break;
801
+ case ImageProtocol.Iterm2:
802
+ sequence = encodeITerm2(image.base64, {
803
+ width: columns,
804
+ height: "auto",
805
+ preserveAspectRatio: true,
806
+ });
807
+ break;
808
+ default:
809
+ return null;
809
810
  }
810
811
 
811
812
  // Reserve space with empty lines, then output image with cursor-up
package/src/index.ts CHANGED
@@ -64,24 +64,24 @@ export { emergencyTerminalRestore, ProcessTerminal, type Terminal } from "./term
64
64
  export {
65
65
  type CellDimensions,
66
66
  calculateImageRows,
67
- detectCapabilities,
68
67
  encodeITerm2,
69
68
  encodeKitty,
70
- getCapabilities,
71
69
  getCellDimensions,
72
70
  getGifDimensions,
73
71
  getImageDimensions,
74
72
  getJpegDimensions,
75
73
  getPngDimensions,
74
+ getTerminalInfo,
76
75
  getWebpDimensions,
77
76
  type ImageDimensions,
78
- type ImageProtocol,
79
77
  type ImageRenderOptions,
80
78
  imageFallback,
81
79
  renderImage,
82
- resetCapabilitiesCache,
83
80
  setCellDimensions,
84
- type TerminalCapabilities,
81
+ TERMINAL_ID,
82
+ TERMINAL_INFO,
83
+ type TerminalId,
84
+ type TerminalInfo,
85
85
  } from "./terminal-image";
86
86
  export { type Component, Container, type OverlayHandle, type SizeValue, TUI } from "./tui";
87
87
  // Utilities
@@ -1,10 +1,81 @@
1
- export type ImageProtocol = "kitty" | "iterm2" | null;
1
+ export enum ImageProtocol {
2
+ Kitty = "\x1b_G",
3
+ Iterm2 = "\x1b]1337;File=",
4
+ }
5
+
6
+ export type TerminalId = "kitty" | "ghostty" | "wezterm" | "iterm2" | "vscode" | "alacritty" | "base" | "trueColor";
7
+
8
+ export class TerminalInfo {
9
+ constructor(
10
+ public readonly id: TerminalId,
11
+ public readonly imageProtocol: ImageProtocol | null,
12
+ public readonly trueColor: boolean,
13
+ public readonly hyperlinks: boolean,
14
+ ) {}
15
+
16
+ isImageLine(line: string): boolean {
17
+ return !!this.imageProtocol && line.trimStart().startsWith(this.imageProtocol);
18
+ }
19
+ }
20
+
21
+ const KNOWN_TERMINALS = Object.freeze({
22
+ // Fallback terminals
23
+ base: new TerminalInfo("base", null, false, true),
24
+ trueColor: new TerminalInfo("trueColor", null, true, true),
25
+ // Recognized terminals
26
+ kitty: new TerminalInfo("kitty", ImageProtocol.Kitty, true, true),
27
+ ghostty: new TerminalInfo("ghostty", ImageProtocol.Kitty, true, true),
28
+ wezterm: new TerminalInfo("wezterm", ImageProtocol.Kitty, true, true),
29
+ iterm2: new TerminalInfo("iterm2", ImageProtocol.Iterm2, true, true),
30
+ vscode: new TerminalInfo("vscode", null, true, true),
31
+ alacritty: new TerminalInfo("alacritty", null, true, true),
32
+ });
33
+
34
+ export const TERMINAL_ID: TerminalId = (() => {
35
+ function caseEq(a: string, b: string): boolean {
36
+ return a.toLowerCase() === b.toLowerCase(); // For compiler to pattern match
37
+ }
2
38
 
3
- export interface TerminalCapabilities {
4
- readonly images: ImageProtocol;
5
- readonly trueColor: boolean;
6
- readonly hyperlinks: boolean;
7
- containsImage(line: string): boolean;
39
+ const {
40
+ KITTY_WINDOW_ID,
41
+ GHOSTTY_RESOURCES_DIR,
42
+ WEZTERM_PANE,
43
+ ITERM_SESSION_ID,
44
+ VSCODE_PID,
45
+ ALACRITTY_WINDOW_ID,
46
+ TERM_PROGRAM,
47
+ TERM,
48
+ COLORTERM,
49
+ } = process.env;
50
+
51
+ if (KITTY_WINDOW_ID) return "kitty";
52
+ if (GHOSTTY_RESOURCES_DIR) return "ghostty";
53
+ if (WEZTERM_PANE) return "wezterm";
54
+ if (ITERM_SESSION_ID) return "iterm2";
55
+ if (VSCODE_PID) return "vscode";
56
+ if (ALACRITTY_WINDOW_ID) return "alacritty";
57
+
58
+ if (TERM_PROGRAM) {
59
+ if (caseEq(TERM_PROGRAM, "kitty")) return "kitty";
60
+ if (caseEq(TERM_PROGRAM, "ghostty")) return "ghostty";
61
+ if (caseEq(TERM_PROGRAM, "wezterm")) return "wezterm";
62
+ if (caseEq(TERM_PROGRAM, "iterm.app")) return "iterm2";
63
+ if (caseEq(TERM_PROGRAM, "vscode")) return "vscode";
64
+ if (caseEq(TERM_PROGRAM, "alacritty")) return "alacritty";
65
+ }
66
+
67
+ if (!!TERM && TERM.toLowerCase().includes("ghostty")) return "ghostty";
68
+
69
+ if (COLORTERM) {
70
+ if (caseEq(COLORTERM, "truecolor") || caseEq(COLORTERM, "24bit")) return "trueColor";
71
+ }
72
+ return "base";
73
+ })();
74
+
75
+ export const TERMINAL_INFO = getTerminalInfo(TERMINAL_ID);
76
+
77
+ export function getTerminalInfo(terminalId: TerminalId): TerminalInfo {
78
+ return KNOWN_TERMINALS[terminalId];
8
79
  }
9
80
 
10
81
  export interface CellDimensions {
@@ -23,8 +94,6 @@ export interface ImageRenderOptions {
23
94
  preserveAspectRatio?: boolean;
24
95
  }
25
96
 
26
- let cachedCapabilities: TerminalCapabilities | null = null;
27
-
28
97
  // Default cell dimensions - updated by TUI when terminal responds to query
29
98
  let cellDimensions: CellDimensions = { widthPx: 9, heightPx: 18 };
30
99
 
@@ -36,85 +105,6 @@ export function setCellDimensions(dims: CellDimensions): void {
36
105
  cellDimensions = dims;
37
106
  }
38
107
 
39
- const kBaseCaps: TerminalCapabilities = {
40
- images: null,
41
- trueColor: false,
42
- hyperlinks: true,
43
- containsImage() {
44
- return false;
45
- },
46
- };
47
-
48
- function createTerminalCaps(parts: Partial<TerminalCapabilities>): TerminalCapabilities {
49
- return Object.freeze({ ...kBaseCaps, ...parts });
50
- }
51
-
52
- const kKittyCaps = createTerminalCaps({
53
- images: "kitty",
54
- trueColor: true,
55
- hyperlinks: true,
56
- containsImage(line: string) {
57
- return line.includes("\x1b_G");
58
- },
59
- });
60
- const kGhosttyCaps = kKittyCaps;
61
- const kWeztermCaps = kKittyCaps;
62
-
63
- const kIterm2Caps = createTerminalCaps({
64
- images: "iterm2",
65
- trueColor: true,
66
- hyperlinks: true,
67
- containsImage(line: string) {
68
- return line.includes("\x1b]1337;File=");
69
- },
70
- });
71
-
72
- const kTrueColorCaps = createTerminalCaps({
73
- ...kBaseCaps,
74
- trueColor: true,
75
- });
76
-
77
- const kVscodeCaps = kTrueColorCaps;
78
- const kAlacrittyCaps = kTrueColorCaps;
79
-
80
- export function detectCapabilities(): TerminalCapabilities {
81
- const termProgram = process.env.TERM_PROGRAM?.toLowerCase() || "";
82
- const term = process.env.TERM?.toLowerCase() || "";
83
- const colorTerm = process.env.COLORTERM?.toLowerCase() || "";
84
-
85
- if (process.env.KITTY_WINDOW_ID || termProgram === "kitty") {
86
- return kKittyCaps;
87
- }
88
- if (termProgram === "ghostty" || term.includes("ghostty") || process.env.GHOSTTY_RESOURCES_DIR) {
89
- return kGhosttyCaps;
90
- }
91
- if (process.env.WEZTERM_PANE || termProgram === "wezterm") {
92
- return kWeztermCaps;
93
- }
94
- if (process.env.ITERM_SESSION_ID || termProgram === "iterm.app") {
95
- return kIterm2Caps;
96
- }
97
- if (termProgram === "vscode") {
98
- return kVscodeCaps;
99
- }
100
- if (termProgram === "alacritty") {
101
- return kAlacrittyCaps;
102
- }
103
- const trueColor = colorTerm === "truecolor" || colorTerm === "24bit";
104
- return trueColor ? kTrueColorCaps : kBaseCaps;
105
- }
106
-
107
- export function getCapabilities(): TerminalCapabilities {
108
- if (!cachedCapabilities) {
109
- cachedCapabilities = detectCapabilities();
110
- }
111
- return cachedCapabilities;
112
- }
113
-
114
- export function resetCapabilitiesCache(): void {
115
- cachedCapabilities = null;
116
- }
117
-
118
108
  export function encodeKitty(
119
109
  base64Data: string,
120
110
  options: {
@@ -341,21 +331,19 @@ export function renderImage(
341
331
  imageDimensions: ImageDimensions,
342
332
  options: ImageRenderOptions = {},
343
333
  ): { sequence: string; rows: number } | null {
344
- const caps = getCapabilities();
345
-
346
- if (!caps.images) {
334
+ if (!TERMINAL_INFO.imageProtocol) {
347
335
  return null;
348
336
  }
349
337
 
350
338
  const maxWidth = options.maxWidthCells ?? 80;
351
339
  const rows = calculateImageRows(imageDimensions, maxWidth, getCellDimensions());
352
340
 
353
- if (caps.images === "kitty") {
341
+ if (TERMINAL_INFO.imageProtocol === ImageProtocol.Kitty) {
354
342
  const sequence = encodeKitty(base64Data, { columns: maxWidth, rows });
355
343
  return { sequence, rows };
356
344
  }
357
345
 
358
- if (caps.images === "iterm2") {
346
+ if (TERMINAL_INFO.imageProtocol === ImageProtocol.Iterm2) {
359
347
  const sequence = encodeITerm2(base64Data, {
360
348
  width: maxWidth,
361
349
  height: "auto",
package/src/tui.ts CHANGED
@@ -6,7 +6,7 @@ import * as os from "node:os";
6
6
  import * as path from "node:path";
7
7
  import { isKeyRelease, matchesKey } from "./keys";
8
8
  import type { Terminal } from "./terminal";
9
- import { getCapabilities, setCellDimensions } from "./terminal-image";
9
+ import { setCellDimensions, TERMINAL_INFO } from "./terminal-image";
10
10
  import { extractSegments, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils";
11
11
 
12
12
  /**
@@ -382,7 +382,7 @@ export class TUI extends Container {
382
382
 
383
383
  private queryCellSize(): void {
384
384
  // Only query if terminal supports images (cell size is only used for image rendering)
385
- if (!getCapabilities().images) {
385
+ if (!TERMINAL_INFO.imageProtocol) {
386
386
  return;
387
387
  }
388
388
  // Query terminal for cell size in pixels: CSI 16 t
@@ -542,7 +542,7 @@ export class TUI extends Container {
542
542
  }
543
543
 
544
544
  static containsImage(line: string): boolean {
545
- return getCapabilities().containsImage(line);
545
+ return TERMINAL_INFO.isImageLine(line);
546
546
  }
547
547
 
548
548
  /**