diff --git a/html/.editorconfig b/html/.editorconfig index e170b416..54ee2b17 100644 --- a/html/.editorconfig +++ b/html/.editorconfig @@ -2,10 +2,13 @@ root = true [*] charset = utf-8 -indent_size = 2 +indent_size = 4 indent_style = space insert_final_newline = true trim_trailing_whitespace = true +[{*.json, *.scss}] +indent_size = 2 + [*.md] trim_trailing_whitespace = false diff --git a/html/prettier.config.js b/html/prettier.config.js new file mode 100644 index 00000000..0545cab0 --- /dev/null +++ b/html/prettier.config.js @@ -0,0 +1,6 @@ +module.exports = { + trailingComma: "es5", + tabWidth: 4, + printWidth: 120, + singleQuote: true, +}; diff --git a/html/src/components/app.tsx b/html/src/components/app.tsx index 69199a18..e9f50ace 100644 --- a/html/src/components/app.tsx +++ b/html/src/components/app.tsx @@ -4,48 +4,41 @@ import { ITerminalOptions, ITheme } from 'xterm'; import { Xterm } from './terminal'; if ((module as any).hot) { - // tslint:disable-next-line:no-var-requires - require('preact/debug'); + // tslint:disable-next-line:no-var-requires + require('preact/debug'); } const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; const wsPath = window.location.pathname.endsWith('/') ? 'ws' : '/ws'; -const url = [ - protocol, - window.location.host, - window.location.pathname, - wsPath, - window.location.search, -].join(''); +const url = [protocol, window.location.host, window.location.pathname, wsPath, window.location.search].join(''); const termOptions = { - fontSize: 13, - fontFamily: - 'Menlo For Powerline,Consolas,Liberation Mono,Menlo,Courier,monospace', - theme: { - foreground: '#d2d2d2', - background: '#2b2b2b', - cursor: '#adadad', - black: '#000000', - red: '#d81e00', - green: '#5ea702', - yellow: '#cfae00', - blue: '#427ab3', - magenta: '#89658e', - cyan: '#00a7aa', - white: '#dbded8', - brightBlack: '#686a66', - brightRed: '#f54235', - brightGreen: '#99e343', - brightYellow: '#fdeb61', - brightBlue: '#84b0d8', - brightMagenta: '#bc94b7', - brightCyan: '#37e6e8', - brightWhite: '#f1f1f0', - } as ITheme, + fontSize: 13, + fontFamily: 'Menlo For Powerline,Consolas,Liberation Mono,Menlo,Courier,monospace', + theme: { + foreground: '#d2d2d2', + background: '#2b2b2b', + cursor: '#adadad', + black: '#000000', + red: '#d81e00', + green: '#5ea702', + yellow: '#cfae00', + blue: '#427ab3', + magenta: '#89658e', + cyan: '#00a7aa', + white: '#dbded8', + brightBlack: '#686a66', + brightRed: '#f54235', + brightGreen: '#99e343', + brightYellow: '#fdeb61', + brightBlue: '#84b0d8', + brightMagenta: '#bc94b7', + brightCyan: '#37e6e8', + brightWhite: '#f1f1f0', + } as ITheme, } as ITerminalOptions; export class App extends Component { - render() { - return ; - } + render() { + return ; + } } diff --git a/html/src/components/modal/index.tsx b/html/src/components/modal/index.tsx index b0b30f34..51ec82a7 100644 --- a/html/src/components/modal/index.tsx +++ b/html/src/components/modal/index.tsx @@ -3,25 +3,25 @@ import { Component, ComponentChildren, h } from 'preact'; import './modal.scss'; interface Props { - show: boolean; - children: ComponentChildren; + show: boolean; + children: ComponentChildren; } export class Modal extends Component { - constructor(props) { - super(props); - } + constructor(props) { + super(props); + } - render({ show, children }: Props) { - return ( - show && ( -
-
-
-
{children}
-
-
- ) - ); - } + render({ show, children }: Props) { + return ( + show && ( +
+
+
+
{children}
+
+
+ ) + ); + } } diff --git a/html/src/components/terminal/index.tsx b/html/src/components/terminal/index.tsx index 3a7a5944..b7c430bb 100644 --- a/html/src/components/terminal/index.tsx +++ b/html/src/components/terminal/index.tsx @@ -10,214 +10,211 @@ import { ZmodemAddon } from '../zmodem'; import 'xterm/dist/xterm.css'; export interface WindowExtended extends Window { - term: Terminal; - tty_auth_token?: string; + term: Terminal; + tty_auth_token?: string; } declare let window: WindowExtended; const enum Command { - // server side - OUTPUT = '0', - SET_WINDOW_TITLE = '1', - SET_PREFERENCES = '2', - SET_RECONNECT = '3', - - // client side - INPUT = '0', - RESIZE_TERMINAL = '1', + // server side + OUTPUT = '0', + SET_WINDOW_TITLE = '1', + SET_PREFERENCES = '2', + SET_RECONNECT = '3', + + // client side + INPUT = '0', + RESIZE_TERMINAL = '1', } interface Props { - id: string; - url: string; - options: ITerminalOptions; + id: string; + url: string; + options: ITerminalOptions; } export class Xterm extends Component { - private textEncoder: TextEncoder; - private textDecoder: TextDecoder; - private container: HTMLElement; - private terminal: Terminal; - private fitAddon: FitAddon; - private overlayAddon: OverlayAddon; - private zmodemAddon: ZmodemAddon; - private socket: WebSocket; - private title: string; - private reconnect: number; - private resizeTimeout: number; - - constructor(props) { - super(props); - - this.textEncoder = new TextEncoder(); - this.textDecoder = new TextDecoder(); - this.fitAddon = new FitAddon(); - this.overlayAddon = new OverlayAddon(); - } - - componentDidMount() { - this.openTerminal(); - } - - componentWillUnmount() { - this.socket.close(); - this.terminal.dispose(); - - window.removeEventListener('resize', this.onWindowResize); - window.removeEventListener('beforeunload', this.onWindowUnload); - } - - render({ id }: Props) { - return ( -
(this.container = c)}> - (this.zmodemAddon = c)} sender={this.sendData} /> -
- ); - } - - @bind - private sendData(data: ArrayLike) { - const { socket } = this; - const payload = new Uint8Array(data.length + 1); - payload[0] = Command.INPUT.charCodeAt(0); - payload.set(data, 1); - socket.send(payload); - } - - @bind - private onWindowResize() { - const { fitAddon } = this; - clearTimeout(this.resizeTimeout); - this.resizeTimeout = setTimeout(() => fitAddon.fit(), 250) as any; - } - - private onWindowUnload(event: BeforeUnloadEvent): string { - const message = 'Close terminal? this will also terminate the command.'; - event.returnValue = message; - return message; - } - - @bind - private openTerminal() { - if (this.terminal) { - this.terminal.dispose(); + private textEncoder: TextEncoder; + private textDecoder: TextDecoder; + private container: HTMLElement; + private terminal: Terminal; + private fitAddon: FitAddon; + private overlayAddon: OverlayAddon; + private zmodemAddon: ZmodemAddon; + private socket: WebSocket; + private title: string; + private reconnect: number; + private resizeTimeout: number; + + constructor(props) { + super(props); + + this.textEncoder = new TextEncoder(); + this.textDecoder = new TextDecoder(); + this.fitAddon = new FitAddon(); + this.overlayAddon = new OverlayAddon(); } - this.socket = new WebSocket(this.props.url, ['tty']); - this.terminal = new Terminal(this.props.options); - const { socket, terminal, container, fitAddon, overlayAddon } = this; - window.term = terminal; - - socket.binaryType = 'arraybuffer'; - socket.onopen = this.onSocketOpen; - socket.onmessage = this.onSocketData; - socket.onclose = this.onSocketClose; - - terminal.loadAddon(fitAddon); - terminal.loadAddon(overlayAddon); - terminal.loadAddon(new WebLinksAddon()); - terminal.loadAddon(this.zmodemAddon); - - terminal.onTitleChange(data => { - if (data && data !== '') { - document.title = data + ' | ' + this.title; - } - }); - terminal.onData(this.onTerminalData); - terminal.onResize(this.onTerminalResize); - if ( - document.queryCommandSupported && - document.queryCommandSupported('copy') - ) { - terminal.onSelectionChange(() => { - overlayAddon.showOverlay('\u2702', 200); - document.execCommand('copy'); - }); + componentDidMount() { + this.openTerminal(); } - terminal.open(container); - terminal.focus(); - - window.addEventListener('resize', this.onWindowResize); - window.addEventListener('beforeunload', this.onWindowUnload); - } - - @bind - private onSocketOpen() { - console.log('[ttyd] Websocket connection opened'); - const { socket, textEncoder, fitAddon } = this; - const authToken = window.tty_auth_token; - - socket.send(textEncoder.encode(JSON.stringify({ AuthToken: authToken }))); - fitAddon.fit(); - } - - @bind - private onSocketClose(event: CloseEvent) { - console.log(`[ttyd] websocket connection closed with code: ${event.code}`); - - const { overlayAddon, openTerminal, reconnect } = this; - overlayAddon.showOverlay('Connection Closed', null); - window.removeEventListener('beforeunload', this.onWindowUnload); - - // 1008: POLICY_VIOLATION - Auth failure - if (event.code === 1008) { - window.location.reload(); + + componentWillUnmount() { + this.socket.close(); + this.terminal.dispose(); + + window.removeEventListener('resize', this.onWindowResize); + window.removeEventListener('beforeunload', this.onWindowUnload); + } + + render({ id }: Props) { + return ( +
(this.container = c)}> + (this.zmodemAddon = c)} sender={this.sendData} /> +
+ ); + } + + @bind + private sendData(data: ArrayLike) { + const { socket } = this; + const payload = new Uint8Array(data.length + 1); + payload[0] = Command.INPUT.charCodeAt(0); + payload.set(data, 1); + socket.send(payload); } - // 1000: CLOSE_NORMAL - if (event.code !== 1000 && reconnect > 0) { - setTimeout(openTerminal, reconnect * 1000); + + @bind + private onWindowResize() { + const { fitAddon } = this; + clearTimeout(this.resizeTimeout); + this.resizeTimeout = setTimeout(() => fitAddon.fit(), 250) as any; } - } - - @bind - private onSocketData(event: MessageEvent) { - const { terminal, textDecoder, zmodemAddon } = this; - const rawData = event.data as ArrayBuffer; - const cmd = String.fromCharCode(new Uint8Array(rawData)[0]); - const data = rawData.slice(1); - - switch (cmd) { - case Command.OUTPUT: - zmodemAddon.consume(data); - break; - case Command.SET_WINDOW_TITLE: - this.title = textDecoder.decode(data); - document.title = this.title; - break; - case Command.SET_PREFERENCES: - const preferences = JSON.parse(textDecoder.decode(data)); - Object.keys(preferences).forEach(key => { - console.log(`[ttyd] setting ${key}: ${preferences[key]}`); - terminal.setOption(key, preferences[key]); + + private onWindowUnload(event: BeforeUnloadEvent): string { + const message = 'Close terminal? this will also terminate the command.'; + event.returnValue = message; + return message; + } + + @bind + private openTerminal() { + if (this.terminal) { + this.terminal.dispose(); + } + + this.socket = new WebSocket(this.props.url, ['tty']); + this.terminal = new Terminal(this.props.options); + const { socket, terminal, container, fitAddon, overlayAddon } = this; + window.term = terminal; + + socket.binaryType = 'arraybuffer'; + socket.onopen = this.onSocketOpen; + socket.onmessage = this.onSocketData; + socket.onclose = this.onSocketClose; + + terminal.loadAddon(fitAddon); + terminal.loadAddon(overlayAddon); + terminal.loadAddon(new WebLinksAddon()); + terminal.loadAddon(this.zmodemAddon); + + terminal.onTitleChange(data => { + if (data && data !== '') { + document.title = data + ' | ' + this.title; + } }); - break; - case Command.SET_RECONNECT: - this.reconnect = Number(textDecoder.decode(data)); - console.log(`[ttyd] enabling reconnect: ${this.reconnect} seconds`); - break; - default: - console.warn(`[ttyd] unknown command: ${cmd}`); - break; + terminal.onData(this.onTerminalData); + terminal.onResize(this.onTerminalResize); + if (document.queryCommandSupported && document.queryCommandSupported('copy')) { + terminal.onSelectionChange(() => { + overlayAddon.showOverlay('\u2702', 200); + document.execCommand('copy'); + }); + } + terminal.open(container); + terminal.focus(); + + window.addEventListener('resize', this.onWindowResize); + window.addEventListener('beforeunload', this.onWindowUnload); } - } - - @bind - private onTerminalResize(size: { cols: number; rows: number }) { - const { overlayAddon, socket, textEncoder } = this; - if (socket.readyState === WebSocket.OPEN) { - const msg = JSON.stringify({ columns: size.cols, rows: size.rows }); - socket.send(textEncoder.encode(Command.RESIZE_TERMINAL + msg)); + + @bind + private onSocketOpen() { + console.log('[ttyd] Websocket connection opened'); + const { socket, textEncoder, fitAddon } = this; + const authToken = window.tty_auth_token; + + socket.send(textEncoder.encode(JSON.stringify({ AuthToken: authToken }))); + fitAddon.fit(); } - setTimeout(() => { - overlayAddon.showOverlay(`${size.cols}x${size.rows}`); - }, 500); - } - - @bind - private onTerminalData(data: string) { - const { socket, textEncoder } = this; - if (socket.readyState === WebSocket.OPEN) { - socket.send(textEncoder.encode(Command.INPUT + data)); + + @bind + private onSocketClose(event: CloseEvent) { + console.log(`[ttyd] websocket connection closed with code: ${event.code}`); + + const { overlayAddon, openTerminal, reconnect } = this; + overlayAddon.showOverlay('Connection Closed', null); + window.removeEventListener('beforeunload', this.onWindowUnload); + + // 1008: POLICY_VIOLATION - Auth failure + if (event.code === 1008) { + window.location.reload(); + } + // 1000: CLOSE_NORMAL + if (event.code !== 1000 && reconnect > 0) { + setTimeout(openTerminal, reconnect * 1000); + } + } + + @bind + private onSocketData(event: MessageEvent) { + const { terminal, textDecoder, zmodemAddon } = this; + const rawData = event.data as ArrayBuffer; + const cmd = String.fromCharCode(new Uint8Array(rawData)[0]); + const data = rawData.slice(1); + + switch (cmd) { + case Command.OUTPUT: + zmodemAddon.consume(data); + break; + case Command.SET_WINDOW_TITLE: + this.title = textDecoder.decode(data); + document.title = this.title; + break; + case Command.SET_PREFERENCES: + const preferences = JSON.parse(textDecoder.decode(data)); + Object.keys(preferences).forEach(key => { + console.log(`[ttyd] setting ${key}: ${preferences[key]}`); + terminal.setOption(key, preferences[key]); + }); + break; + case Command.SET_RECONNECT: + this.reconnect = Number(textDecoder.decode(data)); + console.log(`[ttyd] enabling reconnect: ${this.reconnect} seconds`); + break; + default: + console.warn(`[ttyd] unknown command: ${cmd}`); + break; + } + } + + @bind + private onTerminalResize(size: { cols: number; rows: number }) { + const { overlayAddon, socket, textEncoder } = this; + if (socket.readyState === WebSocket.OPEN) { + const msg = JSON.stringify({ columns: size.cols, rows: size.rows }); + socket.send(textEncoder.encode(Command.RESIZE_TERMINAL + msg)); + } + setTimeout(() => { + overlayAddon.showOverlay(`${size.cols}x${size.rows}`); + }, 500); + } + + @bind + private onTerminalData(data: string) { + const { socket, textEncoder } = this; + if (socket.readyState === WebSocket.OPEN) { + socket.send(textEncoder.encode(Command.INPUT + data)); + } } - } } diff --git a/html/src/components/terminal/overlay.ts b/html/src/components/terminal/overlay.ts index 5179001f..0438acf3 100644 --- a/html/src/components/terminal/overlay.ts +++ b/html/src/components/terminal/overlay.ts @@ -3,13 +3,13 @@ import { ITerminalAddon, Terminal } from 'xterm'; export class OverlayAddon implements ITerminalAddon { - private terminal: Terminal | undefined; - private overlayNode: HTMLElement | null; - private overlayTimeout: number | null; + private terminal: Terminal | undefined; + private overlayNode: HTMLElement | null; + private overlayTimeout: number | null; - constructor() { - this.overlayNode = document.createElement('div'); - this.overlayNode.style.cssText = `border-radius: 15px; + constructor() { + this.overlayNode = document.createElement('div'); + this.overlayNode.style.cssText = `border-radius: 15px; font-size: xx-large; opacity: 0.75; padding: 0.2em 0.5em 0.2em 0.5em; @@ -19,57 +19,57 @@ position: absolute; -moz-user-select: none; -moz-transition: opacity 180ms ease-in;`; - this.overlayNode.addEventListener( - 'mousedown', - e => { - e.preventDefault(); - e.stopPropagation(); - }, - true - ); - } - - activate(terminal: Terminal): void { - this.terminal = terminal; - } + this.overlayNode.addEventListener( + 'mousedown', + e => { + e.preventDefault(); + e.stopPropagation(); + }, + true + ); + } - dispose(): void {} + activate(terminal: Terminal): void { + this.terminal = terminal; + } - showOverlay(msg: string, timeout?: number): void { - const { terminal, overlayNode } = this; + dispose(): void {} - overlayNode.style.color = '#101010'; - overlayNode.style.backgroundColor = '#f0f0f0'; - overlayNode.textContent = msg; - overlayNode.style.opacity = '0.75'; + showOverlay(msg: string, timeout?: number): void { + const { terminal, overlayNode } = this; - if (!overlayNode.parentNode) { - terminal.element.appendChild(overlayNode); - } + overlayNode.style.color = '#101010'; + overlayNode.style.backgroundColor = '#f0f0f0'; + overlayNode.textContent = msg; + overlayNode.style.opacity = '0.75'; - const divSize = terminal.element.getBoundingClientRect(); - const overlaySize = overlayNode.getBoundingClientRect(); + if (!overlayNode.parentNode) { + terminal.element.appendChild(overlayNode); + } - overlayNode.style.top = (divSize.height - overlaySize.height) / 2 + 'px'; - overlayNode.style.left = (divSize.width - overlaySize.width) / 2 + 'px'; + const divSize = terminal.element.getBoundingClientRect(); + const overlaySize = overlayNode.getBoundingClientRect(); - if (this.overlayTimeout) { - clearTimeout(this.overlayTimeout); - } - if (timeout === null) { - return; - } + overlayNode.style.top = (divSize.height - overlaySize.height) / 2 + 'px'; + overlayNode.style.left = (divSize.width - overlaySize.width) / 2 + 'px'; - const self = this; - self.overlayTimeout = setTimeout(() => { - overlayNode.style.opacity = '0'; - self.overlayTimeout = setTimeout(() => { - if (overlayNode.parentNode) { - overlayNode.parentNode.removeChild(overlayNode); + if (this.overlayTimeout) { + clearTimeout(this.overlayTimeout); } - self.overlayTimeout = null; - overlayNode.style.opacity = '0.75'; - }, 200) as any; - }, timeout || 1500) as any; - } + if (timeout === null) { + return; + } + + const self = this; + self.overlayTimeout = setTimeout(() => { + overlayNode.style.opacity = '0'; + self.overlayTimeout = setTimeout(() => { + if (overlayNode.parentNode) { + overlayNode.parentNode.removeChild(overlayNode); + } + self.overlayTimeout = null; + overlayNode.style.opacity = '0.75'; + }, 200) as any; + }, timeout || 1500) as any; + } } diff --git a/html/src/components/zmodem/index.tsx b/html/src/components/zmodem/index.tsx index 5938d2f2..6a931c0c 100644 --- a/html/src/components/zmodem/index.tsx +++ b/html/src/components/zmodem/index.tsx @@ -6,157 +6,149 @@ import * as Zmodem from 'zmodem.js/src/zmodem_browser'; import { Modal } from '../modal'; interface Props { - sender: (data: ArrayLike) => void; + sender: (data: ArrayLike) => void; } interface State { - modal: boolean; + modal: boolean; } -export class ZmodemAddon extends Component - implements ITerminalAddon { - private terminal: Terminal | undefined; - private sentry: Zmodem.Sentry; - private session: Zmodem.Session; - - constructor(props) { - super(props); - - this.sentry = new Zmodem.Sentry({ - to_terminal: (octets: ArrayBuffer) => this.zmodemWrite(octets), - sender: (octets: ArrayLike) => this.zmodemSend(octets), - on_retract: () => this.zmodemRetract(), - on_detect: (detection: any) => this.zmodemDetect(detection), - }); - } - - render(_, { modal }: State) { - return ( - - - - ); - } - - activate(terminal: Terminal): void { - this.terminal = terminal; - } - - dispose(): void {} - - consume(data: ArrayBuffer) { - const { sentry, terminal } = this; - try { - sentry.consume(data); - } catch (e) { - console.log(`[ttyd] zmodem consume: `, e); - terminal.setOption('disableStdin', false); +export class ZmodemAddon extends Component implements ITerminalAddon { + private terminal: Terminal | undefined; + private sentry: Zmodem.Sentry; + private session: Zmodem.Session; + + constructor(props) { + super(props); + + this.sentry = new Zmodem.Sentry({ + to_terminal: (octets: ArrayBuffer) => this.zmodemWrite(octets), + sender: (octets: ArrayLike) => this.zmodemSend(octets), + on_retract: () => this.zmodemRetract(), + on_detect: (detection: any) => this.zmodemDetect(detection), + }); } - } - - @bind - private zmodemWrite(data: ArrayBuffer): void { - this.terminal.writeUtf8(new Uint8Array(data)); - } - - @bind - private zmodemSend(data: ArrayLike): void { - this.props.sender(data); - } - - @bind - private zmodemRetract(): void { - this.terminal.setOption('disableStdin', false); - } - - @bind - private zmodemDetect(detection: Zmodem.Detection): void { - const { terminal, receiveFile } = this; - terminal.setOption('disableStdin', true); - this.session = detection.confirm(); - - if (this.session.type === 'send') { - this.setState({ modal: true }); - } else { - receiveFile(); + + render(_, { modal }: State) { + return ( + + + + ); } - } - - @bind - private sendFile(event: Event) { - this.setState({ modal: false }); - - const { terminal, session, writeProgress } = this; - const files: FileList = (event.target as HTMLInputElement).files; - - Zmodem.Browser.send_files(session, files, { - on_progress: (_, xfer: any) => writeProgress(xfer), - }) - .then(() => { - session.close(); - terminal.setOption('disableStdin', false); - }) - .catch(e => { - console.log(`[ttyd] zmodem send: `, e); - }); - } - - @bind - private receiveFile() { - const { terminal, session, writeProgress } = this; - - session.on('offer', (xfer: any) => { - const fileBuffer = []; - xfer.on('input', payload => { - writeProgress(xfer); - fileBuffer.push(new Uint8Array(payload)); - }); - xfer.accept().then(() => { - Zmodem.Browser.save_to_disk(fileBuffer, xfer.get_details().name); - }); - }); - - session.on('session_end', () => { - terminal.setOption('disableStdin', false); - }); - - session.start(); - } - - @bind - private writeProgress(xfer: any) { - const { terminal, bytesHuman } = this; - - const file = xfer.get_details(); - const name = file.name; - const size = file.size; - const offset = xfer.get_offset(); - const percent = ((100 * offset) / size).toFixed(2); - - terminal.write( - `${name} ${percent}% ${bytesHuman(offset, 2)}/${bytesHuman(size, 2)}\r` - ); - } - - private bytesHuman(bytes: any, precision: number): string { - if (!/^([-+])?|(\.\d+)(\d+(\.\d+)?|(\d+\.)|Infinity)$/.test(bytes)) { - return '-'; + + activate(terminal: Terminal): void { + this.terminal = terminal; + } + + dispose(): void {} + + consume(data: ArrayBuffer) { + const { sentry, terminal } = this; + try { + sentry.consume(data); + } catch (e) { + console.log(`[ttyd] zmodem consume: `, e); + terminal.setOption('disableStdin', false); + } + } + + @bind + private zmodemWrite(data: ArrayBuffer): void { + this.terminal.writeUtf8(new Uint8Array(data)); + } + + @bind + private zmodemSend(data: ArrayLike): void { + this.props.sender(data); + } + + @bind + private zmodemRetract(): void { + this.terminal.setOption('disableStdin', false); + } + + @bind + private zmodemDetect(detection: Zmodem.Detection): void { + const { terminal, receiveFile } = this; + terminal.setOption('disableStdin', true); + this.session = detection.confirm(); + + if (this.session.type === 'send') { + this.setState({ modal: true }); + } else { + receiveFile(); + } + } + + @bind + private sendFile(event: Event) { + this.setState({ modal: false }); + + const { terminal, session, writeProgress } = this; + const files: FileList = (event.target as HTMLInputElement).files; + + Zmodem.Browser.send_files(session, files, { + on_progress: (_, xfer: any) => writeProgress(xfer), + }) + .then(() => { + session.close(); + terminal.setOption('disableStdin', false); + }) + .catch(e => { + console.log(`[ttyd] zmodem send: `, e); + }); + } + + @bind + private receiveFile() { + const { terminal, session, writeProgress } = this; + + session.on('offer', (xfer: any) => { + const fileBuffer = []; + xfer.on('input', payload => { + writeProgress(xfer); + fileBuffer.push(new Uint8Array(payload)); + }); + xfer.accept().then(() => { + Zmodem.Browser.save_to_disk(fileBuffer, xfer.get_details().name); + }); + }); + + session.on('session_end', () => { + terminal.setOption('disableStdin', false); + }); + + session.start(); + } + + @bind + private writeProgress(xfer: any) { + const { terminal, bytesHuman } = this; + + const file = xfer.get_details(); + const name = file.name; + const size = file.size; + const offset = xfer.get_offset(); + const percent = ((100 * offset) / size).toFixed(2); + + terminal.write(`${name} ${percent}% ${bytesHuman(offset, 2)}/${bytesHuman(size, 2)}\r`); + } + + private bytesHuman(bytes: any, precision: number): string { + if (!/^([-+])?|(\.\d+)(\d+(\.\d+)?|(\d+\.)|Infinity)$/.test(bytes)) { + return '-'; + } + if (bytes === 0) return '0'; + if (typeof precision === 'undefined') precision = 1; + const units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; + const num = Math.floor(Math.log(bytes) / Math.log(1024)); + const value = (bytes / Math.pow(1024, Math.floor(num))).toFixed(precision); + return `${value} ${units[num]}`; } - if (bytes === 0) return '0'; - if (typeof precision === 'undefined') precision = 1; - const units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; - const num = Math.floor(Math.log(bytes) / Math.log(1024)); - const value = (bytes / Math.pow(1024, Math.floor(num))).toFixed(precision); - return `${value} ${units[num]}`; - } } diff --git a/src/index.html b/src/index.html index de0ee2d9..2e895df8 100644 --- a/src/index.html +++ b/src/index.html @@ -1 +1 @@ -ttyd - Terminal \ No newline at end of file +ttyd - Terminal \ No newline at end of file