117 lines
3.6 KiB
TypeScript
117 lines
3.6 KiB
TypeScript
import { Terminal } from '@xterm/xterm'
|
|
import { FitAddon } from '@xterm/addon-fit'
|
|
import { SearchAddon } from '@xterm/addon-search'
|
|
import { WebLinksAddon } from '@xterm/addon-web-links'
|
|
import { WebglAddon } from '@xterm/addon-webgl'
|
|
import { useAuthStore } from '~/stores/auth.store'
|
|
import { useSessionStore } from '~/stores/session.store'
|
|
|
|
export function useTerminal() {
|
|
const auth = useAuthStore()
|
|
const sessions = useSessionStore()
|
|
let ws: WebSocket | null = null
|
|
|
|
function createTerminal(container: HTMLElement, options?: Partial<{ fontSize: number; scrollback: number }>) {
|
|
const term = new Terminal({
|
|
cursorBlink: true,
|
|
fontSize: options?.fontSize || 14,
|
|
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace",
|
|
scrollback: options?.scrollback || 10000,
|
|
theme: {
|
|
background: '#0a0a0f',
|
|
foreground: '#e4e4ef',
|
|
cursor: '#5c7cfa',
|
|
selectionBackground: '#364fc744',
|
|
},
|
|
})
|
|
|
|
const fitAddon = new FitAddon()
|
|
const searchAddon = new SearchAddon()
|
|
term.loadAddon(fitAddon)
|
|
term.loadAddon(searchAddon)
|
|
term.loadAddon(new WebLinksAddon())
|
|
|
|
term.open(container)
|
|
|
|
try {
|
|
term.loadAddon(new WebglAddon())
|
|
} catch {
|
|
// WebGL not available, fall back to canvas
|
|
}
|
|
|
|
fitAddon.fit()
|
|
const resizeObserver = new ResizeObserver(() => fitAddon.fit())
|
|
resizeObserver.observe(container)
|
|
|
|
return { term, fitAddon, searchAddon, resizeObserver }
|
|
}
|
|
|
|
function connectToHost(hostId: number, hostName: string, protocol: 'ssh', color: string | null, term: Terminal, fitAddon: FitAddon) {
|
|
const wsUrl = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws/terminal?token=${auth.token}`
|
|
ws = new WebSocket(wsUrl)
|
|
|
|
ws.onopen = () => {
|
|
ws!.send(JSON.stringify({ type: 'connect', hostId }))
|
|
}
|
|
|
|
ws.onmessage = (event) => {
|
|
const msg = JSON.parse(event.data)
|
|
switch (msg.type) {
|
|
case 'connected':
|
|
sessions.addSession({ id: msg.sessionId, hostId, hostName, protocol, color, active: true })
|
|
// Send initial terminal size
|
|
ws!.send(JSON.stringify({ type: 'resize', sessionId: msg.sessionId, cols: term.cols, rows: term.rows }))
|
|
break
|
|
case 'data':
|
|
term.write(msg.data)
|
|
break
|
|
case 'disconnected':
|
|
sessions.removeSession(msg.sessionId)
|
|
break
|
|
case 'host-key-verify':
|
|
// Auto-accept for now — full UX in polish phase
|
|
ws!.send(JSON.stringify({ type: 'host-key-accept' }))
|
|
break
|
|
case 'error':
|
|
term.write(`\r\n\x1b[31mError: ${msg.message}\x1b[0m\r\n`)
|
|
break
|
|
}
|
|
}
|
|
|
|
ws.onclose = () => {
|
|
term.write('\r\n\x1b[33mConnection closed.\x1b[0m\r\n')
|
|
}
|
|
|
|
// Terminal input → WebSocket
|
|
term.onData((data) => {
|
|
if (ws?.readyState === WebSocket.OPEN) {
|
|
const sessionId = sessions.activeSession?.id
|
|
if (sessionId) {
|
|
ws.send(JSON.stringify({ type: 'data', sessionId, data }))
|
|
}
|
|
}
|
|
})
|
|
|
|
// Terminal resize → WebSocket
|
|
term.onResize(({ cols, rows }) => {
|
|
if (ws?.readyState === WebSocket.OPEN) {
|
|
const sessionId = sessions.activeSession?.id
|
|
if (sessionId) {
|
|
ws.send(JSON.stringify({ type: 'resize', sessionId, cols, rows }))
|
|
}
|
|
}
|
|
})
|
|
|
|
return ws
|
|
}
|
|
|
|
function disconnect(sessionId: string) {
|
|
if (ws?.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify({ type: 'disconnect', sessionId }))
|
|
}
|
|
sessions.removeSession(sessionId)
|
|
}
|
|
|
|
return { createTerminal, connectToHost, disconnect }
|
|
}
|