numux 1.7.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -16
- package/dist/numux.js +202 -23
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -265,22 +265,7 @@ Persistent processes that crash are auto-restarted with exponential backoff (1s
|
|
|
265
265
|
|
|
266
266
|
## Keybindings
|
|
267
267
|
|
|
268
|
-
|
|
269
|
-
|-----|--------|
|
|
270
|
-
| `Ctrl+C` | Quit (graceful shutdown) |
|
|
271
|
-
| `R` | Restart active process |
|
|
272
|
-
| `Shift+R` | Restart all processes |
|
|
273
|
-
| `S` | Stop/start active process |
|
|
274
|
-
| `L` | Clear active pane output |
|
|
275
|
-
| `F` | Search in active pane output |
|
|
276
|
-
| `1`–`9` | Jump to tab |
|
|
277
|
-
| `Left/Right` | Cycle tabs |
|
|
278
|
-
| `PageUp/PageDown` | Scroll output by page |
|
|
279
|
-
| `Home/End` | Scroll to top/bottom |
|
|
280
|
-
|
|
281
|
-
While searching: type to filter, `Enter`/`Shift+Enter` to navigate matches, `Escape` to close.
|
|
282
|
-
|
|
283
|
-
Panes are readonly by default — keyboard input is not forwarded to processes. Set `interactive: true` on processes that need stdin (REPLs, shells, etc.).
|
|
268
|
+
Keybindings are shown in the status bar at the bottom of the app. Panes are readonly by default — keyboard input is not forwarded to processes. Set `interactive: true` on processes that need stdin (REPLs, shells, etc.).
|
|
284
269
|
|
|
285
270
|
## Tab icons
|
|
286
271
|
|
package/dist/numux.js
CHANGED
|
@@ -22,7 +22,7 @@ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports,
|
|
|
22
22
|
var require_package = __commonJS((exports, module) => {
|
|
23
23
|
module.exports = {
|
|
24
24
|
name: "numux",
|
|
25
|
-
version: "1.
|
|
25
|
+
version: "1.9.0",
|
|
26
26
|
description: "Terminal multiplexer with dependency orchestration",
|
|
27
27
|
type: "module",
|
|
28
28
|
license: "MIT",
|
|
@@ -92,6 +92,7 @@ function parseArgs(argv) {
|
|
|
92
92
|
timestamps: false,
|
|
93
93
|
noRestart: false,
|
|
94
94
|
noWatch: false,
|
|
95
|
+
autoColors: false,
|
|
95
96
|
configPath: undefined,
|
|
96
97
|
commands: [],
|
|
97
98
|
named: []
|
|
@@ -123,6 +124,8 @@ function parseArgs(argv) {
|
|
|
123
124
|
result.noRestart = true;
|
|
124
125
|
} else if (arg === "--no-watch") {
|
|
125
126
|
result.noWatch = true;
|
|
127
|
+
} else if (arg === "--colors") {
|
|
128
|
+
result.autoColors = true;
|
|
126
129
|
} else if (arg === "--config") {
|
|
127
130
|
result.configPath = consumeValue(arg);
|
|
128
131
|
} else if (arg === "-c" || arg === "--color") {
|
|
@@ -279,7 +282,7 @@ _numux() {
|
|
|
279
282
|
esac
|
|
280
283
|
|
|
281
284
|
if [[ "$cur" == -* ]]; then
|
|
282
|
-
COMPREPLY=( $(compgen -W "-h --help -v --version -c --color --config -n --name -p --prefix --only --exclude --kill-others --no-restart --no-watch -t --timestamps --log-dir --debug" -- "$cur") )
|
|
285
|
+
COMPREPLY=( $(compgen -W "-h --help -v --version -c --color --colors --config -n --name -p --prefix --only --exclude --kill-others --no-restart --no-watch -t --timestamps --log-dir --debug" -- "$cur") )
|
|
283
286
|
else
|
|
284
287
|
local subcmds="init validate exec completions"
|
|
285
288
|
COMPREPLY=( $(compgen -W "$subcmds" -- "$cur") )
|
|
@@ -304,6 +307,7 @@ _numux() {
|
|
|
304
307
|
'(-h --help)'{-h,--help}'[Show help]' \\
|
|
305
308
|
'(-v --version)'{-v,--version}'[Show version]' \\
|
|
306
309
|
'(-c --color)'{-c,--color}'[Comma-separated colors for processes]' \\
|
|
310
|
+
'--colors[Auto-assign colors based on process name]' \\
|
|
307
311
|
'--config[Config file path]:file:_files' \\
|
|
308
312
|
'(-n --name)'{-n,--name}'[Named process (name=command)]:named process' \\
|
|
309
313
|
'(-p --prefix)'{-p,--prefix}'[Prefixed output mode]' \\
|
|
@@ -346,6 +350,7 @@ complete -c numux -n '__fish_seen_subcommand_from completions' -a 'bash zsh fish
|
|
|
346
350
|
complete -c numux -s h -l help -d 'Show help'
|
|
347
351
|
complete -c numux -s v -l version -d 'Show version'
|
|
348
352
|
complete -c numux -s c -l color -r -d 'Comma-separated colors for processes'
|
|
353
|
+
complete -c numux -l colors -d 'Auto-assign colors based on process name'
|
|
349
354
|
complete -c numux -l config -rF -d 'Config file path'
|
|
350
355
|
complete -c numux -s n -l name -r -d 'Named process (name=command)'
|
|
351
356
|
complete -c numux -s p -l prefix -d 'Prefixed output mode'
|
|
@@ -651,6 +656,13 @@ var DEFAULT_ANSI_COLORS = [
|
|
|
651
656
|
"\x1B[95m"
|
|
652
657
|
];
|
|
653
658
|
var DEFAULT_HEX_COLORS = ["#00cccc", "#cccc00", "#cc00cc", "#0000cc", "#00cc00", "#ff5555", "#ffff55", "#ff55ff"];
|
|
659
|
+
function colorFromName(name) {
|
|
660
|
+
let hash = 0;
|
|
661
|
+
for (let i = 0;i < name.length; i++) {
|
|
662
|
+
hash = (hash << 5) - hash + name.charCodeAt(i) | 0;
|
|
663
|
+
}
|
|
664
|
+
return DEFAULT_HEX_COLORS[Math.abs(hash) % DEFAULT_HEX_COLORS.length];
|
|
665
|
+
}
|
|
654
666
|
function resolveColor(color) {
|
|
655
667
|
if (typeof color === "string")
|
|
656
668
|
return color;
|
|
@@ -797,7 +809,8 @@ function validateConfig(raw, warnings) {
|
|
|
797
809
|
stopSignal: validateStopSignal(p.stopSignal),
|
|
798
810
|
color: typeof p.color === "string" ? p.color : Array.isArray(p.color) ? p.color : undefined,
|
|
799
811
|
watch: validateStringOrStringArray(p.watch),
|
|
800
|
-
interactive: typeof p.interactive === "boolean" ? p.interactive : false
|
|
812
|
+
interactive: typeof p.interactive === "boolean" ? p.interactive : false,
|
|
813
|
+
errorMatcher: validateErrorMatcher(name, p.errorMatcher)
|
|
801
814
|
};
|
|
802
815
|
}
|
|
803
816
|
return { processes: validated };
|
|
@@ -810,6 +823,22 @@ function validateStringOrStringArray(value) {
|
|
|
810
823
|
return;
|
|
811
824
|
}
|
|
812
825
|
var validateEnvFile = validateStringOrStringArray;
|
|
826
|
+
function validateErrorMatcher(name, value) {
|
|
827
|
+
if (value === true)
|
|
828
|
+
return true;
|
|
829
|
+
if (typeof value === "string" && value.trim()) {
|
|
830
|
+
try {
|
|
831
|
+
new RegExp(value);
|
|
832
|
+
} catch (err) {
|
|
833
|
+
throw new Error(`Process "${name}".errorMatcher is not a valid regex: "${value}"`, { cause: err });
|
|
834
|
+
}
|
|
835
|
+
return value;
|
|
836
|
+
}
|
|
837
|
+
if (value !== undefined && value !== false) {
|
|
838
|
+
throw new Error(`Process "${name}".errorMatcher must be true or a regex string`);
|
|
839
|
+
}
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
813
842
|
var VALID_STOP_SIGNALS = new Set(["SIGTERM", "SIGINT", "SIGHUP"]);
|
|
814
843
|
function validateStopSignal(value) {
|
|
815
844
|
if (typeof value === "string" && VALID_STOP_SIGNALS.has(value)) {
|
|
@@ -917,8 +946,49 @@ function loadEnvFiles(envFile, cwd) {
|
|
|
917
946
|
return merged;
|
|
918
947
|
}
|
|
919
948
|
|
|
920
|
-
// src/process/
|
|
949
|
+
// src/process/error.ts
|
|
921
950
|
var BUFFER_CAP = 65536;
|
|
951
|
+
var SGR_RE = /\x1b\[([0-9;]*)m/g;
|
|
952
|
+
function hasAnsiRed(text) {
|
|
953
|
+
SGR_RE.lastIndex = 0;
|
|
954
|
+
for (let match = SGR_RE.exec(text);match !== null; match = SGR_RE.exec(text)) {
|
|
955
|
+
const params = match[1].split(";");
|
|
956
|
+
if (params.includes("31") || params.includes("91"))
|
|
957
|
+
return true;
|
|
958
|
+
}
|
|
959
|
+
return false;
|
|
960
|
+
}
|
|
961
|
+
function createErrorChecker(config) {
|
|
962
|
+
const matcher = config.errorMatcher;
|
|
963
|
+
if (!matcher)
|
|
964
|
+
return null;
|
|
965
|
+
const pattern = typeof matcher === "string" ? new RegExp(matcher) : null;
|
|
966
|
+
let buffer = "";
|
|
967
|
+
let triggered = false;
|
|
968
|
+
return {
|
|
969
|
+
feedOutput(data) {
|
|
970
|
+
if (triggered)
|
|
971
|
+
return false;
|
|
972
|
+
buffer += data;
|
|
973
|
+
if (buffer.length > BUFFER_CAP) {
|
|
974
|
+
buffer = buffer.slice(-BUFFER_CAP);
|
|
975
|
+
}
|
|
976
|
+
if (pattern) {
|
|
977
|
+
if (pattern.test(stripAnsi(buffer))) {
|
|
978
|
+
triggered = true;
|
|
979
|
+
return true;
|
|
980
|
+
}
|
|
981
|
+
} else if (hasAnsiRed(buffer)) {
|
|
982
|
+
triggered = true;
|
|
983
|
+
return true;
|
|
984
|
+
}
|
|
985
|
+
return false;
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// src/process/ready.ts
|
|
991
|
+
var BUFFER_CAP2 = 65536;
|
|
922
992
|
function createReadinessChecker(config) {
|
|
923
993
|
const pattern = config.readyPattern ? new RegExp(config.readyPattern) : null;
|
|
924
994
|
const persistent = config.persistent !== false;
|
|
@@ -928,8 +998,8 @@ function createReadinessChecker(config) {
|
|
|
928
998
|
if (!(persistent && pattern))
|
|
929
999
|
return false;
|
|
930
1000
|
outputBuffer += data;
|
|
931
|
-
if (outputBuffer.length >
|
|
932
|
-
outputBuffer = outputBuffer.slice(-
|
|
1001
|
+
if (outputBuffer.length > BUFFER_CAP2) {
|
|
1002
|
+
outputBuffer = outputBuffer.slice(-BUFFER_CAP2);
|
|
933
1003
|
}
|
|
934
1004
|
return pattern.test(outputBuffer);
|
|
935
1005
|
},
|
|
@@ -949,9 +1019,11 @@ class ProcessRunner {
|
|
|
949
1019
|
handler;
|
|
950
1020
|
proc = null;
|
|
951
1021
|
readiness;
|
|
1022
|
+
errorChecker;
|
|
952
1023
|
_ready = false;
|
|
953
1024
|
stopping = false;
|
|
954
1025
|
decoder = new TextDecoder;
|
|
1026
|
+
errorDecoder = new TextDecoder;
|
|
955
1027
|
generation = 0;
|
|
956
1028
|
readyTimer = null;
|
|
957
1029
|
restarting = false;
|
|
@@ -961,6 +1033,7 @@ class ProcessRunner {
|
|
|
961
1033
|
this.config = config;
|
|
962
1034
|
this.handler = handler;
|
|
963
1035
|
this.readiness = createReadinessChecker(config);
|
|
1036
|
+
this.errorChecker = createErrorChecker(config);
|
|
964
1037
|
}
|
|
965
1038
|
get isReady() {
|
|
966
1039
|
return this._ready;
|
|
@@ -995,6 +1068,7 @@ class ProcessRunner {
|
|
|
995
1068
|
return;
|
|
996
1069
|
this.handler.onOutput(data);
|
|
997
1070
|
this.checkReadiness(data);
|
|
1071
|
+
this.checkError(data);
|
|
998
1072
|
}
|
|
999
1073
|
}
|
|
1000
1074
|
});
|
|
@@ -1050,6 +1124,14 @@ class ProcessRunner {
|
|
|
1050
1124
|
this.markReady();
|
|
1051
1125
|
}
|
|
1052
1126
|
}
|
|
1127
|
+
checkError(data) {
|
|
1128
|
+
if (!this.errorChecker)
|
|
1129
|
+
return;
|
|
1130
|
+
const text = this.errorDecoder.decode(data, { stream: true });
|
|
1131
|
+
if (this.errorChecker.feedOutput(text)) {
|
|
1132
|
+
this.handler.onError();
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1053
1135
|
startReadyTimeout(gen) {
|
|
1054
1136
|
const timeout = this.config.readyTimeout;
|
|
1055
1137
|
if (!(timeout && this.config.readyPattern) || this.config.persistent === false)
|
|
@@ -1108,6 +1190,7 @@ class ProcessRunner {
|
|
|
1108
1190
|
this.restarting = false;
|
|
1109
1191
|
this.readyTimedOut = false;
|
|
1110
1192
|
this.readiness = createReadinessChecker(this.config);
|
|
1193
|
+
this.errorChecker = createErrorChecker(this.config);
|
|
1111
1194
|
this.start(cols, rows);
|
|
1112
1195
|
}
|
|
1113
1196
|
async stop(timeoutMs = 5000) {
|
|
@@ -1277,7 +1360,8 @@ class ProcessManager {
|
|
|
1277
1360
|
readyResolved = true;
|
|
1278
1361
|
onInitialReady();
|
|
1279
1362
|
}
|
|
1280
|
-
}
|
|
1363
|
+
},
|
|
1364
|
+
onError: () => this.emit({ type: "error", name })
|
|
1281
1365
|
});
|
|
1282
1366
|
this.runners.set(name, runner);
|
|
1283
1367
|
}
|
|
@@ -1465,6 +1549,26 @@ function evaluateCondition(condition) {
|
|
|
1465
1549
|
// src/ui/app.ts
|
|
1466
1550
|
import { BoxRenderable, createCliRenderer } from "@opentui/core";
|
|
1467
1551
|
|
|
1552
|
+
// src/ui/keybindings.ts
|
|
1553
|
+
var SHORTCUTS = {
|
|
1554
|
+
restartAll: { key: "r", label: "Shift+R", description: "restart all", shift: true },
|
|
1555
|
+
copy: { key: "y", label: "Y", description: "copy" },
|
|
1556
|
+
search: { key: "f", label: "F", description: "search" },
|
|
1557
|
+
restart: { key: "r", label: "R", description: "restart" },
|
|
1558
|
+
stopStart: { key: "s", label: "S", description: "stop/start" },
|
|
1559
|
+
clear: { key: "l", label: "L", description: "clear" }
|
|
1560
|
+
};
|
|
1561
|
+
var STATUS_HINTS = [
|
|
1562
|
+
["\u2190\u2192/1-9", "tabs"],
|
|
1563
|
+
[SHORTCUTS.restart.label, SHORTCUTS.restart.description],
|
|
1564
|
+
[SHORTCUTS.stopStart.label, SHORTCUTS.stopStart.description],
|
|
1565
|
+
[SHORTCUTS.search.label, SHORTCUTS.search.description],
|
|
1566
|
+
[SHORTCUTS.copy.label, SHORTCUTS.copy.description],
|
|
1567
|
+
[SHORTCUTS.clear.label, SHORTCUTS.clear.description],
|
|
1568
|
+
["Ctrl+C", "quit"]
|
|
1569
|
+
];
|
|
1570
|
+
var STATUS_BAR_TEXT = STATUS_HINTS.map(([l, d]) => `${l}: ${d}`).join(" ");
|
|
1571
|
+
|
|
1468
1572
|
// src/ui/pane.ts
|
|
1469
1573
|
import { ScrollBoxRenderable } from "@opentui/core";
|
|
1470
1574
|
import { GhosttyTerminalRenderable } from "ghostty-opentui/terminal-buffer";
|
|
@@ -1474,6 +1578,7 @@ class Pane {
|
|
|
1474
1578
|
terminal;
|
|
1475
1579
|
decoder = new TextDecoder;
|
|
1476
1580
|
_onScroll = null;
|
|
1581
|
+
_onCopy = null;
|
|
1477
1582
|
constructor(renderer, name, cols, rows, interactive = false) {
|
|
1478
1583
|
this.scrollBox = new ScrollBoxRenderable(renderer, {
|
|
1479
1584
|
id: `pane-${name}`,
|
|
@@ -1492,6 +1597,18 @@ class Pane {
|
|
|
1492
1597
|
showCursor: interactive,
|
|
1493
1598
|
trimEnd: true
|
|
1494
1599
|
});
|
|
1600
|
+
const origOnSelectionChanged = this.terminal.onSelectionChanged.bind(this.terminal);
|
|
1601
|
+
this.terminal.onSelectionChanged = (selection) => {
|
|
1602
|
+
const result = origOnSelectionChanged(selection);
|
|
1603
|
+
if (selection?.isActive && !selection.isDragging) {
|
|
1604
|
+
const text = selection.getSelectedText();
|
|
1605
|
+
if (text) {
|
|
1606
|
+
renderer.copyToClipboardOSC52(text);
|
|
1607
|
+
this._onCopy?.(text);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
return result;
|
|
1611
|
+
};
|
|
1495
1612
|
this.scrollBox.add(this.terminal);
|
|
1496
1613
|
}
|
|
1497
1614
|
feed(data) {
|
|
@@ -1520,6 +1637,9 @@ class Pane {
|
|
|
1520
1637
|
onScroll(handler) {
|
|
1521
1638
|
this._onScroll = handler;
|
|
1522
1639
|
}
|
|
1640
|
+
onCopy(handler) {
|
|
1641
|
+
this._onCopy = handler;
|
|
1642
|
+
}
|
|
1523
1643
|
show() {
|
|
1524
1644
|
this.scrollBox.visible = true;
|
|
1525
1645
|
}
|
|
@@ -1583,6 +1703,8 @@ class StatusBar {
|
|
|
1583
1703
|
_searchQuery = "";
|
|
1584
1704
|
_searchMatchCount = 0;
|
|
1585
1705
|
_searchCurrentIndex = -1;
|
|
1706
|
+
_tempMessage = null;
|
|
1707
|
+
_tempTimer = null;
|
|
1586
1708
|
constructor(renderer) {
|
|
1587
1709
|
this.renderable = new TextRenderable(renderer, {
|
|
1588
1710
|
id: "status-bar",
|
|
@@ -1600,13 +1722,25 @@ class StatusBar {
|
|
|
1600
1722
|
this._searchCurrentIndex = currentIndex;
|
|
1601
1723
|
this.renderable.content = this.buildContent();
|
|
1602
1724
|
}
|
|
1725
|
+
showTemporaryMessage(message, duration = 2000) {
|
|
1726
|
+
if (this._tempTimer)
|
|
1727
|
+
clearTimeout(this._tempTimer);
|
|
1728
|
+
this._tempMessage = message;
|
|
1729
|
+
this.renderable.content = this.buildContent();
|
|
1730
|
+
this._tempTimer = setTimeout(() => {
|
|
1731
|
+
this._tempMessage = null;
|
|
1732
|
+
this._tempTimer = null;
|
|
1733
|
+
this.renderable.content = this.buildContent();
|
|
1734
|
+
}, duration);
|
|
1735
|
+
}
|
|
1603
1736
|
buildContent() {
|
|
1737
|
+
if (this._tempMessage) {
|
|
1738
|
+
return new StyledText([cyan(this._tempMessage)]);
|
|
1739
|
+
}
|
|
1604
1740
|
if (this._searchMode) {
|
|
1605
1741
|
return this.buildSearchContent();
|
|
1606
1742
|
}
|
|
1607
|
-
return new StyledText([
|
|
1608
|
-
plain("\u2190\u2192/1-9: tabs R: restart S: stop/start F: search L: clear Ctrl+C: quit")
|
|
1609
|
-
]);
|
|
1743
|
+
return new StyledText([plain(STATUS_BAR_TEXT)]);
|
|
1610
1744
|
}
|
|
1611
1745
|
buildSearchContent() {
|
|
1612
1746
|
const chunks = [];
|
|
@@ -1684,20 +1818,24 @@ class ColoredSelectRenderable extends SelectRenderable {
|
|
|
1684
1818
|
const scrollOffset = this.scrollOffset;
|
|
1685
1819
|
const maxVisibleItems = this.maxVisibleItems;
|
|
1686
1820
|
const linesPerItem = this.linesPerItem;
|
|
1821
|
+
const selectedIndex = this.getSelectedIndex();
|
|
1687
1822
|
const options = this.options;
|
|
1688
1823
|
const visibleCount = Math.min(maxVisibleItems, options.length - scrollOffset);
|
|
1824
|
+
const baseTextColor = this._focused ? this._focusedTextColor : this._textColor;
|
|
1825
|
+
const selectedTextColor = this._selectedTextColor;
|
|
1689
1826
|
for (let i = 0;i < visibleCount; i++) {
|
|
1690
1827
|
const actualIndex = scrollOffset + i;
|
|
1691
|
-
const colors = this._optionColors[actualIndex];
|
|
1692
|
-
if (!colors)
|
|
1693
|
-
continue;
|
|
1694
1828
|
const itemY = i * linesPerItem;
|
|
1695
1829
|
const optName = options[actualIndex].name;
|
|
1696
|
-
|
|
1697
|
-
|
|
1830
|
+
const isSelected = actualIndex === selectedIndex;
|
|
1831
|
+
const defaultColor = isSelected ? selectedTextColor : baseTextColor;
|
|
1832
|
+
const colors = this._optionColors[actualIndex];
|
|
1833
|
+
fb.drawText(`${optName} `, 1, itemY, defaultColor);
|
|
1834
|
+
if (colors?.icon) {
|
|
1835
|
+
fb.drawText(optName.charAt(0), 1, itemY, colors.icon);
|
|
1698
1836
|
}
|
|
1699
|
-
if (colors
|
|
1700
|
-
fb.drawText(optName.slice(2),
|
|
1837
|
+
if (colors?.name) {
|
|
1838
|
+
fb.drawText(optName.slice(2), 3, itemY, colors.name);
|
|
1701
1839
|
}
|
|
1702
1840
|
}
|
|
1703
1841
|
}
|
|
@@ -1711,6 +1849,7 @@ class TabBar {
|
|
|
1711
1849
|
baseDescriptions;
|
|
1712
1850
|
processColors;
|
|
1713
1851
|
inputWaiting = new Set;
|
|
1852
|
+
erroredProcesses = new Set;
|
|
1714
1853
|
constructor(renderer, names, colors) {
|
|
1715
1854
|
this.originalNames = names;
|
|
1716
1855
|
this.names = [...names];
|
|
@@ -1749,6 +1888,9 @@ class TabBar {
|
|
|
1749
1888
|
if (TERMINAL_STATUSES.has(status) || status === "stopping") {
|
|
1750
1889
|
this.inputWaiting.delete(name);
|
|
1751
1890
|
}
|
|
1891
|
+
if (status === "starting") {
|
|
1892
|
+
this.erroredProcesses.delete(name);
|
|
1893
|
+
}
|
|
1752
1894
|
this.refreshOptions();
|
|
1753
1895
|
}
|
|
1754
1896
|
setInputWaiting(name, waiting) {
|
|
@@ -1758,6 +1900,13 @@ class TabBar {
|
|
|
1758
1900
|
this.inputWaiting.delete(name);
|
|
1759
1901
|
this.refreshOptions();
|
|
1760
1902
|
}
|
|
1903
|
+
setError(name, hasError) {
|
|
1904
|
+
if (hasError)
|
|
1905
|
+
this.erroredProcesses.add(name);
|
|
1906
|
+
else
|
|
1907
|
+
this.erroredProcesses.delete(name);
|
|
1908
|
+
this.refreshOptions();
|
|
1909
|
+
}
|
|
1761
1910
|
getNameAtIndex(index) {
|
|
1762
1911
|
return this.names[index];
|
|
1763
1912
|
}
|
|
@@ -1786,13 +1935,16 @@ class TabBar {
|
|
|
1786
1935
|
getDescription(name) {
|
|
1787
1936
|
if (this.inputWaiting.has(name))
|
|
1788
1937
|
return "awaiting input";
|
|
1938
|
+
if (this.erroredProcesses.has(name))
|
|
1939
|
+
return "error detected";
|
|
1789
1940
|
return this.baseDescriptions.get(name) ?? "pending";
|
|
1790
1941
|
}
|
|
1791
1942
|
updateOptionColors() {
|
|
1792
1943
|
const colors = this.names.map((name) => {
|
|
1793
1944
|
const status = this.statuses.get(name);
|
|
1794
1945
|
const waiting = this.inputWaiting.has(name);
|
|
1795
|
-
const
|
|
1946
|
+
const errored = this.erroredProcesses.has(name);
|
|
1947
|
+
const statusHex = waiting ? "#ffaa00" : errored ? "#ff5555" : STATUS_ICON_HEX[status];
|
|
1796
1948
|
const processHex = this.processColors.get(name);
|
|
1797
1949
|
return {
|
|
1798
1950
|
icon: parseColor(statusHex ?? processHex ?? "#888888"),
|
|
@@ -1895,6 +2047,7 @@ class App {
|
|
|
1895
2047
|
for (const name of this.names) {
|
|
1896
2048
|
const interactive = this.config.processes[name].interactive === true;
|
|
1897
2049
|
const pane = new Pane(this.renderer, name, termCols, termRows, interactive);
|
|
2050
|
+
pane.onCopy(() => this.statusBar.showTemporaryMessage("Copied!"));
|
|
1898
2051
|
this.panes.set(name, pane);
|
|
1899
2052
|
paneContainer.add(pane.scrollBox);
|
|
1900
2053
|
}
|
|
@@ -1914,6 +2067,8 @@ class App {
|
|
|
1914
2067
|
if (this.config.processes[event.name]?.interactive) {
|
|
1915
2068
|
this.checkInputWaiting(event.name, event.data);
|
|
1916
2069
|
}
|
|
2070
|
+
} else if (event.type === "error") {
|
|
2071
|
+
this.tabBar.setError(event.name, true);
|
|
1917
2072
|
} else if (event.type === "status") {
|
|
1918
2073
|
const state = this.manager.getState(event.name);
|
|
1919
2074
|
this.tabBar.updateStatus(event.name, event.status, state?.exitCode, state?.restartCount);
|
|
@@ -1956,19 +2111,23 @@ class App {
|
|
|
1956
2111
|
const isInteractive = this.config.processes[this.activePane]?.interactive === true;
|
|
1957
2112
|
if (!isInteractive) {
|
|
1958
2113
|
const name = key.name.toLowerCase();
|
|
1959
|
-
if (key.shift && name ===
|
|
2114
|
+
if (key.shift && name === SHORTCUTS.restartAll.key) {
|
|
1960
2115
|
this.manager.restartAll(this.termCols, this.termRows);
|
|
1961
2116
|
return;
|
|
1962
2117
|
}
|
|
1963
|
-
if (name ===
|
|
2118
|
+
if (name === SHORTCUTS.copy.key) {
|
|
2119
|
+
this.copySelection();
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2122
|
+
if (name === SHORTCUTS.search.key) {
|
|
1964
2123
|
this.enterSearch();
|
|
1965
2124
|
return;
|
|
1966
2125
|
}
|
|
1967
|
-
if (name ===
|
|
2126
|
+
if (name === SHORTCUTS.restart.key) {
|
|
1968
2127
|
this.manager.restart(this.activePane, this.termCols, this.termRows);
|
|
1969
2128
|
return;
|
|
1970
2129
|
}
|
|
1971
|
-
if (name ===
|
|
2130
|
+
if (name === SHORTCUTS.stopStart.key) {
|
|
1972
2131
|
const state = this.manager.getState(this.activePane);
|
|
1973
2132
|
if (state?.status === "stopped" || state?.status === "finished" || state?.status === "failed") {
|
|
1974
2133
|
this.manager.start(this.activePane, this.termCols, this.termRows);
|
|
@@ -1977,7 +2136,7 @@ class App {
|
|
|
1977
2136
|
}
|
|
1978
2137
|
return;
|
|
1979
2138
|
}
|
|
1980
|
-
if (name ===
|
|
2139
|
+
if (name === SHORTCUTS.clear.key) {
|
|
1981
2140
|
this.panes.get(this.activePane)?.clear();
|
|
1982
2141
|
return;
|
|
1983
2142
|
}
|
|
@@ -2065,6 +2224,18 @@ class App {
|
|
|
2065
2224
|
this.tabBar.setInputWaiting(name, false);
|
|
2066
2225
|
}
|
|
2067
2226
|
}
|
|
2227
|
+
copySelection() {
|
|
2228
|
+
const selection = this.renderer.getSelection();
|
|
2229
|
+
if (!selection?.isActive)
|
|
2230
|
+
return false;
|
|
2231
|
+
const text = selection.getSelectedText();
|
|
2232
|
+
if (!text)
|
|
2233
|
+
return false;
|
|
2234
|
+
this.renderer.copyToClipboardOSC52(text);
|
|
2235
|
+
this.renderer.clearSelection();
|
|
2236
|
+
this.statusBar.showTemporaryMessage("Copied!");
|
|
2237
|
+
return true;
|
|
2238
|
+
}
|
|
2068
2239
|
enterSearch() {
|
|
2069
2240
|
this.searchMode = true;
|
|
2070
2241
|
this.searchQuery = "";
|
|
@@ -2437,6 +2608,7 @@ Usage:
|
|
|
2437
2608
|
Options:
|
|
2438
2609
|
-n, --name <name=command> Add a named process
|
|
2439
2610
|
-c, --color <colors> Comma-separated colors (hex or names: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple)
|
|
2611
|
+
--colors Auto-assign colors to processes based on their name
|
|
2440
2612
|
--config <path> Config file path (default: auto-detect)
|
|
2441
2613
|
-p, --prefix Prefixed output mode (no TUI, for CI/scripts)
|
|
2442
2614
|
--only <a,b,...> Only run these processes (+ their dependencies)
|
|
@@ -2614,6 +2786,13 @@ async function main() {
|
|
|
2614
2786
|
if (parsed.only || parsed.exclude) {
|
|
2615
2787
|
config = filterConfig(config, parsed.only, parsed.exclude);
|
|
2616
2788
|
}
|
|
2789
|
+
if (parsed.autoColors) {
|
|
2790
|
+
for (const [name, proc] of Object.entries(config.processes)) {
|
|
2791
|
+
if (!proc.color) {
|
|
2792
|
+
proc.color = colorFromName(name);
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2617
2796
|
const manager = new ProcessManager(config);
|
|
2618
2797
|
let logWriter;
|
|
2619
2798
|
if (parsed.logDir) {
|
package/dist/types.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface NumuxProcessConfig {
|
|
|
14
14
|
color?: string | string[];
|
|
15
15
|
watch?: string | string[];
|
|
16
16
|
interactive?: boolean;
|
|
17
|
+
errorMatcher?: boolean | string;
|
|
17
18
|
}
|
|
18
19
|
/** Config for npm: wildcard entries — command is derived from package.json scripts */
|
|
19
20
|
export type NumuxScriptPattern = Omit<NumuxProcessConfig, 'command'> & {
|
|
@@ -50,4 +51,7 @@ export type ProcessEvent = {
|
|
|
50
51
|
type: 'exit';
|
|
51
52
|
name: string;
|
|
52
53
|
code: number | null;
|
|
54
|
+
} | {
|
|
55
|
+
type: 'error';
|
|
56
|
+
name: string;
|
|
53
57
|
};
|