@dkkoval/tui-preview 0.1.0 → 0.2.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/dist/types.d.ts CHANGED
@@ -4,17 +4,17 @@ export interface TuiRuntimeSize {
4
4
  }
5
5
  export type TuiArgv = string[] | ((size: TuiRuntimeSize) => string[]);
6
6
  export type TuiFitMode = "container" | "none";
7
- export type TuiRenderMode = "terminal" | "static";
7
+ export type TuiRenderMode = "interactive" | "static";
8
8
  export type TuiPreviewStatus = "loading" | "running" | "exited" | "error";
9
9
  export interface TuiTerminalOptions {
10
10
  /** Font size in pixels. Default: 14 */
11
11
  fontSize?: number;
12
12
  /** CSS font family. Default: monospace */
13
13
  fontFamily?: string;
14
- /** ghostty-web theme overrides */
14
+ /** Terminal color theme overrides */
15
15
  theme?: Partial<GhosttyTheme>;
16
- /** Cursor blinking. Default: true */
17
- cursorBlink?: boolean;
16
+ /** URL to libghostty-vt wasm. Default: "/ghostty-vt.wasm" */
17
+ wasmUrl?: string | URL;
18
18
  /** Convert LF to CRLF. Default: true */
19
19
  convertEol?: boolean;
20
20
  }
@@ -37,12 +37,7 @@ export interface TuiPreviewModernProps extends TuiPreviewCommonProps {
37
37
  wasm: string | URL;
38
38
  /** CLI argv (without argv[0]), static or size-aware */
39
39
  argv?: TuiArgv;
40
- /**
41
- * Render mode.
42
- * - `"terminal"` (default): full ghostty-web terminal, supports interactive apps.
43
- * - `"static"`: runs the app once, captures stdout, renders ANSI output as HTML.
44
- * No cursor, no input. Ideal for non-interactive previews in docs.
45
- */
40
+ /** Render mode. Default: "interactive" */
46
41
  mode?: TuiRenderMode;
47
42
  /** "container" auto-fit or "none" fixed size. Default: "container" */
48
43
  fit?: TuiFitMode;
@@ -51,20 +46,7 @@ export interface TuiPreviewModernProps extends TuiPreviewCommonProps {
51
46
  /** Terminal renderer options */
52
47
  terminal?: TuiTerminalOptions;
53
48
  }
54
- /**
55
- * Legacy API retained for backwards compatibility.
56
- * Prefer `TuiPreviewModernProps`.
57
- */
58
- export interface TuiPreviewLegacyProps extends TuiPreviewCommonProps {
59
- app: string | URL;
60
- args?: TuiArgv;
61
- cols?: number;
62
- rows?: number;
63
- fontSize?: number;
64
- fontFamily?: string;
65
- theme?: Partial<GhosttyTheme>;
66
- }
67
- export type TuiPreviewProps = TuiPreviewModernProps | TuiPreviewLegacyProps;
49
+ export type TuiPreviewProps = TuiPreviewModernProps;
68
50
  export interface GhosttyTheme {
69
51
  background: string;
70
52
  foreground: string;
@@ -102,12 +84,12 @@ export interface ResolvedTuiPreviewOptions {
102
84
  mode: TuiRenderMode;
103
85
  fit: TuiFitMode;
104
86
  size: TuiRuntimeSize;
105
- terminal: Required<Omit<TuiTerminalOptions, "theme">> & {
87
+ terminal: Required<Omit<TuiTerminalOptions, "theme" | "wasmUrl">> & {
106
88
  theme?: Partial<GhosttyTheme>;
89
+ wasmUrl?: string | URL;
107
90
  };
108
91
  resolveArgv: (size: TuiRuntimeSize) => string[];
109
92
  onExit?: (code: number) => void;
110
93
  onError?: (error: unknown) => void;
111
94
  onStatusChange?: (status: TuiPreviewStatus) => void;
112
- usedLegacyProps: boolean;
113
95
  }
package/package.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "name": "@dkkoval/tui-preview",
3
- "version": "0.1.0",
4
- "description": "React component for embedding interactive TUI apps via ghostty-web",
3
+ "version": "0.2.0",
4
+ "description": "React component for embedding interactive TUI apps via libghostty",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/dmk/tui-preview"
8
+ },
9
+ "homepage": "https://github.com/dmk/tui-preview#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/dmk/tui-preview/issues"
12
+ },
5
13
  "type": "module",
6
14
  "license": "MIT",
7
15
  "sideEffects": false,
@@ -24,20 +32,19 @@
24
32
  ],
25
33
  "scripts": {
26
34
  "clean": "rm -rf dist dist-example",
35
+ "build:ghostty-wasm": "./scripts/build-libghostty-wasm.sh",
27
36
  "dev": "vite example",
28
37
  "build": "vite build",
29
38
  "build:lib": "tsc -p tsconfig.json && vite build --mode lib",
39
+ "prepack": "tsc -p tsconfig.json && vite build --mode lib && ./scripts/build-libghostty-wasm.sh dist/ghostty-vt.wasm",
30
40
  "typecheck": "tsc --noEmit",
31
- "test": "npm run -s build:lib && node --test tests/*.test.mjs",
32
- "check": "npm run typecheck && npm run test"
41
+ "test": "tsc -p tsconfig.json && vite build --mode lib && node --test tests/*.test.mjs",
42
+ "check": "tsc --noEmit && tsc -p tsconfig.json && vite build --mode lib && node --test tests/*.test.mjs"
33
43
  },
34
44
  "peerDependencies": {
35
45
  "react": ">=18",
36
46
  "react-dom": ">=18"
37
47
  },
38
- "dependencies": {
39
- "ghostty-web": "^0.4.0"
40
- },
41
48
  "devDependencies": {
42
49
  "@types/react": "^18",
43
50
  "@types/react-dom": "^18",
@@ -1 +0,0 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e={};exports.default=e;
@@ -1,4 +0,0 @@
1
- const e = {};
2
- export {
3
- e as default
4
- };
@@ -1,15 +0,0 @@
1
- /**
2
- * Minimal ANSI SGR → HTML converter.
3
- *
4
- * Handles the subset used by typical wasm32-wasi TUI apps:
5
- * - Reset (0)
6
- * - Bold, italic, underline (1/3/4 and their off codes)
7
- * - Standard 16-color fg/bg (30–37, 39, 40–47, 49, 90–97, 100–107)
8
- * - 256-color fg/bg (38;5;N / 48;5;N)
9
- * - 24-bit truecolor fg/bg (38;2;R;G;B / 48;2;R;G;B)
10
- *
11
- * Non-SGR CSI sequences (cursor movement etc.) are silently dropped,
12
- * which is correct for static capture where only the final text matters.
13
- */
14
- /** Convert ANSI escape codes to an HTML string of styled `<span>` elements. */
15
- export declare function ansiToHtml(input: string): string;
package/dist/core/ansi.js DELETED
@@ -1,181 +0,0 @@
1
- /**
2
- * Minimal ANSI SGR → HTML converter.
3
- *
4
- * Handles the subset used by typical wasm32-wasi TUI apps:
5
- * - Reset (0)
6
- * - Bold, italic, underline (1/3/4 and their off codes)
7
- * - Standard 16-color fg/bg (30–37, 39, 40–47, 49, 90–97, 100–107)
8
- * - 256-color fg/bg (38;5;N / 48;5;N)
9
- * - 24-bit truecolor fg/bg (38;2;R;G;B / 48;2;R;G;B)
10
- *
11
- * Non-SGR CSI sequences (cursor movement etc.) are silently dropped,
12
- * which is correct for static capture where only the final text matters.
13
- */
14
- // Standard xterm 16-color palette
15
- const ANSI16 = [
16
- "#000000", "#cd0000", "#00cd00", "#cdcd00",
17
- "#0000ee", "#cd00cd", "#00cdcd", "#e5e5e5",
18
- "#7f7f7f", "#ff0000", "#00ff00", "#ffff00",
19
- "#5c5cff", "#ff00ff", "#00ffff", "#ffffff",
20
- ];
21
- function color256(n) {
22
- if (n < 16)
23
- return ANSI16[n];
24
- if (n < 232) {
25
- const i = n - 16;
26
- const r = Math.floor(i / 36);
27
- const g = Math.floor((i % 36) / 6);
28
- const b = i % 6;
29
- const v = (x) => (x === 0 ? 0 : x * 40 + 55);
30
- return `rgb(${v(r)},${v(g)},${v(b)})`;
31
- }
32
- const grey = (n - 232) * 10 + 8;
33
- return `rgb(${grey},${grey},${grey})`;
34
- }
35
- function escHtml(s) {
36
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
37
- }
38
- /** Convert ANSI escape codes to an HTML string of styled `<span>` elements. */
39
- export function ansiToHtml(input) {
40
- // Current SGR state
41
- let fg = null;
42
- let bg = null;
43
- let bold = false;
44
- let italic = false;
45
- let underline = false;
46
- const result = [];
47
- let buf = "";
48
- let bufStyle = "";
49
- function buildStyle() {
50
- const parts = [];
51
- if (fg)
52
- parts.push(`color:${fg}`);
53
- if (bg)
54
- parts.push(`background-color:${bg}`);
55
- if (bold)
56
- parts.push("font-weight:bold");
57
- if (italic)
58
- parts.push("font-style:italic");
59
- if (underline)
60
- parts.push("text-decoration:underline");
61
- return parts.join(";");
62
- }
63
- function flush() {
64
- if (!buf)
65
- return;
66
- result.push(bufStyle ? `<span style="${bufStyle}">${escHtml(buf)}</span>` : escHtml(buf));
67
- buf = "";
68
- }
69
- function applySgr(params) {
70
- flush();
71
- let i = 0;
72
- while (i < params.length) {
73
- const p = params[i];
74
- if (p === 0) {
75
- fg = null;
76
- bg = null;
77
- bold = false;
78
- italic = false;
79
- underline = false;
80
- }
81
- else if (p === 1) {
82
- bold = true;
83
- }
84
- else if (p === 22) {
85
- bold = false;
86
- }
87
- else if (p === 3) {
88
- italic = true;
89
- }
90
- else if (p === 23) {
91
- italic = false;
92
- }
93
- else if (p === 4) {
94
- underline = true;
95
- }
96
- else if (p === 24) {
97
- underline = false;
98
- }
99
- else if (p >= 30 && p <= 37) {
100
- fg = ANSI16[p - 30];
101
- }
102
- else if (p === 38) {
103
- if (params[i + 1] === 2 && i + 4 < params.length) {
104
- fg = `rgb(${params[i + 2]},${params[i + 3]},${params[i + 4]})`;
105
- i += 4;
106
- }
107
- else if (params[i + 1] === 5 && i + 2 < params.length) {
108
- fg = color256(params[i + 2]);
109
- i += 2;
110
- }
111
- }
112
- else if (p === 39) {
113
- fg = null;
114
- }
115
- else if (p >= 40 && p <= 47) {
116
- bg = ANSI16[p - 40];
117
- }
118
- else if (p === 48) {
119
- if (params[i + 1] === 2 && i + 4 < params.length) {
120
- bg = `rgb(${params[i + 2]},${params[i + 3]},${params[i + 4]})`;
121
- i += 4;
122
- }
123
- else if (params[i + 1] === 5 && i + 2 < params.length) {
124
- bg = color256(params[i + 2]);
125
- i += 2;
126
- }
127
- }
128
- else if (p === 49) {
129
- bg = null;
130
- }
131
- else if (p >= 90 && p <= 97) {
132
- fg = ANSI16[p - 82];
133
- }
134
- else if (p >= 100 && p <= 107) {
135
- bg = ANSI16[p - 92];
136
- }
137
- i++;
138
- }
139
- bufStyle = buildStyle();
140
- }
141
- let i = 0;
142
- while (i < input.length) {
143
- const ch = input[i];
144
- if (ch === "\x1b" && i + 1 < input.length && input[i + 1] === "[") {
145
- // CSI sequence: find the final byte (0x40–0x7e)
146
- const seqStart = i + 2;
147
- let end = seqStart;
148
- while (end < input.length && (input.charCodeAt(end) < 0x40 || input.charCodeAt(end) > 0x7e)) {
149
- end++;
150
- }
151
- if (end < input.length) {
152
- if (input[end] === "m") {
153
- const seq = input.slice(seqStart, end);
154
- const params = seq ? seq.split(";").map((s) => parseInt(s, 10) || 0) : [0];
155
- applySgr(params);
156
- }
157
- // Non-SGR CSI (cursor movement etc.) — silently skip
158
- i = end + 1;
159
- }
160
- else {
161
- // Incomplete sequence at end of input — skip escape char
162
- i++;
163
- }
164
- }
165
- else if (ch === "\r") {
166
- i++;
167
- }
168
- else {
169
- // Regular character — append, flushing if style changed
170
- const style = buildStyle();
171
- if (style !== bufStyle) {
172
- flush();
173
- bufStyle = style;
174
- }
175
- buf += ch;
176
- i++;
177
- }
178
- }
179
- flush();
180
- return result.join("");
181
- }
@@ -1,2 +0,0 @@
1
- /** Load ghostty-web lazily to avoid SSR issues in host apps. */
2
- export declare function loadGhostty(): Promise<typeof import("ghostty-web")>;
@@ -1,11 +0,0 @@
1
- let ghosttyReady = null;
2
- /** Load ghostty-web lazily to avoid SSR issues in host apps. */
3
- export function loadGhostty() {
4
- if (!ghosttyReady) {
5
- ghosttyReady = import("ghostty-web").then(async (mod) => {
6
- await mod.init();
7
- return mod;
8
- });
9
- }
10
- return ghosttyReady;
11
- }