Comparing version
{ | ||
"name": "run-pty", | ||
"version": "2.2.0", | ||
"version": "2.3.0-beta.1", | ||
"author": "Simon Lydell", | ||
@@ -32,5 +32,5 @@ "license": "MIT", | ||
"@types/jest": "26.0.20", | ||
"@typescript-eslint/eslint-plugin": "4.14.0", | ||
"@typescript-eslint/parser": "4.14.0", | ||
"eslint": "7.18.0", | ||
"@typescript-eslint/eslint-plugin": "4.14.1", | ||
"@typescript-eslint/parser": "4.14.1", | ||
"eslint": "7.19.0", | ||
"eslint-plugin-jest": "24.1.3", | ||
@@ -43,3 +43,3 @@ "jest": "26.6.3", | ||
"scripts": { | ||
"start": "node run-pty.js % cat % false % echo hello world % ping localhost % node get-cursor-position.js % node test-keys.js % node signals.js % node slow-kill.js % node slow-kill.js 2000 \"Shutting down…\" % make watch", | ||
"start": "node run-pty.js % cat % false % echo hello world % ping localhost % node get-cursor-position.js % node test-keys.js % node signals.js % node slow-kill.js % node slow-kill.js 2000 \"Shutting down…\" % make watch % make signals", | ||
"example": "node run-pty.js example.json", | ||
@@ -46,0 +46,0 @@ "test": "prettier --check . && eslint . --report-unused-disable-directives && tsc && jest", |
@@ -38,3 +38,5 @@ # run-pty | ||
[1-2] focus command | ||
[1-2] focus command (or click) | ||
[enter] focus selected command | ||
[↑/↓] move selection | ||
[ctrl+c] kill all | ||
@@ -93,3 +95,5 @@ ``` | ||
[1-2] focus command | ||
[1-2] focus command (or click) | ||
[enter] focus selected command | ||
[↑/↓] move selection | ||
[ctrl+c] kill all | ||
@@ -96,0 +100,0 @@ ``` |
291
run-pty.js
@@ -12,3 +12,3 @@ #!/usr/bin/env node | ||
| { tag: "Running", terminal: import("node-pty").IPty } | ||
| { tag: "Killing", terminal: import("node-pty").IPty, slow: boolean } | ||
| { tag: "Killing", terminal: import("node-pty").IPty, slow: boolean, lastKillPress: number | undefined } | ||
| { tag: "Exit", exitCode: number } | ||
@@ -23,6 +23,9 @@ } Status | ||
// node-pty does not support kill signals on Windows. | ||
// This is the same check that node-pty uses. | ||
const IS_WINDOWS = process.platform === "win32"; | ||
const SLOW_KILL = 100; // ms | ||
// This is apparently what Windows uses for double clicks. | ||
const DOUBLE_PRESS = 500; // ms | ||
const MAX_HISTORY_DEFAULT = 1000000; | ||
@@ -43,2 +46,4 @@ | ||
dashboard: "ctrl+z", | ||
navigate: "↑/↓", | ||
enter: "enter", | ||
}; | ||
@@ -50,2 +55,11 @@ | ||
dashboard: "\x1a", | ||
// https://vi.stackexchange.com/questions/15324/up-arrow-key-code-why-a-becomes-oa | ||
up: "\x1B[A", | ||
upAlt: "\x1BOA", | ||
upVim: "k", | ||
down: "\x1B[B", | ||
downAlt: "\x1BOB", | ||
downVim: "j", | ||
enter: "\r", | ||
enterVim: "o", | ||
}; | ||
@@ -61,2 +75,4 @@ | ||
const DISABLE_BRACKETED_PASTE_MODE = "\x1B[?2004l"; | ||
const ENABLE_MOUSE = "\x1B[?1000;1006h"; | ||
const DISABLE_MOUSE = "\x1B[?1000;1006l"; | ||
const RESET_COLOR = "\x1B[m"; | ||
@@ -83,3 +99,4 @@ const CLEAR = IS_WINDOWS ? "\x1B[2J\x1B[0f" : "\x1B[2J\x1B[3J\x1B[H"; | ||
const exitIndicator = (exitCode) => | ||
exitCode === 0 | ||
// 130 commonly means exit by ctrl+c. | ||
exitCode === 0 || exitCode === 130 | ||
? NO_COLOR | ||
@@ -118,8 +135,15 @@ ? "●" | ||
* @param {string} string | ||
* @param {{ pad?: boolean }} pad | ||
* @returns {string} | ||
*/ | ||
const shortcut = (string, { pad = true } = {}) => | ||
dim("[") + | ||
bold(string) + | ||
dim("]") + | ||
const invert = (string) => | ||
NO_COLOR ? string : `\x1B[7m${string}${RESET_COLOR}`; | ||
/** | ||
* @param {string} string | ||
* @param {{ pad?: boolean, highlight?: boolean }} pad | ||
*/ | ||
const shortcut = (string, { pad = true, highlight = false } = {}) => | ||
dim(NO_COLOR && highlight ? "<" : "[") + | ||
bold(highlight ? invert(string) : string) + | ||
dim(NO_COLOR && highlight ? ">" : "]") + | ||
(pad ? " ".repeat(Math.max(0, KEYS.kill.length - string.length)) : ""); | ||
@@ -163,7 +187,2 @@ | ||
${shortcut(summarizeLabels(ALL_LABELS.split("")))} focus command | ||
${shortcut(KEYS.dashboard)} dashboard | ||
${shortcut(KEYS.kill)} kill focused/all | ||
${shortcut(KEYS.restart)} restart killed/exited command | ||
Separate the commands with a character of choice: | ||
@@ -200,3 +219,3 @@ | ||
commands.some((command) => command.status.tag === "Killing") | ||
? "force kill all" | ||
? `kill all ${dim("(double-press to force) ")}` | ||
: commands.every((command) => command.status.tag === "Exit") | ||
@@ -209,9 +228,13 @@ ? "exit" | ||
* @param {number} width | ||
* @param {boolean} attemptedKillAll | ||
* @param {number | undefined} cursorIndex | ||
* @returns {Array<{ line: string, length: number }>} | ||
*/ | ||
const drawDashboard = (commands, width, attemptedKillAll) => { | ||
const lines = commands.map((command) => { | ||
const drawDashboardCommandLines = (commands, width, cursorIndex) => { | ||
const lines = commands.map((command, index) => { | ||
const [icon, status] = statusText(command.status, command.statusFromRules); | ||
return { | ||
label: shortcut(command.label || " ", { pad: false }), | ||
label: shortcut(command.label || " ", { | ||
pad: false, | ||
highlight: index === cursorIndex, | ||
}), | ||
icon, | ||
@@ -228,30 +251,51 @@ status, | ||
const finalLines = lines | ||
.map(({ label, icon, status, title }) => { | ||
const separator = " "; | ||
const start = truncate(`${label}${separator}${icon}`, width); | ||
const startLength = | ||
removeGraphicRenditions(label).length + separator.length + ICON_WIDTH; | ||
const end = | ||
status === undefined | ||
? title | ||
: `${status.padEnd(widestStatus, " ")}${separator}${title}`; | ||
return `${start}${RESET_COLOR}${cursorHorizontalAbsolute( | ||
return lines.map(({ label, icon, status, title }) => { | ||
const separator = " "; | ||
const start = truncate(`${label}${separator}${icon}`, width); | ||
const startLength = | ||
removeGraphicRenditions(label).length + separator.length + ICON_WIDTH; | ||
const end = | ||
status === undefined | ||
? title | ||
: `${status.padEnd(widestStatus, " ")}${separator}${title}`; | ||
const truncatedEnd = truncate(end, width - startLength - separator.length); | ||
const length = | ||
startLength + | ||
separator.length + | ||
removeGraphicRenditions(truncatedEnd).length; | ||
return { | ||
line: `${start}${RESET_COLOR}${cursorHorizontalAbsolute( | ||
startLength + 1 | ||
)}${CLEAR_RIGHT}${separator}${truncate( | ||
end, | ||
width - startLength - separator.length | ||
)}${RESET_COLOR}`; | ||
}) | ||
)}${CLEAR_RIGHT}${separator}${truncatedEnd}${RESET_COLOR}`, | ||
length, | ||
}; | ||
}); | ||
}; | ||
/** | ||
* @param {Array<Command>} commands | ||
* @param {number} width | ||
* @param {boolean} attemptedKillAll | ||
* @param {number | undefined} cursorIndex | ||
* @returns {string} | ||
*/ | ||
const drawDashboard = (commands, width, attemptedKillAll, cursorIndex) => { | ||
const done = | ||
attemptedKillAll && | ||
commands.every((command) => command.status.tag === "Exit"); | ||
const finalLines = drawDashboardCommandLines( | ||
commands, | ||
width, | ||
done ? undefined : cursorIndex | ||
) | ||
.map(({ line }) => line) | ||
.join("\n"); | ||
const label = summarizeLabels(commands.map((command) => command.label)); | ||
if ( | ||
attemptedKillAll && | ||
commands.every((command) => command.status.tag === "Exit") | ||
) { | ||
if (done) { | ||
return `${finalLines}\n`; | ||
} | ||
const label = summarizeLabels(commands.map((command) => command.label)); | ||
// Newlines at the end are wanted here. | ||
@@ -261,3 +305,5 @@ return ` | ||
${shortcut(label)} focus command | ||
${shortcut(label)} focus command ${dim("(or click)")} | ||
${shortcut(KEYS.enter)} focus selected command | ||
${shortcut(KEYS.navigate)} move selection | ||
${shortcut(KEYS.kill)} ${killAllLabel(commands)} | ||
@@ -315,3 +361,3 @@ `.trimStart(); | ||
${shortcut(KEYS.kill)} force kill ${dim(`(pid ${pid})`)} | ||
${shortcut(KEYS.kill)} kill ${dim(`(double-press to force) (pid ${pid})`)} | ||
${shortcut(KEYS.dashboard)} dashboard | ||
@@ -829,3 +875,2 @@ `; | ||
kill() { | ||
// https://www.gnu.org/software/libc/manual/html_node/Termination-Signals.html | ||
switch (this.status.tag) { | ||
@@ -837,2 +882,3 @@ case "Running": | ||
slow: false, | ||
lastKillPress: undefined, | ||
}; | ||
@@ -845,20 +891,23 @@ setTimeout(() => { | ||
} | ||
}, 100); | ||
if (IS_WINDOWS) { | ||
this.status.terminal.kill(); | ||
} else { | ||
// SIGHUP causes a silent exit for `npm run`. | ||
this.status.terminal.kill("SIGHUP"); | ||
// SIGTERM is needed for some programs (but is noisy for `npm run`). | ||
this.status.terminal.kill("SIGTERM"); | ||
} | ||
}, SLOW_KILL); | ||
this.status.terminal.write(KEY_CODES.kill); | ||
return undefined; | ||
case "Killing": | ||
if (IS_WINDOWS) { | ||
this.status.terminal.kill(); | ||
case "Killing": { | ||
const now = Date.now(); | ||
if ( | ||
this.status.lastKillPress !== undefined && | ||
now - this.status.lastKillPress <= DOUBLE_PRESS | ||
) { | ||
if (IS_WINDOWS) { | ||
this.status.terminal.kill(); | ||
} else { | ||
this.status.terminal.kill("SIGKILL"); | ||
} | ||
} else { | ||
this.status.terminal.kill("SIGKILL"); | ||
this.status.terminal.write(KEY_CODES.kill); | ||
} | ||
this.status.lastKillPress = now; | ||
return undefined; | ||
} | ||
@@ -941,2 +990,4 @@ case "Exit": | ||
let attemptedKillAll = false; | ||
/** @type {number | undefined} */ | ||
let cursorIndex = undefined; | ||
@@ -951,2 +1002,3 @@ /** | ||
DISABLE_ALTERNATE_SCREEN + | ||
DISABLE_MOUSE + | ||
RESET_COLOR + | ||
@@ -957,2 +1009,4 @@ CLEAR + | ||
const maybeNewline = /[\r\n][^\S\r\n]*$/.test(command.history) ? "" : "\n"; | ||
switch (command.status.tag) { | ||
@@ -965,3 +1019,5 @@ case "Running": | ||
process.stdout.write( | ||
RESET_COLOR + runningText(command.status.terminal.pid) | ||
RESET_COLOR + | ||
maybeNewline + | ||
runningText(command.status.terminal.pid) | ||
); | ||
@@ -976,2 +1032,3 @@ } | ||
RESET_COLOR + | ||
maybeNewline + | ||
killingText(command, command.status.terminal.pid) | ||
@@ -986,2 +1043,3 @@ ); | ||
RESET_COLOR + | ||
maybeNewline + | ||
exitText(commands, command, command.status.exitCode) | ||
@@ -1001,5 +1059,11 @@ ); | ||
DISABLE_ALTERNATE_SCREEN + | ||
ENABLE_MOUSE + | ||
RESET_COLOR + | ||
CLEAR + | ||
drawDashboard(commands, process.stdout.columns, attemptedKillAll) | ||
drawDashboard( | ||
commands, | ||
process.stdout.columns, | ||
attemptedKillAll, | ||
cursorIndex | ||
) | ||
); | ||
@@ -1021,2 +1085,35 @@ }; | ||
*/ | ||
const switchToCommandAtCursor = () => { | ||
if (cursorIndex !== undefined) { | ||
const command = commands[cursorIndex]; | ||
current = { tag: "Command", index: cursorIndex }; | ||
printHistoryAndExtraText(command); | ||
} | ||
}; | ||
/** | ||
* @param {number} delta | ||
* @returns {void} | ||
*/ | ||
const moveCursor = (delta) => { | ||
if (cursorIndex === undefined) { | ||
cursorIndex = | ||
delta === 0 | ||
? undefined | ||
: delta > 0 | ||
? delta - 1 | ||
: commands.length + delta; | ||
} else { | ||
cursorIndex = (cursorIndex + delta) % commands.length; | ||
if (cursorIndex < 0) { | ||
cursorIndex = commands.length + cursorIndex; | ||
} | ||
} | ||
// Redraw dashboard. | ||
switchToDashboard(); | ||
}; | ||
/** | ||
* @returns {void} | ||
*/ | ||
const killAll = () => { | ||
@@ -1129,2 +1226,4 @@ attemptedKillAll = true; | ||
switchToCommand, | ||
switchToCommandAtCursor, | ||
moveCursor, | ||
killAll, | ||
@@ -1159,3 +1258,3 @@ printHistoryAndExtraText | ||
process.stdout.write( | ||
SHOW_CURSOR + DISABLE_BRACKETED_PASTE_MODE + RESET_COLOR | ||
SHOW_CURSOR + DISABLE_BRACKETED_PASTE_MODE + DISABLE_MOUSE + RESET_COLOR | ||
); | ||
@@ -1177,2 +1276,4 @@ }); | ||
* @param {(index: number) => void} switchToCommand | ||
* @param {() => void} switchToCommandAtCursor | ||
* @param {(delta: number) => void} moveCursor | ||
* @param {() => void} killAll | ||
@@ -1188,2 +1289,4 @@ * @param {(command: Command) => void} printHistoryAndExtraText | ||
switchToCommand, | ||
switchToCommandAtCursor, | ||
moveCursor, | ||
killAll, | ||
@@ -1197,2 +1300,3 @@ printHistoryAndExtraText | ||
case "Running": | ||
case "Killing": | ||
switch (data) { | ||
@@ -1212,16 +1316,2 @@ case KEY_CODES.kill: | ||
case "Killing": | ||
switch (data) { | ||
case KEY_CODES.kill: | ||
command.kill(); | ||
return undefined; | ||
case KEY_CODES.dashboard: | ||
switchToDashboard(); | ||
return undefined; | ||
default: | ||
return undefined; | ||
} | ||
case "Exit": | ||
@@ -1254,2 +1344,19 @@ switch (data) { | ||
case KEY_CODES.enter: | ||
case KEY_CODES.enterVim: | ||
switchToCommandAtCursor(); | ||
return undefined; | ||
case KEY_CODES.up: | ||
case KEY_CODES.upAlt: | ||
case KEY_CODES.upVim: | ||
moveCursor(-1); | ||
return undefined; | ||
case KEY_CODES.down: | ||
case KEY_CODES.downAlt: | ||
case KEY_CODES.downVim: | ||
moveCursor(1); | ||
return undefined; | ||
default: { | ||
@@ -1261,3 +1368,23 @@ const commandIndex = commands.findIndex( | ||
switchToCommand(commandIndex); | ||
return undefined; | ||
} | ||
const mouseupPosition = parseMouseup(data); | ||
if (mouseupPosition !== undefined) { | ||
const { x, y } = mouseupPosition; | ||
const lines = drawDashboardCommandLines( | ||
commands, | ||
process.stdout.columns, | ||
undefined | ||
); | ||
if (y >= 0 && y < lines.length) { | ||
const max = Math.max( | ||
...lines.map((otherLine) => otherLine.length) | ||
); | ||
if (x >= 0 && x < max) { | ||
switchToCommand(y); | ||
} | ||
} | ||
} | ||
return undefined; | ||
@@ -1269,3 +1396,19 @@ } | ||
// eslint-disable-next-line no-control-regex | ||
const MOUSEUP_REGEX = /\x1B\[<0;(\d+);(\d+)M/; | ||
/** | ||
* @param {string} string | ||
* @returns {{ x: number, y: number } | undefined} | ||
*/ | ||
const parseMouseup = (string) => { | ||
const match = MOUSEUP_REGEX.exec(string); | ||
if (match === null) { | ||
return undefined; | ||
} | ||
const [, x, y] = match; | ||
return { x: Number(x) - 1, y: Number(y) - 1 }; | ||
}; | ||
/** | ||
* @returns {undefined} | ||
@@ -1272,0 +1415,0 @@ */ |
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
49450
8.68%1263
11.67%211
1.93%2
100%