mirror of
https://github.com/tsl0922/ttyd.git
synced 2025-12-22 11:54:19 +01:00
Move to xterm.js for CJK and IME support (#22)
This commit is contained in:
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -1 +1 @@
|
|||||||
index.html linguist-vendored
|
src/index.html linguist-vendored
|
||||||
|
|||||||
35
README.md
35
README.md
@@ -1,23 +1,23 @@
|
|||||||
# ttyd - Share your terminal over the web [](https://travis-ci.org/tsl0922/ttyd)
|
# ttyd - Share your terminal over the web [](https://travis-ci.org/tsl0922/ttyd)
|
||||||
|
|
||||||
ttyd is a simple command-line tool for sharing terminal over the web, inspired by [GoTTY](https://github.com/yudai/gotty).
|
ttyd is a simple command-line tool for sharing terminal over the web, inspired by [GoTTY][1].
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
- Build on [libwebsockets](https://libwebsockets.org) with C for speed
|
- Built on top of [Libwebsockets][2] with C for speed
|
||||||
- Full terminal emulation based on [hterm](https://chromium.googlesource.com/apps/libapps/+/HEAD/hterm)
|
- Full terminal emulation based on [Xterm.js][3] with CJK and IME support
|
||||||
- SSL support based on [OpenSSL](https://www.openssl.org)
|
- SSL support based on [OpenSSL][4]
|
||||||
- Run any custom command with options
|
- Run any custom command with options
|
||||||
- Basic authentication support
|
- Basic authentication support and many other custom options
|
||||||
- Cross platform: macOS, Linux, [OpenWrt](https://openwrt.org)/[LEDE](https://www.lede-project.org)
|
- Cross platform: macOS, Linux, [OpenWrt][5]/[LEDE][6]
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
## Install on macOS
|
## Install on macOS
|
||||||
|
|
||||||
Install with [homebrew](http://brew.sh):
|
Install with [homebrew][7]:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install ttyd
|
brew install ttyd
|
||||||
@@ -35,7 +35,7 @@ cmake ..
|
|||||||
make && make install
|
make && make install
|
||||||
```
|
```
|
||||||
|
|
||||||
> **NOTE:** You may need to compile libwebsockets from source for ubuntu versions old than 16.04, since they have outdated `libwebsockets-dev` package ([Issue #6](https://github.com/tsl0922/ttyd/issues/6)).
|
> **NOTE:** You may need to compile libwebsockets from source for ubuntu versions old than 16.04, since they have outdated `libwebsockets-dev` package ([Issue #6][9]).
|
||||||
|
|
||||||
## Install on OpenWrt/LEDE
|
## Install on OpenWrt/LEDE
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ make && make install
|
|||||||
opkg install ttyd
|
opkg install ttyd
|
||||||
```
|
```
|
||||||
|
|
||||||
> **NOTE:** This may only works for [LEDE](https://www.lede-project.org) snapshots currently, if the install command fails, compile it yourself.
|
> **NOTE:** This may only works for [LEDE][6] snapshots currently, if the install command fails, compile it yourself.
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ VERSION:
|
|||||||
1.1.0
|
1.1.0
|
||||||
|
|
||||||
OPTIONS:
|
OPTIONS:
|
||||||
--port, -p Port to listen (default: 7681)
|
--port, -p Port to listen (default: 7681, use `0` for random port)
|
||||||
--interface, -i Network interface to bind
|
--interface, -i Network interface to bind
|
||||||
--credential, -c Credential for Basic Authentication (format: username:password)
|
--credential, -c Credential for Basic Authentication (format: username:password)
|
||||||
--uid, -u User id to run with
|
--uid, -u User id to run with
|
||||||
@@ -88,5 +88,16 @@ Then open <http://localhost:7681>, now you can see and control the `bash` consol
|
|||||||
|
|
||||||
# Credits
|
# Credits
|
||||||
|
|
||||||
- [GoTTY](https://github.com/yudai/gotty): ttyd is a port of GoTTY to `C` language.
|
- [GoTTY][1]: ttyd is a port of GoTTY to `C` language.
|
||||||
- [hterm](https://chromium.googlesource.com/apps/libapps/+/HEAD/hterm): ttyd uses hterm to run a terminal emulator on the web.
|
- [Libwebsockets][2]: used to build the websocket server.
|
||||||
|
- [Xterm.js][3]: used to run the terminal emulator on the web, former: [hterm][8].
|
||||||
|
|
||||||
|
[1]: https://github.com/yudai/gotty
|
||||||
|
[2]: https://libwebsockets.org
|
||||||
|
[3]: https://github.com/sourcelair/xterm.js
|
||||||
|
[4]: https://www.openssl.org
|
||||||
|
[5]: https://openwrt.org
|
||||||
|
[6]: https://www.lede-project.org
|
||||||
|
[7]: http://brew.sh
|
||||||
|
[8]: https://chromium.googlesource.com/apps/libapps/+/HEAD/hterm
|
||||||
|
[9]: https://github.com/tsl0922/ttyd/issues/6
|
||||||
47
html/.gitignore
vendored
Normal file
47
html/.gitignore
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules
|
||||||
|
jspm_packages
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
10
html/README.md
Normal file
10
html/README.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
### Build the inlined html
|
||||||
|
|
||||||
|
install [Yarn](https://yarnpkg.com), then run the following commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn
|
||||||
|
yarn run build
|
||||||
|
```
|
||||||
|
|
||||||
|
this will compile the inlined html to `../src/index.html`.
|
||||||
45
html/css/app.css
Normal file
45
html/css/app.css
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
body, #terminal-container {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0px;
|
||||||
|
font-size:15px;
|
||||||
|
font-variant-ligatures: none;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
background-color: #101010;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal {
|
||||||
|
background-color: #101010;
|
||||||
|
color: #f0f0f0;
|
||||||
|
font-family: "DejaVu Sans Mono","Everson Mono",FreeMono,Menlo,Terminal,monospace;
|
||||||
|
border: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal .xterm-viewport {
|
||||||
|
background-color: #101010;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal .terminal-cursor {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
color: #101010;
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal:not(.focus) .terminal-cursor {
|
||||||
|
outline: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink-cursor {
|
||||||
|
0% {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
color: #101010;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
html/favicon.png
Normal file
BIN
html/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
10
html/gulpfile.js
Normal file
10
html/gulpfile.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
var gulp = require('gulp'),
|
||||||
|
inlinesource = require('gulp-inline-source');
|
||||||
|
|
||||||
|
gulp.task('inlinesource', function () {
|
||||||
|
return gulp.src('*.html')
|
||||||
|
.pipe(inlinesource())
|
||||||
|
.pipe(gulp.dest('../src'));
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('default', ['inlinesource']);
|
||||||
19
html/index.html
Normal file
19
html/index.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||||
|
<title>ttyd - Terminal</title>
|
||||||
|
<link inline rel="icon" type="image/png" href="favicon.png">
|
||||||
|
<link inline href="node_modules/xterm/dist/xterm.css">
|
||||||
|
<link inline href="css/app.css">
|
||||||
|
<script inline src="node_modules/xterm/dist/xterm.js"></script>
|
||||||
|
<script inline src="node_modules/xterm/addons/fit/fit.js"></script>
|
||||||
|
<script inline src="js/overlay/overlay.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="terminal-container"></div>
|
||||||
|
<script src="auth_token.js"></script>
|
||||||
|
<script inline src="js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
109
html/js/app.js
Normal file
109
html/js/app.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
(function() {
|
||||||
|
var terminalContainer = document.getElementById('terminal-container'),
|
||||||
|
httpsEnabled = window.location.protocol == "https:",
|
||||||
|
url = (httpsEnabled ? 'wss://' : 'ws://') + window.location.host + window.location.pathname + 'ws',
|
||||||
|
protocols = ["tty"],
|
||||||
|
autoReconnect = -1,
|
||||||
|
term, pingTimer;
|
||||||
|
|
||||||
|
var openWs = function() {
|
||||||
|
var ws = new WebSocket(url, protocols);
|
||||||
|
|
||||||
|
ws.onopen = function(event) {
|
||||||
|
if (typeof tty_auth_token !== 'undefined') {
|
||||||
|
ws.send(JSON.stringify({AuthToken: tty_auth_token}));
|
||||||
|
}
|
||||||
|
pingTimer = setInterval(sendPing, 30 * 1000, ws);
|
||||||
|
|
||||||
|
if (typeof term !== 'undefined') {
|
||||||
|
term.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
term = new Terminal();
|
||||||
|
|
||||||
|
term.on('resize', function (size) {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send("2" + JSON.stringify({columns: size.cols, rows: size.rows}));
|
||||||
|
}
|
||||||
|
setTimeout(function() {
|
||||||
|
term.showOverlay(size.cols + 'x' + size.rows);
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
term.on("data", function(data) {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send("0" + data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.onresize = function(event) {
|
||||||
|
term.fit();
|
||||||
|
};
|
||||||
|
|
||||||
|
while (terminalContainer.firstChild) {
|
||||||
|
terminalContainer.removeChild(terminalContainer.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
term.open(terminalContainer);
|
||||||
|
term.fit();
|
||||||
|
term.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = function(event) {
|
||||||
|
var data = event.data.slice(1);
|
||||||
|
switch(event.data[0]) {
|
||||||
|
case '0':
|
||||||
|
term.write(decodeURIComponent(escape(window.atob(data))));
|
||||||
|
break;
|
||||||
|
case '1': // pong
|
||||||
|
break;
|
||||||
|
case '2':
|
||||||
|
document.title = data;
|
||||||
|
break;
|
||||||
|
case '3':
|
||||||
|
var preferences = JSON.parse(data);
|
||||||
|
Object.keys(preferences).forEach(function(key) {
|
||||||
|
console.log("Setting " + key + ": " + preferences[key]);
|
||||||
|
term.setOption(key, preferences[key]);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case '4':
|
||||||
|
autoReconnect = JSON.parse(data);
|
||||||
|
console.log("Enabling reconnect: " + autoReconnect + " seconds")
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = function(event) {
|
||||||
|
if (term) {
|
||||||
|
term.off('data');
|
||||||
|
term.off('resize');
|
||||||
|
term.showOverlay("Connection Closed", null);
|
||||||
|
}
|
||||||
|
clearInterval(pingTimer);
|
||||||
|
if (autoReconnect > 0) {
|
||||||
|
setTimeout(openWs, autoReconnect * 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = function(event) {
|
||||||
|
var errorNode = document.createElement('div');
|
||||||
|
errorNode.style.cssText = [
|
||||||
|
"color: red",
|
||||||
|
"background-color: white",
|
||||||
|
"font-size: x-large",
|
||||||
|
"opacity: 0.75",
|
||||||
|
"text-align: center",
|
||||||
|
"margin: 1em",
|
||||||
|
"padding: 0.2em",
|
||||||
|
"border: 0.1em dotted #ccc"
|
||||||
|
].join(";");
|
||||||
|
errorNode.textContent = "Websocket handshake failed!";
|
||||||
|
terminalContainer.insertBefore(errorNode, terminalContainer.firstChild);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var sendPing = function(ws) {
|
||||||
|
ws.send("1");
|
||||||
|
};
|
||||||
|
|
||||||
|
openWs();
|
||||||
|
})()
|
||||||
56
html/js/overlay/overlay.js
Normal file
56
html/js/overlay/overlay.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// ported from hterm.Terminal.prototype.showOverlay
|
||||||
|
Terminal.prototype.showOverlay = function(msg, timeout) {
|
||||||
|
if (!this.overlayNode_) {
|
||||||
|
if (!this.element)
|
||||||
|
return;
|
||||||
|
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;' +
|
||||||
|
'position: absolute;' +
|
||||||
|
'-webkit-user-select: none;' +
|
||||||
|
'-webkit-transition: opacity 180ms ease-in;' +
|
||||||
|
'-moz-user-select: none;' +
|
||||||
|
'-moz-transition: opacity 180ms ease-in;');
|
||||||
|
|
||||||
|
this.overlayNode_.addEventListener('mousedown', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
this.overlayNode_.style.color = "#101010";
|
||||||
|
this.overlayNode_.style.backgroundColor = "#f0f0f0";
|
||||||
|
|
||||||
|
this.overlayNode_.textContent = msg;
|
||||||
|
this.overlayNode_.style.opacity = '0.75';
|
||||||
|
|
||||||
|
if (!this.overlayNode_.parentNode)
|
||||||
|
this.element.appendChild(this.overlayNode_);
|
||||||
|
|
||||||
|
var divSize = this.element.getBoundingClientRect();
|
||||||
|
var overlaySize = this.overlayNode_.getBoundingClientRect();
|
||||||
|
|
||||||
|
this.overlayNode_.style.top =
|
||||||
|
(divSize.height - overlaySize.height) / 2 + 'px';
|
||||||
|
this.overlayNode_.style.left = (divSize.width - overlaySize.width) / 2 + 'px';
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (this.overlayTimeout_)
|
||||||
|
clearTimeout(this.overlayTimeout_);
|
||||||
|
|
||||||
|
if (timeout === null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.overlayTimeout_ = setTimeout(function() {
|
||||||
|
self.overlayNode_.style.opacity = '0';
|
||||||
|
self.overlayTimeout_ = setTimeout(function() {
|
||||||
|
if (self.overlayNode_.parentNode)
|
||||||
|
self.overlayNode_.parentNode.removeChild(self.overlayNode_);
|
||||||
|
self.overlayTimeout_ = null;
|
||||||
|
self.overlayNode_.style.opacity = '0.75';
|
||||||
|
}, 200);
|
||||||
|
}, timeout || 1500);
|
||||||
|
};
|
||||||
20
html/package.json
Normal file
20
html/package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "ttyd",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"description": "Share your terminal over the web",
|
||||||
|
"main": "js/app.js",
|
||||||
|
"repository": {
|
||||||
|
"url": "git@github.com:tsl0922/ttyd.git",
|
||||||
|
"type": "git"
|
||||||
|
},
|
||||||
|
"author": "Shuanglei Tao <tsl0922@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"build": "gulp"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"gulp": "^3.9.1",
|
||||||
|
"gulp-inline-source": "^3.0.0",
|
||||||
|
"xterm": "^2.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
1448
html/yarn.lock
Normal file
1448
html/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
697
src/index.html
vendored
697
src/index.html
vendored
File diff suppressed because one or more lines are too long
@@ -49,12 +49,12 @@ parse_window_size(const char *json) {
|
|||||||
struct json_object *o = NULL;
|
struct json_object *o = NULL;
|
||||||
|
|
||||||
if (!json_object_object_get_ex(obj, "columns", &o)) {
|
if (!json_object_object_get_ex(obj, "columns", &o)) {
|
||||||
lwsl_err("columns field not exists!");
|
lwsl_err("columns field not exists!\n");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
columns = json_object_get_int(o);
|
columns = json_object_get_int(o);
|
||||||
if (!json_object_object_get_ex(obj, "rows", &o)) {
|
if (!json_object_object_get_ex(obj, "rows", &o)) {
|
||||||
lwsl_err("rows field not exists!");
|
lwsl_err("rows field not exists!\n");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
rows = json_object_get_int(o);
|
rows = json_object_get_int(o);
|
||||||
|
|||||||
Reference in New Issue
Block a user