* work * moar work * notes * work * separate to terminal and control channels * stdin working * serve html web client initial * serve static assets loaded with include_dir * merge * enable_web_server config parameter * compile time flag to disable web server capability * rustfmt * add license to all xterm.js assets * mouse working except copy/paste * helpful comment * web client improvements - move script to js file - add favicon - add nerd font - change title TODO: investigate if font license embedded in otf is sufficient * get mouse to work properly * kitty keyboard support initial * fix wrong type in preload link * wip axum websocket handlers - upgrade axum to v0.8.1, enable ws feature - begin setup of websocket handlers - tidy up imports * replace control listener * handle terminal websocket with axum * cleanup Cargo.toml * kitty fixes and bracketed paste * fix(mouse): pane not found crash * initial session switching infra * add `web_client_font` option * session switching, creation and resurrection working through the session manager * move session module to zellij-utils and share logic with web-client * some cleanups * require restart for enable-web-server * use session name from router * write config to disk and watch for config changes * rename session name to ipc path * add basic panic handler, make render_to_client exit on channel close * use while let instead of loop * handle websocket close * add mouse motions * make clipboard work * add weblink handling and webgl rendering * add todo * fix: use session name instead of patch on session switch * use "default" layout for new sessions * ui indication for session being shared * share this session ui * plugin assets * Fix process crash on mac with notify watcher. Use poll watcher instead of recommended as a workaround. * make url session switching and creation work * start welcome screen on root url * scaffold control messages, set font from config * set dimensions on session start * bring back session name from url * send bytes on terminal websocket instead of json - create web client os input and id before websocket connection * draft ui * work * refactor ui * remove otf font, remove margins to avoid scrollbar * version query endpoint for server status * web session info query endpoint * refactor: move stuff around * add web client info to session metadata * make tests pass * populate real data in session list * remove unnecessary endpoint * add web_client node to config, add font option * remove web_client_font * allow disabling the web session through the config - WIP * formalize sharing/not-sharing configuration * fix tests * allow shutting down web server * display error when web clients are forbidden to attach * only show sessions that allow web clients if this is a web client * style(fmt): rustfmt * fix: query web server from Zellij rather than from each plugin * remove log spam * handle some error paths better in the web client * allow controlling the web server through the cli * allow configuring the web server's ip/port * fix tests and format code * use direct WebServerStatus event instead of piggy-backing on SessionInfo * plugin revamp initial * make plugin responsive * adjust plugin title * refactor: share plugin * refactor: share plugin * add cors middleware * some fixes for running without a compiled web server capability * display error when starting the share plugin without web server support * clarify config * add pipelines to compile zellij without web support * display error when unable to start web server * only query web server when share plugin is running * refactor(web-client): connection table * give zellij_server_listener access to the control channel * fixes and clarifications * refactor: consolidate generate_unique_session_name * give proper error when trying to attach to a forbidden session * change browser URL when switching sessions * add keyboard shortcut * enforce https when bound to non-loopback ip * initial authentication token implementation * background color from theme * initial web client theme config * basic token generation ui * refactor set config message creation * also set body background * allow editing scrollback for plugins too * set scrollback to 0 * properly parse colors in config * generate token from plugin * nice login modals * initial token management screen * implement token authentication * refactor(share): token management screen * style(fmt): rustfmt * fix(plugin): some minor bugs * refactor(share): main screen * refactor(share): token screen * refactor(share): main * refactor(share): ui components * fix(responsiveness): properly send usage_width to the render function * fix cli commands and add some verbosity * add support for settings ansi and selection colors * add cursor and cursor accent * basic web client tests * fix tests * refactor: web client * use session tokens for authentication * improve modals * move shutdown to ipc * refactor: ipc logic * serialize theme config for web client * update tests * refactor: move some stuff around to prepare for config hot reload * config live reloading for the web clients * change remember-me UI wording * improve xterm.js link handling * make sure terminal is focused on mousemove * remove deprecated sharing indication from compact-bar * gate deps and functionality behind the web_server_compatibility feature * feat(build): add --no-web flag in all the places * fix some other build flows * add new assets * update CI for no-web (untested) * make more dependencies optional * update axum-extra * add web client configuration options * gracefully close connections on server exit * tests for graceful connection closing * handle client-side reconnect when server is down * fix: make sure ipc bus folder exists before starting * add commands to manage login tokens from the cli * style(fmt): rustfmt * some cleanups * fix(ux): allow alt-right-click on the web client without opening the context menu * fix: prevent attaching to welcome screen * fix: reload config issues * fix long socket path on macos * normalize config conversion and fix color gap in browser * revoke session_token cookie if it is not valid * fix: visual bug with multiple clients in extremely small screen sizes * fix: only include rusqlite for the web server capability builds * update e2e snapshots * refactor(web): client side js * some cleanups * moar cleanups * fix(tests): wait for server instead of using a fixed timeout * debug CI * fix(tests): use spawn_blocking for running the test web server * fix(tests): wait for http rather than tcp port * fix(tests): properly pass config path - hopefully this is the issue... * success! bring back the rest of the tests * attempt to fix the macos CI issue * docs(changelog): add PR --------- Co-authored-by: Thomas Linford <linford.t@gmail.com>
253 lines
8.3 KiB
JavaScript
253 lines
8.3 KiB
JavaScript
/**
|
|
* WebSocket management for terminal and control connections
|
|
*/
|
|
|
|
import { is_https } from './utils.js';
|
|
import { handleReconnection, markConnectionEstablished } from './connection.js';
|
|
|
|
/**
|
|
* Initialize both terminal and control WebSocket connections
|
|
* @param {string} webClientId - Client ID from authentication
|
|
* @param {string} sessionName - Session name from URL
|
|
* @param {Terminal} term - Terminal instance
|
|
* @param {FitAddon} fitAddon - Terminal fit addon
|
|
* @param {function} sendAnsiKey - Function to send ANSI key sequences
|
|
* @returns {object} Object containing WebSocket instances and cleanup function
|
|
*/
|
|
export function initWebSockets(webClientId, sessionName, term, fitAddon, sendAnsiKey) {
|
|
let ownWebClientId = "";
|
|
let wsTerminal;
|
|
let wsControl;
|
|
|
|
const wsUrlPrefix = is_https() ? "wss" : "ws";
|
|
const url = sessionName === ""
|
|
? `${wsUrlPrefix}://${window.location.host}/ws/terminal`
|
|
: `${wsUrlPrefix}://${window.location.host}/ws/terminal/${sessionName}`;
|
|
|
|
const queryString = `?web_client_id=${encodeURIComponent(webClientId)}`;
|
|
const wsTerminalUrl = `${url}${queryString}`;
|
|
|
|
wsTerminal = new WebSocket(wsTerminalUrl);
|
|
|
|
wsTerminal.onopen = function () {
|
|
markConnectionEstablished();
|
|
};
|
|
|
|
wsTerminal.onmessage = function (event) {
|
|
if (ownWebClientId == "") {
|
|
ownWebClientId = webClientId;
|
|
const wsControlUrl = `${wsUrlPrefix}://${window.location.host}/ws/control`;
|
|
wsControl = new WebSocket(wsControlUrl);
|
|
startWsControl(wsControl, term, fitAddon, ownWebClientId);
|
|
}
|
|
|
|
let data = event.data;
|
|
if (typeof data === 'string' && data.includes('\x1b[0 q')) {
|
|
const shouldBlink = term.options.cursorBlink;
|
|
const cursorStyle = term.options.cursorStyle;
|
|
let replacement;
|
|
switch (cursorStyle) {
|
|
case 'block':
|
|
replacement = shouldBlink ? '\x1b[1 q' : '\x1b[2 q';
|
|
break;
|
|
case 'underline':
|
|
replacement = shouldBlink ? '\x1b[3 q' : '\x1b[4 q';
|
|
break;
|
|
case 'bar':
|
|
replacement = shouldBlink ? '\x1b[5 q' : '\x1b[6 q';
|
|
break;
|
|
default:
|
|
replacement = '\x1b[2 q';
|
|
break;
|
|
}
|
|
data = data.replace(/\x1b\[0 q/g, replacement);
|
|
}
|
|
term.write(data);
|
|
};
|
|
|
|
wsTerminal.onclose = function () {
|
|
handleReconnection();
|
|
};
|
|
|
|
// Update sendAnsiKey to use the actual WebSocket
|
|
const originalSendAnsiKey = sendAnsiKey;
|
|
sendAnsiKey = (ansiKey) => {
|
|
if (ownWebClientId !== "") {
|
|
wsTerminal.send(ansiKey);
|
|
}
|
|
};
|
|
|
|
// Setup resize handler
|
|
setupResizeHandler(term, fitAddon, () => wsControl, () => ownWebClientId);
|
|
|
|
return {
|
|
wsTerminal,
|
|
getWsControl: () => wsControl,
|
|
getOwnWebClientId: () => ownWebClientId,
|
|
sendAnsiKey,
|
|
cleanup: () => {
|
|
if (wsTerminal) {
|
|
wsTerminal.close();
|
|
}
|
|
if (wsControl) {
|
|
wsControl.close();
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Start the control WebSocket and set up its handlers
|
|
* @param {WebSocket} wsControl - Control WebSocket instance
|
|
* @param {Terminal} term - Terminal instance
|
|
* @param {FitAddon} fitAddon - Terminal fit addon
|
|
* @param {string} ownWebClientId - Own web client ID
|
|
*/
|
|
function startWsControl(wsControl, term, fitAddon, ownWebClientId) {
|
|
wsControl.onopen = function (event) {
|
|
const fitDimensions = fitAddon.proposeDimensions();
|
|
const { rows, cols } = fitDimensions;
|
|
wsControl.send(
|
|
JSON.stringify({
|
|
web_client_id: ownWebClientId,
|
|
payload: {
|
|
type: "TerminalResize",
|
|
rows,
|
|
cols,
|
|
},
|
|
})
|
|
);
|
|
};
|
|
|
|
wsControl.onmessage = function (event) {
|
|
const msg = JSON.parse(event.data);
|
|
if (msg.type === "SetConfig") {
|
|
const {
|
|
font,
|
|
theme,
|
|
cursor_blink,
|
|
mac_option_is_meta,
|
|
cursor_style,
|
|
cursor_inactive_style
|
|
} = msg;
|
|
term.options.fontFamily = font;
|
|
term.options.theme = theme;
|
|
if (cursor_blink !== 'undefined') {
|
|
term.options.cursorBlink = cursor_blink;
|
|
}
|
|
if (mac_option_is_meta !== 'undefined') {
|
|
term.options.macOptionIsMeta = mac_option_is_meta;
|
|
}
|
|
if (cursor_style !== 'undefined') {
|
|
term.options.cursorStyle = cursor_style;
|
|
}
|
|
if (cursor_inactive_style !== 'undefined') {
|
|
term.options.cursorInactiveStyle = cursor_inactive_style;
|
|
}
|
|
const body = document.querySelector("body");
|
|
body.style.background = theme.background;
|
|
|
|
const terminal = document.getElementById("terminal");
|
|
terminal.style.background = theme.background;
|
|
|
|
const fitDimensions = fitAddon.proposeDimensions();
|
|
if (fitDimensions === undefined) {
|
|
console.warn("failed to get new fit dimensions");
|
|
return;
|
|
}
|
|
|
|
const { rows, cols } = fitDimensions;
|
|
if (rows === term.rows && cols === term.cols) {
|
|
return;
|
|
}
|
|
term.resize(cols, rows);
|
|
|
|
wsControl.send(
|
|
JSON.stringify({
|
|
web_client_id: ownWebClientId,
|
|
payload: {
|
|
type: "TerminalResize",
|
|
rows,
|
|
cols,
|
|
},
|
|
})
|
|
);
|
|
} else if (msg.type === "QueryTerminalSize") {
|
|
const fitDimensions = fitAddon.proposeDimensions();
|
|
const { rows, cols } = fitDimensions;
|
|
if (rows !== term.rows || cols !== term.cols) {
|
|
term.resize(cols, rows);
|
|
}
|
|
wsControl.send(
|
|
JSON.stringify({
|
|
web_client_id: ownWebClientId,
|
|
payload: {
|
|
type: "TerminalResize",
|
|
rows,
|
|
cols,
|
|
},
|
|
})
|
|
);
|
|
} else if (msg.type === "Log") {
|
|
const { lines } = msg;
|
|
for (const line in lines) {
|
|
console.log(line);
|
|
}
|
|
} else if (msg.type === "LogError") {
|
|
const { lines } = msg;
|
|
for (const line in lines) {
|
|
console.error(line);
|
|
}
|
|
} else if (msg.type === "SwitchedSession") {
|
|
const { new_session_name } = msg;
|
|
window.location.pathname = `/${new_session_name}`;
|
|
}
|
|
};
|
|
|
|
wsControl.onclose = function () {
|
|
handleReconnection();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Set up window resize event handler
|
|
* @param {Terminal} term - Terminal instance
|
|
* @param {FitAddon} fitAddon - Terminal fit addon
|
|
* @param {function} getWsControl - Function that returns control WebSocket
|
|
* @param {function} getOwnWebClientId - Function that returns own web client ID
|
|
*/
|
|
export function setupResizeHandler(term, fitAddon, getWsControl, getOwnWebClientId) {
|
|
addEventListener("resize", (event) => {
|
|
const ownWebClientId = getOwnWebClientId();
|
|
if (ownWebClientId === "") {
|
|
return;
|
|
}
|
|
|
|
const fitDimensions = fitAddon.proposeDimensions();
|
|
if (fitDimensions === undefined) {
|
|
console.warn("failed to get new fit dimensions");
|
|
return;
|
|
}
|
|
|
|
const { rows, cols } = fitDimensions;
|
|
if (rows === term.rows && cols === term.cols) {
|
|
return;
|
|
}
|
|
|
|
term.resize(cols, rows);
|
|
|
|
const wsControl = getWsControl();
|
|
if (wsControl) {
|
|
wsControl.send(
|
|
JSON.stringify({
|
|
web_client_id: ownWebClientId,
|
|
payload: {
|
|
type: "TerminalResize",
|
|
rows,
|
|
cols,
|
|
},
|
|
})
|
|
);
|
|
}
|
|
});
|
|
}
|