Add xterm itegration

* Move to TypeScript from legacy JavaScript
* Add support of xterm.js
* Hterm is still available for backward compatibility
This commit is contained in:
Iwasaki Yudai 2017-05-22 08:16:24 +09:00
parent d6c98866b9
commit 8803721f3d
40 changed files with 9051 additions and 124 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
gotty
bindata
builds
js/node_modules/*

2
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "libapps"]
path = libapps
path = js/libapps
url = https://chromium.googlesource.com/apps/libapps

View File

@ -7,7 +7,7 @@ gotty: server/asset.go main.go server/*.go webtty/*.go backend/*.go Makefile
asset: server/asset.go
server/asset.go: bindata/static/js/hterm.js bindata/static/js/gotty.js bindata/static/index.html bindata/static/favicon.png
server/asset.go: bindata/static/js/hterm.js bindata/static/js/bundle.js bindata/static/index.html bindata/static/favicon.png bindata/static/css/index.css bindata/static/css/xterm.css bindata/static/css/xterm_customize.css
go-bindata -prefix bindata -pkg server -ignore=\\.gitkeep -o server/asset.go bindata/...
gofmt -w server/asset.go
@ -26,12 +26,30 @@ bindata/static/favicon.png: bindata/static resources/favicon.png
bindata/static/js: bindata/static
mkdir -p bindata/static/js
bindata/static/js/hterm.js: bindata/static/js libapps/hterm/js/*.js
cd libapps && \
LIBDOT_SEARCH_PATH=`pwd` ./libdot/bin/concat.sh -i ./hterm/concat/hterm_all.concat -o ../bindata/static/js/hterm.js
bindata/static/js/hterm.js: bindata/static/js js/libapps/hterm/js/*.js
cd js/libapps && \
LIBDOT_SEARCH_PATH=`pwd` ./libdot/bin/concat.sh -i ./hterm/concat/hterm_all.concat -o ../../bindata/static/js/hterm.js
bindata/static/js/bundle.js: bindata/static/js js/dist/bundle.js
cp js/dist/bundle.js bindata/static/js/bundle.js
bindata/static/css: bindata/static
mkdir -p bindata/static/css
bindata/static/css/index.css: bindata/static/css resources/index.css
cp resources/index.css bindata/static/css/index.css
bindata/static/css/xterm_customize.css: bindata/static/css resources/xterm_customize.css
cp resources/xterm_customize.css bindata/static/css/xterm_customize.css
bindata/static/css/xterm.css: bindata/static/css js/node_modules/xterm/dist/xterm.css
cp js/node_modules/xterm/dist/xterm.css bindata/static/css/xterm.css
js/dist/bundle.js:
cd js && \
webpack
bindata/static/js/gotty.js: bindata/static/js resources/gotty.js
cp resources/gotty.js bindata/static/js/gotty.js
tools:
go get github.com/tools/godep

6008
js/dist/bundle.js vendored Normal file

File diff suppressed because it is too large Load Diff

1
js/dist/bundle.js.map vendored Normal file

File diff suppressed because one or more lines are too long

24
js/dist/hterm.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
import * as bare from "hterm";
export declare class TermHterm {
elem: HTMLElement;
term: bare.Terminal;
io: bare.IO;
columns: number;
rows: number;
message: string;
constructor(elem: HTMLElement);
info(): {
columns: number;
rows: number;
};
output(data: string): void;
showMessage(message: string, timeout: number): void;
removeMessage(): void;
setWindowTitle(title: string): void;
setPreferences(value: object): void;
onInput(callback: (input: string) => void): void;
onResize(callback: (colmuns: number, rows: number) => void): void;
deactivate(): void;
reset(): void;
close(): void;
}

77
js/dist/hterm.js vendored Normal file
View File

@ -0,0 +1,77 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var bare = require("hterm");
var TermHterm = (function () {
function TermHterm(elem) {
var _this = this;
this.elem = elem;
this.term = new bare.Terminal();
// this.term.defaultStorage = new lib.Storage.Memory();
this.term.getPrefs().set("send-encoding", "raw");
this.term.decorate(this.elem);
this.term.onTerminalReady = function () {
_this.io = _this.term.io.push();
_this.term.installKeyboard();
};
}
;
TermHterm.prototype.info = function () {
return { columns: this.term.screen.getWidth(), rows: this.term.screen.getHeight() };
};
;
TermHterm.prototype.output = function (data) {
if (this.term.io.writeUTF8 != null) {
this.term.io.writeUTF8(data);
}
};
;
TermHterm.prototype.showMessage = function (message, timeout) {
this.term.io.showOverlay(message, timeout);
};
;
TermHterm.prototype.removeMessage = function () {
this.term.io.showOverlay("", 0);
};
TermHterm.prototype.setWindowTitle = function (title) {
this.term.setWindowTitle(title);
};
;
TermHterm.prototype.setPreferences = function (value) {
var _this = this;
Object.keys(value).forEach(function (key) {
_this.term.getPrefs().set(key, value[key]);
});
};
;
TermHterm.prototype.onInput = function (callback) {
this.io.onVTKeystroke = function (data) {
callback(data);
};
this.io.sendString = function (data) {
callback(data);
};
};
;
TermHterm.prototype.onResize = function (callback) {
this.io.onTerminalResize = function (columns, rows) {
callback(columns, rows);
};
};
;
TermHterm.prototype.deactivate = function () {
this.io.onVTKeystroke = null;
this.io.sendString = null;
this.io.onTerminalResize = null;
this.term.uninstallKeyboard();
};
TermHterm.prototype.reset = function () {
this.removeMessage();
// this.term.reset();
};
TermHterm.prototype.close = function () {
this.term.uninstallKeyboard();
};
return TermHterm;
}());
exports.TermHterm = TermHterm;
//# sourceMappingURL=hterm.js.map

1
js/dist/hterm.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"hterm.js","sourceRoot":"","sources":["../src/hterm.ts"],"names":[],"mappings":"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"}

0
js/dist/main.d.ts vendored Normal file
View File

23
js/dist/main.js vendored Normal file
View File

@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var hterm_1 = require("./hterm");
var xterm_1 = require("./xterm");
var webtty_1 = require("./webtty");
var websocket_1 = require("./websocket");
var elem = document.getElementById("terminal");
if (elem !== null) {
var term_1 = new xterm_1.TermXterm(elem);
var httpsEnabled = window.location.protocol == "https:";
var url = (httpsEnabled ? 'wss://' : 'ws://') + window.location.hostname + ":8080/ws";
var args = window.location.search;
var factory = new websocket_1.ConnectionFactory(url, webtty_1.protocols);
var wt = new webtty_1.WebTTY(term_1, factory, args, "");
var closer_1 = wt.open();
new hterm_1.TermHterm(elem);
window.addEventListener("unload", function () {
closer_1();
term_1.close();
});
}
;
//# sourceMappingURL=main.js.map

1
js/dist/main.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";;AAAA,iCAAoC;AACpC,iCAAoC;AACpC,mCAA6C;AAC7C,yCAAgD;AAGhD,IAAM,IAAI,GAAG,QAAQ,CAAC,cAAc,CAAC,UAAU,CAAC,CAAA;AAEhD,EAAE,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC;IAChB,IAAM,MAAI,GAAG,IAAI,iBAAS,CAAC,IAAI,CAAC,CAAC;IACjC,IAAM,YAAY,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,IAAI,QAAQ,CAAC;IAC1D,IAAM,GAAG,GAAG,CAAC,YAAY,GAAG,QAAQ,GAAG,OAAO,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,GAAG,UAAU,CAAC;IACxF,IAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;IACpC,IAAM,OAAO,GAAG,IAAI,6BAAiB,CAAC,GAAG,EAAE,kBAAS,CAAC,CAAC;IACtD,IAAM,EAAE,GAAG,IAAI,eAAM,CAAC,MAAI,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;IAC/C,IAAM,QAAM,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC;IAEzB,IAAI,iBAAS,CAAC,IAAI,CAAC,CAAC;IAEpB,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE;QAC9B,QAAM,EAAE,CAAC;QACT,MAAI,CAAC,KAAK,EAAE,CAAC;IACjB,CAAC,CAAC,CAAC;AACP,CAAC;AAAA,CAAC"}

17
js/dist/websocket.d.ts vendored Normal file
View File

@ -0,0 +1,17 @@
export declare class ConnectionFactory {
url: string;
protocols: string[];
constructor(url: string, protocols: string[]);
create(): Connection;
}
export declare class Connection {
bare: WebSocket;
constructor(url: string, protocols: string[]);
open(): void;
close(): void;
send(data: string): void;
isOpen(): boolean;
onOpen(callback: () => void): void;
onReceive(callback: (data: string) => void): void;
onClose(callback: () => void): void;
}

60
js/dist/websocket.js vendored Normal file
View File

@ -0,0 +1,60 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var ConnectionFactory = (function () {
function ConnectionFactory(url, protocols) {
this.url = url;
this.protocols = protocols;
}
;
ConnectionFactory.prototype.create = function () {
return new Connection(this.url, this.protocols);
};
;
return ConnectionFactory;
}());
exports.ConnectionFactory = ConnectionFactory;
var Connection = (function () {
function Connection(url, protocols) {
this.bare = new WebSocket(url, protocols);
}
Connection.prototype.open = function () {
// nothing todo for websocket
};
;
Connection.prototype.close = function () {
this.bare.close();
};
;
Connection.prototype.send = function (data) {
this.bare.send(data);
};
;
Connection.prototype.isOpen = function () {
if (this.bare.readyState == WebSocket.CONNECTING ||
this.bare.readyState == WebSocket.OPEN) {
return true;
}
return false;
};
Connection.prototype.onOpen = function (callback) {
this.bare.onopen = function (event) {
callback();
};
};
;
Connection.prototype.onReceive = function (callback) {
this.bare.onmessage = function (event) {
callback(event.data);
};
};
;
Connection.prototype.onClose = function (callback) {
this.bare.onclose = function (event) {
callback();
};
};
;
return Connection;
}());
exports.Connection = Connection;
//# sourceMappingURL=websocket.js.map

1
js/dist/websocket.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"websocket.js","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":";;AAAA;IAII,2BAAY,GAAW,EAAE,SAAmB;QACxC,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;IAAA,CAAC;IAEF,kCAAM,GAAN;QACI,MAAM,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;IAAA,CAAC;IACN,wBAAC;AAAD,CAAC,AAZD,IAYC;AAZY,8CAAiB;AAc9B;IAII,oBAAY,GAAW,EAAE,SAAmB;QACxC,IAAI,CAAC,IAAI,GAAG,IAAI,SAAS,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IAC9C,CAAC;IAED,yBAAI,GAAJ;QACI,6BAA6B;IACjC,CAAC;IAAA,CAAC;IAEF,0BAAK,GAAL;QACI,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC;IAAA,CAAC;IAEF,yBAAI,GAAJ,UAAK,IAAY;QACb,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC;IAAA,CAAC;IAEF,2BAAM,GAAN;QACI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,SAAS,CAAC,UAAU;YAC5C,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;YACzC,MAAM,CAAC,IAAI,CAAA;QACf,CAAC;QACD,MAAM,CAAC,KAAK,CAAA;IAChB,CAAC;IAED,2BAAM,GAAN,UAAO,QAAoB;QACvB,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,UAAC,KAAK;YACrB,QAAQ,EAAE,CAAC;QACf,CAAC,CAAA;IACL,CAAC;IAAA,CAAC;IAEF,8BAAS,GAAT,UAAU,QAAgC;QACtC,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,UAAC,KAAK;YACxB,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACzB,CAAC,CAAA;IACL,CAAC;IAAA,CAAC;IAEF,4BAAO,GAAP,UAAQ,QAAoB;QACxB,IAAI,CAAC,IAAI,CAAC,OAAO,GAAG,UAAC,KAAK;YACtB,QAAQ,EAAE,CAAC;QACf,CAAC,CAAC;IACN,CAAC;IAAA,CAAC;IACN,iBAAC;AAAD,CAAC,AA7CD,IA6CC;AA7CY,gCAAU"}

48
js/dist/webtty.d.ts vendored Normal file
View File

@ -0,0 +1,48 @@
export declare const protocols: string[];
export declare const msgInputUnknown = "0";
export declare const msgInput = "1";
export declare const msgPing = "2";
export declare const msgResizeTerminal = "3";
export declare const msgUnknownOutput = "0";
export declare const msgOutput = "1";
export declare const msgPong = "2";
export declare const msgSetWindowTitle = "3";
export declare const msgSetPreferences = "4";
export declare const msgSetReconnect = "5";
export interface Terminal {
info(): {
columns: number;
rows: number;
};
output(data: string): void;
showMessage(message: string, timeout: number): void;
removeMessage(): void;
setWindowTitle(title: string): void;
setPreferences(value: object): void;
onInput(callback: (input: string) => void): void;
onResize(callback: (colmuns: number, rows: number) => void): void;
reset(): void;
deactivate(): void;
close(): void;
}
export interface Connection {
open(): void;
close(): void;
send(data: string): void;
isOpen(): boolean;
onOpen(callback: () => void): void;
onReceive(callback: (data: string) => void): void;
onClose(callback: () => void): void;
}
export interface ConnectionFactory {
create(): Connection;
}
export declare class WebTTY {
term: Terminal;
connectionFactory: ConnectionFactory;
args: string;
authToken: string;
reconnect: number;
constructor(term: Terminal, connectionFactory: ConnectionFactory, args: string, authToken: string);
open(): () => void;
}

99
js/dist/webtty.js vendored Normal file
View File

@ -0,0 +1,99 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.protocols = ["webtty"];
exports.msgInputUnknown = '0';
exports.msgInput = '1';
exports.msgPing = '2';
exports.msgResizeTerminal = '3';
exports.msgUnknownOutput = '0';
exports.msgOutput = '1';
exports.msgPong = '2';
exports.msgSetWindowTitle = '3';
exports.msgSetPreferences = '4';
exports.msgSetReconnect = '5';
var WebTTY = (function () {
function WebTTY(term, connectionFactory, args, authToken) {
this.term = term;
this.connectionFactory = connectionFactory;
this.args = args;
this.authToken = authToken;
this.reconnect = -1;
}
;
WebTTY.prototype.open = function () {
var _this = this;
var connection = this.connectionFactory.create();
var pingTimer;
var reconnectTimeout;
var setup = function () {
connection.onOpen(function () {
var termInfo = _this.term.info();
connection.send(JSON.stringify({
Arguments: _this.args,
AuthToken: _this.authToken,
}));
var resizeHandler = function (colmuns, rows) {
connection.send(exports.msgResizeTerminal + JSON.stringify({
columns: colmuns,
rows: rows
}));
};
_this.term.onResize(resizeHandler);
resizeHandler(termInfo.columns, termInfo.rows);
_this.term.onInput(function (input) {
connection.send(exports.msgInput + input);
});
pingTimer = setInterval(function () {
connection.send(exports.msgPing);
}, 30 * 1000);
});
connection.onReceive(function (data) {
var payload = data.slice(1);
switch (data[0]) {
case exports.msgOutput:
_this.term.output(decodeURIComponent(Array.prototype.map.call(atob(payload), function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join('')));
break;
case exports.msgPong:
break;
case exports.msgSetWindowTitle:
_this.term.setWindowTitle(payload);
break;
case exports.msgSetPreferences:
var preferences = JSON.parse(payload);
_this.term.setPreferences(preferences);
break;
case exports.msgSetReconnect:
var autoReconnect = JSON.parse(payload);
console.log("Enabling reconnect: " + autoReconnect + " seconds");
_this.reconnect = autoReconnect;
break;
}
});
connection.onClose(function () {
clearInterval(pingTimer);
_this.term.deactivate();
_this.term.showMessage("Connection Closed", 0);
if (_this.reconnect > 0) {
reconnectTimeout = setTimeout(function () {
connection = _this.connectionFactory.create();
_this.term.reset();
setup();
}, _this.reconnect * 1000);
}
});
connection.open();
};
setup();
return function () {
clearTimeout(reconnectTimeout);
connection.close();
};
};
;
return WebTTY;
}());
exports.WebTTY = WebTTY;
;
//# sourceMappingURL=webtty.js.map

1
js/dist/webtty.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"webtty.js","sourceRoot":"","sources":["../src/webtty.ts"],"names":[],"mappings":"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"}

24
js/dist/xterm.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
import * as bare from "xterm";
export declare class TermXterm {
elem: HTMLElement;
message: HTMLElement;
messageTimeout: number;
messageTimer: number;
term: bare;
resizeListener: () => void;
constructor(elem: HTMLElement);
info(): {
columns: number;
rows: number;
};
output(data: string): void;
showMessage(message: string, timeout: number): void;
removeMessage(): void;
setWindowTitle(title: string): void;
setPreferences(value: object): void;
onInput(callback: (input: string) => void): void;
onResize(callback: (colmuns: number, rows: number) => void): void;
deactivate(): void;
reset(): void;
close(): void;
}

88
js/dist/xterm.js vendored Normal file
View File

@ -0,0 +1,88 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var bare = require("xterm");
bare.loadAddon("fit");
var TermXterm = (function () {
function TermXterm(elem) {
var _this = this;
this.elem = elem;
this.term = new bare();
this.message = elem.ownerDocument.createElement("div");
this.message.className = "xterm-overlay";
this.messageTimeout = 2000;
this.resizeListener = function () {
_this.term.fit();
_this.term.scrollToBottom();
_this.showMessage(String(_this.term.cols) + "x" + String(_this.term.rows), _this.messageTimeout);
};
this.term.on("open", function () {
_this.term.fit();
_this.term.scrollToBottom();
window.addEventListener("resize", function () { _this.resizeListener(); });
});
this.term.open(elem, true);
}
;
TermXterm.prototype.info = function () {
return { columns: this.term.cols, rows: this.term.rows };
};
;
TermXterm.prototype.output = function (data) {
this.term.write(data);
};
;
TermXterm.prototype.showMessage = function (message, timeout) {
var _this = this;
this.message.textContent = message;
this.elem.appendChild(this.message);
if (this.messageTimer) {
clearTimeout(this.messageTimer);
}
if (timeout > 0) {
this.messageTimer = setTimeout(function () {
_this.elem.removeChild(_this.message);
}, timeout);
}
};
;
TermXterm.prototype.removeMessage = function () {
if (this.message.parentNode == this.elem) {
this.elem.removeChild(this.message);
}
};
TermXterm.prototype.setWindowTitle = function (title) {
document.title = title;
};
;
TermXterm.prototype.setPreferences = function (value) {
};
;
TermXterm.prototype.onInput = function (callback) {
this.term.on("data", function (data) {
callback(data);
});
};
;
TermXterm.prototype.onResize = function (callback) {
this.term.on("resize", function (data) {
callback(data.cols, data.rows);
});
};
;
TermXterm.prototype.deactivate = function () {
this.term.off("data");
this.term.off("resize");
this.term.blur();
};
TermXterm.prototype.reset = function () {
this.removeMessage();
this.term.clear();
};
TermXterm.prototype.close = function () {
window.removeEventListener("resize", this.resizeListener);
this.term.destroy();
};
return TermXterm;
}());
exports.TermXterm = TermXterm;
//# sourceMappingURL=xterm.js.map

1
js/dist/xterm.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"xterm.js","sourceRoot":"","sources":["../src/xterm.ts"],"names":[],"mappings":"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"}

1
js/libapps Submodule

@ -0,0 +1 @@
Subproject commit f05b714d7ff1368b3669b041ae83fcaec1742a61

1873
js/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

10
js/package.json Normal file
View File

@ -0,0 +1,10 @@
{
"devDependencies": {
"ts-loader": "^2.0.3",
"typescript": "^2.3.2",
"webpack": "^2.5.1"
},
"dependencies": {
"xterm": "^2.7.0"
}
}

93
js/src/hterm.ts Normal file
View File

@ -0,0 +1,93 @@
import * as bare from "hterm";
import * as bareLib from "htermLib";
export class TermHterm {
elem: HTMLElement;
term: bare.Terminal;
io: bare.IO;
columns: number;
rows: number;
// to "show" the current message when removeMessage() is called
message: string;
constructor(elem: HTMLElement) {
this.elem = elem;
hterm.defaultStorage = new bareLib.Storage.Memory();
this.term = new bare.Terminal();
this.term.getPrefs().set("send-encoding", "raw");
this.term.decorate(this.elem);
this.io = this.term.io.push();
this.term.installKeyboard();
};
info(): { columns: number, rows: number } {
return { columns: this.columns, rows: this.rows };
};
output(data: string) {
if (this.term.io != null) {
this.term.io.writeUTF16(data);
}
};
showMessage(message: string, timeout: number) {
this.message = message;
if (timeout > 0) {
this.term.io.showOverlay(message, timeout);
} else {
this.term.io.showOverlay(message, null);
}
};
removeMessage(): void {
// there is no hideOverlay(), so show the same message with 0 sec
this.term.io.showOverlay(this.message, 0);
}
setWindowTitle(title: string) {
this.term.setWindowTitle(title);
};
setPreferences(value: object) {
Object.keys(value).forEach((key) => {
this.term.getPrefs().set(key, value[key]);
});
};
onInput(callback: (input: string) => void) {
this.io.onVTKeystroke = (data) => {
callback(data);
};
this.io.sendString = (data) => {
callback(data);
};
};
onResize(callback: (colmuns: number, rows: number) => void) {
this.io.onTerminalResize = (columns: number, rows: number) => {
this.columns = columns;
this.rows = rows;
callback(columns, rows);
};
};
deactivate(): void {
this.io.onVTKeystroke = null;
this.io.sendString = null
this.io.onTerminalResize = null;
this.term.uninstallKeyboard();
}
reset(): void {
this.removeMessage();
this.term.installKeyboard();
}
close(): void {
this.term.uninstallKeyboard();
}
}

30
js/src/main.ts Normal file
View File

@ -0,0 +1,30 @@
import { TermHterm } from "./hterm";
import { TermXterm } from "./xterm";
import { Terminal, WebTTY, protocols } from "./webtty";
import { ConnectionFactory } from "./websocket";
// @TODO remove these
declare var gotty_auth_token: string;
declare var gotty_term: string;
const elem = document.getElementById("terminal")
if (elem !== null) {
var term: Terminal;
if (gotty_term == "hterm") {
term = new TermHterm(elem);
} else {
term = new TermXterm(elem);
}
const httpsEnabled = window.location.protocol == "https:";
const url = (httpsEnabled ? 'wss://' : 'ws://') + window.location.host + window.location.pathname + 'ws';
const args = window.location.search;
const factory = new ConnectionFactory(url, protocols);
const wt = new WebTTY(term, factory, args, gotty_auth_token);
const closer = wt.open();
window.addEventListener("unload", () => {
closer();
term.close();
});
};

60
js/src/websocket.ts Normal file
View File

@ -0,0 +1,60 @@
export class ConnectionFactory {
url: string;
protocols: string[];
constructor(url: string, protocols: string[]) {
this.url = url;
this.protocols = protocols;
};
create(): Connection {
return new Connection(this.url, this.protocols);
};
}
export class Connection {
bare: WebSocket;
constructor(url: string, protocols: string[]) {
this.bare = new WebSocket(url, protocols);
}
open() {
// nothing todo for websocket
};
close() {
this.bare.close();
};
send(data: string) {
this.bare.send(data);
};
isOpen(): boolean {
if (this.bare.readyState == WebSocket.CONNECTING ||
this.bare.readyState == WebSocket.OPEN) {
return true
}
return false
}
onOpen(callback: () => void) {
this.bare.onopen = (event) => {
callback();
}
};
onReceive(callback: (data: string) => void) {
this.bare.onmessage = (event) => {
callback(event.data);
}
};
onClose(callback: () => void) {
this.bare.onclose = (event) => {
callback();
};
};
}

152
js/src/webtty.ts Normal file
View File

@ -0,0 +1,152 @@
export const protocols = ["webtty"];
export const msgInputUnknown = '0';
export const msgInput = '1';
export const msgPing = '2';
export const msgResizeTerminal = '3';
export const msgUnknownOutput = '0';
export const msgOutput = '1';
export const msgPong = '2';
export const msgSetWindowTitle = '3';
export const msgSetPreferences = '4';
export const msgSetReconnect = '5';
export interface Terminal {
info(): { columns: number, rows: number };
output(data: string): void;
showMessage(message: string, timeout: number): void;
removeMessage(): void;
setWindowTitle(title: string): void;
setPreferences(value: object): void;
onInput(callback: (input: string) => void): void;
onResize(callback: (colmuns: number, rows: number) => void): void;
reset(): void;
deactivate(): void;
close(): void;
}
export interface Connection {
open(): void;
close(): void;
send(data: string): void;
isOpen(): boolean;
onOpen(callback: () => void): void;
onReceive(callback: (data: string) => void): void;
onClose(callback: () => void): void;
}
export interface ConnectionFactory {
create(): Connection;
}
export class WebTTY {
term: Terminal;
connectionFactory: ConnectionFactory;
args: string;
authToken: string;
reconnect: number;
constructor(term: Terminal, connectionFactory: ConnectionFactory, args: string, authToken: string) {
this.term = term;
this.connectionFactory = connectionFactory;
this.args = args;
this.authToken = authToken;
this.reconnect = -1;
};
open() {
let connection = this.connectionFactory.create();
let pingTimer: number;
let reconnectTimeout: number;
const setup = () => {
connection.onOpen(() => {
const termInfo = this.term.info();
connection.send(JSON.stringify(
{
Arguments: this.args,
AuthToken: this.authToken,
}
));
const resizeHandler = (colmuns: number, rows: number) => {
connection.send(
msgResizeTerminal + JSON.stringify(
{
columns: colmuns,
rows: rows
}
)
);
};
this.term.onResize(resizeHandler);
resizeHandler(termInfo.columns, termInfo.rows);
this.term.onInput(
(input: string) => {
connection.send(msgInput + input);
}
);
pingTimer = setInterval(() => {
connection.send(msgPing)
}, 30 * 1000);
});
connection.onReceive((data) => {
const payload = data.slice(1);
switch (data[0]) {
case msgOutput:
this.term.output(
decodeURIComponent(Array.prototype.map.call(atob(payload), function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''))
);
break;
case msgPong:
break;
case msgSetWindowTitle:
this.term.setWindowTitle(payload);
break;
case msgSetPreferences:
const preferences = JSON.parse(payload);
this.term.setPreferences(preferences);
break;
case msgSetReconnect:
const autoReconnect = JSON.parse(payload);
console.log("Enabling reconnect: " + autoReconnect + " seconds")
this.reconnect = autoReconnect;
break;
}
});
connection.onClose(() => {
clearInterval(pingTimer);
this.term.deactivate();
this.term.showMessage("Connection Closed", 0);
if (this.reconnect > 0) {
reconnectTimeout = setTimeout(() => {
connection = this.connectionFactory.create();
this.term.reset();
setup();
}, this.reconnect * 1000);
}
});
connection.open();
}
setup();
return () => {
clearTimeout(reconnectTimeout);
connection.close();
}
};
};

101
js/src/xterm.ts Normal file
View File

@ -0,0 +1,101 @@
import * as bare from "xterm";
bare.loadAddon("fit");
export class TermXterm {
elem: HTMLElement;
message: HTMLElement;
messageTimeout: number;
messageTimer: number;
term: bare;
resizeListener: () => void;
constructor(elem: HTMLElement) {
this.elem = elem;
this.term = new bare();
this.message = elem.ownerDocument.createElement("div");
this.message.className = "xterm-overlay";
this.messageTimeout = 2000;
this.resizeListener = () => {
this.term.fit();
this.term.scrollToBottom();
this.showMessage(String(this.term.cols) + "x" + String(this.term.rows), this.messageTimeout);
};
this.term.on("open", () => {
this.resizeListener();
window.addEventListener("resize", () => { this.resizeListener(); });
});
this.term.open(elem, true);
};
info(): { columns: number, rows: number } {
return { columns: this.term.cols, rows: this.term.rows };
};
output(data: string) {
this.term.write(data);
};
showMessage(message: string, timeout: number) {
this.message.textContent = message;
this.elem.appendChild(this.message);
if (this.messageTimer) {
clearTimeout(this.messageTimer);
}
if (timeout > 0) {
this.messageTimer = setTimeout(() => {
this.elem.removeChild(this.message);
}, timeout);
}
};
removeMessage(): void {
if (this.message.parentNode == this.elem) {
this.elem.removeChild(this.message);
}
}
setWindowTitle(title: string) {
document.title = title;
};
setPreferences(value: object) {
};
onInput(callback: (input: string) => void) {
this.term.on("data", (data) => {
callback(data);
});
};
onResize(callback: (colmuns: number, rows: number) => void) {
this.term.on("resize", (data) => {
callback(data.cols, data.rows);
});
};
deactivate(): void {
this.term.off("data");
this.term.off("resize");
this.term.blur();
}
reset(): void {
this.removeMessage();
this.term.clear();
}
close(): void {
window.removeEventListener("resize", this.resizeListener);
this.term.destroy();
}
}

20
js/tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"strictNullChecks": true,
"noUnusedLocals" : true,
"noImplicitThis": true,
"alwaysStrict": true,
"outDir": "./dist/",
"declaration": true,
"sourceMap": true,
"target": "es5",
"module": "commonJS",
"baseUrl": ".",
"paths": {
"*": ["./typings/*"]
}
},
"exclude": [
"node_modules"
]
}

47
js/typings/hterm/index.d.ts vendored Normal file
View File

@ -0,0 +1,47 @@
export interface Terminal {
io: IO;
onTerminalReady: () => void;
getPrefs(): Prefs;
decorate(HTMLElement);
installKeyboard(): void;
uninstallKeyboard(): void;
setWindowTitle(title: string): void;
reset(): void;
softReset(): void;
}
export interface TerminalConstructor {
new (): Terminal;
(): Terminal;
}
export interface IO {
writeUTF8: ((data: string) => void);
writeUTF16: ((data: string) => void);
onVTKeystroke: ((data: string) => void) | null;
sendString: ((data: string) => void) | null;
onTerminalResize: ((columns: number, rows: number) => void) | null;
push(): IO;
writeUTF(data: string);
showOverlay(message: string, timeout: number | null);
}
export interface Prefs {
set(key: string, value: string): void;
}
export interface Storage {
}
export var Terminal: TerminalConstructor;
// @TODO: is there better way?
// exported variables are forced to be read-protected.
declare global {
var hterm: {
defaultStorage: Storage;
};
}

11
js/typings/htermLib/index.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
export interface Storage {
}
export interface Memory {
new (): Storage;
Memory(): Storage
}
export var Storage: {
Memory: Memory
};

23
js/webpack.config.js Normal file
View File

@ -0,0 +1,23 @@
module.exports = {
entry: "./src/main.ts",
output: {
filename: "./dist/bundle.js"
},
externals: {
"hterm": "hterm",
"htermLib": "lib"
},
devtool: "source-map",
resolve: {
extensions: [".ts", ".tsx", ".js"],
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader",
exclude: [/node_modules/],
}
]
}
};

View File

@ -1,95 +0,0 @@
(function() {
var httpsEnabled = window.location.protocol == "https:";
var args = window.location.search;
var url = (httpsEnabled ? 'wss://' : 'ws://') + window.location.host + window.location.pathname + 'ws';
var protocols = ["webtty"];
var autoReconnect = -1;
var openWs = function() {
var ws = new WebSocket(url, protocols);
var term;
var pingTimer;
ws.onopen = function(event) {
ws.send(JSON.stringify({ Arguments: args, AuthToken: gotty_auth_token,}));
pingTimer = setInterval(sendPing, 30 * 1000, ws);
hterm.defaultStorage = new lib.Storage.Memory();
term = new hterm.Terminal();
term.getPrefs().set("send-encoding", "raw");
term.onTerminalReady = function() {
var io = term.io.push();
io.onVTKeystroke = function(str) {
ws.send("1" + str);
};
io.sendString = io.onVTKeystroke;
io.onTerminalResize = function(columns, rows) {
ws.send(
"3" + JSON.stringify(
{
columns: columns,
rows: rows,
}
)
)
};
term.installKeyboard();
};
term.decorate(document.getElementById("terminal"));
};
ws.onmessage = function(event) {
data = event.data.slice(1);
switch(event.data[0]) {
case '1':
term.io.writeUTF8(window.atob(data));
break;
case '2':
// pong
break;
case '3':
term.setWindowTitle(data);
break;
case '4':
preferences = JSON.parse(data);
Object.keys(preferences).forEach(function(key) {
console.log("Setting " + key + ": " + preferences[key]);
term.getPrefs().set(key, preferences[key]);
});
break;
case '5':
autoReconnect = JSON.parse(data);
console.log("Enabling reconnect: " + autoReconnect + " seconds")
break;
}
};
ws.onclose = function(event) {
if (term) {
term.uninstallKeyboard();
term.io.showOverlay("Connection Closed", null);
}
clearInterval(pingTimer);
if (autoReconnect > 0) {
setTimeout(openWs, autoReconnect * 1000);
}
};
}
var sendPing = function(ws) {
ws.send("2");
}
openWs();
})()

7
resources/index.css Normal file
View File

@ -0,0 +1,7 @@
html, body, #terminal {
background: black;
height: 100%;
width: 100%;
padding: 0%;
margin: 0%;
}

View File

@ -2,13 +2,16 @@
<html>
<head>
<title>{{ .title }}</title>
<style>body, #terminal {position: absolute; height: 100%; width: 100%; margin: 0px;}</style>
<link rel="icon" type="image/png" href="favicon.png">
<link rel="stylesheet" href="./css/index.css" />
<link rel="stylesheet" href="./css/xterm.css" />
<link rel="stylesheet" href="./css/xterm_customize.css" />
<script src="/js/hterm.js"></script>
</head>
<body>
<div id="terminal"></div>
<script src="./js/hterm.js"></script>
<script src="./auth_token.js"></script>
<script src="./js/gotty.js"></script>
<script src="./config.js"></script>
<script src="./js/bundle.js"></script>
</body>
</html>

View File

@ -0,0 +1,19 @@
.terminal {
font-family: "DejaVu Sans Mono", "Everson Mono", FreeMono, Menlo, Terminal, monospace;
}
.xterm-overlay {
font-family: "DejaVu Sans Mono", "Everson Mono", FreeMono, Menlo, Terminal, monospace;
border-radius: 15px;
font-size: xx-large;
color: black;
background: white;
opacity: 0.75;
padding: 0.2em 0.5em 0.2em 0.5em;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
user-select: none;
transition: opacity 180ms ease-in;
}

File diff suppressed because one or more lines are too long

View File

@ -218,6 +218,11 @@ func (server *Server) handleAuthToken(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("var gotty_auth_token = '" + server.options.Credential + "';"))
}
func (server *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/javascript")
w.Write([]byte("var gotty_term = '" + server.options.Term + "';"))
}
// titleVariables merges maps in a specified order.
// varUnits are name-keyed maps, whose names will be iterated using order.
func (server *Server) titleVariables(order []string, varUnits map[string]map[string]interface{}) map[string]interface{} {

View File

@ -29,6 +29,7 @@ type Options struct {
Width int `hcl:"width" flagName:"width" flagDescribe:"Static width of the screen, 0(default) means dynamically resize" default:"0"`
Height int `hcl:"height" flagName:"height" flagDescribe:"Static height of the screen, 0(default) means dynamically resize" default:"0"`
WSOrigin string `hcl:"ws_origin" flagName:"ws-origin" flagDescribe:"A regular expression that matches origin URLs to be accepted by WebSocket. No cross origin requests are acceptable by default" default:""`
Term string `hcl:"term" flagName:"term" flagDescribe:"Terminal name to use on the browser, one of xterm or hterm." default:"xterm"`
TitleVariables map[string]interface{}
}

View File

@ -186,7 +186,9 @@ func (server *Server) setupHandlers(ctx context.Context, cancel context.CancelFu
siteMux.HandleFunc(url.Path, server.handleIndex)
siteMux.Handle(url.Path+"js/", http.StripPrefix(url.Path, staticFileHandler))
siteMux.Handle(url.Path+"favicon.png", http.StripPrefix(url.Path, staticFileHandler))
siteMux.Handle(url.Path+"css/", http.StripPrefix(url.Path, staticFileHandler))
siteMux.HandleFunc(url.Path+"auth_token.js", server.handleAuthToken)
siteMux.HandleFunc(url.Path+"config.js", server.handleConfig)
siteHandler := http.Handler(siteMux)