mirror of
https://github.com/tsl0922/ttyd.git
synced 2025-12-22 03:44: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 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
|
||||
|
||||
- Build on [libwebsockets](https://libwebsockets.org) with C for speed
|
||||
- Full terminal emulation based on [hterm](https://chromium.googlesource.com/apps/libapps/+/HEAD/hterm)
|
||||
- SSL support based on [OpenSSL](https://www.openssl.org)
|
||||
- Built on top of [Libwebsockets][2] with C for speed
|
||||
- Full terminal emulation based on [Xterm.js][3] with CJK and IME support
|
||||
- SSL support based on [OpenSSL][4]
|
||||
- Run any custom command with options
|
||||
- Basic authentication support
|
||||
- Cross platform: macOS, Linux, [OpenWrt](https://openwrt.org)/[LEDE](https://www.lede-project.org)
|
||||
- Basic authentication support and many other custom options
|
||||
- Cross platform: macOS, Linux, [OpenWrt][5]/[LEDE][6]
|
||||
|
||||
# Installation
|
||||
|
||||
## Install on macOS
|
||||
|
||||
Install with [homebrew](http://brew.sh):
|
||||
Install with [homebrew][7]:
|
||||
|
||||
```bash
|
||||
brew install ttyd
|
||||
@@ -35,7 +35,7 @@ cmake ..
|
||||
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
|
||||
|
||||
@@ -43,7 +43,7 @@ make && make install
|
||||
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
|
||||
|
||||
@@ -57,7 +57,7 @@ VERSION:
|
||||
1.1.0
|
||||
|
||||
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
|
||||
--credential, -c Credential for Basic Authentication (format: username:password)
|
||||
--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
|
||||
|
||||
- [GoTTY](https://github.com/yudai/gotty): 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.
|
||||
- [GoTTY][1]: ttyd is a port of GoTTY to `C` language.
|
||||
- [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;
|
||||
|
||||
if (!json_object_object_get_ex(obj, "columns", &o)) {
|
||||
lwsl_err("columns field not exists!");
|
||||
lwsl_err("columns field not exists!\n");
|
||||
return NULL;
|
||||
}
|
||||
columns = json_object_get_int(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;
|
||||
}
|
||||
rows = json_object_get_int(o);
|
||||
|
||||
Reference in New Issue
Block a user