mirror of
https://github.com/tsl0922/ttyd.git
synced 2025-12-21 11:24:20 +01:00
html: reformat code
This commit is contained in:
@@ -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
6
html/prettier.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
trailingComma: "es5",
|
||||||
|
tabWidth: 4,
|
||||||
|
printWidth: 120,
|
||||||
|
singleQuote: true,
|
||||||
|
};
|
||||||
@@ -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} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
2
src/index.html
vendored
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user