ZMODEM support added 🎉

This commit is contained in:
Shuanglei Tao
2017-11-23 22:43:10 +08:00
parent 47ba5daa12
commit 8ff3d31380
9 changed files with 1940 additions and 191 deletions

2
html/.gitignore vendored
View File

@@ -45,3 +45,5 @@ jspm_packages
# Yarn Integrity file # Yarn Integrity file
.yarn-integrity .yarn-integrity
js/bundle.js

View File

@@ -13,51 +13,52 @@ html, body {
background-color: #101010; background-color: #101010;
} }
.terminal { .xterm {
background-color: #101010; background-color: #101010;
color: #f0f0f0; color: #f0f0f0;
font-size: 10pt; font-size: 13px;
font-family: "Menlo for Powerline", Menlo,Consolas,"DejaVu Sans Mono","Liberation Mono",Courier,monospace; font-family: "Menlo for Powerline", Menlo, Consolas, "Liberation Mono", Courier, monospace;
font-variant-ligatures: none; font-variant-ligatures: none;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
.terminal .xterm-viewport { .xterm .xterm-viewport {
background-color: rgba(121, 121, 121, 0); background-color: rgba(121, 121, 121, 0);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
transition: background-color 800ms linear; transition: background-color 800ms linear;
overflow-y: auto;
} }
.terminal .xterm-viewport::-webkit-scrollbar { .xterm .xterm-viewport::-webkit-scrollbar {
width: 10px; width: 10px;
} }
.terminal .xterm-viewport::-webkit-scrollbar-track { .xterm .xterm-viewport::-webkit-scrollbar-track {
opacity: 0; opacity: 0;
} }
.terminal .xterm-viewport::-webkit-scrollbar-thumb { .xterm .xterm-viewport::-webkit-scrollbar-thumb {
background-color: rgba(121, 121, 121, 0.4); background-color: rgba(121, 121, 121, 0.4);
} }
.terminal .xterm-viewport::-webkit-scrollbar-thumb:hover { .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover {
transition: opacity 0ms linear; transition: opacity 0ms linear;
background-color: rgba(100, 100, 100, .7); background-color: rgba(100, 100, 100, .7);
} }
.terminal .xterm-viewport::-webkit-scrollbar-thumb:window-inactive { .xterm .xterm-viewport::-webkit-scrollbar-thumb:window-inactive {
background-color: inherit; background-color: inherit;
} }
.terminal .terminal-cursor { .xterm .terminal-cursor {
background-color: #f0f0f0; background-color: #f0f0f0;
color: #101010; color: #101010;
opacity: .7; opacity: .7;
} }
.terminal:not(.focus) .terminal-cursor { .xterm:not(.focus) .terminal-cursor {
outline: 1px solid #f0f0f0; outline: 1px solid #f0f0f0;
} }
@@ -71,3 +72,42 @@ html, body {
color: #f0f0f0; color: #f0f0f0;
} }
} }
#modal strong {
color: #268bd2;
}
#modal span {
color: #2aa198;
}
#modal header {
font-weight: bold;
text-align: center;
padding-bottom: 20px;
margin-bottom: 20px;
border-bottom: 1px solid #ddd;
}
#choose .file-name {
border-color: transparent;
}
#progress {
margin-top: 20px;
padding-top: 20px;
border-top: 1px dashed #ddd;
}
#progress progress {
margin-bottom: 0.5rem;
}
#progress {
color: #93a1a1;
}
#progress span {
font-weight: bold;
}

View File

@@ -1,8 +1,21 @@
var gulp = require('gulp'), var gulp = require('gulp'),
fs = require("fs"),
browserify = require('browserify'),
inlinesource = require('gulp-inline-source'); inlinesource = require('gulp-inline-source');
gulp.task('inlinesource', function () { gulp.task('browserify', function () {
return gulp.src('*.html') return browserify('./js/app.js')
.transform("babelify", {
presets: ["env"],
global: true,
ignore: /\/node_modules\/(?!zmodem.js\/)/
})
.bundle()
.pipe(fs.createWriteStream("./js/bundle.js"));
});
gulp.task('inlinesource', ['browserify'], function () {
return gulp.src('index.html')
.pipe(inlinesource()) .pipe(inlinesource())
.pipe(gulp.dest('../src')); .pipe(gulp.dest('../src'));
}); });

View File

@@ -5,15 +5,49 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>ttyd - Terminal</title> <title>ttyd - Terminal</title>
<link inline rel="icon" type="image/png" href="favicon.png"> <link inline rel="icon" type="image/png" href="favicon.png">
<link inline href="node_modules/xterm/dist/xterm.css"> <link inline href="node_modules/bulma/css/bulma.css">
<link inline href="node_modules/xterm/src/xterm.css">
<link inline href="css/app.css"> <link inline href="css/app.css">
<script inline src="node_modules/xterm/dist/xterm.js"></script>
<script inline src="node_modules/xterm/dist/addons/fit/fit.js"></script>
<script inline src="js/overlay.js"></script>
</head> </head>
<body> <body>
<div id="terminal-container"></div> <div id="terminal-container"></div>
<div class="modal" id="modal">
<div class="modal-background"></div>
<div class="modal-content">
<div class="box">
<header id="header"></header>
<div id="file">
<div><strong>Name: </strong><span id="name"></span></div>
<div><strong>Size: </strong><span id="size"></span></div>
<div><strong>Last modified: </strong><span id="mtime"></span></div>
<div><strong>Mode: </strong><span id="mode"></span></div>
<br>
<div><strong>Files remaining: </strong><span id="files-remaining"></span></div>
<div><strong>Bytes remaining: </strong><span id="bytes-remaining"></span></div>
</div>
<div id="choose" class="file has-name is-fullwidth">
<label class="file-label">
<input id="files" class="file-input" type="file" multiple>
<span class="file-cta">
<strong class="file-label">
Choose file(s)…
</strong>
</span>
<span id="file-names" class="file-name"></span>
</label>
</div>
<div id="progress">
<progress id="progress-bar" class="progress is-link" max="100"></progress>
<p id="progress-info">
<span id="bytes-received">-</span>/<span id="bytes-file">-</span> (<span id="percent-received"></span>)
transferred
<a id="skip" class="button is-link is-small is-pulled-right">Skip</a>
</p>
</div>
</div>
</div>
</div>
<script src="auth_token.js"></script> <script src="auth_token.js"></script>
<script inline src="js/app.js"></script> <script inline src="js/bundle.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,56 +1,231 @@
(function() { var Zmodem = require('zmodem.js/src/zmodem_browser');
var Terminal = require('xterm').Terminal;
require('xterm/lib/addons/fit');
require('./overlay');
function showReceiveModal(xfer) {
resetModal('Receiving files');
var fileInfo = xfer.get_details();
document.getElementById('name').textContent = fileInfo.name;
document.getElementById('size').textContent = bytesHuman(fileInfo.size, 2);
document.getElementById('mtime').textContent = fileInfo.mtime;
document.getElementById('files-remaining').textContent = fileInfo.files_remaining;
document.getElementById('bytes-remaining').textContent = bytesHuman(fileInfo.bytes_remaining, 2);
document.getElementById('mode').textContent = '0' + fileInfo.mode.toString(8);
document.getElementById('choose').style.display = 'none';
document.getElementById('file').style.display = '';
var skip = document.getElementById('skip');
skip.disabled = false;
skip.onclick = function () {
this.disabled = true;
xfer.skip();
};
skip.style.display = '';
document.getElementById('modal').classList.add('is-active');
}
function showSendModal(callback) {
resetModal('Sending files');
document.getElementById('file').style.display = 'none';
document.getElementById('skip').style.display = 'none';
document.getElementById('choose').style.display = '';
var filesInput = document.getElementById('files');
filesInput.disabled = false;
filesInput.value = '';
filesInput.onchange = function () {
this.disabled = true;
var files = this.files;
var fileNames = '';
for (var i = 0; i < files.length; i++) {
if (i === 0) {
fileNames = files[i].name;
} else {
fileNames += ' | ' + files[i].name;
}
}
document.getElementById('file-names').textContent = fileNames;
callback(files);
};
document.getElementById('modal').classList.add('is-active');
}
function hideModal() {
document.getElementById('modal').classList.remove('is-active');
}
function resetModal(title) {
document.getElementById('header').textContent = title;
document.getElementById('bytes-received').textContent = '-';
document.getElementById('percent-received').textContent = '-%';
document.getElementById('progress-info').style.display = 'none';
var progressBar = document.getElementById('progress-bar');
progressBar.textContent = '0%';
progressBar.value = 0;
}
function updateProgress(xfer) {
var size = xfer.get_details().size;
var offset = xfer.get_offset();
document.getElementById('bytes-received').textContent = bytesHuman(offset, 2);
document.getElementById('bytes-file').textContent = bytesHuman(size, 2);
var percentReceived = (100 * offset / size).toFixed(2);
document.getElementById('percent-received').textContent = percentReceived + '%';
document.getElementById('progress-info').style.display = '';
var progressBar = document.getElementById('progress-bar');
progressBar.textContent = percentReceived + '%';
progressBar.setAttribute('value', percentReceived);
}
function bytesHuman (bytes, precision) {
if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) return '-';
if (bytes === 0) return 0;
if (typeof precision === 'undefined') precision = 1;
var units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'],
number = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, Math.floor(number))).toFixed(precision) + ' ' + units[number];
}
function handleSend(zsession) {
return new Promise(function (res) {
showSendModal(function (files) {
Zmodem.Browser.send_files(
zsession,
files,
{
on_progress: function(obj, xfer) {
updateProgress(xfer);
},
on_file_complete: function(obj) {
hideModal();
}
}
).then(
zsession.close.bind(zsession),
console.error.bind(console)
).then(function () {
hideModal();
res();
});
});
});
}
function handleReceive(zsession) {
zsession.on('offer', function (xfer) {
showReceiveModal(xfer);
var fileBuffer = [];
xfer.on('input', function (payload) {
updateProgress(xfer);
fileBuffer.push(new Uint8Array(payload));
});
xfer.accept().then(function () {
Zmodem.Browser.save_to_disk(
fileBuffer,
xfer.get_details().name
);
}, console.error.bind(console));
});
var promise = new Promise(function (res) {
zsession.on('session_end', function () {
hideModal();
res();
});
});
zsession.start();
return promise;
}
var terminalContainer = document.getElementById('terminal-container'), var terminalContainer = document.getElementById('terminal-container'),
httpsEnabled = window.location.protocol === "https:", httpsEnabled = window.location.protocol === 'https:',
url = (httpsEnabled ? 'wss://' : 'ws://') + window.location.host + window.location.pathname + 'ws', url = (httpsEnabled ? 'wss://' : 'ws://') + window.location.host + window.location.pathname + 'ws',
textDecoder = new TextDecoder(),
textEncoder = new TextEncoder(),
authToken = (typeof tty_auth_token !== 'undefined') ? tty_auth_token : null, authToken = (typeof tty_auth_token !== 'undefined') ? tty_auth_token : null,
protocols = ["tty"],
autoReconnect = -1, autoReconnect = -1,
term, pingTimer, wsError; term, pingTimer, wsError;
var openWs = function() { var openWs = function() {
var ws = new WebSocket(url, protocols), var ws = new WebSocket(url, ['tty']);
textDecoder = new TextDecoder(), var sendMessage = function (message) {
textEncoder = new TextEncoder(); if (ws.readyState === WebSocket.OPEN) {
ws.send(textEncoder.encode(message));
}
};
var sendData = function (data) {
sendMessage('0' + data);
};
var unloadCallback = function (event) { var unloadCallback = function (event) {
var message = 'Close terminal? this will also terminate the command.'; var message = 'Close terminal? this will also terminate the command.';
(event || window.event).returnValue = message; (event || window.event).returnValue = message;
return message; return message;
}; };
var sendMessage = function (msg) { var zsentry = new Zmodem.Sentry({
if (ws.readyState === WebSocket.OPEN) { to_terminal: function _to_terminal(octets) {
ws.send(textEncoder.encode(msg)); var buffer = new Uint8Array(octets).buffer;
term.write(textDecoder.decode(buffer));
},
sender: function _ws_sender_func(octets) {
var array = new Uint8Array(octets.length + 1);
array[0] = '0'.charCodeAt(0);
array.set(new Uint8Array(octets), 1);
ws.send(array.buffer);
},
on_retract: function _on_retract() {
// console.log('on_retract');
},
on_detect: function _on_detect(detection) {
term.off('data');
var zsession = detection.confirm();
var promise = zsession.type === 'send' ? handleSend(zsession) : handleReceive(zsession);
promise.catch(console.error.bind(console)).then(function () {
hideModal();
term.on('data', sendData);
});
} }
}; });
var sendPing = function() {
sendMessage("1");
};
ws.binaryType = 'arraybuffer'; ws.binaryType = 'arraybuffer';
ws.onopen = function() { ws.onopen = function(event) {
console.log("Websocket connection opened"); console.log('Websocket connection opened');
wsError = false; wsError = false;
sendMessage(JSON.stringify({AuthToken: authToken})); sendMessage(JSON.stringify({AuthToken: authToken}));
pingTimer = setInterval(sendPing, 30 * 1000); pingTimer = setInterval(function() {
sendMessage('1');
}, 30 * 1000);
if (typeof term !== 'undefined') { if (typeof term !== 'undefined') {
term.destroy(); term.destroy();
} }
term = new Terminal(); term = new Terminal({
fontSize: 13,
fontFamily: '"Menlo for Powerline", Menlo, Consolas, "Liberation Mono", Courier, monospace'
});
term.on('resize', function(size) { term.on('resize', function(size) {
sendMessage("2" + JSON.stringify({columns: size.cols, rows: size.rows})); if (ws.readyState === WebSocket.OPEN) {
sendMessage('2' + JSON.stringify({columns: size.cols, rows: size.rows}));
}
setTimeout(function() { setTimeout(function() {
term.showOverlay(size.cols + 'x' + size.rows); term.showOverlay(size.cols + 'x' + size.rows);
}, 500); }, 500);
}); });
term.on("data", function(data) { term.on('data', sendData);
sendMessage("0" + data);
}); while (terminalContainer.firstChild) {
terminalContainer.removeChild(terminalContainer.firstChild);
}
term.open(terminalContainer, true);
term.on('open', function() {
// https://stackoverflow.com/a/27923937/1727928 // https://stackoverflow.com/a/27923937/1727928
window.addEventListener('resize', function() { window.addEventListener('resize', function() {
clearTimeout(window.resizedFinished); clearTimeout(window.resizedFinished);
@@ -60,48 +235,44 @@
}); });
window.addEventListener('beforeunload', unloadCallback); window.addEventListener('beforeunload', unloadCallback);
term.fit(); term.fit();
});
while (terminalContainer.firstChild) {
terminalContainer.removeChild(terminalContainer.firstChild);
}
term.open(terminalContainer, true);
}; };
ws.onmessage = function(event) { ws.onmessage = function(event) {
var cmd = String.fromCharCode(new DataView(event.data).getUint8()), var cmd = String.fromCharCode(new DataView(event.data).getUint8()),
data = textDecoder.decode(event.data.slice(1)); data = event.data.slice(1);
switch(cmd) { switch(cmd) {
case '0': case '0':
term.write(data); zsentry.consume(data);
break; break;
case '1': // pong case '1': // pong
break; break;
case '2': case '2':
document.title = data; document.title = textDecoder.decode(data);
break; break;
case '3': case '3':
var preferences = JSON.parse(data); var preferences = JSON.parse(textDecoder.decode(data));
Object.keys(preferences).forEach(function(key) { Object.keys(preferences).forEach(function(key) {
console.log("Setting " + key + ": " + preferences[key]); console.log('Setting ' + key + ': ' + preferences[key]);
term.setOption(key, preferences[key]); term.setOption(key, preferences[key]);
}); });
break; break;
case '4': case '4':
autoReconnect = JSON.parse(data); autoReconnect = JSON.parse(textDecoder.decode(data));
console.log("Enabling reconnect: " + autoReconnect + " seconds"); console.log('Enabling reconnect: ' + autoReconnect + ' seconds');
break;
default:
console.log('Unknown command: ' + cmd);
break; break;
} }
}; };
ws.onclose = function(event) { ws.onclose = function(event) {
console.log("Websocket connection closed with code: " + event.code); console.log('Websocket connection closed with code: ' + event.code);
if (term) { if (term) {
term.off('data'); term.off('data');
term.off('resize'); term.off('resize');
if (!wsError) { if (!wsError) {
term.showOverlay("Connection Closed", null); term.showOverlay('Connection Closed', null);
} }
} }
window.removeEventListener('beforeunload', unloadCallback); window.removeEventListener('beforeunload', unloadCallback);
@@ -113,5 +284,8 @@
}; };
}; };
if (document.readyState === 'complete' || document.readyState !== 'loading') {
openWs(); openWs();
})(); } else {
document.addEventListener('DOMContentLoaded', openWs);
}

View File

@@ -1,7 +1,12 @@
// ported from hterm.Terminal.prototype.showOverlay // ported from hterm.Terminal.prototype.showOverlay
// https://chromium.googlesource.com/apps/libapps/+/master/hterm/js/hterm_terminal.js // https://chromium.googlesource.com/apps/libapps/+/master/hterm/js/hterm_terminal.js
Terminal.prototype.showOverlay = function(msg, timeout) { (function (overlay) {
module.exports = overlay(require('xterm').Terminal);
})(function (Terminal) {
var exports = {};
exports.showOverlay = function(msg, timeout) {
if (!this.overlayNode_) { if (!this.overlayNode_) {
if (!this.element) if (!this.element)
return; return;
@@ -56,3 +61,8 @@ Terminal.prototype.showOverlay = function(msg, timeout) {
}, 200); }, 200);
}, timeout || 1500); }, timeout || 1500);
}; };
Terminal.prototype.showOverlay = exports.showOverlay;
return exports;
});

View File

@@ -13,8 +13,14 @@
"build": "gulp" "build": "gulp"
}, },
"dependencies": { "dependencies": {
"babel-core": "^6.26.0",
"babel-preset-env": "^1.6.1",
"babelify": "^8.0.0",
"browserify": "^14.5.0",
"bulma": "^0.6.1",
"gulp": "^3.9.1", "gulp": "^3.9.1",
"gulp-inline-source": "^3.0.0", "gulp-inline-source": "^3.0.0",
"xterm": "^2.9.2" "xterm": "Tyriar/xterm.js#vscode-release/1.19",
"zmodem.js": "^0.1.5"
} }
} }

File diff suppressed because it is too large Load Diff

57
src/index.html vendored

File diff suppressed because one or more lines are too long