1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-21 13:23:07 +01:00
Files
dozzle/assets/components/Terminal.vue
2025-12-12 16:15:12 -08:00

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>