1import { GhosttyModule, mountTerminal, type TerminalHandle } from "@clo/terminal";
2import wasmUrl from "@clo/terminal/ghostty-vt.wasm?url";
3import "@clo/terminal/terminal.css";
4import { createEffect, createMemo, createSignal, onCleanup, onMount, Show } from "solid-js";
5import { Portal } from "solid-js/web";
6import { reason } from "../api.ts";
7import { VMMenu } from "./VMMenu.tsx";
8import Keyboard from "lucide-solid/icons/keyboard";
9import RefreshCw from "lucide-solid/icons/refresh-cw";
10
11let core: Promise<GhosttyModule> | undefined;
12
13export default function VMSerial(props: { name: string; enabled: boolean; active: boolean; toolbar: HTMLDivElement }) {
14 const enabled = createMemo(() => props.enabled);
15 let host!: HTMLDivElement;
16 let socket: WebSocket | undefined;
17 let disposed = false;
18 const [handle, setHandle] = createSignal<TerminalHandle>();
19 const [state, setState] = createSignal("Loading…");
20 const [problem, setProblem] = createSignal("");
21 const [hasOutput, setHasOutput] = createSignal(false);
22 onMount(async () => {
23 try {
24 const module = await (core ??= GhosttyModule.fetch(wasmUrl));
25 if (disposed) return;
26 const mounted = mountTerminal(host, module, {
27 maxScrollbackBytes: 4 * 1024 * 1024,
28 onData: (bytes) => { if (socket?.readyState === WebSocket.OPEN) socket.send(new Uint8Array(bytes)); },
29 });
30 setHandle(mounted);
31 await mounted.ready;
32 } catch (failure) {
33 core = undefined;
34 if (!disposed) setProblem(`Unable to load the console. ${reason(failure)}`);
35 }
36 });
37 const connect = () => {
38 const previous = socket;
39 socket = undefined;
40 previous?.close();
41 if (!enabled() || !handle()) return;
42 setState("Connecting…");
43 setProblem("");
44 const url = new URL(`/api/vms/${encodeURIComponent(props.name)}/serial`, location.href);
45 url.protocol = location.protocol === "https:" ? "wss:" : "ws:";
46 const next = new WebSocket(url);
47 next.binaryType = "arraybuffer";
48 socket = next;
49 next.onopen = () => {
50 if (socket !== next || disposed) return;
51 setState("Connected");
52 };
53 next.onmessage = (event: MessageEvent<ArrayBuffer>) => {
54 if (socket === next && !disposed) {
55 handle()?.write(new Uint8Array(event.data));
56 if (event.data.byteLength) setHasOutput(true);
57 }
58 };
59 next.onclose = (event) => {
60 if (socket !== next || disposed) return;
61 setState("Disconnected");
62 setProblem(event.reason || "The console disconnected. Check that the VM is running, then reconnect.");
63 };
64 };
65 createEffect(connect);
66 createEffect(() => { if (props.active && state() === "Connected") handle()?.view.element.focus({ preventScroll: true }); });
67 onCleanup(() => { disposed = true; socket?.close(); handle()?.dispose(); });
68 return <div class="vm-serial" hidden={!props.active}>
69 <Portal mount={props.toolbar} ref={(element) => { element.className = "vm-serial-tools"; }}>
70 <Show when={props.active}>
71 <VMMenu label="Input">
72 <button disabled={!props.enabled || state() !== "Connected"} onClick={() => socket?.send(new Uint8Array([3]))}><Keyboard size={14} aria-hidden="true" />Ctrl+C</button>
73 </VMMenu>
74 <VMMenu label="View">
75 <button disabled={!props.enabled || !handle() || state() === "Connecting…"} onClick={connect}><RefreshCw size={14} aria-hidden="true" />Reconnect</button>
76 </VMMenu>
77 <span class="spacer" />
78 <span class="vm-console-status" role="status">{props.enabled ? state() : "Off"}</span>
79 </Show>
80 </Portal>
81 <Show when={problem()}><p class="error" role="alert">{problem()}</p></Show>
82 <div class="vm-serial-surface">
83 <div class="vm-serial-terminal" ref={host} inert={!props.enabled || state() !== "Connected"} aria-label={`${props.name} serial console`} />
84 <Show when={props.enabled && state() === "Connected" && !hasOutput()}><div class="vm-serial-empty">
85 <span>Press Enter to show the prompt</span>
86 <button class="button" onClick={() => {
87 socket?.send(new Uint8Array([13]));
88 handle()?.view.element.focus({ preventScroll: true });
89 }}>Show prompt</button>
90 </div></Show>
91 </div>
92 </div>;
93}