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:
@@ -102,34 +102,34 @@ func mockTerminal(c *websocket.Conn, host string) {
|
||||
c.WriteMessage(websocket.TextMessage, []byte(welcome))
|
||||
|
||||
prompt := fmt.Sprintf("\x1b[1;34m%s:~$\x1b[0m ", host)
|
||||
cwd := "~"
|
||||
c.WriteMessage(websocket.TextMessage, []byte(prompt))
|
||||
|
||||
cwd := "~"
|
||||
buf := ""
|
||||
|
||||
for {
|
||||
_, msg, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
input := string(msg)
|
||||
|
||||
switch {
|
||||
case input == "\r":
|
||||
output := handleCommand(buf, host, &cwd)
|
||||
c.WriteMessage(websocket.TextMessage, []byte(output+prompt))
|
||||
buf = ""
|
||||
case input == "\x7f":
|
||||
if len(buf) > 0 {
|
||||
buf = buf[:len(buf)-1]
|
||||
c.WriteMessage(websocket.TextMessage, []byte("\b \b"))
|
||||
}
|
||||
case input == "\x03":
|
||||
buf = ""
|
||||
c.WriteMessage(websocket.TextMessage, []byte("^C\r\n"+prompt))
|
||||
default:
|
||||
if input >= " " && input <= "~" {
|
||||
buf += input
|
||||
c.WriteMessage(websocket.TextMessage, []byte(input))
|
||||
for _, ch := range string(msg) {
|
||||
switch {
|
||||
case ch == '\r':
|
||||
output := handleCommand(buf, host, &cwd)
|
||||
c.WriteMessage(websocket.TextMessage, []byte(output+prompt))
|
||||
buf = ""
|
||||
case ch == '\x7f':
|
||||
if len(buf) > 0 {
|
||||
buf = buf[:len(buf)-1]
|
||||
c.WriteMessage(websocket.TextMessage, []byte("\b \b"))
|
||||
}
|
||||
case ch == '\x03':
|
||||
buf = ""
|
||||
c.WriteMessage(websocket.TextMessage, []byte("^C\r\n"+prompt))
|
||||
case ch >= ' ' && ch <= '~':
|
||||
buf += string(ch)
|
||||
c.WriteMessage(websocket.TextMessage, []byte(string(ch)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,41 +148,71 @@ func handleCommand(cmd, host string, cwd *string) string {
|
||||
return "\x1b[2J\x1b[H"
|
||||
case "exit", "logout":
|
||||
return "logout\r\n\x1b[2J\x1b[H\x1b[1;31mConnection closed.\x1b[0m\r\n"
|
||||
case "pwd":
|
||||
return *cwd + "\r\n"
|
||||
case "whoami":
|
||||
return "deploy\r\n"
|
||||
case "hostname":
|
||||
return host + "\r\n"
|
||||
case "date":
|
||||
return time.Now().Format("Mon Jan 2 15:04:05 MST 2006") + "\r\n"
|
||||
case "uptime":
|
||||
return fmt.Sprintf(" 13:42:17 up %d day, 1 user, load average: 0.08, 0.12, 0.10\r\n", 1+time.Now().Day()%30)
|
||||
case "uname":
|
||||
if len(parts) > 1 && parts[1] == "-a" {
|
||||
return fmt.Sprintf("Linux %s 6.8.0-45-generic #47-Ubuntu SMP PREEMPT x86_64 x86_64 x86_64 GNU/Linux\r\n", host)
|
||||
}
|
||||
return "Linux\r\n"
|
||||
case "help":
|
||||
return "HostKeeper Mock SSH Interactive Command Parser:\r\n" +
|
||||
" help - Display this support manifest list\r\n" +
|
||||
" ls - List contents of the current working directory\r\n" +
|
||||
" docker ps - List simulated running Docker containers on cluster\r\n" +
|
||||
" uname -a - Show operating system and machine kernel data\r\n" +
|
||||
" ping 8.8.8.8 - Probe network gateway performance\r\n" +
|
||||
" cat server.js - Output snippet of remote index server configuration\r\n" +
|
||||
" keychain - Query keychain credentials loaded for target session\r\n" +
|
||||
" clear - Wipe the terminal display buffer clean\r\n"
|
||||
case "ls":
|
||||
if *cwd == "~" || *cwd == "/home/deploy" {
|
||||
return "Documents Downloads projects config.yml README.md .env\r\n"
|
||||
return "drwxr-xr-x 3 root root 4096 Jul 6 12:00 controllers\r\n" +
|
||||
"drwxr-xr-x 2 root root 4096 Jul 6 12:00 models\r\n" +
|
||||
"drwxr-xr-x 2 root root 4096 Jul 6 12:00 routes\r\n" +
|
||||
"-rw-r--r-- 1 root root 280 Jul 6 11:34 .env\r\n" +
|
||||
"-rw-r--r-- 1 root root 1432 Jul 6 14:20 package.json\r\n" +
|
||||
"-rwxr-xr-x 1 root root 8412 Jul 6 15:43 server.js\r\n"
|
||||
}
|
||||
return "total 0\r\n"
|
||||
case "cd":
|
||||
if len(parts) > 1 { *cwd = parts[1] } else { *cwd = "~" }
|
||||
return ""
|
||||
case "docker ps":
|
||||
return "CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS\r\n" +
|
||||
"f87a2d12e9b0 node:18-alpine \"docker-entrypoint.s…\" 2 hours ago Up 2 hours 0.0.0.0:3000->3000/tcp\r\n" +
|
||||
"c23e84bf9211 postgres:15-alpine \"docker-entrypoint.s…\" 5 hours ago Up 5 hours 0.0.0.0:5432->5432/tcp\r\n" +
|
||||
"78da12b84e0c redis:7-alpine \"docker-entrypoint.s…\" 10 hours ago Up 10 hours 0.0.0.0:6379->6379/tcp\r\n"
|
||||
case "uname":
|
||||
if len(parts) > 1 && parts[1] == "-a" {
|
||||
return fmt.Sprintf("Linux %s 5.15.0-101-generic #111-Ubuntu SMP Wed Jul 6 21:27:00 UTC 2026 x86_64 GNU/Linux\r\n", host)
|
||||
}
|
||||
return "Linux\r\n"
|
||||
case "ping":
|
||||
return "64 bytes from 8.8.8.8: icmp_seq=1 ttl=116 time=12.4 ms\r\n" +
|
||||
"64 bytes from 8.8.8.8: icmp_seq=2 ttl=116 time=14.1 ms\r\n" +
|
||||
"64 bytes from 8.8.8.8: icmp_seq=3 ttl=116 time=11.8 ms\r\n" +
|
||||
"--- 8.8.8.8 ping statistics ---\r\n" +
|
||||
"3 packets transmitted, 3 received, 0% packet loss, rtt min/avg/max = 11.8/12.76/14.1 ms\r\n"
|
||||
case "cat":
|
||||
if len(parts) > 1 && parts[1] == "server.js" {
|
||||
return "const express = require(\"express\");\r\n" +
|
||||
"const app = express();\r\n" +
|
||||
"const PORT = process.env.PORT || 3000;\r\n" +
|
||||
"app.get(\"/api/health\", (req, res) => res.send({ status: \"healthy\" }));\r\n" +
|
||||
"app.listen(PORT, () => console.log(\"Server active on cluster ingress\"));\r\n"
|
||||
}
|
||||
if len(parts) > 1 {
|
||||
return fmt.Sprintf("\x1b[1;37m# Content of %s\x1b[0m\r\n(not implemented in mock shell)\r\n", parts[1])
|
||||
return fmt.Sprintf("# Content of %s\r\n(not implemented in mock shell)\r\n", parts[1])
|
||||
}
|
||||
return ""
|
||||
case "keychain":
|
||||
return "Keychain Credential Mapping Selected:\r\n" +
|
||||
" Active Key: id_ed25519_alex (ED25519 standard)\r\n" +
|
||||
" Encryption: AES-256 GCM cryptokey payload\r\n" +
|
||||
" Fingerprint: SHA256:7mP9K9+fVj5bW0vQ8zD1y2u3t4m5n6p7q8r9s0v1w2x\r\n"
|
||||
case "whoami":
|
||||
return "deploy\r\n"
|
||||
case "hostname":
|
||||
return host + "\r\n"
|
||||
case "pwd":
|
||||
return *cwd + "\r\n"
|
||||
case "echo":
|
||||
return strings.Join(parts[1:], " ") + "\r\n"
|
||||
case "ssh":
|
||||
return "\x1b[1;33mSSH forwarding not available in mock mode\x1b[0m\r\n"
|
||||
case "help":
|
||||
return "Available commands: clear, exit, pwd, whoami, hostname, date, uptime, uname, ls, cd, cat, echo, ssh, help\r\n"
|
||||
default:
|
||||
return fmt.Sprintf("\x1b[1;31m%s: command not found\x1b[0m\r\n", parts[0])
|
||||
return fmt.Sprintf("hostkeeper: command not found: \"%s\". Type \"help\" to view custom commands list.\r\n", parts[0])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user