@dkkoval/tui-preview 0.1.1 → 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.
@@ -1,8 +1,11 @@
1
1
  const DEFAULT_SIZE = { cols: 80, rows: 24 };
2
2
  const EMPTY_ENV = {};
3
3
  const EMPTY_ARGV = [];
4
- function isModernProps(props) {
5
- return "wasm" in props;
4
+ function normalizeMode(mode) {
5
+ if (mode === "static") {
6
+ return "static";
7
+ }
8
+ return "interactive";
6
9
  }
7
10
  function resolveArgvInput(argv) {
8
11
  const value = argv ?? EMPTY_ARGV;
@@ -11,80 +14,30 @@ function resolveArgvInput(argv) {
11
14
  }
12
15
  return () => value;
13
16
  }
14
- function resolveLegacySize(props) {
15
- const hasExplicitSize = props.cols !== undefined || props.rows !== undefined;
16
- if (!hasExplicitSize) {
17
- return { fit: "container", size: DEFAULT_SIZE };
18
- }
19
- return {
20
- fit: "none",
21
- size: {
22
- cols: Math.max(1, props.cols ?? DEFAULT_SIZE.cols),
23
- rows: Math.max(1, props.rows ?? DEFAULT_SIZE.rows),
24
- },
25
- };
26
- }
27
- let warnedLegacyProps = false;
28
- export function warnLegacyPropsOnce(usedLegacyProps) {
29
- if (!usedLegacyProps || warnedLegacyProps)
30
- return;
31
- warnedLegacyProps = true;
32
- console.warn("[tui-preview] Legacy props (`app`, `args`, `cols`, `rows`, `fontSize`, `fontFamily`, `theme`) are deprecated. " +
33
- "Use `wasm`, `argv`, `fit`, `size`, and `terminal`.");
34
- }
35
17
  export function resolveTuiPreviewProps(props) {
36
- if (isModernProps(props)) {
37
- const fit = props.fit ?? (props.size ? "none" : "container");
38
- const size = fit === "none"
39
- ? {
40
- cols: Math.max(1, props.size?.cols ?? DEFAULT_SIZE.cols),
41
- rows: Math.max(1, props.size?.rows ?? DEFAULT_SIZE.rows),
42
- }
43
- : {
44
- cols: Math.max(1, props.size?.cols ?? DEFAULT_SIZE.cols),
45
- rows: Math.max(1, props.size?.rows ?? DEFAULT_SIZE.rows),
46
- };
47
- const mode = (props.mode ?? "terminal");
48
- return {
49
- wasm: props.wasm,
50
- env: props.env ?? EMPTY_ENV,
51
- interactive: mode === "static" ? false : (props.interactive ?? true),
52
- mode,
53
- fit,
54
- size,
55
- terminal: {
56
- fontSize: props.terminal?.fontSize ?? 14,
57
- fontFamily: props.terminal?.fontFamily ?? "monospace",
58
- cursorBlink: props.terminal?.cursorBlink ?? true,
59
- convertEol: props.terminal?.convertEol ?? true,
60
- theme: props.terminal?.theme,
61
- },
62
- resolveArgv: resolveArgvInput(props.argv),
63
- onExit: props.onExit,
64
- onError: props.onError,
65
- onStatusChange: props.onStatusChange,
66
- usedLegacyProps: false,
67
- };
68
- }
69
- const { fit, size } = resolveLegacySize(props);
18
+ const fit = props.fit ?? (props.size ? "none" : "container");
19
+ const size = {
20
+ cols: Math.max(1, props.size?.cols ?? DEFAULT_SIZE.cols),
21
+ rows: Math.max(1, props.size?.rows ?? DEFAULT_SIZE.rows),
22
+ };
23
+ const mode = normalizeMode(props.mode);
70
24
  return {
71
- wasm: props.app,
25
+ wasm: props.wasm,
72
26
  env: props.env ?? EMPTY_ENV,
73
- interactive: props.interactive ?? true,
74
- mode: "terminal",
27
+ interactive: mode === "static" ? false : (props.interactive ?? true),
28
+ mode,
75
29
  fit,
76
30
  size,
77
31
  terminal: {
78
- fontSize: props.fontSize ?? 14,
79
- fontFamily: props.fontFamily ?? "monospace",
80
- cursorBlink: true,
81
- convertEol: true,
82
- theme: props.theme,
32
+ fontSize: props.terminal?.fontSize ?? 14,
33
+ fontFamily: props.terminal?.fontFamily ?? "monospace",
34
+ wasmUrl: props.terminal?.wasmUrl,
35
+ convertEol: props.terminal?.convertEol ?? true,
36
+ theme: props.terminal?.theme,
83
37
  },
84
- resolveArgv: resolveArgvInput(props.args),
38
+ resolveArgv: resolveArgvInput(props.argv),
85
39
  onExit: props.onExit,
86
40
  onError: props.onError,
87
41
  onStatusChange: props.onStatusChange,
88
- usedLegacyProps: true,
89
42
  };
90
43
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * WASI bridge — connects a wasm32-wasi TUI app to a ghostty-web terminal.
2
+ * WASI bridge — connects a wasm32-wasi TUI app to a terminal surface.
3
3
  *
4
4
  * Implements a minimal WASI preview1 surface sufficient for interactive TUI apps:
5
5
  * - fd_write (stdout/stderr → terminal)
package/dist/core/wasi.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * WASI bridge — connects a wasm32-wasi TUI app to a ghostty-web terminal.
2
+ * WASI bridge — connects a wasm32-wasi TUI app to a terminal surface.
3
3
  *
4
4
  * Implements a minimal WASI preview1 surface sufficient for interactive TUI apps:
5
5
  * - fd_write (stdout/stderr → terminal)
@@ -12,6 +12,7 @@
12
12
  const WASI_ESUCCESS = 0;
13
13
  const WASI_EAGAIN = 6;
14
14
  const WASI_BADF = 8;
15
+ const WASI_EVENTTYPE_FD_READ = 1;
15
16
  const STDIN_FD = 0;
16
17
  const STDOUT_FD = 1;
17
18
  const STDERR_FD = 2;
@@ -115,7 +116,7 @@ export class WasiBridge {
115
116
  fd_read: (fd, iovsPtr, iovsLen, nreadPtr) => {
116
117
  if (fd !== STDIN_FD)
117
118
  return WASI_BADF;
118
- const chunk = this.inputQueue.shift();
119
+ const chunk = this.inputQueue[0];
119
120
  if (!chunk)
120
121
  return WASI_EAGAIN;
121
122
  const view = this.view();
@@ -128,6 +129,12 @@ export class WasiBridge {
128
129
  u8.set(chunk.subarray(nread, nread + toCopy), ptr);
129
130
  nread += toCopy;
130
131
  }
132
+ if (nread >= chunk.length) {
133
+ this.inputQueue.shift();
134
+ }
135
+ else if (nread > 0) {
136
+ this.inputQueue[0] = chunk.subarray(nread);
137
+ }
131
138
  view.setUint32(nreadPtr, nread, true);
132
139
  return WASI_ESUCCESS;
133
140
  },
@@ -137,7 +144,7 @@ export class WasiBridge {
137
144
  for (let i = 0; i < nsubscriptions; i++) {
138
145
  const subPtr = inPtr + i * 48;
139
146
  const type = view.getUint8(subPtr + 8);
140
- if (type === 0 && this.inputQueue.length > 0) {
147
+ if (type === WASI_EVENTTYPE_FD_READ && this.inputQueue.length > 0) {
141
148
  const evPtr = outPtr + nevents * 32;
142
149
  view.setBigUint64(evPtr, view.getBigUint64(subPtr, true), true);
143
150
  view.setUint16(evPtr + 8, 0, true);
@@ -199,7 +206,13 @@ export async function instantiateApp(source, bridge) {
199
206
  let module = moduleCache.get(key);
200
207
  if (!module) {
201
208
  const response = await fetch(source);
209
+ if (!response.ok) {
210
+ throw new Error(`Failed to load app wasm: ${response.status} ${response.statusText}`);
211
+ }
202
212
  const bytes = await response.arrayBuffer();
213
+ if (bytes.byteLength === 0) {
214
+ throw new Error("App wasm is empty.");
215
+ }
203
216
  module = await WebAssembly.compile(bytes);
204
217
  moduleCache.set(key, module);
205
218
  }
Binary file
package/dist/index.cjs CHANGED
@@ -1 +1,2 @@
1
- "use strict";var V=Object.defineProperty;var G=(e,t,n)=>t in e?V(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var R=(e,t,n)=>G(e,typeof t!="symbol"?t+"":t,n);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const T=require("react/jsx-runtime"),v=require("react");let k=null;function H(){return k||(k=Promise.resolve().then(()=>require("./ghostty-web-DkOZu5AZ.cjs")).then(async e=>(await e.init(),e))),k}const _={cols:80,rows:24},W={},$=[];function Y(e){return"wasm"in e}function C(e){const t=e??$;return typeof t=="function"?t:()=>t}function Z(e){return e.cols!==void 0||e.rows!==void 0?{fit:"none",size:{cols:Math.max(1,e.cols??_.cols),rows:Math.max(1,e.rows??_.rows)}}:{fit:"container",size:_}}let D=!1;function J(e){!e||D||(D=!0,console.warn("[tui-preview] Legacy props (`app`, `args`, `cols`, `rows`, `fontSize`, `fontFamily`, `theme`) are deprecated. Use `wasm`, `argv`, `fit`, `size`, and `terminal`."))}function K(e){var i,o,r,s,c,u,a,h,m;if(Y(e)){const y=e.fit??(e.size?"none":"container"),d=y==="none"?{cols:Math.max(1,((i=e.size)==null?void 0:i.cols)??_.cols),rows:Math.max(1,((o=e.size)==null?void 0:o.rows)??_.rows)}:{cols:Math.max(1,((r=e.size)==null?void 0:r.cols)??_.cols),rows:Math.max(1,((s=e.size)==null?void 0:s.rows)??_.rows)},E=e.mode??"terminal";return{wasm:e.wasm,env:e.env??W,interactive:E==="static"?!1:e.interactive??!0,mode:E,fit:y,size:d,terminal:{fontSize:((c=e.terminal)==null?void 0:c.fontSize)??14,fontFamily:((u=e.terminal)==null?void 0:u.fontFamily)??"monospace",cursorBlink:((a=e.terminal)==null?void 0:a.cursorBlink)??!0,convertEol:((h=e.terminal)==null?void 0:h.convertEol)??!0,theme:(m=e.terminal)==null?void 0:m.theme},resolveArgv:C(e.argv),onExit:e.onExit,onError:e.onError,onStatusChange:e.onStatusChange,usedLegacyProps:!1}}const{fit:t,size:n}=Z(e);return{wasm:e.app,env:e.env??W,interactive:e.interactive??!0,mode:"terminal",fit:t,size:n,terminal:{fontSize:e.fontSize??14,fontFamily:e.fontFamily??"monospace",cursorBlink:!0,convertEol:!0,theme:e.theme},resolveArgv:C(e.args),onExit:e.onExit,onError:e.onError,onStatusChange:e.onStatusChange,usedLegacyProps:!0}}const w=0,X=6,b=8,tt=0,L=1,et=2;class j{constructor(t){R(this,"inputQueue",[]);R(this,"memory");this.opts=t}pushInput(t){const n=typeof t=="string"?new TextEncoder().encode(t):t;this.inputQueue.push(n)}attachMemory(t){this.memory=t}view(){return new DataView(this.memory.buffer)}u8(){return new Uint8Array(this.memory.buffer)}get imports(){return{args_sizes_get:(t,n)=>{const{args:i}=this.opts,o=new TextEncoder,r=i.reduce((s,c)=>s+o.encode(c).length+1,0);return this.view().setUint32(t,i.length,!0),this.view().setUint32(n,r,!0),w},args_get:(t,n)=>{const i=new TextEncoder,o=this.u8(),r=this.view();let s=n;return this.opts.args.forEach((c,u)=>{const a=i.encode(c);o.set(a,s),o[s+a.length]=0,r.setUint32(t+u*4,s,!0),s+=a.length+1}),w},environ_sizes_get:(t,n)=>{const i=this.envEntries(),o=new TextEncoder,r=i.reduce((s,c)=>s+o.encode(c).length+1,0);return this.view().setUint32(t,i.length,!0),this.view().setUint32(n,r,!0),w},environ_get:(t,n)=>{const i=new TextEncoder,o=this.u8(),r=this.view();let s=n;return this.envEntries().forEach((c,u)=>{const a=i.encode(c);o.set(a,s),o[s+a.length]=0,r.setUint32(t+u*4,s,!0),s+=a.length+1}),w},fd_write:(t,n,i,o)=>{if(t!==L&&t!==et)return b;const r=this.view(),s=this.u8();let c=0;const u=[];for(let m=0;m<i;m++){const y=r.getUint32(n+m*8,!0),d=r.getUint32(n+m*8+4,!0);u.push(s.slice(y,y+d)),c+=d}const a=new Uint8Array(c);let h=0;for(const m of u)a.set(m,h),h+=m.length;return t===L?this.opts.stdout(a):this.opts.stderr(a),r.setUint32(o,c,!0),w},fd_read:(t,n,i,o)=>{if(t!==tt)return b;const r=this.inputQueue.shift();if(!r)return X;const s=this.view(),c=this.u8();let u=0;for(let a=0;a<i&&u<r.length;a++){const h=s.getUint32(n+a*8,!0),m=s.getUint32(n+a*8+4,!0),y=Math.min(m,r.length-u);c.set(r.subarray(u,u+y),h),u+=y}return s.setUint32(o,u,!0),w},poll_oneoff:(t,n,i,o)=>{const r=this.view();let s=0;for(let c=0;c<i;c++){const u=t+c*48,a=r.getUint8(u+8);if(a===0&&this.inputQueue.length>0){const h=n+s*32;r.setBigUint64(h,r.getBigUint64(u,!0),!0),r.setUint16(h+8,0,!0),r.setUint8(h+10,a),s++}}return r.setUint32(o,s,!0),w},proc_exit:t=>{throw this.opts.onExit(t),new p(t)},random_get:(t,n)=>(crypto.getRandomValues(new Uint8Array(this.memory.buffer,t,n)),w),fd_close:()=>w,fd_seek:()=>w,fd_fdstat_get:(t,n)=>(this.view().setUint8(n,t<=2?2:0),w),fd_prestat_get:()=>b,fd_prestat_dir_name:()=>b,path_open:()=>b,sched_yield:()=>w,clock_time_get:(t,n,i)=>{const o=BigInt(Date.now())*1000000n;return this.view().setBigUint64(i,o,!0),w}}}envEntries(){const t={TERM:"xterm-256color",COLORTERM:"truecolor",...this.opts.env};return Object.entries(t).map(([n,i])=>`${n}=${i}`)}}class p extends Error{constructor(t){super(`WASI exit: ${t}`),this.code=t}}const I=new Map;async function N(e,t){const n=e.toString();let i=I.get(n);if(!i){const u=await(await fetch(e)).arrayBuffer();i=await WebAssembly.compile(u),I.set(n,i)}const o={wasi_snapshot_preview1:t.imports},r=await WebAssembly.instantiate(i,o);t.attachMemory(r.exports.memory);const s=r.exports._start;if(!s)throw new Error("WASM module has no _start export");return{run:async()=>{try{s()}catch(c){if(!(c instanceof p))throw c}}}}function nt(e){var y;const t=v.useMemo(()=>K(e),[e]),n=v.useRef(null),i=v.useRef(null),o=v.useRef(null),[r,s]=v.useState("loading"),[c,u]=v.useState(""),a=v.useRef(null),[h,m]=v.useState(t.size);return v.useEffect(()=>{J(t.usedLegacyProps)},[t.usedLegacyProps]),v.useEffect(()=>{m(t.size)},[t.fit,t.size.cols,t.size.rows]),v.useEffect(()=>{if(t.fit!=="container"||!n.current)return;const d=new ResizeObserver(([E])=>{var z,A;const{width:x,height:S}=E.contentRect;if(x>0&&S>0){const f=((z=a.current)==null?void 0:z.w)??t.terminal.fontSize*.6,l=((A=a.current)==null?void 0:A.h)??t.terminal.fontSize*1.2;m({cols:Math.max(1,Math.floor(x/f)),rows:Math.max(1,Math.floor(S/l))})}});return d.observe(n.current),()=>d.disconnect()},[t.fit,t.terminal.fontSize]),v.useEffect(()=>{if(!h||!i.current)return;let d=!1;const E=i.current,x=h,S=f=>{var l;s(f),(l=t.onStatusChange)==null||l.call(t,f)},z=f=>{var l;S("error"),u(f instanceof Error?f.message:String(f)),(l=t.onError)==null||l.call(t,f)};S("loading"),u("");async function A(){try{const f=await H();if(d)return;E.innerHTML="";const l=new f.Terminal({cols:x.cols,rows:x.rows,fontSize:t.terminal.fontSize,fontFamily:t.terminal.fontFamily,theme:t.terminal.theme,disableStdin:!t.interactive,cursorBlink:t.terminal.cursorBlink,convertEol:t.terminal.convertEol});o.current=l,l.open(E),t.mode==="static"&&l.write("\x1B[?25l");let U=l.cols,M=l.rows;if(t.fit==="container"){const g=new f.FitAddon;l.loadAddon(g),g.fit(),U=l.cols,M=l.rows,n.current&&U>0&&M>0&&(a.current={w:n.current.clientWidth/U,h:n.current.clientHeight/M})}const q=t.resolveArgv({cols:U,rows:M}),B=new TextDecoder,F=new j({args:[t.wasm.toString(),...q],env:t.env,stdout:g=>l.write(B.decode(g)),stderr:g=>l.write(B.decode(g)),onExit:g=>{var P;d||(S("exited"),(P=t.onExit)==null||P.call(t,g))}});t.interactive&&l.onData(g=>F.pushInput(g));const Q=await N(t.wasm,F);if(d)return;S("running"),queueMicrotask(()=>{d||Q.run().catch(g=>{d||z(g)})})}catch(f){d||z(f)}}return A(),()=>{var f;d=!0,(f=o.current)==null||f.dispose(),o.current=null}},[t.mode,h,t.wasm,t.resolveArgv,t.env,t.fit,t.interactive,t.onExit,t.onError,t.onStatusChange,t.terminal.fontSize,t.terminal.fontFamily,t.terminal.theme,t.terminal.cursorBlink,t.terminal.convertEol]),T.jsxs("div",{ref:n,className:e.className,style:{position:"relative",display:"inline-block",background:((y=t.terminal.theme)==null?void 0:y.background)??"#1a1b26",borderRadius:6,overflow:"hidden",...e.style},children:[T.jsx("div",{ref:i,style:{display:r==="error"?"none":void 0}}),r==="loading"&&T.jsx("div",{style:O,children:"Loading…"}),r==="error"&&T.jsxs("div",{style:{...O,color:"#f7768e"},children:["Error: ",c]})]})}const O={padding:"1rem",fontFamily:"monospace",fontSize:14,color:"#a9b1d6"};exports.TuiPreview=nt;exports.WasiBridge=j;exports.WasiExitError=p;exports.instantiateApp=N;
1
+ "use strict";var st=Object.defineProperty;var it=(e,t,n)=>t in e?st(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var B=(e,t,n)=>it(e,typeof t!="symbol"?t+"":t,n);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const L=require("react/jsx-runtime"),U=require("react"),F=16,M=80,Z="/ghostty-vt.wasm",ot=1,at=2,ct=4,W=8,O=16,lt=32,$=64,H=128,ht=["black","red","green","yellow","blue","magenta","cyan","white","brightBlack","brightRed","brightGreen","brightYellow","brightBlue","brightMagenta","brightCyan","brightWhite"],ut={background:"#1a1b26",foreground:"#a9b1d6",cursor:"#c0caf5",selectionBackground:"#33467c",selectionForeground:"#c0caf5",black:"#15161e",red:"#f7768e",green:"#9ece6a",yellow:"#e0af68",blue:"#7aa2f7",magenta:"#bb9af7",cyan:"#7dcfff",white:"#a9b1d6",brightBlack:"#414868",brightRed:"#f7768e",brightGreen:"#9ece6a",brightYellow:"#e0af68",brightBlue:"#7aa2f7",brightMagenta:"#bb9af7",brightCyan:"#7dcfff",brightWhite:"#c0caf5"};class ft{constructor(t,n){this.wasm=t,this.abi=n}createTerminal(t,n,r){const o=this.wasm.ghostty_wasm_alloc_u8_array(this.abi.terminalConfigSize);if(!o)throw new Error("Failed to allocate terminal config.");try{const i=new DataView(this.wasm.memory.buffer);let s=o;i.setUint32(s,1e4,!0),s+=4,i.setUint32(s,D(r.foreground),!0),s+=4,i.setUint32(s,D(r.background),!0),s+=4,i.setUint32(s,D(r.cursor),!0),s+=4;for(const c of ht)i.setUint32(s,D(r[c]),!0),s+=4;const a=this.wasm.ghostty_terminal_new_with_config(t,n,o);if(!a)throw new Error("Failed to create libghostty terminal.");return new wt(this.wasm,a,t,n,this.abi.cellSize)}finally{this.wasm.ghostty_wasm_free_u8_array(o,this.abi.terminalConfigSize)}}}class wt{constructor(t,n,r,o,i){B(this,"viewportPtr",0);B(this,"viewportLen",0);this.wasm=t,this.handle=n,this.cols=r,this.rows=o,this.cellSize=i}write(t){const n=typeof t=="string"?new TextEncoder().encode(t):t;if(n.length===0)return;const r=this.wasm.ghostty_wasm_alloc_u8_array(n.length);if(!r)throw new Error("Failed to allocate libghostty write buffer.");try{new Uint8Array(this.wasm.memory.buffer).set(n,r),this.wasm.ghostty_terminal_write(this.handle,r,n.length)}finally{this.wasm.ghostty_wasm_free_u8_array(r,n.length)}}resize(t,n){t===this.cols&&n===this.rows||(this.cols=t,this.rows=n,this.wasm.ghostty_terminal_resize(this.handle,t,n),this.releaseViewport())}hasResponse(){return this.wasm.ghostty_terminal_has_response(this.handle)}isDirty(){return this.wasm.ghostty_render_state_update(this.handle)!==0}readResponse(t=4096){const n=this.wasm.ghostty_wasm_alloc_u8_array(t);if(!n)throw new Error("Failed to allocate libghostty response buffer.");try{const r=this.wasm.ghostty_terminal_read_response(this.handle,n,t);if(r<=0)return null;const o=new Uint8Array(this.wasm.memory.buffer,n,r);return new TextDecoder().decode(o)}finally{this.wasm.ghostty_wasm_free_u8_array(n,t)}}getViewportData(){const t=this.wasm.ghostty_render_state_get_cols(this.handle),n=this.wasm.ghostty_render_state_get_rows(this.handle);this.cols=t,this.rows=n;const r=Math.max(1,t*n*this.cellSize);if((r>this.viewportLen||this.viewportPtr===0)&&(this.releaseViewport(),this.viewportPtr=this.wasm.ghostty_wasm_alloc_u8_array(r),this.viewportLen=r,!this.viewportPtr))throw new Error("Failed to allocate libghostty viewport buffer.");const o=this.wasm.ghostty_render_state_get_viewport(this.handle,this.viewportPtr,this.viewportLen),i=new Uint8Array(o);if(o>0){const s=new Uint8Array(this.wasm.memory.buffer,this.viewportPtr,o);i.set(s)}return this.wasm.ghostty_render_state_mark_clean(this.handle),{cols:t,rows:n,buffer:i}}dispose(){this.releaseViewport(),this.wasm.ghostty_terminal_free(this.handle)}releaseViewport(){this.viewportPtr!==0&&(this.wasm.ghostty_wasm_free_u8_array(this.viewportPtr,this.viewportLen),this.viewportPtr=0,this.viewportLen=0)}}class mt{constructor(t,n,r,o,i,s){B(this,"ctx");B(this,"dpr");B(this,"metrics");this.canvas=t,this.cols=n,this.rows=r,this.fontSize=o,this.fontFamily=i,this.theme=s;const a=t.getContext("2d");if(!a)throw new Error("Failed to create 2D canvas context.");this.ctx=a,this.dpr=window.devicePixelRatio||1,this.metrics=this.measureFont(),this.resizeCanvas(n,r)}get cellSize(){return{w:this.metrics.width,h:this.metrics.height}}render(t){(t.cols!==this.cols||t.rows!==this.rows)&&(this.cols=t.cols,this.rows=t.rows,this.resizeCanvas(t.cols,t.rows));const{cols:n,rows:r,buffer:o}=t,i=new DataView(o.buffer,o.byteOffset,o.byteLength),s=this.ctx,a=this.metrics.width,c=this.metrics.height;s.fillStyle=this.theme.background,s.fillRect(0,0,n*a,r*c);for(let l=0;l<r;l++)for(let u=0;u<n;u++){const h=(l*n+u)*F,f=i.getUint8(h+11);if(f===0)continue;const w=i.getUint8(h+10),b=R(i,h+4),y=R(i,h+7),_=x(w,O),g=x(w,W)?P(b):this.theme.foreground,d=x(w,$)?P(y):this.theme.background;(_||x(w,$))&&(s.fillStyle=_?g:d,s.fillRect(u*a,l*c,f*a,c))}for(let l=0;l<r;l++)for(let u=0;u<n;u++){const h=(l*n+u)*F,f=i.getUint8(h+11);if(f===0)continue;const w=i.getUint8(h+10);if(x(w,lt))continue;const b=i.getUint32(h,!0);if(b===0)continue;const y=x(w,O),_=R(i,h+4),g=R(i,h+7),d=x(w,W)?P(_):this.theme.foreground,m=x(w,$)?P(g):this.theme.background;s.fillStyle=y?m:d;let p="";x(w,at)&&(p+="italic "),x(w,ot)&&(p+="bold "),s.font=`${p}${this.fontSize}px ${this.fontFamily}`,x(w,H)&&(s.globalAlpha=.5);const E=bt(b);if(s.fillText(E,u*a,l*c+this.metrics.baseline),x(w,H)&&(s.globalAlpha=1),x(w,ct)){const v=l*c+this.metrics.baseline+2;s.strokeStyle=s.fillStyle,s.lineWidth=1,s.beginPath(),s.moveTo(u*a,v),s.lineTo(u*a+f*a,v),s.stroke()}}}dispose(){this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height)}measureFont(){this.ctx.font=`${this.fontSize}px ${this.fontFamily}`;const t=this.ctx.measureText("M"),n=Math.ceil(t.width),r=t.actualBoundingBoxAscent||this.fontSize*.8,o=t.actualBoundingBoxDescent||this.fontSize*.2;return{width:n,height:Math.ceil(r+o)+2,baseline:Math.ceil(r)+1}}resizeCanvas(t,n){const r=t*this.metrics.width,o=n*this.metrics.height;this.canvas.width=Math.max(1,Math.floor(r*this.dpr)),this.canvas.height=Math.max(1,Math.floor(o*this.dpr)),this.canvas.style.width=`${r}px`,this.canvas.style.height=`${o}px`,this.ctx.setTransform(this.dpr,0,0,this.dpr,0,0),this.ctx.textBaseline="alphabetic",this.ctx.textAlign="left"}}function J(e,t){const r=document.createElement("canvas").getContext("2d");if(!r)return{w:Math.ceil(e*.6),h:Math.ceil(e*1.2)};r.font=`${e}px ${t}`;const o=r.measureText("M"),i=Math.ceil(o.width),s=o.actualBoundingBoxAscent||e*.8,a=o.actualBoundingBoxDescent||e*.2;return{w:i,h:Math.ceil(s+a)+2}}const K=new Map;function dt(e=Z){const t=e.toString(),n=K.get(t);if(n)return n;const r=(async()=>{const o=await fetch(e);if(!o.ok)throw new Error(`Failed to load libghostty wasm: ${o.status} ${o.statusText}`);const i=await o.arrayBuffer();if(i.byteLength===0)throw new Error("libghostty wasm is empty.");const s=await WebAssembly.compile(i),c=(await WebAssembly.instantiate(s,{env:{log:()=>{}}})).exports;if(!c.memory||!c.ghostty_terminal_new||!c.ghostty_render_state_get_viewport)throw new Error("Invalid libghostty wasm exports.");return yt(c),new ft(c,{cellSize:F,terminalConfigSize:M})})();return K.set(t,r),r}async function gt(e){var _,g;const t=await dt(e.wasmUrl??Z),n={...ut,...e.theme};let r,o;const i=J(e.fontSize,e.fontFamily);e.widthPx!=null&&e.heightPx!=null?(r=Math.max(1,Math.floor(e.widthPx/i.w)),o=Math.max(1,Math.floor(e.heightPx/i.h))):(r=e.cols??80,o=e.rows??24),e.container.innerHTML="";const s=document.createElement("canvas");s.style.display="block",s.style.outline="none",s.tabIndex=e.interactive?0:-1,e.container.appendChild(s);const a=t.createTerminal(r,o,n),c=new mt(s,r,o,e.fontSize,e.fontFamily,n);e.showCursor||a.write("\x1B[?25l"),c.render(a.getViewportData());const l=((_=window.requestAnimationFrame)==null?void 0:_.bind(window))??(d=>window.setTimeout(d,16)),u=((g=window.cancelAnimationFrame)==null?void 0:g.bind(window))??window.clearTimeout.bind(window);let h=null,f=!1;const w=()=>{h=null,!(f||!a.isDirty())&&c.render(a.getViewportData())},b=()=>{h!==null||f||(h=l(w))},y=e.interactive?pt(s,d=>{var m;return(m=e.onInput)==null?void 0:m.call(e,d)}):()=>{};return{cols:r,rows:o,cellSize:c.cellSize,write(d){if(f)return;const m=e.convertEol?_t(d):d;a.write(m),b()},drainResponses(){if(f)return[];const d=[];for(;a.hasResponse();){const m=a.readResponse();if(!m)break;d.push(m)}return d},dispose(){f=!0,h!==null&&(u(h),h=null),y(),c.dispose(),a.dispose(),s.parentElement===e.container&&e.container.removeChild(s)}}}function yt(e){const t=e.ghostty_wasm_alloc_u8_array(M);if(!t)throw new Error("Failed to allocate ABI probe config buffer.");let n=0,r=0,o=0,i=0;const s=1122867,a=4478310,[c,l,u]=j(s),[h,f,w]=j(a);try{new Uint8Array(e.memory.buffer,t,M).fill(0);const b=new DataView(e.memory.buffer,t,M);if(b.setUint32(0,16,!0),b.setUint32(4,s,!0),b.setUint32(8,a,!0),b.setUint32(12,7833753,!0),n=e.ghostty_terminal_new_with_config(2,1,t),!n)throw new Error("Failed to create ABI probe terminal.");const y=new TextEncoder().encode("\x1B[7mX");if(o=y.length,r=e.ghostty_wasm_alloc_u8_array(o),!r)throw new Error("Failed to allocate ABI probe write buffer.");if(new Uint8Array(e.memory.buffer,r,o).set(y),e.ghostty_terminal_write(n,r,o),i=e.ghostty_wasm_alloc_u8_array(F),!i)throw new Error("Failed to allocate ABI probe viewport buffer.");const _=e.ghostty_render_state_get_viewport(n,i,F);if(_!==F)throw new Error(`Incompatible libghostty ABI: expected cell size ${F}, got ${_}.`);const g=new DataView(e.memory.buffer,i,F),d=g.getUint8(10),m=x(d,W),p=x(d,$),E=g.getUint8(4)===c&&g.getUint8(5)===l&&g.getUint8(6)===u,v=g.getUint8(7)===h&&g.getUint8(8)===f&&g.getUint8(9)===w;if(!m||!p||!E||!v)throw new Error("Incompatible libghostty ABI: terminal config layout mismatch.")}finally{i&&e.ghostty_wasm_free_u8_array(i,F),r&&o>0&&e.ghostty_wasm_free_u8_array(r,o),n&&e.ghostty_terminal_free(n),e.ghostty_wasm_free_u8_array(t,M)}}function _t(e){return e.replace(/\r?\n/g,`\r
2
+ `)}function D(e){if(e.startsWith("#")){let i=e.slice(1);i.length===3&&(i=`${i[0]}${i[0]}${i[1]}${i[1]}${i[2]}${i[2]}`);const s=Number.parseInt(i,16);return Number.isNaN(s)?0:s}const t=e.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);if(!t)return 0;const n=Number.parseInt(t[1],10),r=Number.parseInt(t[2],10),o=Number.parseInt(t[3],10);return n<<16|r<<8|o}function j(e){return[e>>16&255,e>>8&255,e&255]}function R(e,t){return{r:e.getUint8(t),g:e.getUint8(t+1),b:e.getUint8(t+2)}}function x(e,t){return(e&t)!==0}function P(e){return`rgb(${e.r}, ${e.g}, ${e.b})`}function bt(e){try{return String.fromCodePoint(e)}catch{return" "}}function pt(e,t){const n=()=>e.focus(),r=i=>{var a;const s=(a=i.clipboardData)==null?void 0:a.getData("text");s&&(i.preventDefault(),t(s))},o=i=>{const s=Et(i);s&&(i.preventDefault(),t(s))};return e.addEventListener("mousedown",n),e.addEventListener("paste",r),e.addEventListener("keydown",o),()=>{e.removeEventListener("mousedown",n),e.removeEventListener("paste",r),e.removeEventListener("keydown",o)}}function Et(e){if(e.isComposing||e.metaKey)return null;let t=null;switch(e.key){case"Enter":t="\r";break;case"Backspace":t="";break;case"Tab":t=e.shiftKey?"\x1B[Z":" ";break;case"Escape":t="\x1B";break;case"ArrowUp":t="\x1B[A";break;case"ArrowDown":t="\x1B[B";break;case"ArrowRight":t="\x1B[C";break;case"ArrowLeft":t="\x1B[D";break;case"Home":t="\x1B[H";break;case"End":t="\x1B[F";break;case"Delete":t="\x1B[3~";break;case"PageUp":t="\x1B[5~";break;case"PageDown":t="\x1B[6~";break}return!t&&e.ctrlKey&&(t=vt(e.key)),!t&&e.key.length===1&&!e.ctrlKey&&(t=e.key),t?e.altKey&&!t.startsWith("\x1B")?`\x1B${t}`:t:null}function vt(e){if(e.length!==1)return null;const t=e.toUpperCase();if(t>="A"&&t<="Z")return String.fromCharCode(t.charCodeAt(0)-64);switch(e){case"@":case" ":return"\0";case"[":return"\x1B";case"\\":return"";case"]":return"";case"^":return"";case"_":return"";default:return null}}const Y={cols:80,rows:24},xt={},At=[];function St(e){return e==="static"?"static":"interactive"}function Ut(e){const t=e??At;return typeof t=="function"?t:()=>t}function Ft(e){var o,i,s,a,c,l,u;const t=e.fit??(e.size?"none":"container"),n={cols:Math.max(1,((o=e.size)==null?void 0:o.cols)??Y.cols),rows:Math.max(1,((i=e.size)==null?void 0:i.rows)??Y.rows)},r=St(e.mode);return{wasm:e.wasm,env:e.env??xt,interactive:r==="static"?!1:e.interactive??!0,mode:r,fit:t,size:n,terminal:{fontSize:((s=e.terminal)==null?void 0:s.fontSize)??14,fontFamily:((a=e.terminal)==null?void 0:a.fontFamily)??"monospace",wasmUrl:(c=e.terminal)==null?void 0:c.wasmUrl,convertEol:((l=e.terminal)==null?void 0:l.convertEol)??!0,theme:(u=e.terminal)==null?void 0:u.theme},resolveArgv:Ut(e.argv),onExit:e.onExit,onError:e.onError,onStatusChange:e.onStatusChange}}const A=0,Bt=6,C=8,Tt=1,Ct=0,q=1,Mt=2;class tt{constructor(t){B(this,"inputQueue",[]);B(this,"memory");this.opts=t}pushInput(t){const n=typeof t=="string"?new TextEncoder().encode(t):t;this.inputQueue.push(n)}attachMemory(t){this.memory=t}view(){return new DataView(this.memory.buffer)}u8(){return new Uint8Array(this.memory.buffer)}get imports(){return{args_sizes_get:(t,n)=>{const{args:r}=this.opts,o=new TextEncoder,i=r.reduce((s,a)=>s+o.encode(a).length+1,0);return this.view().setUint32(t,r.length,!0),this.view().setUint32(n,i,!0),A},args_get:(t,n)=>{const r=new TextEncoder,o=this.u8(),i=this.view();let s=n;return this.opts.args.forEach((a,c)=>{const l=r.encode(a);o.set(l,s),o[s+l.length]=0,i.setUint32(t+c*4,s,!0),s+=l.length+1}),A},environ_sizes_get:(t,n)=>{const r=this.envEntries(),o=new TextEncoder,i=r.reduce((s,a)=>s+o.encode(a).length+1,0);return this.view().setUint32(t,r.length,!0),this.view().setUint32(n,i,!0),A},environ_get:(t,n)=>{const r=new TextEncoder,o=this.u8(),i=this.view();let s=n;return this.envEntries().forEach((a,c)=>{const l=r.encode(a);o.set(l,s),o[s+l.length]=0,i.setUint32(t+c*4,s,!0),s+=l.length+1}),A},fd_write:(t,n,r,o)=>{if(t!==q&&t!==Mt)return C;const i=this.view(),s=this.u8();let a=0;const c=[];for(let h=0;h<r;h++){const f=i.getUint32(n+h*8,!0),w=i.getUint32(n+h*8+4,!0);c.push(s.slice(f,f+w)),a+=w}const l=new Uint8Array(a);let u=0;for(const h of c)l.set(h,u),u+=h.length;return t===q?this.opts.stdout(l):this.opts.stderr(l),i.setUint32(o,a,!0),A},fd_read:(t,n,r,o)=>{if(t!==Ct)return C;const i=this.inputQueue[0];if(!i)return Bt;const s=this.view(),a=this.u8();let c=0;for(let l=0;l<r&&c<i.length;l++){const u=s.getUint32(n+l*8,!0),h=s.getUint32(n+l*8+4,!0),f=Math.min(h,i.length-c);a.set(i.subarray(c,c+f),u),c+=f}return c>=i.length?this.inputQueue.shift():c>0&&(this.inputQueue[0]=i.subarray(c)),s.setUint32(o,c,!0),A},poll_oneoff:(t,n,r,o)=>{const i=this.view();let s=0;for(let a=0;a<r;a++){const c=t+a*48,l=i.getUint8(c+8);if(l===Tt&&this.inputQueue.length>0){const u=n+s*32;i.setBigUint64(u,i.getBigUint64(c,!0),!0),i.setUint16(u+8,0,!0),i.setUint8(u+10,l),s++}}return i.setUint32(o,s,!0),A},proc_exit:t=>{throw this.opts.onExit(t),new N(t)},random_get:(t,n)=>(crypto.getRandomValues(new Uint8Array(this.memory.buffer,t,n)),A),fd_close:()=>A,fd_seek:()=>A,fd_fdstat_get:(t,n)=>(this.view().setUint8(n,t<=2?2:0),A),fd_prestat_get:()=>C,fd_prestat_dir_name:()=>C,path_open:()=>C,sched_yield:()=>A,clock_time_get:(t,n,r)=>{const o=BigInt(Date.now())*1000000n;return this.view().setBigUint64(r,o,!0),A}}}envEntries(){const t={TERM:"xterm-256color",COLORTERM:"truecolor",...this.opts.env};return Object.entries(t).map(([n,r])=>`${n}=${r}`)}}class N extends Error{constructor(t){super(`WASI exit: ${t}`),this.code=t}}const Q=new Map;async function et(e,t){const n=e.toString();let r=Q.get(n);if(!r){const a=await fetch(e);if(!a.ok)throw new Error(`Failed to load app wasm: ${a.status} ${a.statusText}`);const c=await a.arrayBuffer();if(c.byteLength===0)throw new Error("App wasm is empty.");r=await WebAssembly.compile(c),Q.set(n,r)}const o={wasi_snapshot_preview1:t.imports},i=await WebAssembly.instantiate(r,o);t.attachMemory(i.exports.memory);const s=i.exports._start;if(!s)throw new Error("WASM module has no _start export");return{run:async()=>{try{s()}catch(a){if(!(a instanceof N))throw a}}}}function kt(e){var h;const t=U.useMemo(()=>Ft(e),[e]),n=U.useRef(null),r=U.useRef(null),[o,i]=U.useState("loading"),[s,a]=U.useState(""),c=U.useRef(null),[l,u]=U.useState(t.fit==="container"?null:t.size);return U.useEffect(()=>{u(t.fit==="container"?null:t.size)},[t.fit,t.size.cols,t.size.rows]),U.useEffect(()=>{if(t.fit!=="container"||!n.current)return;const f=n.current,w=J(t.terminal.fontSize,t.terminal.fontFamily),b=(g,d)=>{var k,I;const m=((k=c.current)==null?void 0:k.w)??w.w,p=((I=c.current)==null?void 0:I.h)??w.h,E=Math.max(1,Math.floor(g/m)),v=Math.max(1,Math.floor(d/p));u(T=>T&&T.cols===E&&T.rows===v?T:{cols:E,rows:v})},y=f.getBoundingClientRect();y.width>0&&y.height>0&&b(y.width,y.height);const _=new ResizeObserver(([g])=>{const{width:d,height:m}=g.contentRect;d>0&&m>0&&b(d,m)});return _.observe(f),()=>_.disconnect()},[t.fit,t.terminal.fontSize,t.terminal.fontFamily]),U.useEffect(()=>{if(!l||!r.current)return;let f=!1,w=null;const b=r.current,y=l,_=m=>{var p;i(m),(p=t.onStatusChange)==null||p.call(t,m)},g=m=>{var p;_("error"),a(m instanceof Error?m.message:String(m)),(p=t.onError)==null||p.call(t,m)};_("loading"),a("");async function d(){try{let m=y.cols,p=y.rows,E=null;const v=await gt({container:b,cols:y.cols,rows:y.rows,fontSize:t.terminal.fontSize,fontFamily:t.terminal.fontFamily,theme:t.terminal.theme,convertEol:t.terminal.convertEol,interactive:t.mode!=="static"&&t.interactive,showCursor:t.mode!=="static",wasmUrl:t.terminal.wasmUrl,onInput:S=>E==null?void 0:E.pushInput(S)});if(f){v.dispose();return}w=()=>v.dispose(),m=v.cols,p=v.rows,c.current=v.cellSize;const k=t.resolveArgv({cols:m,rows:p}),I=new TextDecoder,T=new TextDecoder,G=(S,z)=>{const V=z.decode(S,{stream:!0});V&&v.write(V);for(const rt of v.drainResponses())E==null||E.pushInput(rt)};E=new tt({args:[t.wasm.toString(),...k],env:t.env,stdout:S=>G(S,I),stderr:S=>G(S,T),onExit:S=>{var z;f||(_("exited"),(z=t.onExit)==null||z.call(t,S))}});const nt=await et(t.wasm,E);if(f)return;_("running"),queueMicrotask(()=>{f||nt.run().catch(S=>{f||g(S)})})}catch(m){f||g(m)}}return d(),()=>{f=!0,w==null||w(),w=null}},[t.mode,l,t.wasm,t.resolveArgv,t.env,t.fit,t.interactive,t.onExit,t.onError,t.onStatusChange,t.terminal.fontSize,t.terminal.fontFamily,t.terminal.theme,t.terminal.wasmUrl,t.terminal.convertEol]),L.jsxs("div",{ref:n,className:e.className,style:{position:"relative",display:t.fit==="container"?"block":"inline-block",background:((h=t.terminal.theme)==null?void 0:h.background)??"#1a1b26",borderRadius:6,overflow:"hidden",...e.style},children:[L.jsx("div",{ref:r,style:{display:o==="error"?"none":void 0}}),o==="loading"&&L.jsx("div",{style:X,children:"Loading…"}),o==="error"&&L.jsxs("div",{style:{...X,color:"#f7768e"},children:["Error: ",s]})]})}const X={padding:"1rem",fontFamily:"monospace",fontSize:14,color:"#a9b1d6"};exports.TuiPreview=kt;exports.WasiBridge=tt;exports.WasiExitError=N;exports.instantiateApp=et;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { TuiPreview } from "./TuiPreview.js";
2
- export type { GhosttyTheme, ResolvedTuiPreviewOptions, TuiArgv, TuiFitMode, TuiRenderMode, TuiPreviewCommonProps, TuiPreviewLegacyProps, TuiPreviewModernProps, TuiPreviewProps, TuiPreviewStatus, TuiRuntimeSize, TuiTerminalOptions, WasiOptions, } from "./types.js";
3
- export { WasiBridge, WasiExitError, instantiateApp } from "./wasi.js";
2
+ export type { GhosttyTheme, ResolvedTuiPreviewOptions, TuiArgv, TuiFitMode, TuiRenderMode, TuiPreviewCommonProps, TuiPreviewModernProps, TuiPreviewProps, TuiPreviewStatus, TuiRuntimeSize, TuiTerminalOptions, WasiOptions, } from "./types.js";
3
+ export { WasiBridge, WasiExitError, instantiateApp } from "./core/wasi.js";