mirror of
https://github.com/yudai/gotty.git
synced 2026-02-21 23:44:07 +01:00
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:
parent
d6c98866b9
commit
8803721f3d
40 changed files with 9051 additions and 124 deletions
93
js/src/hterm.ts
Normal file
93
js/src/hterm.ts
Normal 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
30
js/src/main.ts
Normal 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
60
js/src/websocket.ts
Normal 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
152
js/src/webtty.ts
Normal 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
101
js/src/xterm.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue