<html lang="en"> <head> <meta charset="utf-8" /> <style> html { font-family: arial; } </style> <link rel="stylesheet" href="https://unpkg.com/xterm@4.11.0/css/xterm.css" /> </head> <body> <span style="font-size: small" >status: <span style="font-size: small" id="status">connecting...</span></span > <div style="width: 100%; height: calc(100% - 50px)" id="terminal"></div> <!-- xterm --> <script src="https://unpkg.com/xterm@4.11.0/lib/xterm.js"></script> <script src="https://unpkg.com/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.js"></script> <script src="https://unpkg.com/xterm-addon-web-links@0.4.0/lib/xterm-addon-web-links.js"></script> <script src="https://unpkg.com/xterm-addon-search@0.8.0/lib/xterm-addon-sear ch.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script> <script> const term = new Terminal({ cursorBlink: true, macOptionIsMeta: true, scrollback: true, }); term.attachCustomKeyEventHandler(customKeyEventHandler); // https://github.com/xtermjs/xterm.js/issues/2941 const fit = new FitAddon.FitAddon(); term.loadAddon(fit); term.loadAddon(new WebLinksAddon.WebLinksAddon()); term.loadAddon(new SearchAddon.SearchAddon()); term.open(document.getElementById("terminal")); fit.fit(); term.resize(15, 50); console.log(`size: ${term.cols} columns, ${term.rows} rows`); fit.fit(); term.writeln("Copy: [ctrl]+[shift]+[x] Paste: [ctrl]+[shift]+[v]"); term.writeln('') term.onData((data) => { console.log("browser terminal received new data:", data); socket.emit("pty-input", { input: data }); }); const socket = io.connect("/pty"); const status = document.getElementById("status"); socket.on("pty-output", function (data) { console.log("new output received from server:", data.output); term.write(data.output); }); socket.on("connect", () => { fitToscreen(); status.innerHTML = '<span style="background-color: lightgreen;">connected</span>'; }); socket.on("disconnect", () => { status.innerHTML = '<span style="background-color: #ff8383;">disconnected</span>'; }); function fitToscreen() { fit.fit(); const dims = { cols: term.cols, rows: term.rows }; console.log("sending new dimensions to server's pty", dims); socket.emit("resize", dims); } function debounce(func, wait_ms) { let timeout; return function (...args) { const context = this; clearTimeout(timeout); timeout = setTimeout(() => func.apply(context, args), wait_ms); }; } /** * Handle copy and paste events */ function customKeyEventHandler(e) { if (e.type !== "keydown") { return true; } if (e.ctrlKey && e.shiftKey) { const key = e.key.toLowerCase(); if (key === "v") { // ctrl+shift+v: paste whatever is in the clipboard navigator.clipboard.readText().then((toPaste) => { term.writeText(toPaste); }); return false; } else if (key === "c" || key === "x") { // ctrl+shift+x: copy whatever is highlighted to clipboard // 'x' is used as an alternate to 'c' because ctrl+c is taken // by the terminal (SIGINT) and ctrl+shift+c is taken by the browser // (open devtools). // I'm not aware of ctrl+shift+x being used by anything in the terminal // or browser const toCopy = term.getSelection(); navigator.clipboard.writeText(toCopy); term.focus(); return false; } } return true; } const wait_ms = 50; window.onresize = debounce(fitToscreen, wait_ms); </script> </body> </html>