mirror of
https://github.com/tsl0922/ttyd.git
synced 2025-12-22 11:54:19 +01:00
319 lines
11 KiB
JavaScript
319 lines
11 KiB
JavaScript
require("babel-polyfill");
|
|
require('fast-text-encoding');
|
|
|
|
var Zmodem = require('zmodem.js/src/zmodem_browser');
|
|
var Terminal = require('xterm').Terminal;
|
|
|
|
Terminal.applyAddon(require('xterm/lib/addons/fit'));
|
|
Terminal.applyAddon(require('xterm/lib/addons/winptyCompat'));
|
|
Terminal.applyAddon(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 () {
|
|
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 () {
|
|
res();
|
|
});
|
|
});
|
|
zsession.start();
|
|
return promise;
|
|
}
|
|
|
|
var terminalContainer = document.getElementById('terminal-container'),
|
|
httpsEnabled = window.location.protocol === 'https:',
|
|
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,
|
|
autoReconnect = -1,
|
|
term, title, wsError;
|
|
|
|
var openWs = function() {
|
|
var ws = new WebSocket(url, ['tty']);
|
|
var sendMessage = function (message) {
|
|
if (ws.readyState === WebSocket.OPEN) {
|
|
ws.send(textEncoder.encode(message));
|
|
}
|
|
};
|
|
var sendData = function (data) {
|
|
sendMessage('0' + data);
|
|
};
|
|
var unloadCallback = function (event) {
|
|
var message = 'Close terminal? this will also terminate the command.';
|
|
(event || window.event).returnValue = message;
|
|
return message;
|
|
};
|
|
var zsentry = new Zmodem.Sentry({
|
|
to_terminal: function _to_terminal(octets) {
|
|
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.setOption('disableStdin', true);
|
|
var zsession = detection.confirm();
|
|
var promise = zsession.type === 'send' ? handleSend(zsession) : handleReceive(zsession);
|
|
promise.catch(console.error.bind(console)).then(function () {
|
|
hideModal();
|
|
term.setOption('disableStdin', false);
|
|
});
|
|
}
|
|
});
|
|
|
|
ws.binaryType = 'arraybuffer';
|
|
|
|
ws.onopen = function(event) {
|
|
console.log('Websocket connection opened');
|
|
wsError = false;
|
|
sendMessage(JSON.stringify({AuthToken: authToken}));
|
|
|
|
if (typeof term !== 'undefined') {
|
|
term.destroy();
|
|
}
|
|
|
|
term = 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'
|
|
}
|
|
});
|
|
|
|
term.on('resize', function(size) {
|
|
if (ws.readyState === WebSocket.OPEN) {
|
|
sendMessage('1' + JSON.stringify({columns: size.cols, rows: size.rows}));
|
|
}
|
|
setTimeout(function() {
|
|
term.showOverlay(size.cols + 'x' + size.rows);
|
|
}, 500);
|
|
});
|
|
|
|
term.on('title', function (data) {
|
|
if (data && data !== '') {
|
|
document.title = (data + ' | ' + title);
|
|
}
|
|
});
|
|
|
|
term.on('data', sendData);
|
|
|
|
while (terminalContainer.firstChild) {
|
|
terminalContainer.removeChild(terminalContainer.firstChild);
|
|
}
|
|
|
|
// https://stackoverflow.com/a/27923937/1727928
|
|
window.addEventListener('resize', function() {
|
|
clearTimeout(window.resizedFinished);
|
|
window.resizedFinished = setTimeout(function () {
|
|
term.fit();
|
|
}, 250);
|
|
});
|
|
window.addEventListener('beforeunload', unloadCallback);
|
|
|
|
term.open(terminalContainer, true);
|
|
term.winptyCompatInit();
|
|
term.fit();
|
|
term.focus();
|
|
};
|
|
|
|
ws.onmessage = function(event) {
|
|
var rawData = new Uint8Array(event.data),
|
|
cmd = String.fromCharCode(rawData[0]),
|
|
data = rawData.slice(1).buffer;
|
|
switch(cmd) {
|
|
case '0':
|
|
zsentry.consume(data);
|
|
break;
|
|
case '1':
|
|
title = textDecoder.decode(data);
|
|
document.title = title;
|
|
break;
|
|
case '2':
|
|
var preferences = JSON.parse(textDecoder.decode(data));
|
|
Object.keys(preferences).forEach(function(key) {
|
|
console.log('Setting ' + key + ': ' + preferences[key]);
|
|
term.setOption(key, preferences[key]);
|
|
});
|
|
break;
|
|
case '3':
|
|
autoReconnect = JSON.parse(textDecoder.decode(data));
|
|
console.log('Enabling reconnect: ' + autoReconnect + ' seconds');
|
|
break;
|
|
default:
|
|
console.log('Unknown command: ' + cmd);
|
|
break;
|
|
}
|
|
};
|
|
|
|
ws.onclose = function(event) {
|
|
console.log('Websocket connection closed with code: ' + event.code);
|
|
if (term) {
|
|
term.off('data');
|
|
term.off('resize');
|
|
if (!wsError) {
|
|
term.showOverlay('Connection Closed', null);
|
|
}
|
|
}
|
|
window.removeEventListener('beforeunload', unloadCallback);
|
|
// 1000: CLOSE_NORMAL
|
|
if (event.code !== 1000 && autoReconnect > 0) {
|
|
setTimeout(openWs, autoReconnect * 1000);
|
|
}
|
|
};
|
|
};
|
|
|
|
if (document.readyState === 'complete' || document.readyState !== 'loading') {
|
|
openWs();
|
|
} else {
|
|
document.addEventListener('DOMContentLoaded', openWs);
|
|
}
|