@oh-my-pi/pi-tui 10.6.2 → 11.0.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 +3 -2
- package/src/components/image.ts +8 -2
- package/src/components/markdown.ts +6 -6
- package/src/index.ts +1 -22
- package/src/{terminal-image.ts → terminal-capabilities.ts} +40 -12
- package/src/terminal.ts +2 -2
- package/src/tui.ts +7 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-tui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "11.0.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,8 @@
|
|
|
47
47
|
"bun": ">=1.3.7"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@oh-my-pi/pi-natives": "
|
|
50
|
+
"@oh-my-pi/pi-natives": "11.0.0",
|
|
51
|
+
"@oh-my-pi/pi-utils": "11.0.0",
|
|
51
52
|
"@types/mime-types": "^3.0.1",
|
|
52
53
|
"chalk": "^5.6.2",
|
|
53
54
|
"marked": "^17.0.1",
|
package/src/components/image.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getImageDimensions,
|
|
3
|
+
type ImageDimensions,
|
|
4
|
+
imageFallback,
|
|
5
|
+
renderImage,
|
|
6
|
+
TERMINAL,
|
|
7
|
+
} from "../terminal-capabilities";
|
|
2
8
|
import type { Component } from "../tui";
|
|
3
9
|
|
|
4
10
|
export interface ImageTheme {
|
|
@@ -49,7 +55,7 @@ export class Image implements Component {
|
|
|
49
55
|
|
|
50
56
|
let lines: string[];
|
|
51
57
|
|
|
52
|
-
if (
|
|
58
|
+
if (TERMINAL.imageProtocol) {
|
|
53
59
|
const result = renderImage(this.base64Data, this.dimensions, { maxWidthCells: maxWidth });
|
|
54
60
|
|
|
55
61
|
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, getCellDimensions, ImageProtocol,
|
|
4
|
+
import { encodeITerm2, encodeKitty, getCellDimensions, ImageProtocol, TERMINAL } from "../terminal-capabilities";
|
|
5
5
|
import type { Component } from "../tui";
|
|
6
6
|
import { applyBackgroundToLine, padding, visibleWidth, wrapTextWithAnsi } from "../utils";
|
|
7
7
|
|
|
@@ -143,7 +143,7 @@ export class Markdown implements Component {
|
|
|
143
143
|
const wrappedLines: string[] = [];
|
|
144
144
|
for (const line of renderedLines) {
|
|
145
145
|
// Skip wrapping for image protocol lines (would corrupt escape sequences)
|
|
146
|
-
if (
|
|
146
|
+
if (TERMINAL.isImageLine(line)) {
|
|
147
147
|
wrappedLines.push(line);
|
|
148
148
|
} else {
|
|
149
149
|
wrappedLines.push(...wrapTextWithAnsi(line, contentWidth));
|
|
@@ -158,7 +158,7 @@ export class Markdown implements Component {
|
|
|
158
158
|
|
|
159
159
|
for (const line of wrappedLines) {
|
|
160
160
|
// Image lines must be output raw - no margins or background
|
|
161
|
-
if (
|
|
161
|
+
if (TERMINAL.isImageLine(line)) {
|
|
162
162
|
contentLines.push(line);
|
|
163
163
|
continue;
|
|
164
164
|
}
|
|
@@ -314,7 +314,7 @@ export class Markdown implements Component {
|
|
|
314
314
|
const hash = Bun.hash(token.text.trim()).toString(16);
|
|
315
315
|
const image = this.theme.getMermaidImage(hash);
|
|
316
316
|
|
|
317
|
-
if (image &&
|
|
317
|
+
if (image && TERMINAL.imageProtocol) {
|
|
318
318
|
const imageLines = this.renderMermaidImage(image, width);
|
|
319
319
|
if (imageLines) {
|
|
320
320
|
lines.push(...imageLines);
|
|
@@ -809,7 +809,7 @@ export class Markdown implements Component {
|
|
|
809
809
|
* Returns array of lines (image placeholder rows) or null if rendering fails.
|
|
810
810
|
*/
|
|
811
811
|
private renderMermaidImage(image: MermaidImage, availableWidth: number): string[] | null {
|
|
812
|
-
if (!
|
|
812
|
+
if (!TERMINAL.imageProtocol) return null;
|
|
813
813
|
|
|
814
814
|
const cellDims = getCellDimensions();
|
|
815
815
|
const scale = 0.5; // Render at 50% of natural size
|
|
@@ -833,7 +833,7 @@ export class Markdown implements Component {
|
|
|
833
833
|
}
|
|
834
834
|
|
|
835
835
|
let sequence: string;
|
|
836
|
-
switch (
|
|
836
|
+
switch (TERMINAL.imageProtocol) {
|
|
837
837
|
case ImageProtocol.Kitty:
|
|
838
838
|
sequence = encodeKitty(image.base64, { columns, rows });
|
|
839
839
|
break;
|
package/src/index.ts
CHANGED
|
@@ -59,28 +59,7 @@ export type { BoxSymbols, SymbolTheme } from "./symbols";
|
|
|
59
59
|
// Terminal interface and implementations
|
|
60
60
|
export { emergencyTerminalRestore, ProcessTerminal, type Terminal } from "./terminal";
|
|
61
61
|
// Terminal image support
|
|
62
|
-
export
|
|
63
|
-
type CellDimensions,
|
|
64
|
-
calculateImageRows,
|
|
65
|
-
encodeITerm2,
|
|
66
|
-
encodeKitty,
|
|
67
|
-
getCellDimensions,
|
|
68
|
-
getGifDimensions,
|
|
69
|
-
getImageDimensions,
|
|
70
|
-
getJpegDimensions,
|
|
71
|
-
getPngDimensions,
|
|
72
|
-
getTerminalInfo,
|
|
73
|
-
getWebpDimensions,
|
|
74
|
-
type ImageDimensions,
|
|
75
|
-
type ImageRenderOptions,
|
|
76
|
-
imageFallback,
|
|
77
|
-
renderImage,
|
|
78
|
-
setCellDimensions,
|
|
79
|
-
TERMINAL_ID,
|
|
80
|
-
TERMINAL_INFO,
|
|
81
|
-
type TerminalId,
|
|
82
|
-
type TerminalInfo,
|
|
83
|
-
} from "./terminal-image";
|
|
62
|
+
export * from "./terminal-capabilities";
|
|
84
63
|
export { type Component, Container, type OverlayHandle, type SizeValue, TUI } from "./tui";
|
|
85
64
|
// Utilities
|
|
86
65
|
export { Ellipsis, padding, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "./utils";
|
|
@@ -1,35 +1,63 @@
|
|
|
1
|
+
import { $env } from "@oh-my-pi/pi-utils";
|
|
2
|
+
|
|
1
3
|
export enum ImageProtocol {
|
|
2
4
|
Kitty = "\x1b_G",
|
|
3
5
|
Iterm2 = "\x1b]1337;File=",
|
|
4
6
|
}
|
|
5
7
|
|
|
8
|
+
export enum NotifyProtocol {
|
|
9
|
+
Bell = "\x07",
|
|
10
|
+
Osc99 = "\x1b]99;;",
|
|
11
|
+
Osc9 = "\x1b]9;",
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
export type TerminalId = "kitty" | "ghostty" | "wezterm" | "iterm2" | "vscode" | "alacritty" | "base" | "trueColor";
|
|
7
15
|
|
|
16
|
+
/** Terminal capability details used for rendering and protocol selection. */
|
|
8
17
|
export class TerminalInfo {
|
|
9
18
|
constructor(
|
|
10
19
|
public readonly id: TerminalId,
|
|
11
20
|
public readonly imageProtocol: ImageProtocol | null,
|
|
12
21
|
public readonly trueColor: boolean,
|
|
13
22
|
public readonly hyperlinks: boolean,
|
|
23
|
+
public readonly notifyProtocol: NotifyProtocol = NotifyProtocol.Bell,
|
|
14
24
|
) {}
|
|
15
25
|
|
|
16
26
|
isImageLine(line: string): boolean {
|
|
17
27
|
if (!this.imageProtocol) return false;
|
|
18
28
|
return line.slice(0, 64).includes(this.imageProtocol);
|
|
19
29
|
}
|
|
30
|
+
|
|
31
|
+
formatNotification(message: string): string {
|
|
32
|
+
if (this.notifyProtocol === NotifyProtocol.Bell) {
|
|
33
|
+
return NotifyProtocol.Bell;
|
|
34
|
+
}
|
|
35
|
+
return `${this.notifyProtocol}${message}\x1b\\`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
sendNotification(message: string): void {
|
|
39
|
+
if (isNotificationSuppressed()) return;
|
|
40
|
+
process.stdout.write(this.formatNotification(message));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isNotificationSuppressed(): boolean {
|
|
45
|
+
const value = $env.PI_NOTIFICATIONS;
|
|
46
|
+
if (!value) return false;
|
|
47
|
+
return value === "off" || value === "0" || value === "false";
|
|
20
48
|
}
|
|
21
49
|
|
|
22
50
|
const KNOWN_TERMINALS = Object.freeze({
|
|
23
51
|
// Fallback terminals
|
|
24
|
-
base: new TerminalInfo("base", null, false, true),
|
|
25
|
-
trueColor: new TerminalInfo("trueColor", null, true, true),
|
|
52
|
+
base: new TerminalInfo("base", null, false, true, NotifyProtocol.Bell),
|
|
53
|
+
trueColor: new TerminalInfo("trueColor", null, true, true, NotifyProtocol.Bell),
|
|
26
54
|
// Recognized terminals
|
|
27
|
-
kitty: new TerminalInfo("kitty", ImageProtocol.Kitty, true, true),
|
|
28
|
-
ghostty: new TerminalInfo("ghostty", ImageProtocol.Kitty, true, true),
|
|
29
|
-
wezterm: new TerminalInfo("wezterm", ImageProtocol.Kitty, true, true),
|
|
30
|
-
iterm2: new TerminalInfo("iterm2", ImageProtocol.Iterm2, true, true),
|
|
31
|
-
vscode: new TerminalInfo("vscode", null, true, true),
|
|
32
|
-
alacritty: new TerminalInfo("alacritty", null, true, true),
|
|
55
|
+
kitty: new TerminalInfo("kitty", ImageProtocol.Kitty, true, true, NotifyProtocol.Osc99),
|
|
56
|
+
ghostty: new TerminalInfo("ghostty", ImageProtocol.Kitty, true, true, NotifyProtocol.Osc9),
|
|
57
|
+
wezterm: new TerminalInfo("wezterm", ImageProtocol.Kitty, true, true, NotifyProtocol.Osc9),
|
|
58
|
+
iterm2: new TerminalInfo("iterm2", ImageProtocol.Iterm2, true, true, NotifyProtocol.Osc9),
|
|
59
|
+
vscode: new TerminalInfo("vscode", null, true, true, NotifyProtocol.Bell),
|
|
60
|
+
alacritty: new TerminalInfo("alacritty", null, true, true, NotifyProtocol.Bell),
|
|
33
61
|
});
|
|
34
62
|
|
|
35
63
|
export const TERMINAL_ID: TerminalId = (() => {
|
|
@@ -73,7 +101,7 @@ export const TERMINAL_ID: TerminalId = (() => {
|
|
|
73
101
|
return "base";
|
|
74
102
|
})();
|
|
75
103
|
|
|
76
|
-
export const
|
|
104
|
+
export const TERMINAL = getTerminalInfo(TERMINAL_ID);
|
|
77
105
|
|
|
78
106
|
export function getTerminalInfo(terminalId: TerminalId): TerminalInfo {
|
|
79
107
|
return KNOWN_TERMINALS[terminalId];
|
|
@@ -332,19 +360,19 @@ export function renderImage(
|
|
|
332
360
|
imageDimensions: ImageDimensions,
|
|
333
361
|
options: ImageRenderOptions = {},
|
|
334
362
|
): { sequence: string; rows: number } | null {
|
|
335
|
-
if (!
|
|
363
|
+
if (!TERMINAL.imageProtocol) {
|
|
336
364
|
return null;
|
|
337
365
|
}
|
|
338
366
|
|
|
339
367
|
const maxWidth = options.maxWidthCells ?? 80;
|
|
340
368
|
const rows = calculateImageRows(imageDimensions, maxWidth, getCellDimensions());
|
|
341
369
|
|
|
342
|
-
if (
|
|
370
|
+
if (TERMINAL.imageProtocol === ImageProtocol.Kitty) {
|
|
343
371
|
const sequence = encodeKitty(base64Data, { columns: maxWidth, rows });
|
|
344
372
|
return { sequence, rows };
|
|
345
373
|
}
|
|
346
374
|
|
|
347
|
-
if (
|
|
375
|
+
if (TERMINAL.imageProtocol === ImageProtocol.Iterm2) {
|
|
348
376
|
const sequence = encodeITerm2(base64Data, {
|
|
349
377
|
width: maxWidth,
|
|
350
378
|
height: "auto",
|
package/src/terminal.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { $env, logger } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import { setKittyProtocolActive } from "./keys";
|
|
4
4
|
import { StdinBuffer } from "./stdin-buffer";
|
|
5
5
|
|
|
@@ -82,7 +82,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
82
82
|
private stdinBuffer?: StdinBuffer;
|
|
83
83
|
private stdinDataHandler?: (data: string) => void;
|
|
84
84
|
private dead = false;
|
|
85
|
-
private writeLogPath =
|
|
85
|
+
private writeLogPath = $env.PI_TUI_WRITE_LOG || "";
|
|
86
86
|
|
|
87
87
|
get kittyProtocolActive(): boolean {
|
|
88
88
|
return this._kittyProtocolActive;
|
package/src/tui.ts
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import * as path from "node:path";
|
|
7
|
+
import { $env } from "@oh-my-pi/pi-utils";
|
|
7
8
|
import { isKeyRelease, matchesKey } from "./keys";
|
|
8
9
|
import type { Terminal } from "./terminal";
|
|
9
|
-
import { setCellDimensions,
|
|
10
|
+
import { setCellDimensions, TERMINAL } from "./terminal-capabilities";
|
|
10
11
|
import { extractSegments, padding, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils";
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -227,7 +228,7 @@ export class TUI extends Container {
|
|
|
227
228
|
private hardwareCursorRow = 0; // Actual terminal cursor row (may differ due to IME positioning)
|
|
228
229
|
private inputBuffer = ""; // Buffer for parsing terminal responses
|
|
229
230
|
private cellSizeQueryPending = false;
|
|
230
|
-
private showHardwareCursor =
|
|
231
|
+
private showHardwareCursor = $env.PI_HARDWARE_CURSOR === "1";
|
|
231
232
|
private maxLinesRendered = 0; // Track terminal's working area (max lines ever rendered)
|
|
232
233
|
private previousViewportTop = 0; // Track previous viewport top for resize-aware cursor moves
|
|
233
234
|
private fullRedrawCount = 0;
|
|
@@ -384,7 +385,7 @@ export class TUI extends Container {
|
|
|
384
385
|
|
|
385
386
|
private queryCellSize(): void {
|
|
386
387
|
// Only query if terminal supports images (cell size is only used for image rendering)
|
|
387
|
-
if (!
|
|
388
|
+
if (!TERMINAL.imageProtocol) {
|
|
388
389
|
return;
|
|
389
390
|
}
|
|
390
391
|
// Query terminal for cell size in pixels: CSI 16 t
|
|
@@ -767,7 +768,7 @@ export class TUI extends Container {
|
|
|
767
768
|
const reset = TUI.SEGMENT_RESET;
|
|
768
769
|
for (let i = 0; i < lines.length; i++) {
|
|
769
770
|
const line = lines[i];
|
|
770
|
-
if (!
|
|
771
|
+
if (!TERMINAL.isImageLine(line)) {
|
|
771
772
|
lines[i] = line + reset;
|
|
772
773
|
}
|
|
773
774
|
}
|
|
@@ -846,7 +847,7 @@ export class TUI extends Container {
|
|
|
846
847
|
overlayWidth: number,
|
|
847
848
|
totalWidth: number,
|
|
848
849
|
): string {
|
|
849
|
-
if (
|
|
850
|
+
if (TERMINAL.isImageLine(baseLine)) return baseLine;
|
|
850
851
|
|
|
851
852
|
// Single pass through baseLine extracts both before and after segments
|
|
852
853
|
const afterStart = startCol + overlayWidth;
|
|
@@ -1081,7 +1082,7 @@ export class TUI extends Container {
|
|
|
1081
1082
|
if (i > firstChanged) buffer += "\r\n";
|
|
1082
1083
|
buffer += "\x1b[2K"; // Clear current line
|
|
1083
1084
|
let line = newLines[i];
|
|
1084
|
-
const isImageLine =
|
|
1085
|
+
const isImageLine = TERMINAL.isImageLine(line);
|
|
1085
1086
|
if (!isImageLine && visibleWidth(line) > width) {
|
|
1086
1087
|
if (process.platform === "win32") {
|
|
1087
1088
|
line = sliceByColumn(line, 0, width, true);
|