@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 +2 -2
- package/src/components/image.ts +2 -9
- package/src/components/markdown.ts +16 -15
- package/src/index.ts +5 -5
- package/src/terminal-image.ts +80 -92
- package/src/tui.ts +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-tui",
|
|
3
|
-
"version": "9.
|
|
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.
|
|
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",
|
package/src/components/image.ts
CHANGED
|
@@ -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 (
|
|
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,
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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
|
-
|
|
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
|
package/src/terminal-image.ts
CHANGED
|
@@ -1,10 +1,81 @@
|
|
|
1
|
-
export
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 {
|
|
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 (!
|
|
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
|
|
545
|
+
return TERMINAL_INFO.isImageLine(line);
|
|
546
546
|
}
|
|
547
547
|
|
|
548
548
|
/**
|