This repository has been archived by the owner on Oct 22, 2021. It is now read-only.
Permalink
Cannot retrieve contributors at this time
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?
edex-ui/src/classes/terminal.class.js /
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
489 lines (445 sloc)
20.2 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| }; |