mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-21 13:23:07 +01:00
127 lines
3.1 KiB
Vue
127 lines
3.1 KiB
Vue
<template>
|
|
<aside class="flex h-[calc(100svh-50px)] flex-col gap-2">
|
|
<header class="flex items-center gap-4">
|
|
<material-symbols:terminal class="size-8" />
|
|
<h1 class="text-2xl max-md:hidden">{{ container.name }}</h1>
|
|
<h2 class="text-sm">Started <RelativeTime :date="container.created" /></h2>
|
|
</header>
|
|
|
|
<div ref="host" class="shell flex-1"></div>
|
|
</aside>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { Container } from "@/models/Container";
|
|
import "@xterm/xterm/css/xterm.css";
|
|
const { container, action } = defineProps<{ container: Container; action: "attach" | "exec" }>();
|
|
|
|
const { Terminal } = await import("@xterm/xterm");
|
|
const { WebLinksAddon } = await import("@xterm/addon-web-links");
|
|
const { FitAddon } = await import("@xterm/addon-fit");
|
|
|
|
const host = useTemplateRef<HTMLDivElement>("host");
|
|
const terminal = new Terminal({
|
|
cursorBlink: true,
|
|
cursorStyle: "block",
|
|
});
|
|
terminal.loadAddon(new WebLinksAddon());
|
|
const fitAddon = new FitAddon();
|
|
terminal.loadAddon(fitAddon);
|
|
|
|
let ws: WebSocket | null = null;
|
|
|
|
function sendEvent(type: "userinput" | "resize", data?: string, width?: number, height?: number) {
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
|
|
const event: { type: string; data?: string; width?: number; height?: number } = { type };
|
|
if (data !== undefined) event.data = data;
|
|
if (width !== undefined) event.width = width;
|
|
if (height !== undefined) event.height = height;
|
|
|
|
ws.send(JSON.stringify(event));
|
|
}
|
|
|
|
onMounted(() => {
|
|
terminal.open(host.value!);
|
|
fitAddon.fit();
|
|
|
|
ws = new WebSocket(withBase(`/api/hosts/${container.host}/containers/${container.id}/${action}`));
|
|
ws.onopen = () => {
|
|
terminal.writeln(`Attaching to ${container.name} 🚀`);
|
|
|
|
// Send initial resize event
|
|
sendEvent("resize", undefined, terminal.cols, terminal.rows);
|
|
|
|
if (action === "attach") {
|
|
sendEvent("userinput", "\r");
|
|
}
|
|
|
|
terminal.onData((data) => {
|
|
sendEvent("userinput", data);
|
|
});
|
|
|
|
// Handle terminal resize
|
|
terminal.onResize(({ cols, rows }) => {
|
|
sendEvent("resize", undefined, cols, rows);
|
|
});
|
|
|
|
terminal.focus();
|
|
};
|
|
|
|
ws.onmessage = (event) => terminal.write(event.data);
|
|
ws.addEventListener("close", () => {
|
|
terminal.writeln("⚠️ Connection closed");
|
|
});
|
|
|
|
// Handle window resize
|
|
const { width, height } = useWindowSize();
|
|
watch([width, height], () => {
|
|
requestAnimationFrame(() => {
|
|
fitAddon.fit();
|
|
});
|
|
});
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
console.log("Closing WebSocket");
|
|
terminal.dispose();
|
|
ws?.close();
|
|
});
|
|
</script>
|
|
<style scoped>
|
|
@reference "@/main.css";
|
|
|
|
.shell {
|
|
& :deep(.terminal) {
|
|
@apply overflow-hidden rounded border p-2;
|
|
&:is(.focus) {
|
|
@apply border-primary;
|
|
}
|
|
}
|
|
|
|
& :deep(.xterm-viewport) {
|
|
@apply bg-base-200!;
|
|
}
|
|
|
|
& :deep(.xterm-rows) {
|
|
@apply text-base-content;
|
|
}
|
|
|
|
& :deep(.xterm-cursor-block.xterm-cursor-blink) {
|
|
animation-name: blink !important;
|
|
}
|
|
}
|
|
|
|
@keyframes blink {
|
|
0% {
|
|
background-color: var(--color-base-content);
|
|
color: #000000;
|
|
}
|
|
|
|
50% {
|
|
background-color: inherit;
|
|
color: var(--color-base-content);
|
|
}
|
|
}
|
|
</style>
|