/** * 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, }, }) ); } }); }