feat: upgrade terminal to xterm.js + WebSocket

- Rewrite terminal.js: xterm.js Terminal per tab, WebSocket per connection
- Light theme terminal (Lumina: white bg, blue cursor, proper ANSI colors)
- Alpine.js keeps tab management, xterm.js handles rendering
- Each tab gets its own WebSocket to /terminal/ws/:hostID
- Mock handler iterates chars (fixes multi-char xterm.js messages)
- Sync mock commands with React template (help, ls, docker ps, etc.)
- Add xterm.css custom scrollbar styles
- terminal_content.html: xterm containers with Alpine show/hide per tab
This commit is contained in:
swanadiva
2026-07-09 18:32:33 +07:00
parent 7accb86203
commit ef4e44984b
5 changed files with 228 additions and 173 deletions
+11 -9
View File
@@ -33,7 +33,7 @@
<!-- Terminal shell frame -->
<div class="flex-1 bg-white border border-outline-variant rounded-2xl shadow-sm flex flex-col overflow-hidden min-h-0">
<!-- Terminal Header -->
<div class="px-4 py-2 bg-surface-container-low border-b border-outline-variant/30 flex justify-between items-center">
<div class="px-4 py-2 bg-surface-container-low border-b border-outline-variant/30 flex justify-between items-center shrink-0">
<div class="flex items-center gap-1.5">
<span class="w-3 h-3 rounded-full bg-error/80"></span>
<span class="w-3 h-3 rounded-full bg-primary-container/80"></span>
@@ -49,18 +49,20 @@
</div>
</div>
<!-- Console display -->
<div class="flex-1 overflow-y-auto min-h-0 font-mono text-sm leading-relaxed p-5 bg-white" id="terminal-output">
<template x-for="(line, idx) in history" :key="idx">
<div :class="line.type === 'input' ? 'text-on-surface font-bold' : line.type === 'error' ? 'text-error bg-error-container/20 px-2.5 py-1 rounded-md' : line.type === 'success' ? 'text-secondary bg-secondary-container/10 px-2.5 py-1 rounded-md font-semibold' : 'text-[#2244aa]'"
class="whitespace-pre-wrap" x-text="line.text"></div>
<!-- xterm.js containers — one per tab, shown/hidden by Alpine -->
<div class="flex-1 relative min-h-0">
<template x-for="(tab, idx) in tabs" :key="tab.id">
<div x-show="idx === activeTab"
x-cloak
class="absolute inset-0">
<div :id="'xterm-' + tab.id" class="w-full h-full"></div>
</div>
</template>
<div class="h-4"></div>
</div>
<!-- Input prompt -->
<form @submit.prevent="submitCommand()"
class="p-3 border-t border-outline-variant/40 bg-surface-container-low flex items-center gap-2.5">
class="p-3 border-t border-outline-variant/40 bg-surface-container-low flex items-center gap-2.5 shrink-0">
<span class="font-mono text-xs font-bold text-primary pl-2 shrink-0 flex items-center gap-1">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.75 17.25L12 21m0 0l-3.75-3.75M12 21V3"/></svg>
root@<span x-text="tabs.length ? tabs[activeTab].name : 'host'"></span>:~$
@@ -88,4 +90,4 @@
<button type="button" @click="insertCommand('clear')" class="px-2.5 py-1 rounded-md bg-surface-container border border-outline-variant/30 hover:border-primary hover:bg-primary-container/10 transition-colors">clear</button>
</div>
</div>
{{end}}
{{end}}