Skip to content
This repository has been archived by the owner on Oct 22, 2021. It is now read-only.
Permalink
04a00c4079
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
489 lines (445 sloc) 20.2 KB
class Terminal {
constructor(opts) {
if (opts.role === "client") {
if (!opts.parentId) throw "Missing options";
this.xTerm = require("xterm").Terminal;
const {AttachAddon} = require("xterm-addon-attach");
const {FitAddon} = require("xterm-addon-fit");
const {LigaturesAddon} = require("xterm-addon-ligatures");
const {WebglAddon} = require("xterm-addon-webgl");
this.Ipc = require("electron").ipcRenderer;
this.port = opts.port || 3000;
this.cwd = "";
this.oncwdchange = () => {};
this._sendSizeToServer = () => {
let cols = this.term.cols.toString();
let rows = this.term.rows.toString();
while (cols.length < 3) {
cols = "0"+cols;
}
while (rows.length < 3) {
rows = "0"+rows;
}
this.Ipc.send("terminal_channel-"+this.port, "Resize", cols, rows);
};
// Support for custom color filters on the terminal - see #483
let doCustomFilter = (window.isTermFilterValidated) ? true : false;
// Parse & validate color filter
if (window.isTermFilterValidated !== true && typeof window.theme.terminal.colorFilter === "object" && window.theme.terminal.colorFilter.length > 0) {
doCustomFilter = window.theme.terminal.colorFilter.every((step, i, a) => {
let func = step.slice(0, step.indexOf("("));
switch(func) {
case "negate":
case "grayscale":
a[i] = {
func,
arg: []
};
return true;
case "lighten":
case "darken":
case "saturate":
case "desaturate":
case "whiten":
case "blacken":
case "fade":
case "opaquer":
case "rotate":
case "mix":
break;
default:
return false;
}
let arg = step.slice(step.indexOf("(")+1, step.indexOf(")"));
if (typeof Number(arg) === "number") {
a[i] = {
func,
arg: [Number(arg)]
};
window.isTermFilterValidated = true;
return true;
}
return false;
});
}
let color = require("color");
let colorify;
if (doCustomFilter) {
colorify = (base, target) => {
let newColor = color(base);
target = color(target);
for (let i = 0; i < window.theme.terminal.colorFilter.length; i++) {
if (window.theme.terminal.colorFilter[i].func === "mix") {
newColor = newColor[window.theme.terminal.colorFilter[i].func](target, ...window.theme.terminal.colorFilter[i].arg);
} else {
newColor = newColor[window.theme.terminal.colorFilter[i].func](...window.theme.terminal.colorFilter[i].arg);
}
}
return newColor.hex();
};
} else {
colorify = (base, target) => {
return color(base).grayscale().mix(color(target), 0.3).hex();
};
}
let themeColor = `rgb(${window.theme.r}, ${window.theme.g}, ${window.theme.b})`;
this.term = new this.xTerm({
cols: 80,
rows: 24,
cursorBlink: window.theme.terminal.cursorBlink || true,
cursorStyle: window.theme.terminal.cursorStyle || "block",
allowTransparency: window.theme.terminal.allowTransparency || false,
fontFamily: window.theme.terminal.fontFamily || "Fira Mono",
fontSize: window.theme.terminal.fontSize || window.settings.termFontSize || 15,
fontWeight: window.theme.terminal.fontWeight || "normal",
fontWeightBold: window.theme.terminal.fontWeightBold || "bold",
letterSpacing: window.theme.terminal.letterSpacing || 0,
lineHeight: window.theme.terminal.lineHeight || 1,
scrollback: 1500,
bellStyle: "none",
theme: {
foreground: window.theme.terminal.foreground,
background: window.theme.terminal.background,
cursor: window.theme.terminal.cursor,
cursorAccent: window.theme.terminal.cursorAccent,
selection: window.theme.terminal.selection,
black: window.theme.colors.black || colorify("#2e3436", themeColor),
red: window.theme.colors.red || colorify("#cc0000", themeColor),
green: window.theme.colors.green || colorify("#4e9a06", themeColor),
yellow: window.theme.colors.yellow || colorify("#c4a000", themeColor),
blue: window.theme.colors.blue || colorify("#3465a4", themeColor),
magenta: window.theme.colors.magenta || colorify("#75507b", themeColor),
cyan: window.theme.colors.cyan || colorify("#06989a", themeColor),
white: window.theme.colors.white || colorify("#d3d7cf", themeColor),
brightBlack: window.theme.colors.brightBlack || colorify("#555753", themeColor),
brightRed: window.theme.colors.brightRed || colorify("#ef2929", themeColor),
brightGreen: window.theme.colors.brightGreen || colorify("#8ae234", themeColor),
brightYellow: window.theme.colors.brightYellow || colorify("#fce94f", themeColor),
brightBlue: window.theme.colors.brightBlue || colorify("#729fcf", themeColor),
brightMagenta: window.theme.colors.brightMagenta || colorify("#ad7fa8", themeColor),
brightCyan: window.theme.colors.brightCyan || colorify("#34e2e2", themeColor),
brightWhite: window.theme.colors.brightWhite || colorify("#eeeeec", themeColor)
}
});
let fitAddon = new FitAddon();
this.term.loadAddon(fitAddon);
this.term.open(document.getElementById(opts.parentId));
this.term.loadAddon(new WebglAddon());
let ligaturesAddon = new LigaturesAddon();
this.term.loadAddon(ligaturesAddon);
this.term.attachCustomKeyEventHandler(e => {
window.keyboard.keydownHandler(e);
return true;
});
// Prevent soft-keyboard on touch devices #733
document.querySelectorAll('.xterm-helper-textarea').forEach(textarea => textarea.setAttribute('readonly', 'readonly'))
this.term.focus();
this.Ipc.send("terminal_channel-"+this.port, "Renderer startup");
this.Ipc.on("terminal_channel-"+this.port, (e, ...args) => {
switch(args[0]) {
case "New cwd":
this.cwd = args[1];
this.oncwdchange(this.cwd);
break;
case "Fallback cwd":
this.cwd = "FALLBACK |-- "+args[1];
this.oncwdchange(this.cwd);
break;
case "New process":
if (this.onprocesschange) {
this.onprocesschange(args[1]);
}
break;
default:
return;
}
});
this.resendCWD = () => {
this.oncwdchange(this.cwd || null);
};
let sockHost = opts.host || "127.0.0.1";
let sockPort = this.port;
this.socket = new WebSocket("ws://"+sockHost+":"+sockPort);
this.socket.onopen = () => {
let attachAddon = new AttachAddon(this.socket);
this.term.loadAddon(attachAddon);
this.fit();
};
this.socket.onerror = e => {throw JSON.stringify(e)};
this.socket.onclose = e => {
if (this.onclose) {
this.onclose(e);
}
};
this.lastSoundFX = Date.now();
this.socket.addEventListener("message", e => {
let d = Date.now();
if (d - this.lastSoundFX > 30) {
if(window.passwordMode == "false")
window.audioManager.stdout.play();
this.lastSoundFX = d;
}
if (d - this.lastRefit > 10000) {
this.fit();
}
// See #397
if (!window.settings.experimentalGlobeFeatures) return;
let ips = e.data.match(/((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/g);
if (ips !== null && ips.length >= 1) {
ips = ips.filter((val, index, self) => { return self.indexOf(val) === index; });
ips.forEach(ip => {
window.mods.globe.addTemporaryConnectedMarker(ip);
});
}
});
let parent = document.getElementById(opts.parentId);
parent.addEventListener("wheel", e => {
this.term.scrollLines(Math.round(e.deltaY/10));
});
this._lastTouchY = null;
parent.addEventListener("touchstart", e => {
this._lastTouchY = e.targetTouches[0].screenY;
});
parent.addEventListener("touchmove", e => {
if (this._lastTouchY) {
let y = e.changedTouches[0].screenY;
let deltaY = y - this._lastTouchY;
this._lastTouchY = y;
this.term.scrollLines(-Math.round(deltaY/10));
}
});
parent.addEventListener("touchend", e => {
this._lastTouch = null;
});
parent.addEventListener("touchcancel", e => {
this._lastTouch = null;
});
document.querySelector(".xterm-helper-textarea").addEventListener("keydown", e => {
if (e.key === "F11" && window.settings.allowWindowed) {
e.preventDefault();
window.toggleFullScreen();
}
});
this.fit = () => {
this.lastRefit = Date.now();
let {cols, rows} = fitAddon.proposeDimensions();
// Apply custom fixes based on screen ratio, see #302
let w = screen.width;
let h = screen.height;
let x = 1;
let y = 0;
function gcd(a, b) {
return (b == 0) ? a : gcd(b, a%b);
}
let d = gcd(w, h);
if (d === 100) { y = 1; x = 3;}
// if (d === 120) y = 1;
if (d === 256) x = 2;
if (window.settings.termFontSize < 15) y = y - 1;
cols = cols+x;
rows = rows+y;
if (this.term.cols !== cols || this.term.rows !== rows) {
this.resize(cols, rows);
}
};
this.resize = (cols, rows) => {
this.term.resize(cols, rows);
this._sendSizeToServer();
};
this.write = cmd => {
this.socket.send(cmd);
};
this.writelr = cmd => {
this.socket.send(cmd+"\r");
};
this.clipboard = {
copy: () => {
if (!this.term.hasSelection()) return false;
document.execCommand("copy");
this.term.clearSelection();
this.clipboard.didCopy = true;
},
paste: () => {
this.write(remote.clipboard.readText());
this.clipboard.didCopy = false;
},
didCopy: false
};
} else if (opts.role === "server") {
this.Pty = require("node-pty");
this.Websocket = require("ws").Server;
this.Ipc = require("electron").ipcMain;
this.renderer = null;
this.port = opts.port || 3000;
this._closed = false;
this.onclosed = () => {};
this.onopened = () => {};
this.onresize = () => {};
this.ondisconnected = () => {};
this._disableCWDtracking = false;
this._getTtyCWD = tty => {
return new Promise((resolve, reject) => {
let pid = tty._pid;
switch(require("os").type()) {
case "Linux":
require("fs").readlink(`/proc/${pid}/cwd`, (e, cwd) => {
if (e !== null) {
reject(e);
} else {
resolve(cwd);
}
});
break;
case "Darwin":
require("child_process").exec(`lsof -a -d cwd -p ${pid} | tail -1 | awk '{ for (i=9; i<=NF; i++) printf "%s ", $i }'`, (e, cwd) => {
if (e !== null) {
reject(e);
} else {
resolve(cwd.trim());
}
});
break;
default:
reject("Unsupported OS");
}
});
};
this._getTtyProcess = tty => {
return new Promise((resolve, reject) => {
let pid = tty._pid;
switch(require("os").type()) {
case "Linux":
case "Darwin":
require("child_process").exec(`ps -o comm --no-headers --sort=+pid -g ${pid} | tail -1`, (e, proc) => {
if (e !== null) {
reject(e);
} else {
resolve(proc.trim());
}
});
break;
default:
reject("Unsupported OS");
}
});
};
this._nextTickUpdateTtyCWD = false;
this._nextTickUpdateProcess = false;
this._tick = setInterval(() => {
if (this._nextTickUpdateTtyCWD && this._disableCWDtracking === false) {
this._nextTickUpdateTtyCWD = false;
this._getTtyCWD(this.tty).then(cwd => {
if (this.tty._cwd === cwd) return;
this.tty._cwd = cwd;
if (this.renderer) {
this.renderer.send("terminal_channel-"+this.port, "New cwd", cwd);
}
}).catch(e => {
if (!this._closed) {
console.log("Error while tracking TTY working directory: ", e);
this._disableCWDtracking = true;
try {
this.renderer.send("terminal_channel-"+this.port, "Fallback cwd", opts.cwd || process.env.PWD);
} catch(e) {
// renderer closed
}
}
});
}
if (this.renderer && this._nextTickUpdateProcess) {
this._nextTickUpdateProcess = false;
this._getTtyProcess(this.tty).then(process => {
if (this.tty._process === process) return;
this.tty._process = process;
if (this.renderer) {
this.renderer.send("terminal_channel-"+this.port, "New process", process);
}
}).catch(e => {
if (!this._closed) {
console.log("Error while retrieving TTY subprocess: ", e);
try {
this.renderer.send("terminal_channel-"+this.port, "New process", "");
} catch(e) {
// renderer closed
}
}
});
}
}, 1000);
this.tty = this.Pty.spawn(opts.shell || "bash", (opts.params.length > 0 ? opts.params : (process.platform === "win32" ? [] : ["--login"])), {
name: opts.env.TERM || "xterm-256color",
cols: 80,
rows: 24,
cwd: opts.cwd || process.env.PWD,
env: opts.env || process.env
});
this.tty.onExit((code, signal) => {
this._closed = true;
this.onclosed(code, signal);
});
this.wss = new this.Websocket({
port: this.port,
clientTracking: true,
verifyClient: info => {
if (this.wss.clients.length >= 1) {
return false;
} else {
return true;
}
}
});
this.Ipc.on("terminal_channel-"+this.port, (e, ...args) => {
switch(args[0]) {
case "Renderer startup":
this.renderer = e.sender;
if (!this._disableCWDtracking && this.tty._cwd) {
this.renderer.send("terminal_channel-"+this.port, "New cwd", this.tty._cwd);
}
if (this._disableCWDtracking) {
this.renderer.send("terminal_channel-"+this.port, "Fallback cwd", opts.cwd || process.env.PWD);
}
break;
case "Resize":
let cols = args[1];
let rows = args[2];
try {
this.tty.resize(Number(cols), Number(rows));
} catch (error) {
//Keep going, it'll work anyways.
}
this.onresized(cols, rows);
break;
default:
return;
}
});
this.wss.on("connection", ws => {
this.onopened(this.tty._pid);
ws.on("close", (code, reason) => {
this.ondisconnected(code, reason);
});
ws.on("message", msg => {
this.tty.write(msg);
});
this.tty.onData(data => {
this._nextTickUpdateTtyCWD = true;
this._nextTickUpdateProcess = true;
try {
ws.send(data);
} catch (e) {
// Websocket closed
}
});
});
this.close = () => {
this.tty.kill();
this._closed = true;
};
} else {
throw "Unknown purpose";
}
}
}
module.exports = {
Terminal
};