import { useRef, useLayoutEffect } from "preact/hooks";
const CACHE_BUST = "spa";
function loadScript(src, { module = false } = {}) {
return new Promise((resolve, reject) => {
const el = document.createElement("script");
el.src = src;
if (module) el.type = "module";
el.async = false; el.onload = () => resolve();
el.onerror = () => reject(new Error(`failed to load ${src}`));
document.body.appendChild(el);
});
}
export function TerminalIsland({ session, peer = "" }) {
const rootRef = useRef(null);
const bootedRef = useRef(false);
const resizeObsRef = useRef(null);
useLayoutEffect(() => {
if (bootedRef.current) return; bootedRef.current = true;
window.MOBUX_SESSION = session;
window.MOBUX_PEER = peer || "";
window.MOBUX_DEV = false;
let renderer = "xterm";
try {
const s = localStorage.getItem("mobux:renderer");
if (s === "sterk" || s === "xterm") renderer = s;
} catch (_) {}
window.__mobuxRenderer = renderer;
const v = `?v=${CACHE_BUST}`;
const bundle = renderer === "sterk" ? "sterk.bundle.js" : "xterm.bundle.js";
if (renderer === "xterm") {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = `/static/vendor/xterm.css${v}`;
document.head.appendChild(link);
}
(async () => {
try {
await loadScript(`/static/vendor/${bundle}${v}`);
if (!window.MobuxMesh) {
await loadScript(`/static/mesh-client.js${v}`);
}
await loadScript(`/static/host-picker.js${v}`);
await loadScript(`/static/terminal.js${v}`, { module: true });
if (!window.__mobuxChime) {
await loadScript(`/static/chime.js${v}`);
}
} catch (e) {
const q = rootRef.current?.querySelector("#quote");
if (q) q.textContent = `Terminal failed to load: ${e.message}`;
return;
}
const kick = () => window.dispatchEvent(new Event("resize"));
requestAnimationFrame(() => requestAnimationFrame(kick));
const host = rootRef.current?.querySelector("#terminal");
if (host && "ResizeObserver" in window) {
let last = 0;
const ro = new ResizeObserver(() => {
const h = host.clientHeight;
if (h && h !== last) {
last = h;
kick();
}
});
ro.observe(host);
resizeObsRef.current = ro;
}
})();
return () => {
resizeObsRef.current?.disconnect();
resizeObsRef.current = null;
};
}, []);
return (
<div ref={rootRef} class="term-body-spa">
<div id="terminal" />
<div id="reader" class="hidden" />
<div id="loadquote">
<q id="quote" />
<br />
<cite id="qauthor" />
</div>
<div id="touchOverlay" />
<div id="paneIndicator" />
<div id="cmdOverlayBg" />
<div id="cmdPickList">
<div class="cmd-header">
<h3>tmux</h3>
<button class="cmd-close" id="cmdCloseBtn" aria-label="Close">
Close
</button>
</div>
<button class="cmd-item" data-cmd="new-window">
New window
</button>
<button class="cmd-item" data-cmd="kill-window">
Close window
</button>
<div class="cmd-separator" />
<button class="cmd-item" data-cmd="split-h">
Split horizontal
</button>
<button class="cmd-item" data-cmd="split-v">
Split vertical
</button>
<button class="cmd-item" data-cmd="kill-pane">
Close pane
</button>
<div class="cmd-separator" />
<button class="cmd-item" data-cmd="next-window">
Next window
</button>
<button class="cmd-item" data-cmd="prev-window">
Previous window
</button>
<button class="cmd-item" data-cmd="next-pane">
Next pane
</button>
<button class="cmd-item" data-cmd="prev-pane">
Previous pane
</button>
<div class="cmd-separator" />
<button class="cmd-item" data-cmd="zoom-pane">
Zoom pane
</button>
</div>
<div id="inputBar" class="input-bar hidden">
<div id="inputRibbon" class="input-ribbon">
<button id="viewToggleBtn" title="Toggle reader/terminal view">
📖
</button>
<button id="uploadBtn" title="Attach file">
📎
</button>
<button id="micBtn" title="Dictate (speech to text)">
🎤
</button>
<button id="settingsBtn" title="Settings">
⚙
</button>
<button data-key="\x7f">⌫</button>
<button data-key="\r">⏎</button>
<button data-key="\x1b[D">←</button>
<button data-key="\x1b[C">→</button>
<button data-key="\x1b[A">↑</button>
<button data-key="\x1b[B">↓</button>
<button data-key="\x03">^C</button>
<button data-key="\x04">^D</button>
<button data-key="\x1b">Esc</button>
<button data-key="\t">Tab</button>
<button data-key="\x1a">^Z</button>
<button data-key="\x1b[3~">Del</button>
<button data-key="\x1b[H">Home</button>
<button data-key="\x1b[F">End</button>
<button data-key="\x15">^U</button>
<button data-key="\x0c">^L</button>
<button data-key="/clear\r">/clear</button>
<button data-key="/quit\r">/quit</button>
</div>
<div
id="inputToast"
class="input-toast hidden"
role="status"
aria-live="polite"
/>
<div class="input-row">
<input
id="inputText"
type="text"
enterkeyhint="send"
placeholder="Type here…"
autocomplete="off"
autocorrect="on"
autocapitalize="off"
spellcheck={false}
/>
<button id="inputSend" class="input-send" title="Send without Enter">
▶
</button>
</div>
</div>
</div>
);
}