diff --git a/html/package.json b/html/package.json
index e5767ab..55ec364 100644
--- a/html/package.json
+++ b/html/package.json
@@ -50,6 +50,7 @@
"dependencies": {
"decko": "^1.2.0",
"preact": "^8.4.2",
- "xterm": "^3.13.2"
+ "xterm": "^3.14.2",
+ "xterm-addon-fit": "^0.1.0-beta1"
}
}
diff --git a/html/src/components/app.tsx b/html/src/components/app.tsx
index a5d04cc..52c2bb9 100644
--- a/html/src/components/app.tsx
+++ b/html/src/components/app.tsx
@@ -1,7 +1,7 @@
import { Component, h } from 'preact';
import { ITerminalOptions, ITheme } from 'xterm';
-import Terminal from './terminal';
+import Xterm from './terminal';
if ((module as any).hot) {
// tslint:disable-next-line:no-var-requires
@@ -40,7 +40,7 @@ const termOptions = {
export default class App extends Component {
public render() {
return (
-
+
);
}
}
diff --git a/html/src/components/terminal/index.tsx b/html/src/components/terminal/index.tsx
index 36a0e01..0c606e6 100644
--- a/html/src/components/terminal/index.tsx
+++ b/html/src/components/terminal/index.tsx
@@ -1,9 +1,10 @@
import { bind } from 'decko';
import { Component, h } from 'preact';
-import { ITerminalOptions, Terminal as Xterm } from 'xterm';
+import { ITerminalOptions, Terminal } from 'xterm';
+import { FitAddon } from 'xterm-addon-fit';
+import { OverlayAddon } from './overlay';
+
import 'xterm/dist/xterm.css';
-import * as fit from 'xterm/lib/addons/fit/fit';
-import * as overlay from './overlay';
const enum Command {
// server side
@@ -17,22 +18,19 @@ const enum Command {
RESIZE_TERMINAL = '1'
}
-interface ITerminal extends Xterm {
- fit(): void;
- showOverlay(msg: string, timeout?: number): void;
-}
-
interface Props {
id: string;
url: string;
options: ITerminalOptions;
}
-export default class Terminal extends Component {
+export default class Xterm extends Component {
private textEncoder: TextEncoder;
private textDecoder: TextDecoder;
private container: HTMLElement;
- private terminal: ITerminal;
+ private terminal: Terminal;
+ private fitAddon: FitAddon;
+ private overlayAddon: OverlayAddon;
private socket: WebSocket;
private title: string;
private autoReconnect: number;
@@ -41,11 +39,10 @@ export default class Terminal extends Component {
constructor(props) {
super(props);
- Xterm.applyAddon(fit);
- Xterm.applyAddon(overlay);
-
this.textEncoder = new TextEncoder();
this.textDecoder = new TextDecoder();
+ this.fitAddon = new FitAddon();
+ this.overlayAddon = new OverlayAddon();
}
public componentDidMount() {
@@ -68,9 +65,9 @@ export default class Terminal extends Component {
@bind
private onWindowResize() {
- const { terminal } = this;
+ const { fitAddon } = this;
clearTimeout(this.resizeTimeout);
- this.resizeTimeout = setTimeout(() => terminal.fit(), 250) as any;
+ this.resizeTimeout = setTimeout(() => fitAddon.fit(), 250) as any;
}
private onWindowUnload(event: BeforeUnloadEvent): string {
@@ -86,7 +83,7 @@ export default class Terminal extends Component {
}
this.socket = new WebSocket(this.props.url, ['tty']);
- this.terminal = new Xterm(this.props.options) as ITerminal;
+ this.terminal = new Terminal(this.props.options);
const { socket, terminal, container } = this;
socket.binaryType = 'arraybuffer';
@@ -94,6 +91,9 @@ export default class Terminal extends Component {
socket.onmessage = this.onSocketData;
socket.onclose = this.onSocketClose;
+ terminal.loadAddon(this.fitAddon);
+ terminal.loadAddon(this.overlayAddon);
+
terminal.onTitleChange((data) => {
if (data && data !== '') {
document.title = (data + ' | ' + this.title);
@@ -111,18 +111,18 @@ export default class Terminal extends Component {
@bind
private onSocketOpen() {
console.log('Websocket connection opened');
- const { socket, textEncoder, terminal } = this;
+ const { socket, textEncoder, fitAddon } = this;
socket.send(textEncoder.encode(JSON.stringify({AuthToken: ''})));
- terminal.fit();
+ fitAddon.fit();
}
@bind
private onSocketClose(event: CloseEvent) {
console.log('Websocket connection closed with code: ' + event.code);
- const { terminal, openTerminal, autoReconnect } = this;
- terminal.showOverlay('Connection Closed', null);
+ const { overlayAddon, openTerminal, autoReconnect } = this;
+ overlayAddon.showOverlay('Connection Closed', null);
window.removeEventListener('beforeunload', this.onWindowUnload);
// 1008: POLICY_VIOLATION - Auth failure
@@ -141,25 +141,25 @@ export default class Terminal extends Component {
const rawData = new Uint8Array(event.data);
const cmd = String.fromCharCode(rawData[0]);
- const data = rawData.slice(1).buffer;
+ const data = rawData.slice(1);
switch(cmd) {
case Command.OUTPUT:
- terminal.write(textDecoder.decode(data));
+ terminal.writeUtf8(data);
break;
case Command.SET_WINDOW_TITLE:
- this.title = textDecoder.decode(data);
+ this.title = textDecoder.decode(data.buffer);
document.title = this.title;
break;
case Command.SET_PREFERENCES:
- const preferences = JSON.parse(textDecoder.decode(data));
+ const preferences = JSON.parse(textDecoder.decode(data.buffer));
Object.keys(preferences).forEach((key) => {
console.log('Setting ' + key + ': ' + preferences[key]);
terminal.setOption(key, preferences[key]);
});
break;
case Command.SET_RECONNECT:
- this.autoReconnect = JSON.parse(textDecoder.decode(data));
+ this.autoReconnect = parseInt(textDecoder.decode(data.buffer));
console.log('Enabling reconnect: ' + this.autoReconnect + ' seconds');
break;
default:
@@ -170,12 +170,12 @@ export default class Terminal extends Component {
@bind
private onTerminalResize(size: {cols: number, rows: number}) {
- const { terminal, socket, textEncoder } = this;
+ 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(() => {terminal.showOverlay(size.cols + 'x' + size.rows)}, 500);
+ setTimeout(() => {overlayAddon.showOverlay(size.cols + 'x' + size.rows)}, 500);
}
@bind
diff --git a/html/src/components/terminal/overlay.ts b/html/src/components/terminal/overlay.ts
index 2d35752..cb41dae 100644
--- a/html/src/components/terminal/overlay.ts
+++ b/html/src/components/terminal/overlay.ts
@@ -1,20 +1,65 @@
// ported from hterm.Terminal.prototype.showOverlay
// https://chromium.googlesource.com/apps/libapps/+/master/hterm/js/hterm_terminal.js
-import { Terminal } from 'xterm';
+import { ITerminalAddon, Terminal } from 'xterm';
-interface IOverlayAddonTerminal extends Terminal {
- __overlayNode: HTMLElement | null;
- __overlayTimeout: number | null;
-}
+export class OverlayAddon implements ITerminalAddon {
+ private terminal: Terminal | undefined;
+ private overlayNode: HTMLElement | null;
+ private overlayTimeout: number | null;
-export function showOverlay(term: Terminal, msg: string, timeout?: number): void {
- const addonTerminal = term;
- if (!addonTerminal.__overlayNode) {
- if (!term.element) {
+ constructor() {}
+
+ public activate(terminal: Terminal): void {
+ this.terminal = terminal;
+ this.overlayNode = this.createOverlayNode();
+ }
+
+ public dispose(): void {
+ document.removeChild(this.overlayNode);
+ }
+
+ public 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();
+ 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;
}
- addonTerminal.__overlayNode = document.createElement('div');
- addonTerminal.__overlayNode.style.cssText = (
+
+ 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);
+ }, timeout || 1500);
+ }
+
+ private createOverlayNode(): HTMLElement {
+ const overlayNode = document.createElement('div');
+
+ overlayNode.style.cssText = (
'border-radius: 15px;' +
'font-size: xx-large;' +
'opacity: 0.75;' +
@@ -25,50 +70,11 @@ export function showOverlay(term: Terminal, msg: string, timeout?: number): void
'-moz-user-select: none;' +
'-moz-transition: opacity 180ms ease-in;');
- addonTerminal.__overlayNode.addEventListener('mousedown', (e) => {
+ overlayNode.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
}, true);
+
+ return overlayNode;
}
-
- addonTerminal.__overlayNode.style.color = '#101010';
- addonTerminal.__overlayNode.style.backgroundColor = '#f0f0f0';
-
- addonTerminal.__overlayNode.textContent = msg;
- addonTerminal.__overlayNode.style.opacity = '0.75';
-
- if (!addonTerminal.__overlayNode.parentNode) {
- term.element.appendChild(addonTerminal.__overlayNode);
- }
-
- const divSize = term.element.getBoundingClientRect();
- const overlaySize = addonTerminal.__overlayNode.getBoundingClientRect();
-
- addonTerminal.__overlayNode.style.top = (divSize.height - overlaySize.height) / 2 + 'px';
- addonTerminal.__overlayNode.style.left = (divSize.width - overlaySize.width) / 2 + 'px';
-
- if (addonTerminal.__overlayTimeout) {
- clearTimeout(addonTerminal.__overlayTimeout);
- }
-
- if (timeout === null) {
- return;
- }
-
- addonTerminal.__overlayTimeout = setTimeout(() => {
- addonTerminal.__overlayNode.style.opacity = '0';
- addonTerminal.__overlayTimeout = setTimeout(() => {
- if (addonTerminal.__overlayNode.parentNode) {
- addonTerminal.__overlayNode.parentNode.removeChild(addonTerminal.__overlayNode);
- }
- addonTerminal.__overlayTimeout = null;
- addonTerminal.__overlayNode.style.opacity = '0.75';
- }, 200);
- }, timeout || 1500);
-}
-
-export function apply(terminalConstructor: typeof Terminal): void {
- (terminalConstructor.prototype).showOverlay = function (msg: string, timeout?: number): void {
- return showOverlay(this, msg, timeout);
- };
}
diff --git a/html/yarn.lock b/html/yarn.lock
index 809f82d..fe147ed 100644
--- a/html/yarn.lock
+++ b/html/yarn.lock
@@ -8358,10 +8358,15 @@ xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1:
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68=
-xterm@^3.13.2:
- version "3.13.2"
- resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.13.2.tgz#987c3a7fe3d23d6c03ce0eaf06c0554c38935570"
- integrity sha512-4utKoF16/pzH6+EkFaaay+qPCozf5RP2P0JuH6rvIGHY0CRwgU2LwbQ/24DY+TvaZ5m+kwvIUvPqIBoMZYfgOg==
+xterm-addon-fit@^0.1.0-beta1:
+ version "0.1.0-beta1"
+ resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.1.0-beta1.tgz#ec483fe4cad466de290be7e5539c8beed757b851"
+ integrity sha512-4HVm1RVqvBiIxBUxjjN63KIumI/mZYJ5lXkoR/onO//mh2Z2e257DmX1UqgCd2gi99EmZTt5CtEUinUANAkbvg==
+
+xterm@^3.14.2:
+ version "3.14.2"
+ resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.14.2.tgz#f1285288bdc7df7056ef4bde64311900748c3e5d"
+ integrity sha512-L50XMhfAC953/3EzmL+lq8jyD6CQ3SZ83UDrxalpKzE3d7Wo5JqehSd4yOrHYZYrijOT3pX6kBsZEI9uuZ1lmQ==
y18n@^3.2.1:
version "3.2.1"
diff --git a/src/index.html b/src/index.html
index 72a9e97..d15274c 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