diff options
Diffstat (limited to 'html/src/components')
-rw-r--r-- | html/src/components/terminal/index.tsx | 109 |
1 files changed, 65 insertions, 44 deletions
diff --git a/html/src/components/terminal/index.tsx b/html/src/components/terminal/index.tsx index b344287..7368915 100644 --- a/html/src/components/terminal/index.tsx +++ b/html/src/components/terminal/index.tsx @@ -1,5 +1,4 @@ import { bind } from 'decko'; -import * as backoff from 'backoff'; import { Component, h } from 'preact'; import { ITerminalOptions, Terminal } from 'xterm'; import { FitAddon } from 'xterm-addon-fit'; @@ -58,18 +57,17 @@ export class Xterm extends Component<Props> { private fitAddon: FitAddon; private overlayAddon: OverlayAddon; private zmodemAddon: ZmodemAddon; + private webglAddon: WebglAddon; private socket: WebSocket; private token: string; + private opened = false; private title: string; private titleFixed: string; private resizeTimeout: number; private resizeOverlay = true; - - private backoff: backoff.Backoff; - private backoffLock = false; - private doBackoff = true; - private reconnect = false; + private reconnect = true; + private doReconnect = true; constructor(props: Props) { super(props); @@ -78,22 +76,6 @@ export class Xterm extends Component<Props> { this.textDecoder = new TextDecoder(); this.fitAddon = new FitAddon(); this.overlayAddon = new OverlayAddon(); - this.backoff = backoff.exponential({ - initialDelay: 100, - maxDelay: 10000, - }); - this.backoff.failAfter(15); - this.backoff.on('ready', () => { - this.backoffLock = false; - this.refreshToken().then(this.connect); - }); - this.backoff.on('backoff', (_, delay: number) => { - console.log(`[ttyd] will attempt to reconnect websocket in ${delay}ms`); - this.backoffLock = true; - }); - this.backoff.on('fail', () => { - this.backoffLock = true; // break backoff - }); } async componentDidMount() { @@ -225,8 +207,7 @@ export class Xterm extends Component<Props> { } @bind - private applyOptions(options: any) { - const { terminal, fitAddon } = this; + private setRendererType(value: string) { const isWebGL2Available = () => { try { const canvas = document.createElement('canvas'); @@ -236,14 +217,38 @@ export class Xterm extends Component<Props> { } }; + const { terminal } = this; + switch (value) { + case 'webgl': + if (this.webglAddon) return; + if (isWebGL2Available()) { + this.webglAddon = new WebglAddon(); + terminal.loadAddon(this.webglAddon); + console.log(`[ttyd] WebGL renderer enabled`); + } + break; + default: + try { + this.webglAddon?.dispose(); + } catch { + // ignore + } + this.webglAddon = undefined; + console.log(`[ttyd] option: rendererType=${value}`); + terminal.setOption('rendererType', value); + break; + } + } + + @bind + private applyOptions(options: any) { + const { terminal, fitAddon } = this; + Object.keys(options).forEach(key => { const value = options[key]; switch (key) { case 'rendererType': - if (value === 'webgl' && isWebGL2Available()) { - terminal.loadAddon(new WebglAddon()); - console.log(`[ttyd] WebGL renderer enabled`); - } + this.setRendererType(value); break; case 'disableLeaveAlert': if (value) { @@ -260,7 +265,7 @@ export class Xterm extends Component<Props> { case 'disableReconnect': if (value) { console.log(`[ttyd] Reconnect disabled`); - this.doBackoff = false; + this.reconnect = false; } break; case 'titleFixed': @@ -281,23 +286,29 @@ export class Xterm extends Component<Props> { @bind private onSocketOpen() { console.log('[ttyd] websocket connection opened'); - this.backoff.reset(); const { socket, textEncoder, terminal, fitAddon, overlayAddon } = this; - socket.send(textEncoder.encode(JSON.stringify({ AuthToken: this.token }))); + const dims = fitAddon.proposeDimensions(); + socket.send( + textEncoder.encode( + JSON.stringify({ + AuthToken: this.token, + columns: dims.cols, + rows: dims.rows, + }) + ) + ); - if (this.reconnect) { - const dims = fitAddon.proposeDimensions(); + if (this.opened) { terminal.reset(); terminal.resize(dims.cols, dims.rows); - this.onTerminalResize(dims); // may not be triggered by terminal.resize overlayAddon.showOverlay('Reconnected', 300); } else { - this.reconnect = true; + this.opened = true; fitAddon.fit(); } - this.applyOptions(this.props.clientOptions); + this.doReconnect = this.reconnect; terminal.focus(); } @@ -306,22 +317,31 @@ export class Xterm extends Component<Props> { private onSocketClose(event: CloseEvent) { console.log(`[ttyd] websocket connection closed with code: ${event.code}`); - const { backoff, doBackoff, backoffLock, overlayAddon } = this; + const { refreshToken, connect, doReconnect, overlayAddon } = this; overlayAddon.showOverlay('Connection Closed', null); // 1000: CLOSE_NORMAL - if (event.code !== 1000 && doBackoff && !backoffLock) { - backoff.backoff(); + if (event.code !== 1000 && doReconnect) { + overlayAddon.showOverlay('Reconnecting...', null); + refreshToken().then(connect); + } else { + const { terminal } = this; + const keyDispose = terminal.onKey(e => { + const event = e.domEvent; + if (event.key === 'Enter') { + keyDispose.dispose(); + overlayAddon.showOverlay('Reconnecting...', null); + refreshToken().then(connect); + } + }); + overlayAddon.showOverlay('Press ⏎ to Reconnect', null); } } @bind private onSocketError(event: Event) { console.error('[ttyd] websocket connection error: ', event); - const { backoff, doBackoff, backoffLock } = this; - if (doBackoff && !backoffLock) { - backoff.backoff(); - } + this.doReconnect = false; } @bind @@ -340,7 +360,8 @@ export class Xterm extends Component<Props> { document.title = this.title; break; case Command.SET_PREFERENCES: - this.applyOptions(JSON.parse(textDecoder.decode(data))); + const prefs = JSON.parse(textDecoder.decode(data)); + this.applyOptions(Object.assign({}, this.props.clientOptions, prefs)); break; default: console.warn(`[ttyd] unknown command: ${cmd}`); |