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, command: '', _terminals: {}, _sockets: {}, _containers: {}, init() { this._wrapper = document.getElementById('xterm-wrapper'); this.addTab(); }, _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 }); // Create xterm container DOM element directly const container = document.createElement('div'); container.id = 'xterm-' + id; container.className = 'absolute inset-0'; container.style.display = 'none'; this._wrapper.appendChild(container); this._containers[id] = container; // Create xterm.js terminal 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; term.open(container); // Connect WebSocket const wsUrl = (location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + location.host + '/terminal/ws/' + host.id; console.log('[terminal] connecting to', wsUrl); const ws = new WebSocket(wsUrl); this._sockets[id] = ws; ws.onopen = () => { console.log('[terminal] ws open for', host.id); term.focus(); }; ws.onmessage = (ev) => { term.write(ev.data); }; ws.onclose = () => { console.log('[terminal] ws closed for', host.id); term.write('\r\n\x1b[33m[Connection closed]\x1b[0m\r\n'); }; ws.onerror = (err) => { console.error('[terminal] ws error for', host.id, err); term.write('\r\n\x1b[31m[Connection error]\x1b[0m\r\n'); }; term.onData((data) => { if (ws.readyState === WebSocket.OPEN) { ws.send(data); } }); // Switch to new tab this.activeTab = this.tabs.length - 1; this._showActiveTerminal(); }, switchTab(i) { this.activeTab = i; this._showActiveTerminal(); }, _showActiveTerminal() { const cur = this.tabs[this.activeTab]; if (!cur) return; Object.keys(this._containers).forEach((key) => { this._containers[key].style.display = key === cur.id ? 'block' : 'none'; }); if (this._terminals[cur.id]) { this._terminals[cur.id].focus(); this._terminals[cur.id].refresh(0, this._terminals[cur.id].rows); } }, 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]; } if (this._containers[id]) { this._containers[id].remove(); delete this._containers[id]; } this.tabs.splice(i, 1); if (this.activeTab >= this.tabs.length) { this.activeTab = this.tabs.length - 1; } this._showActiveTerminal(); }, 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'); } }, })); });