127 lines
4.0 KiB
HTML
127 lines
4.0 KiB
HTML
<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>
|