Public Access
Stop one tab's status banner from speaking for all the others #10
@@ -84,14 +84,57 @@ const RELEASE_FOCUS_MESSAGE = 'dodossh.release-focus';
|
|||||||
const root = document.getElementById('root');
|
const root = document.getElementById('root');
|
||||||
const statusBanner = document.getElementById('status');
|
const statusBanner = document.getElementById('status');
|
||||||
|
|
||||||
/** @type {Map<number, {term: object, fit: object, pane: HTMLElement}>} */
|
/** @type {Map<number, {term: object, fit: object, pane: HTMLElement, notice: string}>} */
|
||||||
const sessions = new Map();
|
const sessions = new Map();
|
||||||
|
|
||||||
/** @type {WebSocket | null} */
|
/** @type {WebSocket | null} */
|
||||||
let socket = null;
|
let socket = null;
|
||||||
|
|
||||||
function setStatus(text) {
|
/** Whose pane is showing, or null before there is one — see activate(). */
|
||||||
statusBanner.textContent = text ?? '';
|
let activeSessionId = null;
|
||||||
|
|
||||||
|
/*
|
||||||
|
── THE BANNER BELONGS TO ONE PANE AT A TIME ─────────────────────────────────────────────────────────
|
||||||
|
There is one #status element for the whole page, because there is one page for every terminal: the
|
||||||
|
panes are stacked in the same box and all but the active one are hidden. What goes in it comes from
|
||||||
|
two sources that are not the same size, and the difference is the whole of this.
|
||||||
|
|
||||||
|
The socket's troubles are the page's. There is a single socket behind every pane, so "the view is
|
||||||
|
reconnecting" is true of whatever is on screen and true of the panes behind it.
|
||||||
|
|
||||||
|
A session's last words are not. "The remote closed the session." is a fact about one terminal and says
|
||||||
|
nothing whatever about the others — so it is held on the session and drawn only while that session's
|
||||||
|
pane is the one showing. Written straight into the shared element, which is what this used to do, it
|
||||||
|
outlived the tab it described: switching to a live terminal left the dead one's epitaph sitting under
|
||||||
|
it, and opening or closing any other tab wiped the message whether or not it belonged to that tab.
|
||||||
|
|
||||||
|
The socket's half wins when both have something to say: a page whose socket is down is not showing
|
||||||
|
live output on any pane, which makes what became of one session the less urgent of the two.
|
||||||
|
*/
|
||||||
|
let transportStatus = statusBanner.textContent ?? '';
|
||||||
|
|
||||||
|
function renderStatus() {
|
||||||
|
const notice = activeSessionId === null ? '' : sessions.get(activeSessionId)?.notice ?? '';
|
||||||
|
|
||||||
|
statusBanner.textContent = transportStatus || notice;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Says something about the socket, which every pane shares. */
|
||||||
|
function setTransportStatus(text) {
|
||||||
|
transportStatus = text ?? '';
|
||||||
|
renderStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Records what became of one session, to be drawn only while that session's pane is showing. */
|
||||||
|
function setSessionNotice(sessionId, text) {
|
||||||
|
const session = sessions.get(sessionId);
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
session.notice = text ?? '';
|
||||||
|
renderStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Builds a frame: opcode, big-endian session id, then payload. */
|
/** Builds a frame: opcode, big-endian session id, then payload. */
|
||||||
@@ -292,7 +335,7 @@ function createSession(sessionId) {
|
|||||||
|
|
||||||
term.onResize(() => sendResize(sessionId, term, pane));
|
term.onResize(() => sendResize(sessionId, term, pane));
|
||||||
|
|
||||||
const session = { term, fit, pane };
|
const session = { term, fit, pane, notice: '' };
|
||||||
sessions.set(sessionId, session);
|
sessions.set(sessionId, session);
|
||||||
|
|
||||||
activate(sessionId);
|
activate(sessionId);
|
||||||
@@ -306,6 +349,11 @@ function activate(sessionId) {
|
|||||||
session.pane.dataset.active = String(id === sessionId);
|
session.pane.dataset.active = String(id === sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The banner follows the pane. Whatever this session has to say for itself replaces whatever the
|
||||||
|
// session that was showing had to say for its own, which is the point of holding it per session.
|
||||||
|
activeSessionId = sessionId;
|
||||||
|
renderStatus();
|
||||||
|
|
||||||
const active = sessions.get(sessionId);
|
const active = sessions.get(sessionId);
|
||||||
if (active) {
|
if (active) {
|
||||||
active.term.focus();
|
active.term.focus();
|
||||||
@@ -373,7 +421,10 @@ function handleFrame(buffer) {
|
|||||||
session.term.write(REPLAY_BANNER);
|
session.term.write(REPLAY_BANNER);
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus('');
|
// This session's own line, and only this one's: a session that is open has nothing to say about
|
||||||
|
// how it ended. The page's own "Connecting…" is cleared by the socket opening, which happens
|
||||||
|
// before any frame can arrive.
|
||||||
|
setSessionNotice(sessionId, '');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,7 +477,14 @@ function handleFrame(buffer) {
|
|||||||
session.pane.remove();
|
session.pane.remove();
|
||||||
sessions.delete(sessionId);
|
sessions.delete(sessionId);
|
||||||
|
|
||||||
setStatus('');
|
// The notice went with the session record it was held on, but the page can still be pointing at
|
||||||
|
// the pane that is now gone. Cleared rather than left dangling, so the banner stops describing a
|
||||||
|
// closed tab while the host decides which pane to show next.
|
||||||
|
if (activeSessionId === sessionId) {
|
||||||
|
activeSessionId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderStatus();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,14 +561,19 @@ function handleFrame(buffer) {
|
|||||||
const session = sessions.get(sessionId);
|
const session = sessions.get(sessionId);
|
||||||
const reason = new TextDecoder().decode(payload);
|
const reason = new TextDecoder().decode(payload);
|
||||||
|
|
||||||
if (session) {
|
if (!session) {
|
||||||
// The pane and its scrollback stay. The user was probably reading the last thing the
|
// No pane, so there is nothing this page can honestly hang the reason on. It used to go into
|
||||||
// remote said, and that is usually why the session ended.
|
// the banner anyway, which printed one session's ending underneath whichever pane happened to
|
||||||
session.term.write(`\r\n\x1b[38;5;244m── ${reason} ──\x1b[0m\r\n`);
|
// be showing at the time.
|
||||||
session.term.options.cursorBlink = false;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus(reason);
|
// The pane and its scrollback stay. The user was probably reading the last thing the
|
||||||
|
// remote said, and that is usually why the session ended.
|
||||||
|
session.term.write(`\r\n\x1b[38;5;244m── ${reason} ──\x1b[0m\r\n`);
|
||||||
|
session.term.options.cursorBlink = false;
|
||||||
|
|
||||||
|
setSessionNotice(sessionId, reason);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,7 +598,7 @@ function scheduleReconnect() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus('Reconnecting the terminal view…');
|
setTransportStatus('Reconnecting the terminal view…');
|
||||||
|
|
||||||
reconnectTimer = setTimeout(() => {
|
reconnectTimer = setTimeout(() => {
|
||||||
reconnectTimer = null;
|
reconnectTimer = null;
|
||||||
@@ -555,7 +618,7 @@ function connect() {
|
|||||||
socket.binaryType = 'arraybuffer';
|
socket.binaryType = 'arraybuffer';
|
||||||
|
|
||||||
socket.addEventListener('open', () => {
|
socket.addEventListener('open', () => {
|
||||||
setStatus('');
|
setTransportStatus('');
|
||||||
|
|
||||||
// Back to the quick attempt for whatever the next failure turns out to be. Kept slow between
|
// Back to the quick attempt for whatever the next failure turns out to be. Kept slow between
|
||||||
// attempts within one outage, reset once the outage is actually over.
|
// attempts within one outage, reset once the outage is actually over.
|
||||||
|
|||||||
Reference in New Issue
Block a user