{
- 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