numux 2.16.2 → 2.17.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 +2 -0
- package/dist/man/numux.1 +15 -1
- package/dist/numux.js +264 -38
- package/dist/types.d.ts +8 -0
- package/dist/utils/color.d.ts +3 -3
- package/dist/utils/theme.d.ts +75 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -228,6 +228,7 @@ export default defineConfig({
|
|
|
228
228
|
| `--no-watch` | Disable file watching even if config has watch patterns |
|
|
229
229
|
| `-t,` `--timestamps` `[<format>]` | Add timestamps to output (default HH:mm:ss.SSS, or pass a format string) |
|
|
230
230
|
| `--log-dir` `<path>` | Write per-process logs to directory |
|
|
231
|
+
| `--theme` `<light|dark|auto>` | TUI theme (auto detects terminal background) |
|
|
231
232
|
| `--debug` | Enable debug logging to .numux/debug.log |
|
|
232
233
|
| `-h,` `--help` | Show this help |
|
|
233
234
|
| `-v,` `--version` | Show version |
|
|
@@ -284,6 +285,7 @@ Top-level options apply to all processes (process-level settings override):
|
|
|
284
285
|
| `killOthersOnFail` | `boolean` | Kill all processes when any one exits with a non-zero exit code |
|
|
285
286
|
| `noWatch` | `boolean` | Disable file watching even if processes have watch patterns |
|
|
286
287
|
| `logDir` | `string` | Directory to write per-process log files |
|
|
288
|
+
| `theme` | `ThemePref` | TUI color theme. `'auto'` detects the terminal background via OSC 11 (falling back to `COLORFGBG` then dark). `'light'`/`'dark'` skip detection. |
|
|
287
289
|
<!-- /generated:config-global -->
|
|
288
290
|
|
|
289
291
|
```ts
|
package/dist/man/numux.1
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.TH "NUMUX" "1" "May 2026" "2.
|
|
1
|
+
.TH "NUMUX" "1" "May 2026" "2.17.0" "numux manual"
|
|
2
2
|
.SH "NAME"
|
|
3
3
|
\fBnumux\fR
|
|
4
4
|
.P
|
|
@@ -349,6 +349,12 @@ Write per\-process logs to directory
|
|
|
349
349
|
T}
|
|
350
350
|
_
|
|
351
351
|
T{
|
|
352
|
+
\fB\-\-theme\fP `<light
|
|
353
|
+
T}|T{
|
|
354
|
+
dark
|
|
355
|
+
T}
|
|
356
|
+
_
|
|
357
|
+
T{
|
|
352
358
|
\fB\-\-debug\fP
|
|
353
359
|
T}|T{
|
|
354
360
|
Enable debug logging to \.numux/debug\.log
|
|
@@ -538,6 +544,14 @@ T}|T{
|
|
|
538
544
|
T}|T{
|
|
539
545
|
Directory to write per\-process log files
|
|
540
546
|
T}
|
|
547
|
+
_
|
|
548
|
+
T{
|
|
549
|
+
\fBtheme\fP
|
|
550
|
+
T}|T{
|
|
551
|
+
\fBThemePref\fP
|
|
552
|
+
T}|T{
|
|
553
|
+
TUI color theme\. \fB'auto'\fP detects the terminal background via OSC 11 (falling back to \fBCOLORFGBG\fP then dark)\. \fB'light'\fP/\fB'dark'\fP skip detection\.
|
|
554
|
+
T}
|
|
541
555
|
.TE
|
|
542
556
|
<!\-\- /generated:config\-global \-\->
|
|
543
557
|
|
package/dist/numux.js
CHANGED
|
@@ -246,6 +246,7 @@ export default defineConfig({
|
|
|
246
246
|
| \`--no-watch\` | Disable file watching even if config has watch patterns |
|
|
247
247
|
| \`-t,\` \`--timestamps\` \`[<format>]\` | Add timestamps to output (default HH:mm:ss.SSS, or pass a format string) |
|
|
248
248
|
| \`--log-dir\` \`<path>\` | Write per-process logs to directory |
|
|
249
|
+
| \`--theme\` \`<light|dark|auto>\` | TUI theme (auto detects terminal background) |
|
|
249
250
|
| \`--debug\` | Enable debug logging to .numux/debug.log |
|
|
250
251
|
| \`-h,\` \`--help\` | Show this help |
|
|
251
252
|
| \`-v,\` \`--version\` | Show version |
|
|
@@ -290,6 +291,7 @@ numux logs api | tail -f # Follow process log output
|
|
|
290
291
|
| \`killOthersOnFail\` | \`boolean\` | Kill all processes when any one exits with a non-zero exit code |
|
|
291
292
|
| \`noWatch\` | \`boolean\` | Disable file watching even if processes have watch patterns |
|
|
292
293
|
| \`logDir\` | \`string\` | Directory to write per-process log files |
|
|
294
|
+
| \`theme\` | \`ThemePref\` | TUI color theme. \`'auto'\` detects the terminal background via OSC 11 (falling back to \`COLORFGBG\` then dark). \`'light'\`/\`'dark'\` skip detection. |
|
|
293
295
|
<!-- /generated:config-global -->
|
|
294
296
|
|
|
295
297
|
\`\`\`ts
|
|
@@ -548,7 +550,7 @@ var init_help = __esm(() => {
|
|
|
548
550
|
var require_package = __commonJS((exports, module) => {
|
|
549
551
|
module.exports = {
|
|
550
552
|
name: "numux",
|
|
551
|
-
version: "2.
|
|
553
|
+
version: "2.17.0",
|
|
552
554
|
description: "Terminal multiplexer with dependency orchestration",
|
|
553
555
|
type: "module",
|
|
554
556
|
license: "MIT",
|
|
@@ -876,6 +878,20 @@ var FLAGS = [
|
|
|
876
878
|
valueName: "<path>",
|
|
877
879
|
completionHint: "directory"
|
|
878
880
|
},
|
|
881
|
+
{
|
|
882
|
+
type: "value",
|
|
883
|
+
long: "--theme",
|
|
884
|
+
key: "theme",
|
|
885
|
+
description: "TUI theme (auto detects terminal background)",
|
|
886
|
+
valueName: "<light|dark|auto>",
|
|
887
|
+
completionHint: "none",
|
|
888
|
+
parse(raw, flag) {
|
|
889
|
+
if (raw !== "light" && raw !== "dark" && raw !== "auto") {
|
|
890
|
+
throw new Error(`${flag} must be light, dark, or auto. Got "${raw}"`);
|
|
891
|
+
}
|
|
892
|
+
return raw;
|
|
893
|
+
}
|
|
894
|
+
},
|
|
879
895
|
{
|
|
880
896
|
type: "boolean",
|
|
881
897
|
long: "--debug",
|
|
@@ -1704,10 +1720,11 @@ function resolveColor(color) {
|
|
|
1704
1720
|
return color[0];
|
|
1705
1721
|
return;
|
|
1706
1722
|
}
|
|
1707
|
-
function buildProcessColorMap(names, config) {
|
|
1723
|
+
function buildProcessColorMap(names, config, palette = DEFAULT_PALETTE) {
|
|
1708
1724
|
const map = new Map;
|
|
1709
1725
|
if ("NO_COLOR" in process.env)
|
|
1710
1726
|
return map;
|
|
1727
|
+
const ansiPalette = palette === DEFAULT_PALETTE ? DEFAULT_ANSI_COLORS : palette.map(hexToAnsi);
|
|
1711
1728
|
let paletteIndex = 0;
|
|
1712
1729
|
for (const name of names) {
|
|
1713
1730
|
const explicit = resolveColor(config.processes[name]?.color);
|
|
@@ -1716,15 +1733,15 @@ function buildProcessColorMap(names, config) {
|
|
|
1716
1733
|
if (hex)
|
|
1717
1734
|
map.set(name, hexToAnsi(hex));
|
|
1718
1735
|
else
|
|
1719
|
-
map.set(name,
|
|
1736
|
+
map.set(name, ansiPalette[paletteIndex++ % ansiPalette.length]);
|
|
1720
1737
|
} else {
|
|
1721
|
-
map.set(name,
|
|
1738
|
+
map.set(name, ansiPalette[paletteIndex % ansiPalette.length]);
|
|
1722
1739
|
paletteIndex++;
|
|
1723
1740
|
}
|
|
1724
1741
|
}
|
|
1725
1742
|
return map;
|
|
1726
1743
|
}
|
|
1727
|
-
function buildProcessHexColorMap(names, config) {
|
|
1744
|
+
function buildProcessHexColorMap(names, config, palette = DEFAULT_PALETTE) {
|
|
1728
1745
|
const map = new Map;
|
|
1729
1746
|
if ("NO_COLOR" in process.env)
|
|
1730
1747
|
return map;
|
|
@@ -1736,9 +1753,9 @@ function buildProcessHexColorMap(names, config) {
|
|
|
1736
1753
|
if (hex)
|
|
1737
1754
|
map.set(name, hex);
|
|
1738
1755
|
else
|
|
1739
|
-
map.set(name,
|
|
1756
|
+
map.set(name, palette[paletteIndex++ % palette.length]);
|
|
1740
1757
|
} else {
|
|
1741
|
-
map.set(name,
|
|
1758
|
+
map.set(name, palette[paletteIndex % palette.length]);
|
|
1742
1759
|
paletteIndex++;
|
|
1743
1760
|
}
|
|
1744
1761
|
}
|
|
@@ -1783,6 +1800,7 @@ function validateConfig(raw, _warnings) {
|
|
|
1783
1800
|
const killOthersOnFail = config.killOthersOnFail === true ? true : undefined;
|
|
1784
1801
|
const noWatch = config.noWatch === true ? true : undefined;
|
|
1785
1802
|
const logDir = typeof config.logDir === "string" && config.logDir.trim() ? config.logDir.trim() : undefined;
|
|
1803
|
+
const theme = validateTheme(config.theme);
|
|
1786
1804
|
const validated = {};
|
|
1787
1805
|
for (const name of names) {
|
|
1788
1806
|
let proc = processes[name];
|
|
@@ -1881,9 +1899,19 @@ function validateConfig(raw, _warnings) {
|
|
|
1881
1899
|
...killOthersOnFail ? { killOthersOnFail } : {},
|
|
1882
1900
|
...noWatch ? { noWatch } : {},
|
|
1883
1901
|
...logDir ? { logDir } : {},
|
|
1902
|
+
...theme ? { theme } : {},
|
|
1884
1903
|
processes: validated
|
|
1885
1904
|
};
|
|
1886
1905
|
}
|
|
1906
|
+
var VALID_THEME_VALUES = new Set(["light", "dark", "auto"]);
|
|
1907
|
+
function validateTheme(value) {
|
|
1908
|
+
if (value === undefined)
|
|
1909
|
+
return;
|
|
1910
|
+
if (typeof value !== "string" || !VALID_THEME_VALUES.has(value)) {
|
|
1911
|
+
throw new Error(`theme must be one of: light, dark, auto. Got "${String(value)}"`);
|
|
1912
|
+
}
|
|
1913
|
+
return value;
|
|
1914
|
+
}
|
|
1887
1915
|
function validateStringOrStringArray(value) {
|
|
1888
1916
|
if (typeof value === "string")
|
|
1889
1917
|
return value;
|
|
@@ -2949,6 +2977,178 @@ function setupShutdownHandlers(app, logWriter) {
|
|
|
2949
2977
|
});
|
|
2950
2978
|
}
|
|
2951
2979
|
|
|
2980
|
+
// src/utils/theme.ts
|
|
2981
|
+
var DARK_THEME = {
|
|
2982
|
+
mode: "dark",
|
|
2983
|
+
statusBarBg: "#1a1a1a",
|
|
2984
|
+
statusBarText: "#cccccc",
|
|
2985
|
+
helpBackdropBg: "#000000",
|
|
2986
|
+
helpBoxBg: "#1a1a2e",
|
|
2987
|
+
helpBorder: "#444444",
|
|
2988
|
+
helpText: "#cccccc",
|
|
2989
|
+
sidebarBg: "#1a1a1a",
|
|
2990
|
+
sidebarBorder: "#444444",
|
|
2991
|
+
tabSelectedBg: "#334455",
|
|
2992
|
+
tabSelectedText: "#ffffff",
|
|
2993
|
+
tabText: "#888888",
|
|
2994
|
+
tabDescriptionText: "#888888",
|
|
2995
|
+
tabSelectedDescriptionText: "#cccccc",
|
|
2996
|
+
scrollTrackBg: "#252527",
|
|
2997
|
+
scrollThumbBg: "#9a9ea3",
|
|
2998
|
+
searchCurrentBg: "#b58900",
|
|
2999
|
+
searchMatchBg: "#073642",
|
|
3000
|
+
palette: ["#00cccc", "#cccc00", "#cc00cc", "#5577ff", "#00cc00", "#ff5555", "#ffa500", "#cc88ff"],
|
|
3001
|
+
status: {
|
|
3002
|
+
ready: "#00cc00",
|
|
3003
|
+
failed: "#ff5555",
|
|
3004
|
+
stopped: "#888888",
|
|
3005
|
+
finished: "#66aa66",
|
|
3006
|
+
skipped: "#888888"
|
|
3007
|
+
},
|
|
3008
|
+
inputWaiting: "#ffaa00",
|
|
3009
|
+
errorIndicator: "#ff5555",
|
|
3010
|
+
searchMatchTab: "#b58900",
|
|
3011
|
+
iconDefault: "#888888"
|
|
3012
|
+
};
|
|
3013
|
+
var LIGHT_THEME = {
|
|
3014
|
+
mode: "light",
|
|
3015
|
+
statusBarBg: "#e8e8e8",
|
|
3016
|
+
statusBarText: "#000000",
|
|
3017
|
+
helpBackdropBg: "#ffffff",
|
|
3018
|
+
helpBoxBg: "#f5f5f5",
|
|
3019
|
+
helpBorder: "#aaaaaa",
|
|
3020
|
+
helpText: "#1a1a1a",
|
|
3021
|
+
sidebarBg: "#f0f0f0",
|
|
3022
|
+
sidebarBorder: "#aaaaaa",
|
|
3023
|
+
tabSelectedBg: "#7a9bbf",
|
|
3024
|
+
tabSelectedText: "#ffffff",
|
|
3025
|
+
tabText: "#444444",
|
|
3026
|
+
tabDescriptionText: "#666666",
|
|
3027
|
+
tabSelectedDescriptionText: "#e8e8e8",
|
|
3028
|
+
scrollTrackBg: "#d0d0d0",
|
|
3029
|
+
scrollThumbBg: "#888888",
|
|
3030
|
+
searchCurrentBg: "#ffaa33",
|
|
3031
|
+
searchMatchBg: "#d0e4b8",
|
|
3032
|
+
palette: ["#008888", "#886600", "#880088", "#0033aa", "#006600", "#aa0000", "#cc5500", "#6622aa"],
|
|
3033
|
+
status: {
|
|
3034
|
+
ready: "#006600",
|
|
3035
|
+
failed: "#aa0000",
|
|
3036
|
+
stopped: "#666666",
|
|
3037
|
+
finished: "#2a7a2a",
|
|
3038
|
+
skipped: "#666666"
|
|
3039
|
+
},
|
|
3040
|
+
inputWaiting: "#cc7a00",
|
|
3041
|
+
errorIndicator: "#aa0000",
|
|
3042
|
+
searchMatchTab: "#cc7a00",
|
|
3043
|
+
iconDefault: "#666666"
|
|
3044
|
+
};
|
|
3045
|
+
function themeFor(mode) {
|
|
3046
|
+
return mode === "light" ? LIGHT_THEME : DARK_THEME;
|
|
3047
|
+
}
|
|
3048
|
+
function relativeLuminance(r, g, b) {
|
|
3049
|
+
const norm = (c) => {
|
|
3050
|
+
const s = c / 255;
|
|
3051
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
3052
|
+
};
|
|
3053
|
+
return 0.2126 * norm(r) + 0.7152 * norm(g) + 0.0722 * norm(b);
|
|
3054
|
+
}
|
|
3055
|
+
function isLightRgb(r, g, b) {
|
|
3056
|
+
return relativeLuminance(r, g, b) > 0.5;
|
|
3057
|
+
}
|
|
3058
|
+
function parseOSC11Response(data) {
|
|
3059
|
+
const match = data.match(/rgb:([0-9a-f]+)\/([0-9a-f]+)\/([0-9a-f]+)/i);
|
|
3060
|
+
if (!match)
|
|
3061
|
+
return null;
|
|
3062
|
+
const scale = (hex) => {
|
|
3063
|
+
if (hex.length === 0 || hex.length > 4)
|
|
3064
|
+
return Number.NaN;
|
|
3065
|
+
const val = Number.parseInt(hex, 16);
|
|
3066
|
+
if (Number.isNaN(val))
|
|
3067
|
+
return Number.NaN;
|
|
3068
|
+
const max = 16 ** hex.length - 1;
|
|
3069
|
+
return Math.round(val / max * 255);
|
|
3070
|
+
};
|
|
3071
|
+
const r = scale(match[1]);
|
|
3072
|
+
const g = scale(match[2]);
|
|
3073
|
+
const b = scale(match[3]);
|
|
3074
|
+
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b))
|
|
3075
|
+
return null;
|
|
3076
|
+
return { r, g, b };
|
|
3077
|
+
}
|
|
3078
|
+
function parseColorFgBg(value) {
|
|
3079
|
+
if (!value)
|
|
3080
|
+
return null;
|
|
3081
|
+
const parts = value.split(";");
|
|
3082
|
+
if (parts.length < 2)
|
|
3083
|
+
return null;
|
|
3084
|
+
const bgRaw = parts[parts.length - 1].trim();
|
|
3085
|
+
const bg = Number.parseInt(bgRaw, 10);
|
|
3086
|
+
if (Number.isNaN(bg))
|
|
3087
|
+
return null;
|
|
3088
|
+
return bg >= 7 && bg <= 15 ? "light" : "dark";
|
|
3089
|
+
}
|
|
3090
|
+
function queryOSC11(timeoutMs = 100) {
|
|
3091
|
+
const stdin = process.stdin;
|
|
3092
|
+
const stdout = process.stdout;
|
|
3093
|
+
if (!(stdin.isTTY && stdout.isTTY))
|
|
3094
|
+
return Promise.resolve(null);
|
|
3095
|
+
return new Promise((resolve8) => {
|
|
3096
|
+
let settled = false;
|
|
3097
|
+
let buf = "";
|
|
3098
|
+
let timer = null;
|
|
3099
|
+
const wasRaw = stdin.isRaw;
|
|
3100
|
+
const finish = (result) => {
|
|
3101
|
+
if (settled)
|
|
3102
|
+
return;
|
|
3103
|
+
settled = true;
|
|
3104
|
+
if (timer)
|
|
3105
|
+
clearTimeout(timer);
|
|
3106
|
+
stdin.off("data", onData);
|
|
3107
|
+
try {
|
|
3108
|
+
if (!wasRaw)
|
|
3109
|
+
stdin.setRawMode(false);
|
|
3110
|
+
} catch {}
|
|
3111
|
+
stdin.pause();
|
|
3112
|
+
resolve8(result);
|
|
3113
|
+
};
|
|
3114
|
+
const onData = (chunk) => {
|
|
3115
|
+
buf += chunk.toString("utf8");
|
|
3116
|
+
const match = buf.match(/\x1b\]1[01];rgb:[0-9a-f/]+(?:\x07|\x1b\\)/i);
|
|
3117
|
+
if (!match)
|
|
3118
|
+
return;
|
|
3119
|
+
const parsed = parseOSC11Response(match[0]);
|
|
3120
|
+
if (!parsed) {
|
|
3121
|
+
finish(null);
|
|
3122
|
+
return;
|
|
3123
|
+
}
|
|
3124
|
+
finish(isLightRgb(parsed.r, parsed.g, parsed.b) ? "light" : "dark");
|
|
3125
|
+
};
|
|
3126
|
+
try {
|
|
3127
|
+
stdin.setRawMode(true);
|
|
3128
|
+
stdin.resume();
|
|
3129
|
+
stdin.on("data", onData);
|
|
3130
|
+
timer = setTimeout(() => finish(null), timeoutMs);
|
|
3131
|
+
stdout.write("\x1B]11;?\x1B\\");
|
|
3132
|
+
} catch {
|
|
3133
|
+
finish(null);
|
|
3134
|
+
}
|
|
3135
|
+
});
|
|
3136
|
+
}
|
|
3137
|
+
async function detectThemeMode(timeoutMs = 100) {
|
|
3138
|
+
const osc = await queryOSC11(timeoutMs);
|
|
3139
|
+
if (osc)
|
|
3140
|
+
return osc;
|
|
3141
|
+
return parseColorFgBg(process.env.COLORFGBG);
|
|
3142
|
+
}
|
|
3143
|
+
async function resolveTheme(pref = "auto") {
|
|
3144
|
+
if (pref === "light")
|
|
3145
|
+
return LIGHT_THEME;
|
|
3146
|
+
if (pref === "dark")
|
|
3147
|
+
return DARK_THEME;
|
|
3148
|
+
const detected = await detectThemeMode();
|
|
3149
|
+
return themeFor(detected ?? "dark");
|
|
3150
|
+
}
|
|
3151
|
+
|
|
2952
3152
|
// src/ui/help-overlay.ts
|
|
2953
3153
|
import { BoxRenderable, TextRenderable } from "@opentui/core";
|
|
2954
3154
|
|
|
@@ -3002,7 +3202,7 @@ var STATUS_BAR_TEXT = STATUS_HINTS_COMPACT.map((h) => {
|
|
|
3002
3202
|
class HelpOverlay {
|
|
3003
3203
|
renderable;
|
|
3004
3204
|
textRenderable;
|
|
3005
|
-
constructor(renderer) {
|
|
3205
|
+
constructor(renderer, theme = DARK_THEME) {
|
|
3006
3206
|
this.renderable = new BoxRenderable(renderer, {
|
|
3007
3207
|
id: "help-overlay",
|
|
3008
3208
|
position: "absolute",
|
|
@@ -3018,7 +3218,7 @@ class HelpOverlay {
|
|
|
3018
3218
|
position: "absolute",
|
|
3019
3219
|
width: "100%",
|
|
3020
3220
|
height: "100%",
|
|
3021
|
-
backgroundColor:
|
|
3221
|
+
backgroundColor: theme.helpBackdropBg,
|
|
3022
3222
|
opacity: 0.7
|
|
3023
3223
|
});
|
|
3024
3224
|
const box = new BoxRenderable(renderer, {
|
|
@@ -3026,9 +3226,9 @@ class HelpOverlay {
|
|
|
3026
3226
|
flexDirection: "column",
|
|
3027
3227
|
padding: 1,
|
|
3028
3228
|
paddingX: 5,
|
|
3029
|
-
backgroundColor:
|
|
3229
|
+
backgroundColor: theme.helpBoxBg,
|
|
3030
3230
|
border: true,
|
|
3031
|
-
borderColor:
|
|
3231
|
+
borderColor: theme.helpBorder,
|
|
3032
3232
|
zIndex: 101
|
|
3033
3233
|
});
|
|
3034
3234
|
const lines = [
|
|
@@ -3045,7 +3245,7 @@ class HelpOverlay {
|
|
|
3045
3245
|
id: "help-text",
|
|
3046
3246
|
content: lines.join(`
|
|
3047
3247
|
`),
|
|
3048
|
-
fg:
|
|
3248
|
+
fg: theme.helpText
|
|
3049
3249
|
});
|
|
3050
3250
|
box.add(this.textRenderable);
|
|
3051
3251
|
this.renderable.add(backdrop);
|
|
@@ -3576,8 +3776,10 @@ class Pane {
|
|
|
3576
3776
|
_onScroll = null;
|
|
3577
3777
|
_onCopy = null;
|
|
3578
3778
|
_onLinkClick = null;
|
|
3579
|
-
|
|
3779
|
+
theme;
|
|
3780
|
+
constructor(renderer, name, cols, rows, interactive = false, theme = DARK_THEME) {
|
|
3580
3781
|
this.renderer = renderer;
|
|
3782
|
+
this.theme = theme;
|
|
3581
3783
|
this.scrollBox = new ScrollBoxRenderable(renderer, {
|
|
3582
3784
|
id: `pane-${name}`,
|
|
3583
3785
|
flexGrow: 1,
|
|
@@ -3585,7 +3787,13 @@ class Pane {
|
|
|
3585
3787
|
stickyScroll: true,
|
|
3586
3788
|
stickyStart: "bottom",
|
|
3587
3789
|
visible: false,
|
|
3588
|
-
onMouseScroll: () => this._onScroll?.()
|
|
3790
|
+
onMouseScroll: () => this._onScroll?.(),
|
|
3791
|
+
scrollbarOptions: {
|
|
3792
|
+
trackOptions: {
|
|
3793
|
+
backgroundColor: theme.scrollTrackBg,
|
|
3794
|
+
foregroundColor: theme.scrollThumbBg
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3589
3797
|
});
|
|
3590
3798
|
this.terminal = new TailingTerminal(renderer, {
|
|
3591
3799
|
id: `term-${name}`,
|
|
@@ -3721,7 +3929,7 @@ class Pane {
|
|
|
3721
3929
|
line: m.line,
|
|
3722
3930
|
start: m.start,
|
|
3723
3931
|
end: m.end,
|
|
3724
|
-
backgroundColor: i === currentIndex ?
|
|
3932
|
+
backgroundColor: i === currentIndex ? this.theme.searchCurrentBg : this.theme.searchMatchBg
|
|
3725
3933
|
});
|
|
3726
3934
|
}
|
|
3727
3935
|
this.terminal.highlights = regions;
|
|
@@ -4026,11 +4234,11 @@ class StatusBar {
|
|
|
4026
4234
|
_tempMessage = null;
|
|
4027
4235
|
_tempTimer = null;
|
|
4028
4236
|
_inputMode = false;
|
|
4029
|
-
constructor(renderer) {
|
|
4237
|
+
constructor(renderer, theme = DARK_THEME) {
|
|
4030
4238
|
this.renderable = new BoxRenderable2(renderer, {
|
|
4031
4239
|
id: "status-bar",
|
|
4032
4240
|
width: "100%",
|
|
4033
|
-
backgroundColor:
|
|
4241
|
+
backgroundColor: theme.statusBarBg,
|
|
4034
4242
|
paddingX: 1,
|
|
4035
4243
|
minHeight: 1
|
|
4036
4244
|
});
|
|
@@ -4038,6 +4246,7 @@ class StatusBar {
|
|
|
4038
4246
|
id: "status-bar-text",
|
|
4039
4247
|
width: "100%",
|
|
4040
4248
|
wrapMode: "word",
|
|
4249
|
+
fg: theme.statusBarText,
|
|
4041
4250
|
content: this.buildContent()
|
|
4042
4251
|
});
|
|
4043
4252
|
this.text.selectable = false;
|
|
@@ -4123,13 +4332,15 @@ var STATUS_ICONS = {
|
|
|
4123
4332
|
failed: "\u2716",
|
|
4124
4333
|
skipped: "\u2298"
|
|
4125
4334
|
};
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4335
|
+
function getStatusIconHex(theme) {
|
|
4336
|
+
return {
|
|
4337
|
+
ready: theme.status.ready,
|
|
4338
|
+
finished: theme.status.finished,
|
|
4339
|
+
failed: theme.status.failed,
|
|
4340
|
+
stopped: theme.status.stopped,
|
|
4341
|
+
skipped: theme.status.skipped
|
|
4342
|
+
};
|
|
4343
|
+
}
|
|
4133
4344
|
var TERMINAL_STATUSES = new Set(["finished", "stopped", "failed", "skipped"]);
|
|
4134
4345
|
function getDisplayOrder(originalNames, statuses) {
|
|
4135
4346
|
const active = originalNames.filter((n) => !TERMINAL_STATUSES.has(statuses.get(n)));
|
|
@@ -4149,16 +4360,17 @@ function formatDescription(status, exitCode, restartCount) {
|
|
|
4149
4360
|
}
|
|
4150
4361
|
return desc;
|
|
4151
4362
|
}
|
|
4152
|
-
function resolveOptionColors(names, statuses, processColors, inputWaiting, erroredProcesses, searchMatchProcesses) {
|
|
4363
|
+
function resolveOptionColors(names, statuses, processColors, inputWaiting, erroredProcesses, theme, searchMatchProcesses) {
|
|
4364
|
+
const statusIconHex = getStatusIconHex(theme);
|
|
4153
4365
|
return names.map((name) => {
|
|
4154
4366
|
const status = statuses.get(name);
|
|
4155
4367
|
const waiting = inputWaiting.has(name);
|
|
4156
4368
|
const errored = erroredProcesses.has(name);
|
|
4157
4369
|
const hasSearchMatch = searchMatchProcesses?.has(name);
|
|
4158
|
-
const statusHex = hasSearchMatch ?
|
|
4370
|
+
const statusHex = hasSearchMatch ? theme.searchMatchTab : waiting ? theme.inputWaiting : errored ? theme.errorIndicator : statusIconHex[status];
|
|
4159
4371
|
const processHex = processColors.get(name);
|
|
4160
4372
|
return {
|
|
4161
|
-
iconHex: statusHex ?? processHex ??
|
|
4373
|
+
iconHex: statusHex ?? processHex ?? theme.iconDefault,
|
|
4162
4374
|
nameHex: processHex ?? null
|
|
4163
4375
|
};
|
|
4164
4376
|
});
|
|
@@ -4242,13 +4454,15 @@ class TabBar {
|
|
|
4242
4454
|
inputWaiting = new Set;
|
|
4243
4455
|
erroredProcesses = new Set;
|
|
4244
4456
|
searchMatchCounts = new Map;
|
|
4245
|
-
|
|
4457
|
+
theme;
|
|
4458
|
+
constructor(renderer, names, colors, theme = DARK_THEME, reorderByStatus = false) {
|
|
4246
4459
|
this.originalNames = names;
|
|
4247
4460
|
this.names = [...names];
|
|
4248
4461
|
this.reorderByStatus = reorderByStatus;
|
|
4249
4462
|
this.statuses = new Map(names.map((n) => [n, "pending"]));
|
|
4250
4463
|
this.baseDescriptions = new Map(names.map((n) => [n, "pending"]));
|
|
4251
4464
|
this.processColors = colors ?? new Map;
|
|
4465
|
+
this.theme = theme;
|
|
4252
4466
|
this.renderable = new ColoredSelectRenderable(renderer, {
|
|
4253
4467
|
id: "tab-bar",
|
|
4254
4468
|
width: "100%",
|
|
@@ -4257,9 +4471,13 @@ class TabBar {
|
|
|
4257
4471
|
name: formatTab(n, "pending"),
|
|
4258
4472
|
description: "pending"
|
|
4259
4473
|
})),
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4474
|
+
backgroundColor: theme.sidebarBg,
|
|
4475
|
+
focusedBackgroundColor: theme.sidebarBg,
|
|
4476
|
+
selectedBackgroundColor: theme.tabSelectedBg,
|
|
4477
|
+
selectedTextColor: theme.tabSelectedText,
|
|
4478
|
+
textColor: theme.tabText,
|
|
4479
|
+
descriptionColor: theme.tabDescriptionText,
|
|
4480
|
+
selectedDescriptionColor: theme.tabSelectedDescriptionText,
|
|
4263
4481
|
showDescription: true,
|
|
4264
4482
|
wrapSelection: true
|
|
4265
4483
|
});
|
|
@@ -4343,7 +4561,7 @@ class TabBar {
|
|
|
4343
4561
|
}
|
|
4344
4562
|
updateOptionColors() {
|
|
4345
4563
|
const searchProcesses = this.searchMatchCounts.size > 0 ? new Set(this.searchMatchCounts.keys()) : undefined;
|
|
4346
|
-
const resolved = resolveOptionColors(this.names, this.statuses, this.processColors, this.inputWaiting, this.erroredProcesses, searchProcesses);
|
|
4564
|
+
const resolved = resolveOptionColors(this.names, this.statuses, this.processColors, this.inputWaiting, this.erroredProcesses, this.theme, searchProcesses);
|
|
4347
4565
|
const colors = resolved.map((c) => ({
|
|
4348
4566
|
icon: parseColor(c.iconHex),
|
|
4349
4567
|
name: c.nameHex ? parseColor(c.nameHex) : null
|
|
@@ -4376,6 +4594,7 @@ class App {
|
|
|
4376
4594
|
sidebarWidth = 20;
|
|
4377
4595
|
config;
|
|
4378
4596
|
logWriter;
|
|
4597
|
+
theme = DARK_THEME;
|
|
4379
4598
|
resizeTimer = null;
|
|
4380
4599
|
inputWaitTimers = new Map;
|
|
4381
4600
|
awaitingInput = new Set;
|
|
@@ -4386,6 +4605,9 @@ class App {
|
|
|
4386
4605
|
this.names = manager.getProcessNames();
|
|
4387
4606
|
}
|
|
4388
4607
|
async start() {
|
|
4608
|
+
log(`theme detect: pref=${this.config.theme ?? "auto"} stdin.isTTY=${process.stdin.isTTY} stdout.isTTY=${process.stdout.isTTY} COLORFGBG=${process.env.COLORFGBG ?? "(unset)"}`);
|
|
4609
|
+
this.theme = await resolveTheme(this.config.theme);
|
|
4610
|
+
log(`theme resolved: ${this.theme.mode}`);
|
|
4389
4611
|
this.renderer = await createCliRenderer({
|
|
4390
4612
|
exitOnCtrlC: false,
|
|
4391
4613
|
useMouse: true,
|
|
@@ -4404,8 +4626,8 @@ class App {
|
|
|
4404
4626
|
height: "100%",
|
|
4405
4627
|
border: false
|
|
4406
4628
|
});
|
|
4407
|
-
const processHexColors = buildProcessHexColorMap(this.names, this.config);
|
|
4408
|
-
this.tabBar = new TabBar(this.renderer, this.names, processHexColors, this.config.sort === "status");
|
|
4629
|
+
const processHexColors = buildProcessHexColorMap(this.names, this.config, this.theme.palette);
|
|
4630
|
+
this.tabBar = new TabBar(this.renderer, this.names, processHexColors, this.theme, this.config.sort === "status");
|
|
4409
4631
|
const contentRow = new BoxRenderable3(this.renderer, {
|
|
4410
4632
|
id: "content-row",
|
|
4411
4633
|
flexDirection: "row",
|
|
@@ -4418,7 +4640,8 @@ class App {
|
|
|
4418
4640
|
width: this.sidebarWidth,
|
|
4419
4641
|
height: "100%",
|
|
4420
4642
|
border: ["right"],
|
|
4421
|
-
borderColor:
|
|
4643
|
+
borderColor: this.theme.sidebarBorder,
|
|
4644
|
+
backgroundColor: this.theme.sidebarBg
|
|
4422
4645
|
});
|
|
4423
4646
|
sidebar.add(this.tabBar.renderable);
|
|
4424
4647
|
const paneContainer = new BoxRenderable3(this.renderer, {
|
|
@@ -4426,8 +4649,8 @@ class App {
|
|
|
4426
4649
|
flexGrow: 1,
|
|
4427
4650
|
border: false
|
|
4428
4651
|
});
|
|
4429
|
-
this.statusBar = new StatusBar(this.renderer);
|
|
4430
|
-
this.helpOverlay = new HelpOverlay(this.renderer);
|
|
4652
|
+
this.statusBar = new StatusBar(this.renderer, this.theme);
|
|
4653
|
+
this.helpOverlay = new HelpOverlay(this.renderer, this.theme);
|
|
4431
4654
|
this.search = new SearchController({
|
|
4432
4655
|
logWriter: this.logWriter,
|
|
4433
4656
|
statusBar: this.statusBar,
|
|
@@ -4437,7 +4660,7 @@ class App {
|
|
|
4437
4660
|
});
|
|
4438
4661
|
for (const name of this.names) {
|
|
4439
4662
|
const interactive = this.config.processes[name].interactive === true;
|
|
4440
|
-
const pane = new Pane(this.renderer, name, termCols, termRows, interactive);
|
|
4663
|
+
const pane = new Pane(this.renderer, name, termCols, termRows, interactive, this.theme);
|
|
4441
4664
|
if (this.config.timestamps) {
|
|
4442
4665
|
pane.setTimestamps(this.config.timestamps);
|
|
4443
4666
|
}
|
|
@@ -5455,6 +5678,9 @@ async function main() {
|
|
|
5455
5678
|
if (parsed.only || parsed.exclude) {
|
|
5456
5679
|
config = filterConfig(config, parsed.only, parsed.exclude);
|
|
5457
5680
|
}
|
|
5681
|
+
if (parsed.theme) {
|
|
5682
|
+
config.theme = parsed.theme;
|
|
5683
|
+
}
|
|
5458
5684
|
if (parsed.autoColors) {
|
|
5459
5685
|
for (const [name, proc] of Object.entries(config.processes)) {
|
|
5460
5686
|
if (!proc.color) {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Color } from './utils/color';
|
|
2
|
+
import type { ThemePref } from './utils/theme';
|
|
2
3
|
export interface NumuxProcessConfig<K extends string = string> {
|
|
3
4
|
/** Shell command to run. Supports `$dep.group` references from dependency capture groups */
|
|
4
5
|
command: string;
|
|
@@ -122,6 +123,12 @@ export interface NumuxConfig<K extends string = string> {
|
|
|
122
123
|
noWatch?: boolean;
|
|
123
124
|
/** Directory to write per-process log files */
|
|
124
125
|
logDir?: string;
|
|
126
|
+
/**
|
|
127
|
+
* TUI color theme. `'auto'` detects the terminal background via OSC 11 (falling back
|
|
128
|
+
* to `COLORFGBG` then dark). `'light'`/`'dark'` skip detection.
|
|
129
|
+
* @default 'auto'
|
|
130
|
+
*/
|
|
131
|
+
theme?: ThemePref;
|
|
125
132
|
processes: Record<K, NumuxProcessConfig<K> | NumuxScriptPattern<K> | string | true>;
|
|
126
133
|
}
|
|
127
134
|
export type SortOrder = 'config' | 'alphabetical' | 'topological' | 'status';
|
|
@@ -138,6 +145,7 @@ export interface ResolvedNumuxConfig {
|
|
|
138
145
|
killOthersOnFail?: boolean;
|
|
139
146
|
noWatch?: boolean;
|
|
140
147
|
logDir?: string;
|
|
148
|
+
theme?: ThemePref;
|
|
141
149
|
processes: Record<string, ResolvedProcessConfig>;
|
|
142
150
|
}
|
|
143
151
|
export type ProcessStatus = 'pending' | 'starting' | 'ready' | 'running' | 'stopping' | 'stopped' | 'finished' | 'failed' | 'skipped';
|
package/dist/utils/color.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export declare const ANSI_RESET = "\u001B[0m";
|
|
|
34
34
|
export declare function stripAnsi(str: string): string;
|
|
35
35
|
/** Pick a deterministic color from the default palette based on the process name */
|
|
36
36
|
export declare function colorFromName(name: string): Color;
|
|
37
|
-
/** Build a map of process names to ANSI color codes, using explicit config colors or a
|
|
38
|
-
export declare function buildProcessColorMap(names: string[], config: ResolvedNumuxConfig): Map<string, string>;
|
|
37
|
+
/** Build a map of process names to ANSI color codes, using explicit config colors or a palette. */
|
|
38
|
+
export declare function buildProcessColorMap(names: string[], config: ResolvedNumuxConfig, palette?: readonly string[]): Map<string, string>;
|
|
39
39
|
/** Build a map of process names to hex color strings (for StyledText rendering). */
|
|
40
|
-
export declare function buildProcessHexColorMap(names: string[], config: ResolvedNumuxConfig): Map<string, string>;
|
|
40
|
+
export declare function buildProcessHexColorMap(names: string[], config: ResolvedNumuxConfig, palette?: readonly string[]): Map<string, string>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Light/dark theme resolution. Detects terminal background via OSC 11
|
|
3
|
+
* query, falling back to COLORFGBG env var, then to dark. Explicit
|
|
4
|
+
* user config (`theme: 'light' | 'dark'`) skips detection entirely.
|
|
5
|
+
*/
|
|
6
|
+
export type ThemeMode = 'light' | 'dark';
|
|
7
|
+
export type ThemePref = ThemeMode | 'auto';
|
|
8
|
+
export interface StatusColors {
|
|
9
|
+
ready: string;
|
|
10
|
+
failed: string;
|
|
11
|
+
stopped: string;
|
|
12
|
+
finished: string;
|
|
13
|
+
skipped: string;
|
|
14
|
+
}
|
|
15
|
+
export interface Theme {
|
|
16
|
+
mode: ThemeMode;
|
|
17
|
+
statusBarBg: string;
|
|
18
|
+
statusBarText: string;
|
|
19
|
+
helpBackdropBg: string;
|
|
20
|
+
helpBoxBg: string;
|
|
21
|
+
helpBorder: string;
|
|
22
|
+
helpText: string;
|
|
23
|
+
sidebarBg: string;
|
|
24
|
+
sidebarBorder: string;
|
|
25
|
+
tabSelectedBg: string;
|
|
26
|
+
tabSelectedText: string;
|
|
27
|
+
tabText: string;
|
|
28
|
+
tabDescriptionText: string;
|
|
29
|
+
tabSelectedDescriptionText: string;
|
|
30
|
+
scrollTrackBg: string;
|
|
31
|
+
scrollThumbBg: string;
|
|
32
|
+
searchCurrentBg: string;
|
|
33
|
+
searchMatchBg: string;
|
|
34
|
+
palette: readonly string[];
|
|
35
|
+
status: StatusColors;
|
|
36
|
+
inputWaiting: string;
|
|
37
|
+
errorIndicator: string;
|
|
38
|
+
searchMatchTab: string;
|
|
39
|
+
iconDefault: string;
|
|
40
|
+
}
|
|
41
|
+
export declare const DARK_THEME: Theme;
|
|
42
|
+
export declare const LIGHT_THEME: Theme;
|
|
43
|
+
export declare function themeFor(mode: ThemeMode): Theme;
|
|
44
|
+
/** WCAG relative luminance (0–1). */
|
|
45
|
+
export declare function relativeLuminance(r: number, g: number, b: number): number;
|
|
46
|
+
export declare function isLightRgb(r: number, g: number, b: number): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Parse OSC 11 response body. Accepts `rgb:RRRR/GGGG/BBBB` (4 hex digits,
|
|
49
|
+
* xterm/Ghostty/kitty/alacritty/iTerm2) or 2-digit short form. Returns null
|
|
50
|
+
* on malformed input.
|
|
51
|
+
*/
|
|
52
|
+
export declare function parseOSC11Response(data: string): {
|
|
53
|
+
r: number;
|
|
54
|
+
g: number;
|
|
55
|
+
b: number;
|
|
56
|
+
} | null;
|
|
57
|
+
/**
|
|
58
|
+
* Parse COLORFGBG env var (e.g. `"15;0"` = white fg on black bg = dark).
|
|
59
|
+
* Convention: bg index ≥7 is light, otherwise dark. iTerm2 sometimes sets
|
|
60
|
+
* the middle field to `default`; we read the last segment as the bg.
|
|
61
|
+
*/
|
|
62
|
+
export declare function parseColorFgBg(value: string | undefined): ThemeMode | null;
|
|
63
|
+
/**
|
|
64
|
+
* Query the terminal's background color via OSC 11. Resolves to `null` on
|
|
65
|
+
* non-TTY, timeout, or unparseable response. Runs synchronously-ish in under
|
|
66
|
+
* `timeoutMs` (default 100ms). Must be called before any renderer takes
|
|
67
|
+
* over stdin.
|
|
68
|
+
*/
|
|
69
|
+
export declare function queryOSC11(timeoutMs?: number): Promise<ThemeMode | null>;
|
|
70
|
+
export declare function detectThemeMode(timeoutMs?: number): Promise<ThemeMode | null>;
|
|
71
|
+
/**
|
|
72
|
+
* Resolve final theme. Explicit `'light'`/`'dark'` skips detection;
|
|
73
|
+
* `'auto'` (or undefined) runs detection, falling back to dark.
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveTheme(pref?: ThemePref | undefined): Promise<Theme>;
|