diff --git a/app/backend/static/css/output.css b/app/backend/static/css/output.css index 551c3af..20b6fd8 100644 --- a/app/backend/static/css/output.css +++ b/app/backend/static/css/output.css @@ -369,6 +369,9 @@ .block { display: block; } + .contents { + display: contents; + } .flex { display: flex; } diff --git a/app/backend/static/js/terminal.js b/app/backend/static/js/terminal.js index b850351..e682fce 100644 --- a/app/backend/static/js/terminal.js +++ b/app/backend/static/js/terminal.js @@ -31,61 +31,16 @@ document.addEventListener('alpine:init', () => { Alpine.data('terminalManager', () => ({ tabs: [], activeTab: 0, - terminals: {}, - sockets: {}, command: '', + _terminals: {}, + _sockets: {}, + _containers: {}, init() { + this._wrapper = document.getElementById('xterm-wrapper'); 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) { @@ -104,30 +59,82 @@ document.addEventListener('alpine:init', () => { 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); + // 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; - const tryOpen = (retries) => { - const container = document.getElementById('xterm-' + id); - if (container) { - this.terminals[id].open(container); - this._connectWebSocket(id, host.id, host.name); - } else if (retries > 0) { - requestAnimationFrame(() => tryOpen(retries - 1)); - } + // 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(); }; - this.$nextTick(() => tryOpen(10)); + + 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.$nextTick(() => { - const cur = this.tabs[this.activeTab]; - if (cur && this.terminals[cur.id]) { - this.terminals[cur.id].focus(); - } + 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) { @@ -135,24 +142,24 @@ document.addEventListener('alpine:init', () => { const tab = this.tabs[i]; const id = tab.id; - if (this.sockets[id]) { - this.sockets[id].close(); - delete this.sockets[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._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; } - - const cur = this.tabs[this.activeTab]; - if (cur && this.terminals[cur.id]) { - this.$nextTick(() => this.terminals[cur.id].focus()); - } + this._showActiveTerminal(); }, submitCommand() { @@ -160,7 +167,7 @@ document.addEventListener('alpine:init', () => { if (!cmd) return; const cur = this.tabs[this.activeTab]; if (!cur) return; - const ws = this.sockets[cur.id]; + const ws = this._sockets[cur.id]; if (ws && ws.readyState === WebSocket.OPEN) { ws.send(cmd + '\r'); } @@ -175,7 +182,7 @@ document.addEventListener('alpine:init', () => { clearBuffer() { const cur = this.tabs[this.activeTab]; if (!cur) return; - const term = this.terminals[cur.id]; + const term = this._terminals[cur.id]; if (term) { term.clear(); term.write('\x1b[2J\x1b[H'); diff --git a/app/backend/views/terminal_content.html b/app/backend/views/terminal_content.html index c9bcb13..4e7a427 100644 --- a/app/backend/views/terminal_content.html +++ b/app/backend/views/terminal_content.html @@ -49,16 +49,8 @@ - -