mirror of
https://github.com/tsl0922/ttyd.git
synced 2026-02-09 11:34:23 +01:00
preact: intial commit
This commit is contained in:
4
preact/.gitignore
vendored
Normal file
4
preact/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
/build
|
||||
/dist
|
||||
/*.log
|
||||
24
preact/package.json
Normal file
24
preact/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "ttyd",
|
||||
"version": "1.0.0",
|
||||
"description": "Share your terminal over the web",
|
||||
"repository": {
|
||||
"url": "git@github.com:tsl0922/ttyd.git",
|
||||
"type": "git"
|
||||
},
|
||||
"author": "Shuanglei Tao <tsl0922@gmail.com>",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"start": "preact watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"preact-cli": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"decko": "^1.2.0",
|
||||
"fast-text-encoding": "^1.0.0",
|
||||
"preact": "^8.2.7",
|
||||
"xterm": "^3.3.0"
|
||||
}
|
||||
}
|
||||
19
preact/preact.config.js
Normal file
19
preact/preact.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Function that mutates original webpack config.
|
||||
* Supports asynchronous changes when promise is returned.
|
||||
*
|
||||
* @param {object} config - original webpack config.
|
||||
* @param {object} env - options passed to CLI.
|
||||
* @param {WebpackConfigHelpers} helpers - object with useful helpers when working with config.
|
||||
**/
|
||||
export default function (config, env, helpers) {
|
||||
const { devServer } = config;
|
||||
if (devServer) {
|
||||
devServer.proxy = {
|
||||
'/ws': {
|
||||
target: 'ws://127.0.0.1:7681',
|
||||
ws: true
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
5
preact/src/.babelrc
Normal file
5
preact/src/.babelrc
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"presets": [
|
||||
["preact-cli/babel", { "modules": "commonjs" }]
|
||||
]
|
||||
}
|
||||
BIN
preact/src/assets/favicon.ico
Normal file
BIN
preact/src/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
15
preact/src/components/app.js
Normal file
15
preact/src/components/app.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import Terminal from './terminal';
|
||||
|
||||
if (module.hot) {
|
||||
require('preact/debug');
|
||||
}
|
||||
|
||||
export default class App extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Terminal />
|
||||
);
|
||||
}
|
||||
}
|
||||
179
preact/src/components/terminal/index.js
Normal file
179
preact/src/components/terminal/index.js
Normal file
@@ -0,0 +1,179 @@
|
||||
import { h, Component } from 'preact';
|
||||
import { bind } from 'decko';
|
||||
import { Terminal as TERMINAL } from 'xterm';
|
||||
import style from 'xterm/dist/xterm.css';
|
||||
|
||||
require('fast-text-encoding');
|
||||
|
||||
export default class Terminal extends Component {
|
||||
componentDidMount() {
|
||||
TERMINAL.applyAddon(require('xterm/dist/addons/fit'));
|
||||
TERMINAL.applyAddon(require('xterm/dist/addons/winptyCompat'));
|
||||
TERMINAL.applyAddon(require('./overlay'));
|
||||
|
||||
this.url = (window.location.protocol === 'https:' ? 'wss://' : 'ws://')
|
||||
+ window.location.host + window.location.pathname + 'ws';
|
||||
|
||||
this.autoReconnect = 0;
|
||||
this.textDecoder = new TextDecoder();
|
||||
this.textEncoder = new TextEncoder();
|
||||
this.connectWebsocket();
|
||||
|
||||
window.addEventListener('resize', this.onWindowSize);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.onWindowSize);
|
||||
}
|
||||
|
||||
@bind
|
||||
onWindowSize() {
|
||||
const { terminal } = this;
|
||||
|
||||
clearTimeout(this.resizedFinished);
|
||||
this.resizedFinished = setTimeout(function () {
|
||||
if (terminal) {
|
||||
terminal.fit();
|
||||
}
|
||||
}, 250);
|
||||
}
|
||||
|
||||
@bind
|
||||
connectWebsocket() {
|
||||
this.socket = new WebSocket(this.url, ['tty']);
|
||||
const { socket } = this;
|
||||
|
||||
socket.binaryType = 'arraybuffer';
|
||||
|
||||
socket.onopen = this.onSocketOpen;
|
||||
socket.onmessage = this.onSocketData;
|
||||
socket.onclose = this.onSocketClose;
|
||||
}
|
||||
|
||||
@bind
|
||||
onSocketOpen(event) {
|
||||
console.log('Websocket connection opened');
|
||||
const { socket, textEncoder } = this;
|
||||
socket.send(textEncoder.encode(JSON.stringify({AuthToken: ''})));
|
||||
|
||||
if (this.terminal) {
|
||||
this.terminal.destroy();
|
||||
}
|
||||
|
||||
this.terminal = new TERMINAL({
|
||||
fontSize: 13,
|
||||
fontFamily: '"Menlo for Powerline", Menlo, Consolas, "Liberation Mono", Courier, monospace',
|
||||
theme: {
|
||||
foreground: '#d2d2d2',
|
||||
background: '#2b2b2b',
|
||||
cursor: '#adadad',
|
||||
black: '#000000',
|
||||
red: '#d81e00',
|
||||
green: '#5ea702',
|
||||
yellow: '#cfae00',
|
||||
blue: '#427ab3',
|
||||
magenta: '#89658e',
|
||||
cyan: '#00a7aa',
|
||||
white: '#dbded8',
|
||||
brightBlack: '#686a66',
|
||||
brightRed: '#f54235',
|
||||
brightGreen: '#99e343',
|
||||
brightYellow: '#fdeb61',
|
||||
brightBlue: '#84b0d8',
|
||||
brightMagenta: '#bc94b7',
|
||||
brightCyan: '#37e6e8',
|
||||
brightWhite: '#f1f1f0'
|
||||
},
|
||||
});
|
||||
|
||||
const { terminal } = this;
|
||||
|
||||
terminal.on('title', this.onTerminalTitle);
|
||||
terminal.on('data', this.onTerminalData);
|
||||
terminal.on('resize', this.onTerminalResize);
|
||||
|
||||
terminal.open(this.container, true);
|
||||
terminal.winptyCompatInit();
|
||||
terminal.fit();
|
||||
terminal.focus();
|
||||
}
|
||||
|
||||
@bind
|
||||
onSocketClose(event) {
|
||||
console.log('Websocket connection closed with code: ' + event.code);
|
||||
|
||||
const { terminal, autoReconnect, connectWebsocket } = this;
|
||||
terminal.showOverlay('Connection Closed', null);
|
||||
|
||||
// 1000: CLOSE_NORMAL
|
||||
if (event.code !== 1000 && autoReconnect > 0) {
|
||||
setTimeout(connectWebsocket, autoReconnect * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
onSocketData(event) {
|
||||
const { terminal, textDecoder } = this;
|
||||
|
||||
let rawData = new Uint8Array(event.data),
|
||||
cmd = String.fromCharCode(rawData[0]),
|
||||
data = rawData.slice(1).buffer;
|
||||
|
||||
switch(cmd) {
|
||||
case '0':
|
||||
terminal.write(textDecoder.decode(data));
|
||||
break;
|
||||
case '1':
|
||||
let title = textDecoder.decode(data);
|
||||
document.title = title;
|
||||
break;
|
||||
case '2':
|
||||
let preferences = JSON.parse(textDecoder.decode(data));
|
||||
Object.keys(preferences).forEach(function(key) {
|
||||
console.log('Setting ' + key + ': ' + preferences[key]);
|
||||
terminal.setOption(key, preferences[key]);
|
||||
});
|
||||
break;
|
||||
case '3':
|
||||
this.autoReconnect = JSON.parse(textDecoder.decode(data));
|
||||
console.log('Enabling reconnect: ' + this.autoReconnect + ' seconds');
|
||||
break;
|
||||
default:
|
||||
console.warn('Unknown command: ' + cmd);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
onTerminalTitle(title) {
|
||||
if (title && title !== '') {
|
||||
document.title = title;
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
onTerminalResize({ cols, rows }) {
|
||||
const { terminal, socket, textEncoder } = this;
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
let msg = '1' + JSON.stringify({columns: cols, rows: rows});
|
||||
socket.send(textEncoder.encode(msg));
|
||||
}
|
||||
setTimeout(function() {
|
||||
terminal.showOverlay(cols + 'x' + rows);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
@bind
|
||||
onTerminalData(data) {
|
||||
const { terminal, socket, textEncoder } = this;
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(textEncoder.encode('0' + data));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="terminal-container" ref={(div) => { this.container = div; }}></div>
|
||||
);
|
||||
}
|
||||
}
|
||||
66
preact/src/components/terminal/overlay.js
Normal file
66
preact/src/components/terminal/overlay.js
Normal file
@@ -0,0 +1,66 @@
|
||||
// ported from hterm.Terminal.prototype.showOverlay
|
||||
// https://chromium.googlesource.com/apps/libapps/+/master/hterm/js/hterm_terminal.js
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
|
||||
function showOverlay(term, msg, timeout) {
|
||||
if (!term.overlayNode_) {
|
||||
if (!term.element)
|
||||
return;
|
||||
term.overlayNode_ = document.createElement('div');
|
||||
term.overlayNode_.style.cssText = (
|
||||
'border-radius: 15px;' +
|
||||
'font-size: xx-large;' +
|
||||
'opacity: 0.75;' +
|
||||
'padding: 0.2em 0.5em 0.2em 0.5em;' +
|
||||
'position: absolute;' +
|
||||
'-webkit-user-select: none;' +
|
||||
'-webkit-transition: opacity 180ms ease-in;' +
|
||||
'-moz-user-select: none;' +
|
||||
'-moz-transition: opacity 180ms ease-in;');
|
||||
|
||||
term.overlayNode_.addEventListener('mousedown', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}, true);
|
||||
}
|
||||
term.overlayNode_.style.color = "#101010";
|
||||
term.overlayNode_.style.backgroundColor = "#f0f0f0";
|
||||
|
||||
term.overlayNode_.textContent = msg;
|
||||
term.overlayNode_.style.opacity = '0.75';
|
||||
|
||||
if (!term.overlayNode_.parentNode)
|
||||
term.element.appendChild(term.overlayNode_);
|
||||
|
||||
var divSize = term.element.getBoundingClientRect();
|
||||
var overlaySize = term.overlayNode_.getBoundingClientRect();
|
||||
|
||||
term.overlayNode_.style.top =
|
||||
(divSize.height - overlaySize.height) / 2 + 'px';
|
||||
term.overlayNode_.style.left = (divSize.width - overlaySize.width) / 2 + 'px';
|
||||
|
||||
if (term.overlayTimeout_)
|
||||
clearTimeout(term.overlayTimeout_);
|
||||
|
||||
if (timeout === null)
|
||||
return;
|
||||
|
||||
term.overlayTimeout_ = setTimeout(function() {
|
||||
term.overlayNode_.style.opacity = '0';
|
||||
term.overlayTimeout_ = setTimeout(function() {
|
||||
if (term.overlayNode_.parentNode)
|
||||
term.overlayNode_.parentNode.removeChild(term.overlayNode_);
|
||||
term.overlayTimeout_ = null;
|
||||
term.overlayNode_.style.opacity = '0.75';
|
||||
}, 200);
|
||||
}, timeout || 1500);
|
||||
}
|
||||
exports.showOverlay = showOverlay;
|
||||
|
||||
function apply(terminalConstructor) {
|
||||
terminalConstructor.prototype.showOverlay = function (msg, timeout) {
|
||||
return showOverlay(this, msg, timeout);
|
||||
};
|
||||
}
|
||||
exports.apply = apply;
|
||||
4
preact/src/index.js
Normal file
4
preact/src/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import './style';
|
||||
import App from './components/app';
|
||||
|
||||
export default App;
|
||||
18
preact/src/style/index.css
Normal file
18
preact/src/style/index.css
Normal file
@@ -0,0 +1,18 @@
|
||||
html, body {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#terminal-container {
|
||||
width: auto;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
background-color: #2b2b2b;
|
||||
}
|
||||
|
||||
#terminal-container .terminal {
|
||||
padding: 5px;
|
||||
}
|
||||
7033
preact/yarn.lock
Normal file
7033
preact/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user