import { TerminalCore } from './terminal-core.js';
import { ReaderView } from './reader-view.js';
import { createGestureRecognizer } from './touch.js';
import { createInputBar } from './input-bar.js';
import { applyTheme, getStoredThemeId } from './themes.js';
const session = window.MOBUX_SESSION;
const termEl = document.getElementById("terminal");
const readerEl = document.getElementById("reader");
const overlay = document.getElementById("touchOverlay");
const loadquote = document.getElementById("loadquote");
const paneIndicator = document.getElementById("paneIndicator");
const cmdPickList = document.getElementById("cmdPickList");
const cmdOverlayBg = document.getElementById("cmdOverlayBg");
const cmdCloseBtn = document.getElementById("cmdCloseBtn");
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\u2019ve 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 [text, author] = quotes[Math.floor(Math.random() * quotes.length)];
document.getElementById("quote").textContent = text;
document.getElementById("qauthor").textContent = "\u2014 " + author;
}
function navigateToUrl(url) {
window.location.assign(url);
}
function openExternal(url) {
const isTWA = document.referrer.startsWith('android-app://');
if (isTWA && /^https?:\/\//.test(url)) {
const urlObj = new URL(url);
const intentUrl = `intent://${urlObj.host}${urlObj.pathname}${urlObj.search}${urlObj.hash}#Intent;action=android.intent.action.VIEW;scheme=${urlObj.protocol.replace(':', '')};S.browser_fallback_url=${encodeURIComponent(url)};end;`;
window.__mobuxNavigateToUrl(intentUrl);
return;
}
const a = document.createElement('a');
a.href = url;
a.target = '_blank';
a.rel = 'noopener noreferrer';
a.style.display = 'none';
document.body.appendChild(a);
a.click();
a.remove();
}
window.__mobuxNavigateToUrl = navigateToUrl;
window.__mobuxOpenExternal = openExternal;
const isMobile = window.matchMedia('(pointer: coarse)').matches || window.innerWidth < 620;
const core = new TerminalCore({ session, host: termEl });
const getEditor = () => core.term?._sterk?.renderer?.getEditor?.();
applyTheme(getStoredThemeId(), { editor: getEditor() });
function onThemeChange() {
applyTheme(getStoredThemeId(), { editor: getEditor() });
}
window.addEventListener('storage', (e) => {
if (e.key === 'mobux:theme') onThemeChange();
});
window.addEventListener('mobux:theme', onThemeChange);
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})`;
}
}
core.addEventListener('panes', () => {
updatePaneUI();
pruneViewPrefs();
applyStoredViewForActiveWindow();
});
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';
}
}
cmdPickList.addEventListener('click', (e) => {
const cmdItem = e.target.closest('[data-cmd]');
if (cmdItem) { core.runTmuxCmd(cmdItem.dataset.cmd); hideCmdList(); return; }
});
cmdCloseBtn.addEventListener('click', hideCmdList);
cmdOverlayBg.addEventListener('click', hideCmdList);
function scrollByPixels(dy) {
const lines = Math.round(dy / core.cellSize().height);
if (lines !== 0) core.scrollLines(lines);
}
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(pull, vh) {
if (pull > vh * 0.08) paneIndicator.textContent = '↻ Release to reload';
else if (pull > vh * 0.03) paneIndicator.textContent = '↓ Pull to reload...';
},
onTwoPullEnd(pull, vh) {
if (pull > vh * 0.08) location.reload(true);
else updatePaneUI();
},
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(x, y) {
if (inputBar) {
inputBar.show();
return;
}
overlay.style.pointerEvents = 'none';
setTimeout(() => { overlay.style.pointerEvents = 'auto'; }, 500);
const el = document.elementFromPoint(x, y);
if (el) {
el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: x, clientY: y }));
el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, clientX: x, clientY: y }));
el.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX: x, clientY: y }));
}
},
onHSwipe: (dir) => core.switchWindow(dir),
onLongPress: showCmdList,
onSwipeUp: showCmdList,
});
let readerGestures = null;
function mountReaderGestures() {
if (readerGestures) return;
readerGestures = createGestureRecognizer(readerEl, {
onReconnect: () => core.reconnect(),
onLongPress: showCmdList,
onSwipeUp: showCmdList,
onHSwipe: (dir) => core.switchWindow(dir),
onTap: () => {},
onDoubleTap: () => { swapView('xterm'); if (inputBar) inputBar.show(); },
onScroll: (dy) => reader.scrollBy(dy),
onTwoPullMove(pull, vh) {
if (pull > vh * 0.08) paneIndicator.textContent = '↻ Release to reload';
else if (pull > vh * 0.03) paneIndicator.textContent = '↓ Pull to reload...';
},
onTwoPullEnd(pull, vh) {
if (pull > vh * 0.08) location.reload(true);
else updatePaneUI();
},
}, { passiveScroll: false });
}
function unmountReaderGestures() {
if (!readerGestures) return;
readerGestures.destroy();
readerGestures = null;
}
let revealScheduled = false;
function scheduleReveal() {
if (revealScheduled) return;
if (!loadquote || !loadquote.parentNode) return;
revealScheduled = true;
setTimeout(() => {
core.scrollToBottom();
loadquote.style.opacity = '0';
setTimeout(() => { if (loadquote.parentNode) loadquote.remove(); }, 300);
}, 200);
}
core.addEventListener('data', scheduleReveal);
let inputBar = null;
if (isMobile) {
inputBar = createInputBar(core.term, (d) => core.send(d));
}
const reader = new ReaderView({ host: readerEl, core, overlay });
let currentView = 'xterm';
const VIEW_DEFAULT_KEY = 'mobux.view.default';
const viewPrefKey = (windowId) => `mobux.view.${session}.${windowId}`;
function activeWindowId() {
const p = core.panes[core.activeIndex];
return p?.id || null;
}
function storedDefaultView() {
try { return localStorage.getItem(VIEW_DEFAULT_KEY) || 'xterm'; }
catch (_) { return 'xterm'; }
}
function storedViewFor(windowId) {
if (!windowId) return null;
try { return localStorage.getItem(viewPrefKey(windowId)); }
catch (_) { return null; }
}
function updateToggleLabel() {
const btn = document.getElementById('viewToggleBtn');
if (!btn) return;
if (currentView === 'reader') {
btn.textContent = '▣';
btn.title = 'Switch to terminal view';
} else {
btn.textContent = '📖';
btn.title = 'Switch to reader view';
}
}
function applyView(mode, { persist = true } = {}) {
if (mode !== 'xterm' && mode !== 'reader') return;
if (mode === currentView) { updateToggleLabel(); return; }
if (mode === 'reader') {
termEl.classList.add('hidden');
overlay.style.pointerEvents = 'none';
reader.mount();
mountReaderGestures();
} else {
unmountReaderGestures();
reader.unmount();
termEl.classList.remove('hidden');
if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
overlay.style.pointerEvents = 'auto';
}
setTimeout(() => core.resize(), 0);
}
currentView = mode;
if (persist) {
try {
localStorage.setItem(VIEW_DEFAULT_KEY, mode);
const wid = activeWindowId();
if (wid) localStorage.setItem(viewPrefKey(wid), mode);
} catch (_) {}
}
updateToggleLabel();
window.dispatchEvent(new CustomEvent('mobux:viewchange', { detail: mode }));
}
function swapView(mode) { applyView(mode, { persist: true }); }
const viewToggleBtn = document.getElementById('viewToggleBtn');
if (viewToggleBtn) {
viewToggleBtn.addEventListener('mousedown', (e) => e.preventDefault());
viewToggleBtn.addEventListener('click', (e) => {
e.preventDefault();
swapView(currentView === 'xterm' ? 'reader' : 'xterm');
});
}
function applyStoredViewForActiveWindow() {
const wid = activeWindowId();
const stored = storedViewFor(wid);
const mode = stored || storedDefaultView();
applyView(mode, { persist: false });
}
function pruneViewPrefs() {
const live = new Set(core.panes.map((p) => p.id).filter(Boolean));
const prefix = `mobux.view.${session}.`;
try {
for (let i = localStorage.length - 1; i >= 0; i--) {
const k = localStorage.key(i);
if (k?.startsWith(prefix) && !live.has(k.slice(prefix.length))) {
localStorage.removeItem(k);
}
}
} catch (_) {}
}
window.__mobuxView = {
swap: swapView,
get current() { return currentView; },
send: (d) => core.send(d),
test: {
inject: (str) => {
core.intentionalClose = true;
try { core.ws?.close(); } catch (_) {}
return new Promise((resolve) =>
core.term.write('\x1b[?1049l' + str.replace(/\n/g, '\r\n'), resolve));
},
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 new Promise((resolve) => core.term.write(s, resolve));
},
injectLinesPlain: (n, prefix = 'inject') => {
try { core.ws?.close(); } catch (_) {}
let s = '';
for (let i = 0; i < n; i++) s += `${prefix} ${i}\r\n`;
return new Promise((resolve) => core.term.write(s, resolve));
},
readerAwaitRender: () => reader.awaitNextRender(),
bufferLength: () => core.getActiveBuffer().length,
isAlternate: () => {
if (core.term?._sterk?.buffer) {
return core.term._sterk.buffer.alternate === core.term._sterk.buffer.active;
}
const t = core.term?.buffer?.active?.type;
return t === 'alternate';
},
readerAtBottom: () => reader._atBottom,
readerForceScrollTop: () => { reader._atBottom = false; reader._scrollY = 0; reader._applyTransform?.(); },
terminalRows: () => core.term.rows,
cols: () => core.term.cols,
rows: () => core.term.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,
readerScrollY: () => reader.scrollY,
readerMaxScroll: () => reader.maxScroll,
readerInnerHeight: () => reader.innerHeight,
readerScrollBy: (dy) => reader.scrollBy(dy),
readerStickToBottom: () => reader.stickToBottom(),
readerForceRender: () => reader._render(),
switchWindow: (dir) => core.switchWindow(dir),
statusBarOffsetHeight: () => document.querySelector('.reader-statusbar')?.offsetHeight ?? 0,
statusBarFilled: () => document.querySelector('.reader-statusbar')?.classList.contains('reader-statusbar--filled') ?? false,
},
};
const bootDefault = storedDefaultView();
if (bootDefault === 'reader') {
setTimeout(() => applyView('reader', { persist: false }), 0);
}
updateToggleLabel();
function selectWindow(windowIndex) {
if (windowIndex == null || windowIndex === '') return;
fetch(
`/api/sessions/${encodeURIComponent(session)}/panes/${encodeURIComponent(windowIndex)}/select`,
{ method: 'POST' },
).then(() => {
core.clear();
core.scrollToBottom();
setTimeout(() => { 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) {
navigator.serviceWorker.addEventListener('message', (ev) => {
if (ev.data?.type === 'mobux-navigate') {
selectWindow(windowFromUrl(ev.data.url));
}
});
}
let booted = false;
(async () => {
await core.reloadHistory();
core.connect();
booted = true;
const w = windowFromUrl(location.href);
if (w != null) {
setTimeout(() => selectWindow(w), 500);
}
})();
window.addEventListener("resize", () => core.resize());
setTimeout(() => core.resize(), 100);
setInterval(() => core.refreshPanes(), 5000);
function autoReconnect() {
if (!booted) return;
core.reconnect();
}
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') autoReconnect();
});
window.addEventListener('online', autoReconnect);
window.addEventListener('pageshow', autoReconnect);
window.addEventListener('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'));
}
};
vv.addEventListener('resize', trackKeyboard);
vv.addEventListener('scroll', trackKeyboard);
}
{
const TAP_MOVE_PX = 10; const TAP_MAX_MS = 250; let downX = 0;
let downY = 0;
let downT = 0;
let tracking = false;
termEl.addEventListener('pointerdown', (e) => {
downX = e.clientX;
downY = e.clientY;
downT = e.timeStamp;
tracking = true;
});
termEl.addEventListener('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();
}
});
termEl.addEventListener('pointercancel', () => { tracking = false; });
}