UNPKG

stmux

Version:

Simple Terminal Multiplexing for Node Environments

232 lines (219 loc) 10.8 kB
/* ** 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. */ import Blessed from "blessed" export default class stmuxKeys { handleKeys () { /* handle keys */ let prefixMode = 0 this.screen.on("keypress", (ch, key) => { if ((prefixMode === 0 || prefixMode === 2) && key.full === `C-${this.argv.activator}`) { /* enter prefix mode */ prefixMode = 1 this.terms[this.focused].enableInput(false) } else if (prefixMode === 1) { /* handle prefix mode */ prefixMode = 2 if (key.full === this.argv.activator) { /* handle special prefix activator character */ const ch = String.fromCharCode(1 + this.argv.activator.charCodeAt(0) - "a".charCodeAt(0)) this.terms[this.focused].injectInput(ch) } else if (this.zoomed === -1 && key.full === "backspace") { /* handle terminal focus change (step-by-step, sequenced) */ this.terms[this.focused].resetScroll() this.focused-- if (this.focused < 0) this.focused = this.terms.length - 1 this.terms[this.focused].focus() this.screen.render() } else if (this.zoomed === -1 && key.full === "space") { /* handle terminal focus change (step-by-step, sequenced) */ this.terms[this.focused].resetScroll() this.focused++ if (this.focused > this.terms.length - 1) this.focused = 0 this.terms[this.focused].focus() this.screen.render() } else if ( this.zoomed === -1 && ( key.full === "left" || key.full === "right" || key.full === "up" || key.full === "down" )) { /* handle terminal focus change (step-by-step, directional) */ /* determine border of focused terminal where we want to logically break through */ let leave, enteron if (key.full === "left") { leave = this.border(this.terms[this.focused], "left") enteron = "right" } else if (key.full === "right") { leave = this.border(this.terms[this.focused], "right") enteron = "left" } else if (key.full === "up") { leave = this.border(this.terms[this.focused], "top") enteron = "bottom" } else if (key.full === "down") { leave = this.border(this.terms[this.focused], "bottom") enteron = "top" } /* find the touchpoints of terminals with our border */ const touchpoints = [] for (let i = 0; i < this.terms.length; i++) { if (i === this.focused) touchpoints[i] = { i, touches: 0 } else { const enter = this.border(this.terms[i], enteron) if ((enteron === "left" && enter.x1 === (leave.x1 + 1)) || (enteron === "right" && enter.x1 === (leave.x1 - 1))) touchpoints[i] = { i, touches: this.touches(leave.y1, leave.y2, enter.y1, enter.y2) } else if ((enteron === "top" && enter.y1 === (leave.y1 + 1)) || (enteron === "bottom" && enter.y1 === (leave.y1 - 1))) touchpoints[i] = { i, touches: this.touches(leave.x1, leave.x2, enter.x1, enter.x2) } else touchpoints[i] = { i, touches: 0 } } } /* determine best matching terminal */ const bestMatch = touchpoints.sort((t1, t2) => t2.touches - t1.touches)[0] /* switch to best matching one */ if (bestMatch.touches > 0) { this.terms[this.focused].resetScroll() this.focused = bestMatch.i this.terms[this.focused].focus() this.screen.render() } } else if (this.zoomed === -1 && key.full.match(/^[1-9]$/)) { /* handle terminal focus change (directly) */ const n = parseInt(key.full) if (n <= this.terms.length) { this.focused = n - 1 this.terms[this.focused].focus() this.screen.render() } } else if (key.full === "n") { /* handle number toggling */ this.argv.number = !this.argv.number this.provisionAgain() this.terms[this.focused].focus() this.screen.render() } else if (key.full === "l") { /* handle manual screen redrawing (by forcing Blessed to redraw everything via temporarily opening a dummy box) */ this.provisionAgain() this.dummyBox = new Blessed.Box({ left: 0, top: 0, width: this.screenWidth, height: this.screenHeight, content: "" }) this.screen.append(this.dummyBox) this.screen.render() this.screen.remove(this.dummyBox) this.screen.render() } else if (key.full === "z") { /* handle zooming */ this.zoomed = (this.zoomed === -1 ? this.focused : -1) this.provisionAgain() this.terms[this.focused].setFront() this.terms[this.focused].focus() this.screen.render() } else if (key.full === "v") { /* handle scrolling/visual mode */ this.terms[this.focused].scroll(0) } else if (key.full === "r") { /* handle manual restarting */ this.terms[this.focused].terminate() this.terms[this.focused].spawn(this.terms[this.focused].stmuxShell, this.terms[this.focused].stmuxArgs) this.terminated-- } else if (key.full === "?") { /* handle help screen toggling */ this.helpBox.show() this.screen.render() } else if (key.full === "k") { /* send CTRL+c to all terminals to give processes a chance to gracefully terminate */ this.terms.forEach((term) => term.injectInput("\x03")) setTimeout(() => { /* terminate all terminal processes */ this.terms.forEach((term) => term.terminate()) setTimeout(() => { /* finally kill the program */ this.terminate() }, 500) }, 500) } } else if (prefixMode === 2) { /* leave prefix mode */ this.terms[this.focused].enableInput(true) prefixMode = 0 if (this.helpBox.visible) { this.helpBox.hide() this.screen.render() } } }) /* handle mouse */ if (this.argv.mouse) { this.terms.forEach((term) => { term.on("wheeldown", (...args) => { /* on-the-fly start scrolling */ if (!term.scrolling) term.scroll(0) /* scroll 10% downwards */ const n = Math.max(1, Math.floor(term.height * 0.10)) term.scroll(+n) /* reset/stop scrolling once we reached the end (again) */ if (Math.ceil(term.getScrollPerc()) === 100) term.resetScroll() }) term.on("wheelup", (...args) => { /* on-the-fly start scrolling */ if (!term.scrolling) term.scroll(0) /* scroll 10% upwards */ const n = Math.max(1, Math.floor(term.height * 0.10)) term.scroll(-n) /* reset/stop scrolling once we reached the end (again) */ if (Math.ceil(term.getScrollPerc()) === 100) term.resetScroll() }) }) } } }