ypu
This commit is contained in:
parent
cc7ac8fa69
commit
7c1285fdfa
1 changed files with 140 additions and 73 deletions
|
|
@ -70,32 +70,24 @@ HTML_PAGE = """<!DOCTYPE html>
|
||||||
border: 1px solid #2b2b2b;
|
border: 1px solid #2b2b2b;
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
}
|
}
|
||||||
#terminal .xterm {
|
#terminal .xterm {
|
||||||
width: auto !important;
|
width: 100% !important;
|
||||||
height: auto !important;
|
height: 100% !important;
|
||||||
}
|
|
||||||
#terminal .xterm-viewport {
|
|
||||||
background-color: #000 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
width: auto !important;
|
|
||||||
}
|
|
||||||
#terminal .xterm-rows,
|
|
||||||
#terminal .xterm-scroll-area {
|
|
||||||
width: auto !important;
|
|
||||||
height: auto !important;
|
|
||||||
}
|
|
||||||
#terminal .xterm-screen {
|
|
||||||
width: auto !important;
|
|
||||||
height: auto !important;
|
|
||||||
}
|
}
|
||||||
#terminal .xterm .xterm-helper-textarea {
|
#terminal .xterm .xterm-helper-textarea {
|
||||||
width: 0 !important;
|
width: 0 !important;
|
||||||
height: 0 !important;
|
height: 0 !important;
|
||||||
}
|
}
|
||||||
|
#terminal .xterm-viewport,
|
||||||
|
#terminal .xterm-screen,
|
||||||
|
#terminal .xterm-rows,
|
||||||
|
#terminal .xterm-scroll-area {
|
||||||
|
width: 640px !important;
|
||||||
|
height: 400px !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -107,69 +99,144 @@ HTML_PAGE = """<!DOCTYPE html>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { Terminal } from "https://cdn.jsdelivr.net/npm/xterm@5.3.0/+esm";
|
import { Terminal } from "https://cdn.jsdelivr.net/npm/xterm@5.3.0/+esm";
|
||||||
|
|
||||||
const INITIAL_COLS = 80;
|
(async () => {
|
||||||
const INITIAL_ROWS = 25;
|
const COLS = 80;
|
||||||
const term = new Terminal({
|
const ROWS = 25;
|
||||||
cursorBlink: true,
|
const CHAR_HEIGHT = 16;
|
||||||
convertEol: true,
|
const VIEWPORT_WIDTH = 640;
|
||||||
fontFamily: "DOSVGA, Fira Code, Cascadia Mono, Hack, monospace",
|
const VIEWPORT_HEIGHT = 400;
|
||||||
fontSize: 16,
|
const TARGET_ROW_HEIGHT = VIEWPORT_HEIGHT / ROWS;
|
||||||
cols: INITIAL_COLS,
|
|
||||||
rows: INITIAL_ROWS,
|
|
||||||
theme: {
|
|
||||||
background: "#000000",
|
|
||||||
foreground: "#f5f5f5"
|
|
||||||
},
|
|
||||||
});
|
|
||||||
term.open(document.getElementById("terminal"));
|
|
||||||
|
|
||||||
const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws";
|
const terminalHost = document.getElementById("terminal");
|
||||||
const socket = new WebSocket(`${wsProtocol}://${window.location.host}/ws`);
|
|
||||||
const textDecoder = new TextDecoder("utf-8", { fatal: false });
|
|
||||||
|
|
||||||
function decodeBase64ToString(b64) {
|
async function waitForDosvgaFont() {
|
||||||
const binary = atob(b64);
|
if (!document.fonts || typeof document.fonts.load !== "function") {
|
||||||
const bytes = new Uint8Array(binary.length);
|
return;
|
||||||
for (let i = 0; i < binary.length; i += 1) {
|
}
|
||||||
bytes[i] = binary.charCodeAt(i);
|
|
||||||
}
|
try {
|
||||||
return textDecoder.decode(bytes);
|
await Promise.race([
|
||||||
}
|
document.fonts.load(`${CHAR_HEIGHT}px "DOSVGA"`),
|
||||||
|
new Promise((resolve) => setTimeout(resolve, 2000)),
|
||||||
socket.addEventListener("open", () => {
|
]);
|
||||||
socket.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows }));
|
} catch (err) {
|
||||||
term.focus();
|
console.debug("DOSVGA font load race rejected", err);
|
||||||
});
|
|
||||||
|
|
||||||
socket.addEventListener("message", (event) => {
|
|
||||||
try {
|
|
||||||
const payload = JSON.parse(event.data);
|
|
||||||
if (payload.type === "output") {
|
|
||||||
term.write(decodeBase64ToString(payload.data));
|
|
||||||
} else if (payload.type === "exit") {
|
|
||||||
term.write("\\r\\n[dos-shell exited]\\r\\n");
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error("failed to process message", err);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
socket.addEventListener("close", (event) => {
|
function applyViewportClamp() {
|
||||||
term.write(`\\r\\n[connection closed: ${event.code}]\\r\\n`);
|
terminalHost.style.width = `${VIEWPORT_WIDTH}px`;
|
||||||
});
|
terminalHost.style.height = `${VIEWPORT_HEIGHT}px`;
|
||||||
|
|
||||||
socket.addEventListener("error", (event) => {
|
const viewport = terminalHost.querySelector(".xterm-viewport");
|
||||||
console.error("websocket error", event);
|
if (viewport) {
|
||||||
term.write("\\r\\n[websocket error]\\r\\n");
|
viewport.style.width = `${VIEWPORT_WIDTH}px`;
|
||||||
});
|
viewport.style.height = `${VIEWPORT_HEIGHT}px`;
|
||||||
|
viewport.style.overflow = "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
term.onData((data) => {
|
const scrollArea = terminalHost.querySelector(".xterm-scroll-area");
|
||||||
socket.send(JSON.stringify({ type: "input", data }));
|
if (scrollArea) {
|
||||||
});
|
scrollArea.style.width = `${VIEWPORT_WIDTH}px`;
|
||||||
|
scrollArea.style.height = `${VIEWPORT_HEIGHT}px`;
|
||||||
|
}
|
||||||
|
|
||||||
term.onResize(({ cols, rows }) => {
|
const screen = terminalHost.querySelector(".xterm-screen");
|
||||||
socket.send(JSON.stringify({ type: "resize", cols, rows }));
|
if (screen) {
|
||||||
});
|
screen.style.width = `${VIEWPORT_WIDTH}px`;
|
||||||
|
screen.style.height = `${VIEWPORT_HEIGHT}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = terminalHost.querySelector(".xterm-rows");
|
||||||
|
if (rows) {
|
||||||
|
rows.style.width = `${VIEWPORT_WIDTH}px`;
|
||||||
|
rows.style.height = `${VIEWPORT_HEIGHT}px`;
|
||||||
|
const rowDivs = rows.querySelectorAll("div");
|
||||||
|
rowDivs.forEach((div) => {
|
||||||
|
div.style.height = `${TARGET_ROW_HEIGHT}px`;
|
||||||
|
div.style.lineHeight = `${TARGET_ROW_HEIGHT}px`;
|
||||||
|
div.style.width = `${VIEWPORT_WIDTH}px`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitForDosvgaFont();
|
||||||
|
|
||||||
|
const term = new Terminal({
|
||||||
|
rendererType: "canvas",
|
||||||
|
cursorBlink: true,
|
||||||
|
convertEol: true,
|
||||||
|
fontFamily: "DOSVGA, Fira Code, Cascadia Mono, Hack, monospace",
|
||||||
|
fontSize: CHAR_HEIGHT,
|
||||||
|
lineHeight: 1,
|
||||||
|
cols: COLS,
|
||||||
|
rows: ROWS,
|
||||||
|
theme: {
|
||||||
|
background: "#000000",
|
||||||
|
foreground: "#f5f5f5"
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
term.open(terminalHost);
|
||||||
|
term.resize(COLS, ROWS);
|
||||||
|
applyViewportClamp();
|
||||||
|
|
||||||
|
const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws";
|
||||||
|
const socket = new WebSocket(`${wsProtocol}://${window.location.host}/ws`);
|
||||||
|
const textDecoder = new TextDecoder("utf-8", { fatal: false });
|
||||||
|
|
||||||
|
function decodeBase64ToString(b64) {
|
||||||
|
const binary = atob(b64);
|
||||||
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
for (let i = 0; i < binary.length; i += 1) {
|
||||||
|
bytes[i] = binary.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return textDecoder.decode(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.addEventListener("open", () => {
|
||||||
|
socket.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows }));
|
||||||
|
term.focus();
|
||||||
|
applyViewportClamp();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.addEventListener("message", (event) => {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(event.data);
|
||||||
|
if (payload.type === "output") {
|
||||||
|
term.write(decodeBase64ToString(payload.data));
|
||||||
|
} else if (payload.type === "exit") {
|
||||||
|
term.write("\\r\\n[dos-shell exited]\\r\\n");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("failed to process message", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.addEventListener("close", (event) => {
|
||||||
|
term.write(`\\r\\n[connection closed: ${event.code}]\\r\\n`);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.addEventListener("error", (event) => {
|
||||||
|
console.error("websocket error", event);
|
||||||
|
term.write("\\r\\n[websocket error]\\r\\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
term.onData((data) => {
|
||||||
|
socket.send(JSON.stringify({ type: "input", data }));
|
||||||
|
});
|
||||||
|
|
||||||
|
term.onResize(({ cols, rows }) => {
|
||||||
|
socket.send(JSON.stringify({ type: "resize", cols, rows }));
|
||||||
|
applyViewportClamp();
|
||||||
|
});
|
||||||
|
|
||||||
|
term.onRender(() => {
|
||||||
|
applyViewportClamp();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("resize", () => applyViewportClamp(), { passive: true });
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue