193 lines
5.1 KiB
JavaScript
193 lines
5.1 KiB
JavaScript
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');
|
|
}
|
|
},
|
|
}));
|
|
});
|