document.addEventListener('alpine:init', () => { const el = document.getElementById('hosts-data'); const hostOptions = el ? JSON.parse(el.textContent) : []; const selEl = document.getElementById('selected-host'); const selectedHostId = selEl ? JSON.parse(selEl.textContent) : ''; const termTheme = { background: '#ffffff', foreground: '#191c1e', cursor: '#0050cb', cursorAccent: '#ffffff', selectionBackground: '#0050cb33', black: '#191c1e', red: '#b3261e', green: '#006e2f', yellow: '#a86e0a', blue: '#0050cb', magenta: '#7e23cc', cyan: '#006493', white: '#191c1e', brightBlack: '#727686', brightRed: '#dc362e', brightGreen: '#008a42', brightYellow: '#c58800', brightBlue: '#2962d5', brightMagenta: '#9a40e8', brightCyan: '#007ab5', brightWhite: '#424656', }; Alpine.data('terminalManager', () => ({ tabs: [], activeTab: 0, terminals: {}, sockets: {}, command: '', init() { this.addTab(); }, _createTerminal(id, hostName) { const term = new Terminal({ theme: termTheme, fontFamily: "'JetBrains Mono', monospace", fontSize: 13, lineHeight: 1.5, cursorBlink: true, cursorStyle: 'bar', allowProposedApi: true, scrollback: 5000, }); this.terminals[id] = term; return term; }, _connectWebSocket(id, hostID, hostName) { const ws = new WebSocket( (location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + location.host + '/terminal/ws/' + hostID ); this.sockets[id] = ws; const term = this.terminals[id]; ws.onopen = () => { term.focus(); }; ws.onmessage = (ev) => { term.write(ev.data); }; ws.onclose = () => { term.write('\r\n\x1b[33m[Connection closed]\x1b[0m\r\n'); }; ws.onerror = () => { term.write('\r\n\x1b[31m[Connection error]\x1b[0m\r\n'); }; term.onData((data) => { if (ws.readyState === WebSocket.OPEN) { ws.send(data); } }); }, _getHostForTab() { let host; if (selectedHostId) { host = hostOptions.find((h) => h.id === selectedHostId) || hostOptions[0]; } if (!host) { host = hostOptions.length > 0 ? hostOptions[Math.floor(Math.random() * hostOptions.length)] : { id: 'h1', name: 'localhost', ip: '127.0.0.1' }; } return host; }, addTab() { const host = this._getHostForTab(); const id = 'tab-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6); this.tabs.push({ id, name: host.name, host: host.ip, hostID: host.id }); this.activeTab = this.tabs.length - 1; this._createTerminal(id, host.name); this.$nextTick(() => { const container = document.getElementById('xterm-' + id); if (container) { this.terminals[id].open(container); this._connectWebSocket(id, host.id, host.name); } }); }, switchTab(i) { this.activeTab = i; this.$nextTick(() => { const cur = this.tabs[this.activeTab]; if (cur && this.terminals[cur.id]) { this.terminals[cur.id].focus(); } }); }, closeTab(i) { if (this.tabs.length <= 1) return; const tab = this.tabs[i]; const id = tab.id; if (this.sockets[id]) { this.sockets[id].close(); delete this.sockets[id]; } if (this.terminals[id]) { this.terminals[id].dispose(); delete this.terminals[id]; } this.tabs.splice(i, 1); if (this.activeTab >= this.tabs.length) { this.activeTab = this.tabs.length - 1; } const cur = this.tabs[this.activeTab]; if (cur && this.terminals[cur.id]) { this.$nextTick(() => this.terminals[cur.id].focus()); } }, submitCommand() { const cmd = this.command.trim(); if (!cmd) return; const cur = this.tabs[this.activeTab]; if (!cur) return; const ws = this.sockets[cur.id]; if (ws && ws.readyState === WebSocket.OPEN) { ws.send(cmd + '\r'); } this.command = ''; }, insertCommand(cmd) { this.command = cmd; this.$nextTick(() => this.submitCommand()); }, clearBuffer() { const cur = this.tabs[this.activeTab]; if (!cur) return; const term = this.terminals[cur.id]; if (term) { term.clear(); term.write('\x1b[2J\x1b[H'); } }, })); });