UNPKG

stmux

Version:

Simple Terminal Multiplexing for Node Environments

290 lines (266 loc) 9.89 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _os = _interopRequireDefault(require("os")); var _chalk = _interopRequireDefault(require("chalk")); var _blessedXterm = _interopRequireDefault(require("blessed-xterm")); /* ** stmux -- Simple Terminal Multiplexing for Node Environments ** Copyright (c) 2017-2024 Dr. Ralf S. Engelschall <rse@engelschall.com> ** ** Permission is hereby granted, free of charge, to any person obtaining ** a copy of this software and associated documentation files (the ** "Software"), to deal in the Software without restriction, including ** without limitation the rights to use, copy, modify, merge, publish, ** distribute, sublicense, and/or sell copies of the Software, and to ** permit persons to whom the Software is furnished to do so, subject to ** the following conditions: ** ** The above copyright notice and this permission notice shall be included ** in all copies or substantial portions of the Software. ** ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ class stmuxTerminal { initializer() { this.terms = []; this.focused = -1; this.zoomed = -1; this.terminated = 0; this.terminatedError = 0; } provisionCommand(x, y, w, h, node, initially) { if (node.type() !== "command") this.fatal("invalid AST node (expected \"command\")"); /* determine XTerm widget */ let term; if (initially) { /* create XTerm widget */ term = new _blessedXterm.default({ left: x, top: y, width: w, height: h, shell: null, args: [], env: process.env, cwd: process.cwd(), cursorType: this.argv.cursor, cursorBlink: true, ignoreKeys: [], controlKey: "none", fg: "normal", tags: true, border: "line", scrollback: 1000, style: { fg: "default", bg: "default", border: { fg: "default" }, focus: { border: { fg: "green" } }, scrolling: { border: { fg: "yellow" } } } }); node.term = term; term.node = node; /* place XTerm widget on screen */ this.screen.append(term); term.stmuxNumber = this.terms.length + 1; this.terms.push(term); } else { /* reuse XTerm widget */ term = node.term; /* reconfigure size and position */ term.left = x; term.top = y; term.width = w; term.height = h; } /* determine zoom */ if (this.zoomed !== -1 && this.zoomed === term.stmuxNumber - 1) { term.left = 0; term.top = 0; term.width = this.screenWidth; term.height = this.screenHeight; term.setIndex(2); } else term.setIndex(1); /* set terminal title */ this.setTerminalTitle(term); /* some initial initializations */ if (initially) { /* optionally enable mouse event handling */ if (this.argv.mouse) term.enableMouse(); /* determine initial focus */ if (node.get("focus") === true) { if (this.focused >= 0) this.fatal("only a single command can be focused"); this.focused = this.terms.length - 1; } /* handle focus/blur events */ term.on("focus", () => { /* redetermine our view of the current focused terminal */ for (let i = 0; i < this.terms.length; i++) { if (this.terms[i].focused) { this.focused = i; break; } } /* repaint focused */ this.setTerminalTitle(term); this.screen.render(); }); term.on("blur", () => { /* repaint blurred */ this.setTerminalTitle(term); this.screen.render(); }); /* handle scrolling events */ term.on("scrolling-start", () => { this.setTerminalTitle(term); this.screen.render(); }); term.on("scrolling-end", () => { this.setTerminalTitle(term); this.screen.render(); }); /* handle beep events */ term.on("beep", () => { /* pass-through to program */ this.screen.program.output.write("\x07"); }); /* handle error observation */ term.stmuxUpdate = false; term.on("update", () => { term.stmuxUpdate = true; }); /* spawn command */ if (_os.default.platform() === "win32") { term.stmuxShell = "cmd.exe"; term.stmuxArgs = ["/d", "/s", "/c", node.get("cmd")]; } else { term.stmuxShell = "sh"; term.stmuxArgs = ["-c", node.get("cmd")]; } term.spawn(term.stmuxShell, term.stmuxArgs); /* handle command termination (and optional restarting) */ term.on("exit", code => { if (code === 0) term.write("\r\n" + _chalk.default.green.inverse(" ..::") + _chalk.default.green.bold.inverse(" PROGRAM TERMINATED ") + _chalk.default.green.inverse("::.. ") + "\r\n\r\n");else term.write("\r\n" + _chalk.default.red.inverse(" ..::") + _chalk.default.red.bold.inverse(` PROGRAM TERMINATED (code: ${code}) `) + _chalk.default.red.inverse("::.. ") + "\r\n\r\n"); /* handle termination and restarting */ if (node.get("restart") === true) { /* restart command */ const delay = node.get("delay"); if (delay > 0) setTimeout(() => term.spawn(term.stmuxShell, term.stmuxArgs), delay);else term.spawn(term.stmuxShell, term.stmuxArgs); } else { /* handle automatic program termination */ this.terminated++; if (code !== 0) this.terminatedError++; if (this.terminated >= this.terms.length) { if (this.argv.wait === "" || this.argv.wait === "error" && this.terminatedError === 0) setTimeout(() => this.terminate(), 2 * 1000); } } }); } } provisionSplit(x, y, w, h, node, initially) { if (node.type() !== "split") this.fatal("invalid AST node (expected \"split\")"); /* provision terminals in a particular direction */ const childs = node.childs(); const divide = (s, l, childs) => { /* sanity check situation */ const n = childs.length; if (l < n * 3) this.fatal("terminal too small"); const k = Math.floor(l / n); if (k === 0) this.fatal("terminal too small"); /* pass 1: calculate size of explicitly sized terminals */ const sizes = []; for (let i = 0; i < n; i++) { sizes[i] = -1; let size = childs[i].get("size"); if (size) { let m; if (size.match(/^\d+$/)) size = parseInt(size);else if (size.match(/^\d+\.\d+$/)) size = Math.floor(l * parseFloat(size));else if (m = size.match(/^(\d+)\/(\d+)$/)) size = Math.floor(l * (parseInt(m[1]) / parseInt(m[2])));else if (m = size.match(/^(\d+)%$/)) size = Math.floor(l * (parseInt(m[1]) / 100)); if (size < 3) size = 3;else if (size > l) size = l; sizes[i] = size; } } /* pass 2: calculate size of implicitly sized terminals */ for (let i = 0; i < n; i++) { if (sizes[i] === -1) { let size = Math.floor(l / n); if (size < 3) size = 3; sizes[i] = size; } } /* pass 3: optionally shrink/grow sizes to fit total available size */ while (true) { let requested = 0; for (let i = 0; i < n; i++) requested += sizes[i]; if (requested > l) { let shrink = requested - l; for (let i = 0; i < n && shrink > 0; i++) { if (sizes[i] > 3) { sizes[i]--; shrink--; } } continue; } else if (requested < l) { let grow = l - requested; for (let i = 0; i < n && grow > 0; i++) { sizes[i]++; grow--; } continue; } break; } /* pass 4: provide results */ const SL = []; for (let i = 0; i < n; i++) { SL.push({ s, l: sizes[i] }); s += sizes[i]; } return SL; }; if (node.get("horizontal") === true) { const SL = divide(x, w, childs); for (let i = 0; i < childs.length; i++) this.provision(SL[i].s, y, SL[i].l, h, childs[i], initially); } else if (node.get("vertical") === true) { const SL = divide(y, h, childs); for (let i = 0; i < childs.length; i++) this.provision(x, SL[i].s, w, SL[i].l, childs[i], initially); } } provision(x, y, w, h, node, initially) { if (node.type() === "split") return this.provisionSplit(x, y, w, h, node, initially);else if (node.type() === "command") return this.provisionCommand(x, y, w, h, node, initially);else this.fatal("invalid AST node (expected \"split\" or \"command\")"); } provisionInitially() { this.provision(0, 0, this.screenWidth, this.screenHeight, this.ast, true); /* manage initial terminal focus */ if (this.focused === -1) this.focused = 0; this.terms[this.focused].focus(); } provisionAgain() { this.provision(0, 0, this.screenWidth, this.screenHeight, this.ast, false); } } exports.default = stmuxTerminal;