html: reformat code

This commit is contained in:
Shuanglei Tao
2019-07-04 21:09:20 +08:00
parent 84ac40a614
commit b0ed073a00
8 changed files with 424 additions and 433 deletions

View File

@@ -2,10 +2,13 @@ root = true
[*] [*]
charset = utf-8 charset = utf-8
indent_size = 2 indent_size = 4
indent_style = space indent_style = space
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[{*.json, *.scss}]
indent_size = 2
[*.md] [*.md]
trim_trailing_whitespace = false trim_trailing_whitespace = false

6
html/prettier.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
trailingComma: "es5",
tabWidth: 4,
printWidth: 120,
singleQuote: true,
};

View File

@@ -4,48 +4,41 @@ import { ITerminalOptions, ITheme } from 'xterm';
import { Xterm } from './terminal'; import { Xterm } from './terminal';
if ((module as any).hot) { if ((module as any).hot) {
// tslint:disable-next-line:no-var-requires // tslint:disable-next-line:no-var-requires
require('preact/debug'); require('preact/debug');
} }
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
const wsPath = window.location.pathname.endsWith('/') ? 'ws' : '/ws'; const wsPath = window.location.pathname.endsWith('/') ? 'ws' : '/ws';
const url = [ const url = [protocol, window.location.host, window.location.pathname, wsPath, window.location.search].join('');
protocol,
window.location.host,
window.location.pathname,
wsPath,
window.location.search,
].join('');
const termOptions = { const termOptions = {
fontSize: 13, fontSize: 13,
fontFamily: fontFamily: 'Menlo For Powerline,Consolas,Liberation Mono,Menlo,Courier,monospace',
'Menlo For Powerline,Consolas,Liberation Mono,Menlo,Courier,monospace', theme: {
theme: { foreground: '#d2d2d2',
foreground: '#d2d2d2', background: '#2b2b2b',
background: '#2b2b2b', cursor: '#adadad',
cursor: '#adadad', black: '#000000',
black: '#000000', red: '#d81e00',
red: '#d81e00', green: '#5ea702',
green: '#5ea702', yellow: '#cfae00',
yellow: '#cfae00', blue: '#427ab3',
blue: '#427ab3', magenta: '#89658e',
magenta: '#89658e', cyan: '#00a7aa',
cyan: '#00a7aa', white: '#dbded8',
white: '#dbded8', brightBlack: '#686a66',
brightBlack: '#686a66', brightRed: '#f54235',
brightRed: '#f54235', brightGreen: '#99e343',
brightGreen: '#99e343', brightYellow: '#fdeb61',
brightYellow: '#fdeb61', brightBlue: '#84b0d8',
brightBlue: '#84b0d8', brightMagenta: '#bc94b7',
brightMagenta: '#bc94b7', brightCyan: '#37e6e8',
brightCyan: '#37e6e8', brightWhite: '#f1f1f0',
brightWhite: '#f1f1f0', } as ITheme,
} as ITheme,
} as ITerminalOptions; } as ITerminalOptions;
export class App extends Component { export class App extends Component {
render() { render() {
return <Xterm id="terminal-container" url={url} options={termOptions} />; return <Xterm id="terminal-container" url={url} options={termOptions} />;
} }
} }

View File

@@ -3,25 +3,25 @@ import { Component, ComponentChildren, h } from 'preact';
import './modal.scss'; import './modal.scss';
interface Props { interface Props {
show: boolean; show: boolean;
children: ComponentChildren; children: ComponentChildren;
} }
export class Modal extends Component<Props> { export class Modal extends Component<Props> {
constructor(props) { constructor(props) {
super(props); super(props);
} }
render({ show, children }: Props) { render({ show, children }: Props) {
return ( return (
show && ( show && (
<div className="modal"> <div className="modal">
<div className="modal-background" /> <div className="modal-background" />
<div className="modal-content"> <div className="modal-content">
<div className="box">{children}</div> <div className="box">{children}</div>
</div> </div>
</div> </div>
) )
); );
} }
} }

View File

@@ -10,214 +10,211 @@ import { ZmodemAddon } from '../zmodem';
import 'xterm/dist/xterm.css'; import 'xterm/dist/xterm.css';
export interface WindowExtended extends Window { export interface WindowExtended extends Window {
term: Terminal; term: Terminal;
tty_auth_token?: string; tty_auth_token?: string;
} }
declare let window: WindowExtended; declare let window: WindowExtended;
const enum Command { const enum Command {
// server side // server side
OUTPUT = '0', OUTPUT = '0',
SET_WINDOW_TITLE = '1', SET_WINDOW_TITLE = '1',
SET_PREFERENCES = '2', SET_PREFERENCES = '2',
SET_RECONNECT = '3', SET_RECONNECT = '3',
// client side // client side
INPUT = '0', INPUT = '0',
RESIZE_TERMINAL = '1', RESIZE_TERMINAL = '1',
} }
interface Props { interface Props {
id: string; id: string;
url: string; url: string;
options: ITerminalOptions; options: ITerminalOptions;
} }
export class Xterm extends Component<Props> { export class Xterm extends Component<Props> {
private textEncoder: TextEncoder; private textEncoder: TextEncoder;
private textDecoder: TextDecoder; private textDecoder: TextDecoder;
private container: HTMLElement; private container: HTMLElement;
private terminal: Terminal; private terminal: Terminal;
private fitAddon: FitAddon; private fitAddon: FitAddon;
private overlayAddon: OverlayAddon; private overlayAddon: OverlayAddon;
private zmodemAddon: ZmodemAddon; private zmodemAddon: ZmodemAddon;
private socket: WebSocket; private socket: WebSocket;
private title: string; private title: string;
private reconnect: number; private reconnect: number;
private resizeTimeout: number; private resizeTimeout: number;
constructor(props) { constructor(props) {
super(props); super(props);
this.textEncoder = new TextEncoder(); this.textEncoder = new TextEncoder();
this.textDecoder = new TextDecoder(); this.textDecoder = new TextDecoder();
this.fitAddon = new FitAddon(); this.fitAddon = new FitAddon();
this.overlayAddon = new OverlayAddon(); 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 (
<div id={id} ref={c => (this.container = c)}>
<ZmodemAddon ref={c => (this.zmodemAddon = c)} sender={this.sendData} />
</div>
);
}
@bind
private sendData(data: ArrayLike<number>) {
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();
} }
this.socket = new WebSocket(this.props.url, ['tty']); componentDidMount() {
this.terminal = new Terminal(this.props.options); this.openTerminal();
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');
});
} }
terminal.open(container);
terminal.focus();
window.addEventListener('resize', this.onWindowResize); componentWillUnmount() {
window.addEventListener('beforeunload', this.onWindowUnload); this.socket.close();
} this.terminal.dispose();
@bind window.removeEventListener('resize', this.onWindowResize);
private onSocketOpen() { window.removeEventListener('beforeunload', this.onWindowUnload);
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();
} }
// 1000: CLOSE_NORMAL
if (event.code !== 1000 && reconnect > 0) { render({ id }: Props) {
setTimeout(openTerminal, reconnect * 1000); return (
<div id={id} ref={c => (this.container = c)}>
<ZmodemAddon ref={c => (this.zmodemAddon = c)} sender={this.sendData} />
</div>
);
} }
}
@bind @bind
private onSocketData(event: MessageEvent) { private sendData(data: ArrayLike<number>) {
const { terminal, textDecoder, zmodemAddon } = this; const { socket } = this;
const rawData = event.data as ArrayBuffer; const payload = new Uint8Array(data.length + 1);
const cmd = String.fromCharCode(new Uint8Array(rawData)[0]); payload[0] = Command.INPUT.charCodeAt(0);
const data = rawData.slice(1); payload.set(data, 1);
socket.send(payload);
}
switch (cmd) { @bind
case Command.OUTPUT: private onWindowResize() {
zmodemAddon.consume(data); const { fitAddon } = this;
break; clearTimeout(this.resizeTimeout);
case Command.SET_WINDOW_TITLE: this.resizeTimeout = setTimeout(() => fitAddon.fit(), 250) as any;
this.title = textDecoder.decode(data); }
document.title = this.title;
break; private onWindowUnload(event: BeforeUnloadEvent): string {
case Command.SET_PREFERENCES: const message = 'Close terminal? this will also terminate the command.';
const preferences = JSON.parse(textDecoder.decode(data)); event.returnValue = message;
Object.keys(preferences).forEach(key => { return message;
console.log(`[ttyd] setting ${key}: ${preferences[key]}`); }
terminal.setOption(key, preferences[key]);
@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; terminal.onData(this.onTerminalData);
case Command.SET_RECONNECT: terminal.onResize(this.onTerminalResize);
this.reconnect = Number(textDecoder.decode(data)); if (document.queryCommandSupported && document.queryCommandSupported('copy')) {
console.log(`[ttyd] enabling reconnect: ${this.reconnect} seconds`); terminal.onSelectionChange(() => {
break; overlayAddon.showOverlay('\u2702', 200);
default: document.execCommand('copy');
console.warn(`[ttyd] unknown command: ${cmd}`); });
break; }
} terminal.open(container);
} terminal.focus();
@bind window.addEventListener('resize', this.onWindowResize);
private onTerminalResize(size: { cols: number; rows: number }) { window.addEventListener('beforeunload', this.onWindowUnload);
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 @bind
private onTerminalData(data: string) { private onSocketOpen() {
const { socket, textEncoder } = this; console.log('[ttyd] Websocket connection opened');
if (socket.readyState === WebSocket.OPEN) { const { socket, textEncoder, fitAddon } = this;
socket.send(textEncoder.encode(Command.INPUT + data)); 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();
}
// 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));
}
} }
}
} }

View File

@@ -3,13 +3,13 @@
import { ITerminalAddon, Terminal } from 'xterm'; import { ITerminalAddon, Terminal } from 'xterm';
export class OverlayAddon implements ITerminalAddon { export class OverlayAddon implements ITerminalAddon {
private terminal: Terminal | undefined; private terminal: Terminal | undefined;
private overlayNode: HTMLElement | null; private overlayNode: HTMLElement | null;
private overlayTimeout: number | null; private overlayTimeout: number | null;
constructor() { constructor() {
this.overlayNode = document.createElement('div'); this.overlayNode = document.createElement('div');
this.overlayNode.style.cssText = `border-radius: 15px; this.overlayNode.style.cssText = `border-radius: 15px;
font-size: xx-large; font-size: xx-large;
opacity: 0.75; opacity: 0.75;
padding: 0.2em 0.5em 0.2em 0.5em; padding: 0.2em 0.5em 0.2em 0.5em;
@@ -19,57 +19,57 @@ position: absolute;
-moz-user-select: none; -moz-user-select: none;
-moz-transition: opacity 180ms ease-in;`; -moz-transition: opacity 180ms ease-in;`;
this.overlayNode.addEventListener( this.overlayNode.addEventListener(
'mousedown', 'mousedown',
e => { e => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
}, },
true true
); );
}
activate(terminal: Terminal): void {
this.terminal = terminal;
}
dispose(): void {}
showOverlay(msg: string, timeout?: number): void {
const { terminal, overlayNode } = this;
overlayNode.style.color = '#101010';
overlayNode.style.backgroundColor = '#f0f0f0';
overlayNode.textContent = msg;
overlayNode.style.opacity = '0.75';
if (!overlayNode.parentNode) {
terminal.element.appendChild(overlayNode);
} }
const divSize = terminal.element.getBoundingClientRect(); activate(terminal: Terminal): void {
const overlaySize = overlayNode.getBoundingClientRect(); this.terminal = terminal;
overlayNode.style.top = (divSize.height - overlaySize.height) / 2 + 'px';
overlayNode.style.left = (divSize.width - overlaySize.width) / 2 + 'px';
if (this.overlayTimeout) {
clearTimeout(this.overlayTimeout);
}
if (timeout === null) {
return;
} }
const self = this; dispose(): void {}
self.overlayTimeout = setTimeout(() => {
overlayNode.style.opacity = '0'; showOverlay(msg: string, timeout?: number): void {
self.overlayTimeout = setTimeout(() => { const { terminal, overlayNode } = this;
if (overlayNode.parentNode) {
overlayNode.parentNode.removeChild(overlayNode); overlayNode.style.color = '#101010';
} overlayNode.style.backgroundColor = '#f0f0f0';
self.overlayTimeout = null; overlayNode.textContent = msg;
overlayNode.style.opacity = '0.75'; overlayNode.style.opacity = '0.75';
}, 200) as any;
}, timeout || 1500) as any; if (!overlayNode.parentNode) {
} terminal.element.appendChild(overlayNode);
}
const divSize = terminal.element.getBoundingClientRect();
const overlaySize = overlayNode.getBoundingClientRect();
overlayNode.style.top = (divSize.height - overlaySize.height) / 2 + 'px';
overlayNode.style.left = (divSize.width - overlaySize.width) / 2 + 'px';
if (this.overlayTimeout) {
clearTimeout(this.overlayTimeout);
}
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;
}
} }

View File

@@ -6,157 +6,149 @@ import * as Zmodem from 'zmodem.js/src/zmodem_browser';
import { Modal } from '../modal'; import { Modal } from '../modal';
interface Props { interface Props {
sender: (data: ArrayLike<number>) => void; sender: (data: ArrayLike<number>) => void;
} }
interface State { interface State {
modal: boolean; modal: boolean;
} }
export class ZmodemAddon extends Component<Props, State> export class ZmodemAddon extends Component<Props, State> implements ITerminalAddon {
implements ITerminalAddon { private terminal: Terminal | undefined;
private terminal: Terminal | undefined; private sentry: Zmodem.Sentry;
private sentry: Zmodem.Sentry; private session: Zmodem.Session;
private session: Zmodem.Session;
constructor(props) { constructor(props) {
super(props); super(props);
this.sentry = new Zmodem.Sentry({ this.sentry = new Zmodem.Sentry({
to_terminal: (octets: ArrayBuffer) => this.zmodemWrite(octets), to_terminal: (octets: ArrayBuffer) => this.zmodemWrite(octets),
sender: (octets: ArrayLike<number>) => this.zmodemSend(octets), sender: (octets: ArrayLike<number>) => this.zmodemSend(octets),
on_retract: () => this.zmodemRetract(), on_retract: () => this.zmodemRetract(),
on_detect: (detection: any) => this.zmodemDetect(detection), on_detect: (detection: any) => this.zmodemDetect(detection),
}); });
}
render(_, { modal }: State) {
return (
<Modal show={modal}>
<label class="file-label">
<input
onChange={this.sendFile}
class="file-input"
type="file"
multiple
/>
<span class="file-cta">
<strong>Choose files</strong>
</span>
</label>
</Modal>
);
}
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 render(_, { modal }: State) {
private zmodemWrite(data: ArrayBuffer): void { return (
this.terminal.writeUtf8(new Uint8Array(data)); <Modal show={modal}>
} <label class="file-label">
<input onChange={this.sendFile} class="file-input" type="file" multiple />
@bind <span class="file-cta">
private zmodemSend(data: ArrayLike<number>): void { <strong>Choose files</strong>
this.props.sender(data); </span>
} </label>
</Modal>
@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 activate(terminal: Terminal): void {
private sendFile(event: Event) { this.terminal = terminal;
this.setState({ modal: false }); }
const { terminal, session, writeProgress } = this; dispose(): void {}
const files: FileList = (event.target as HTMLInputElement).files;
consume(data: ArrayBuffer) {
Zmodem.Browser.send_files(session, files, { const { sentry, terminal } = this;
on_progress: (_, xfer: any) => writeProgress(xfer), try {
}) sentry.consume(data);
.then(() => { } catch (e) {
session.close(); console.log(`[ttyd] zmodem consume: `, e);
terminal.setOption('disableStdin', false); terminal.setOption('disableStdin', false);
}) }
.catch(e => { }
console.log(`[ttyd] zmodem send: `, e);
}); @bind
} private zmodemWrite(data: ArrayBuffer): void {
this.terminal.writeUtf8(new Uint8Array(data));
@bind }
private receiveFile() {
const { terminal, session, writeProgress } = this; @bind
private zmodemSend(data: ArrayLike<number>): void {
session.on('offer', (xfer: any) => { this.props.sender(data);
const fileBuffer = []; }
xfer.on('input', payload => {
writeProgress(xfer); @bind
fileBuffer.push(new Uint8Array(payload)); private zmodemRetract(): void {
}); this.terminal.setOption('disableStdin', false);
xfer.accept().then(() => { }
Zmodem.Browser.save_to_disk(fileBuffer, xfer.get_details().name);
}); @bind
}); private zmodemDetect(detection: Zmodem.Detection): void {
const { terminal, receiveFile } = this;
session.on('session_end', () => { terminal.setOption('disableStdin', true);
terminal.setOption('disableStdin', false); this.session = detection.confirm();
});
if (this.session.type === 'send') {
session.start(); this.setState({ modal: true });
} } else {
receiveFile();
@bind }
private writeProgress(xfer: any) { }
const { terminal, bytesHuman } = this;
@bind
const file = xfer.get_details(); private sendFile(event: Event) {
const name = file.name; this.setState({ modal: false });
const size = file.size;
const offset = xfer.get_offset(); const { terminal, session, writeProgress } = this;
const percent = ((100 * offset) / size).toFixed(2); const files: FileList = (event.target as HTMLInputElement).files;
terminal.write( Zmodem.Browser.send_files(session, files, {
`${name} ${percent}% ${bytesHuman(offset, 2)}/${bytesHuman(size, 2)}\r` on_progress: (_, xfer: any) => writeProgress(xfer),
); })
} .then(() => {
session.close();
private bytesHuman(bytes: any, precision: number): string { terminal.setOption('disableStdin', false);
if (!/^([-+])?|(\.\d+)(\d+(\.\d+)?|(\d+\.)|Infinity)$/.test(bytes)) { })
return '-'; .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]}`;
}
} }

2
src/index.html vendored

File diff suppressed because one or more lines are too long