import { TerminalEngine } from "./terminal-engine.js";
import { createXtermRenderer } from "./renderer-xterm.js";
import { createSterkRenderer } from "./renderer-sterk.js";
import { createGestureRecognizer } from "./touch.js";
import { createInputBar } from "./input-bar.js";
import { createTopBar } from "./top-bar.js";
import { applyTheme, getStoredThemeId } from "./themes.js";
import {
navigateToUrl,
openExternal,
isExternalUrl,
installExternalLinkHandler,
} from "./external-link.js";
const quotes = [
["Simplicity is prerequisite for reliability.", "Edsger W. Dijkstra"],
[
"If debugging is the process of removing bugs, then programming must be the process of putting them in.",
"Edsger W. Dijkstra",
],
[
"The Analytical Engine weaves algebraical patterns just as the Jacquard loom weaves flowers and leaves.",
"Ada Lovelace",
],
[
"We can only see a short distance ahead, but we can see plenty there that needs to be done.",
"Alan Turing",
],
["Those who can imagine anything, can create the impossible.", "Alan Turing"],
[
"The most dangerous phrase in the language is: we’ve always done it this way.",
"Grace Hopper",
],
["The best way to predict the future is to invent it.", "Alan Kay"],
["Premature optimization is the root of all evil.", "Donald Knuth"],
["Talk is cheap. Show me the code.", "Linus Torvalds"],
[
"Controlling complexity is the essence of computer programming.",
"Brian Kernighan",
],
[
"Any sufficiently advanced technology is indistinguishable from magic.",
"Arthur C. Clarke",
],
["Information is the resolution of uncertainty.", "Claude Shannon"],
[
"Looking back, we were the luckiest people in the world; there was no choice but to be pioneers.",
"Margaret Hamilton",
],
[
"Any fool can write code that a computer can understand. Good programmers write code that humans can understand.",
"Martin Fowler",
],
["Truth can only be found in one place: the code.", "Robert C. Martin"],
[
"I'm not a great programmer; I'm just a good programmer with great habits.",
"Kent Beck",
],
["Duplication is far cheaper than the wrong abstraction.", "Sandi Metz"],
["It's harder to read code than to write it.", "Joel Spolsky"],
["I call it my billion-dollar mistake.", "Tony Hoare, on null"],
[
"Programmers know the value of everything and the cost of nothing.",
"Rich Hickey",
],
[
"Fancy algorithms are slow when n is small, and n is usually small.",
"Rob Pike",
],
[
"There are only two kinds of programming languages: the ones people complain about and the ones nobody uses.",
"Bjarne Stroustrup",
],
["Ruby is designed to make programmers happy.", "Yukihiro Matsumoto"],
[
"The three chief virtues of a programmer are: laziness, impatience, and hubris.",
"Larry Wall",
],
[
"If you're not failing every now and again, it's a sign you're not doing anything very innovative.",
"John Carmack",
],
];
const RENDERER_OPTIONS = {
fontFamily:
"'SF Mono', 'Cascadia Code', 'Consolas', 'Liberation Mono', monospace",
fontSize: 13,
scrollback: 10000,
altScreen: false,
};
export function createTerminal({
node = "",
session,
host,
renderer,
build = "",
viewToggle = null,
readToggle = null,
} = {}) {
const $ = (id) => host.querySelector(`#${id}`);
const nodeQuery = () => (node ? `?node=${encodeURIComponent(node)}` : "");
const termEl = $("terminal");
const overlay = $("touchOverlay");
const loadquote = $("loadquote");
const paneIndicator = $("paneIndicator");
const cmdPickList = $("cmdPickList");
const cmdOverlayBg = $("cmdOverlayBg");
const cmdCloseBtn = $("cmdCloseBtn");
let disposed = false;
const cleanups = [];
const on = (target, type, fn, opts) => {
target.addEventListener(type, fn, opts);
cleanups.push(() => target.removeEventListener(type, fn, opts));
};
const later = (fn, ms) => {
if (disposed) return;
const t = setTimeout(() => {
if (!disposed) fn();
}, ms);
cleanups.push(() => clearTimeout(t));
};
const every = (fn, ms) => {
if (disposed) return;
const t = setInterval(fn, ms);
cleanups.push(() => clearInterval(t));
};
{
const [text, author] = quotes[Math.floor(Math.random() * quotes.length)];
const quoteEl = $("quote");
const qauthorEl = $("qauthor");
if (quoteEl) quoteEl.textContent = text;
if (qauthorEl) qauthorEl.textContent = "— " + author;
}
window.__mobuxNavigateToUrl = navigateToUrl;
window.__mobuxOpenExternal = openExternal;
window.__mobuxIsExternalUrl = isExternalUrl;
installExternalLinkHandler();
const isMobile =
window.matchMedia("(pointer: coarse)").matches || window.innerWidth < 620;
const rendererImpl =
renderer === "sterk"
? createSterkRenderer(termEl, RENDERER_OPTIONS)
: createXtermRenderer(termEl, RENDERER_OPTIONS);
const core = new TerminalEngine({
session,
node,
host: termEl,
renderer: rendererImpl,
build,
});
applyTheme(getStoredThemeId(), { engine: core });
function onThemeChange() {
applyTheme(getStoredThemeId(), { engine: core });
}
on(window, "mobux:theme", onThemeChange);
on(window, "mobux:prefschange", (e) => {
if (e.detail?.key === "theme") onThemeChange();
});
const linkSub = core.onLink((uri) => openExternal(uri));
cleanups.push(() => linkSub?.dispose?.());
if ("ontouchstart" in window || navigator.maxTouchPoints > 0) {
overlay.style.pointerEvents = "auto";
}
function updatePaneUI() {
const { panes, activeIndex } = core;
if (panes.length <= 1) {
paneIndicator.textContent = panes.length === 1 ? panes[0].title : "";
} else {
const current = panes[activeIndex];
paneIndicator.textContent = `${current ? current.title : "?"} (${activeIndex + 1}/${panes.length})`;
}
}
on(core, "panes", () => {
if (disposed) return;
updatePaneUI();
});
function showCmdList() {
cmdPickList.classList.add("visible");
cmdOverlayBg.classList.add("visible");
overlay.style.pointerEvents = "none";
}
function hideCmdList() {
cmdPickList.classList.remove("visible");
cmdOverlayBg.classList.remove("visible");
if ("ontouchstart" in window || navigator.maxTouchPoints > 0) {
overlay.style.pointerEvents = "auto";
}
}
on(cmdPickList, "click", (e) => {
const cmdItem = e.target.closest("[data-cmd]");
if (cmdItem) {
core.runTmuxCmd(cmdItem.dataset.cmd);
hideCmdList();
return;
}
});
on(cmdCloseBtn, "click", hideCmdList);
on(cmdOverlayBg, "click", hideCmdList);
function scrollByPixels(dy) {
const lines = Math.round(dy / core.cellSize().height);
if (lines !== 0) core.scrollLines(lines);
}
function twoPullMove(pull, vh) {
if (pull > vh * 0.08) paneIndicator.textContent = "↻ Release to reload";
else if (pull > vh * 0.03)
paneIndicator.textContent = "↓ Pull to reload...";
}
function twoPullEnd(pull, vh) {
if (pull > vh * 0.08) location.reload(true);
else updatePaneUI();
}
const gestures = createGestureRecognizer(overlay, {
onScroll: scrollByPixels,
onReconnect: () => core.reconnect(),
getFontSize: () => core.getFontSize(),
onPinch(scale, startSize) {
const newSize = Math.round(Math.max(8, Math.min(32, startSize * scale)));
core.setFontSize(newSize);
},
onTwoPullMove: twoPullMove,
onTwoPullEnd: twoPullEnd,
onTap(x, y) {
const cell = core.cellSize();
const rect = termEl.getBoundingClientRect();
const col = Math.floor((x - rect.left) / cell.width);
const row = Math.floor((y - rect.top) / cell.height);
const buffer = core.getActiveBuffer();
const bufferRow = buffer.viewportY + row;
const line = buffer.getLine(bufferRow);
if (!line) return;
const text = line.translateToString(true);
const urlRe = /https?:\/\/[^\s)"'>]+/g;
let match;
while ((match = urlRe.exec(text)) !== null) {
if (col >= match.index && col < match.index + match[0].length) {
openExternal(match[0]);
return;
}
}
},
onDoubleTap() {
ensureInputBar().show();
},
onHSwipe: (dir) => core.switchWindow(dir),
onLongPress: showCmdList,
onSwipeUp: showCmdList,
});
let revealScheduled = false;
function scheduleReveal() {
if (disposed || revealScheduled) return;
if (!loadquote || !loadquote.parentNode) return;
revealScheduled = true;
later(() => {
core.scrollToBottom();
loadquote.style.opacity = "0";
later(() => {
if (loadquote.parentNode) loadquote.remove();
}, 300);
}, 200);
}
on(core, "data", scheduleReveal);
const REVEAL_FALLBACK_MS = 1500;
on(core, "open", () => {
later(scheduleReveal, REVEAL_FALLBACK_MS);
});
let inputBar = null;
function ensureInputBar() {
if (!inputBar) {
inputBar = createInputBar(core, (d) => core.send(d), node);
}
return inputBar;
}
if (isMobile) {
ensureInputBar();
}
try {
const coarse = window.matchMedia("(pointer: coarse)");
const onPointerChange = (e) => {
if (e.matches) ensureInputBar();
};
if (coarse.addEventListener) {
on(coarse, "change", onPointerChange);
} else if (coarse.addListener) {
coarse.addListener(onPointerChange);
cleanups.push(() => coarse.removeListener(onPointerChange));
}
} catch (_) {
}
function refreshViewToggle() {
const isReader = !!viewToggle?.isReader?.();
const btn = $("viewToggleBtn");
if (btn) {
if (!viewToggle) {
btn.hidden = true;
} else {
btn.hidden = false;
btn.textContent = isReader ? "▣" : "📖";
btn.title = isReader
? "Switch to terminal view"
: "Switch to reader view";
}
}
const readBtn = $("readModeBtn");
if (readBtn) {
if (!readToggle) {
readBtn.hidden = true;
} else {
readBtn.hidden = false;
const isRead = !!readToggle.isRead?.();
readBtn.textContent = isRead ? "▣" : "💬";
readBtn.title = isRead
? "Switch to terminal view"
: "Switch to read mode";
}
}
topBar?.sync?.();
}
const viewToggleBtn = $("viewToggleBtn");
if (viewToggleBtn) {
if (viewToggle) {
on(viewToggleBtn, "mousedown", (e) => e.preventDefault());
on(viewToggleBtn, "click", (e) => {
e.preventDefault();
viewToggle.toggle();
});
} else {
viewToggleBtn.hidden = true;
}
}
const readModeBtn = $("readModeBtn");
if (readModeBtn) {
if (readToggle) {
on(readModeBtn, "mousedown", (e) => e.preventDefault());
on(readModeBtn, "click", (e) => {
e.preventDefault();
readToggle.toggle();
});
} else {
readModeBtn.hidden = true;
}
}
let topBar = null;
function ensureTopBar() {
if (topBar || isMobile) return;
topBar = createTopBar({
send: (d) => core.send(d),
toggleReader: viewToggle ? () => viewToggle.toggle() : null,
isReader: () => !!viewToggle?.isReader?.(),
toggleRead: readToggle ? () => readToggle.toggle() : null,
isRead: () => !!readToggle?.isRead?.(),
node,
});
}
if (!isMobile) ensureTopBar();
try {
const coarse = window.matchMedia("(pointer: coarse)");
const onCoarse = (e) => {
if (e.matches && topBar) {
topBar.destroy();
topBar = null;
}
};
if (coarse.addEventListener) {
on(coarse, "change", onCoarse);
} else if (coarse.addListener) {
coarse.addListener(onCoarse);
cleanups.push(() => coarse.removeListener(onCoarse));
}
} catch (_) {
}
const openCommandMenu = showCmdList;
const testApi = {
inject: (str) => {
core.intentionalClose = true;
try {
core.ws?.close();
} catch (_) {}
return core.write("\x1b[?1049l" + str.replace(/\n/g, "\r\n"));
},
injectLines: (n, prefix = "inject") => {
core.intentionalClose = true;
try {
core.ws?.close();
} catch (_) {}
let s = "\x1b[?1049l";
for (let i = 0; i < n; i++) s += `${prefix} ${i}\r\n`;
return core.write(s);
},
injectLinesPlain: (n, prefix = "inject") => {
try {
core.ws?.close();
} catch (_) {}
let s = "";
for (let i = 0; i < n; i++) s += `${prefix} ${i}\r\n`;
return core.write(s);
},
bufferLength: () => core.getActiveBuffer().length,
isAlternate: () => core.isAlternateScreenActive(),
terminalRows: () => core.rows,
cols: () => core.cols,
rows: () => core.rows,
viewportY: () => core.getActiveBuffer().viewportY,
scrollToBottom: () => core.scrollToBottom(),
wsReady: () => core.ws?.readyState === WebSocket.OPEN,
forceDrop: () => {
core.intentionalClose = false;
try {
core.ws?.close();
} catch (_) {}
},
oscDetected: () => !!core.oscDetected,
switchWindow: (dir) => core.switchWindow(dir),
writeData: (str) => core.write(str),
awaitBufferChange: () =>
new Promise((resolve) => {
const sub = core.onBufferChanged(() => {
sub.dispose();
resolve();
});
}),
getSelection: () => core.getSelection(),
hasSelection: () => core.hasSelection(),
clearSelection: () => core.clearSelection(),
selectAll: () => core.selectAll(),
awaitSelectionChange: () =>
new Promise((resolve) => {
const sub = core.onSelectionChange(() => {
sub.dispose();
resolve();
});
}),
awaitLink: () =>
new Promise((resolve) => {
const sub = core.onLink((uri) => {
sub.dispose();
resolve(uri);
});
}),
awaitBell: () =>
new Promise((resolve) => {
const sub = core.onBell(() => {
sub.dispose();
resolve();
});
}),
measure: () => core.measure(),
cellMetrics: () => core.cellSize(),
scrollLines: (n) => core.scrollLines(n),
getFontSize: () => core.getFontSize(),
setFontSize: (px) => core.setFontSize(px),
focus: () => core.focus(),
setNativeInputEnabled: (enabled) => core.setNativeInputEnabled(enabled),
lineText: (y) => {
const line = core.getActiveBuffer().getLine(y);
return line ? line.translateToString(true) : null;
},
cellInfo: (y, x) => {
const line = core.getActiveBuffer().getLine(y);
if (!line) return null;
const cell = line.getCell(x);
return {
chars: cell.getChars(),
code: cell.getCode(),
bold: cell.isBold(),
fgDefault: cell.isFgDefault(),
bgDefault: cell.isBgDefault(),
};
},
oscMarkerCount: () => core.oscMarkers.size,
};
refreshViewToggle();
function selectWindow(windowIndex) {
if (windowIndex == null || windowIndex === "") return;
fetch(
`/api/sessions/${encodeURIComponent(session)}/panes/${encodeURIComponent(windowIndex)}/select${nodeQuery()}`,
{ method: "POST" },
)
.then(() => {
core.clear();
core.scrollToBottom();
later(() => {
core.refreshPanes();
core.reloadHistory();
}, 300);
})
.catch(() => {});
}
function windowFromUrl(href) {
try {
return new URL(href, location.origin).searchParams.get("w");
} catch (_) {
return null;
}
}
if ("serviceWorker" in navigator) {
on(navigator.serviceWorker, "message", (ev) => {
if (ev.data?.type === "mobux-navigate") {
selectWindow(windowFromUrl(ev.data.url));
}
});
}
let booted = false;
(async () => {
await core.reloadHistory();
if (disposed) return;
core.connect();
booted = true;
const w = windowFromUrl(location.href);
if (w != null) {
later(() => selectWindow(w), 500);
}
})();
on(window, "resize", () => core.resize());
later(() => core.resize(), 100);
every(() => core.refreshPanes(), 5000);
function autoReconnect() {
if (!booted || disposed) return;
core.reconnect();
}
on(document, "visibilitychange", () => {
if (document.visibilityState === "visible") autoReconnect();
});
on(window, "online", autoReconnect);
on(window, "pageshow", autoReconnect);
on(window, "pagehide", () => {
core.intentionalClose = true;
});
if (window.visualViewport) {
const vv = window.visualViewport;
let lastH = vv.height;
const trackKeyboard = () => {
const shrunk = vv.height < window.innerHeight - 1;
document.body.style.height = shrunk ? `${vv.height}px` : "";
if (Math.abs(vv.height - lastH) > 0.5) {
lastH = vv.height;
window.dispatchEvent(new Event("resize"));
}
};
on(vv, "resize", trackKeyboard);
on(vv, "scroll", trackKeyboard);
}
{
const TAP_MOVE_PX = 10; const TAP_MAX_MS = 250; let downX = 0;
let downY = 0;
let downT = 0;
let tracking = false;
on(termEl, "pointerdown", (e) => {
downX = e.clientX;
downY = e.clientY;
downT = e.timeStamp;
tracking = true;
});
on(termEl, "pointerup", (e) => {
if (!tracking) return;
tracking = false;
const moved = Math.hypot(e.clientX - downX, e.clientY - downY);
const elapsed = e.timeStamp - downT;
if (moved < TAP_MOVE_PX && elapsed < TAP_MAX_MS) {
core.scrollToBottom();
}
});
on(termEl, "pointercancel", () => {
tracking = false;
});
}
function dispose() {
if (disposed) return;
disposed = true;
core.intentionalClose = true;
gestures.destroy();
if (topBar) {
topBar.destroy();
topBar = null;
}
if (inputBar) {
inputBar.destroy();
inputBar = null;
}
for (const fn of cleanups.splice(0)) {
try {
fn();
} catch (_) {}
}
core.dispose();
document.body.style.height = "";
}
return {
core,
document: core.document,
dispose,
openCommandMenu,
refreshViewToggle,
showInputBar: () => ensureInputBar().show(),
twoPullMove,
twoPullEnd,
test: testApi,
};
}