stmux
Version:
Simple Terminal Multiplexing for Node Environments
290 lines (266 loc) • 9.89 kB
JavaScript
"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;