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);
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}`);
await loadScript(`/static/mesh-client.js${v}`);
await loadScript(`/static/host-picker.js${v}`);
await loadScript(`/static/terminal.js${v}`, { module: true });
} catch (e) {
const q = rootRef.current?.querySelector('#quote');
if (q) q.textContent = `Terminal failed to load: ${e.message}`;
}
})();
}, []);
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>
);
}