numux 2.16.2 → 2.17.1

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/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
@@ -502,6 +504,341 @@ Search mode (after pressing \`F\`):
502
504
  };
503
505
  });
504
506
 
507
+ // node_modules/clone/clone.js
508
+ var require_clone = __commonJS((exports, module) => {
509
+ var clone = function() {
510
+ function clone2(parent, circular, depth, prototype) {
511
+ var filter;
512
+ if (typeof circular === "object") {
513
+ depth = circular.depth;
514
+ prototype = circular.prototype;
515
+ filter = circular.filter;
516
+ circular = circular.circular;
517
+ }
518
+ var allParents = [];
519
+ var allChildren = [];
520
+ var useBuffer = typeof Buffer != "undefined";
521
+ if (typeof circular == "undefined")
522
+ circular = true;
523
+ if (typeof depth == "undefined")
524
+ depth = Infinity;
525
+ function _clone(parent2, depth2) {
526
+ if (parent2 === null)
527
+ return null;
528
+ if (depth2 == 0)
529
+ return parent2;
530
+ var child;
531
+ var proto;
532
+ if (typeof parent2 != "object") {
533
+ return parent2;
534
+ }
535
+ if (clone2.__isArray(parent2)) {
536
+ child = [];
537
+ } else if (clone2.__isRegExp(parent2)) {
538
+ child = new RegExp(parent2.source, __getRegExpFlags(parent2));
539
+ if (parent2.lastIndex)
540
+ child.lastIndex = parent2.lastIndex;
541
+ } else if (clone2.__isDate(parent2)) {
542
+ child = new Date(parent2.getTime());
543
+ } else if (useBuffer && Buffer.isBuffer(parent2)) {
544
+ if (Buffer.allocUnsafe) {
545
+ child = Buffer.allocUnsafe(parent2.length);
546
+ } else {
547
+ child = new Buffer(parent2.length);
548
+ }
549
+ parent2.copy(child);
550
+ return child;
551
+ } else {
552
+ if (typeof prototype == "undefined") {
553
+ proto = Object.getPrototypeOf(parent2);
554
+ child = Object.create(proto);
555
+ } else {
556
+ child = Object.create(prototype);
557
+ proto = prototype;
558
+ }
559
+ }
560
+ if (circular) {
561
+ var index = allParents.indexOf(parent2);
562
+ if (index != -1) {
563
+ return allChildren[index];
564
+ }
565
+ allParents.push(parent2);
566
+ allChildren.push(child);
567
+ }
568
+ for (var i in parent2) {
569
+ var attrs;
570
+ if (proto) {
571
+ attrs = Object.getOwnPropertyDescriptor(proto, i);
572
+ }
573
+ if (attrs && attrs.set == null) {
574
+ continue;
575
+ }
576
+ child[i] = _clone(parent2[i], depth2 - 1);
577
+ }
578
+ return child;
579
+ }
580
+ return _clone(parent, depth);
581
+ }
582
+ clone2.clonePrototype = function clonePrototype(parent) {
583
+ if (parent === null)
584
+ return null;
585
+ var c = function() {};
586
+ c.prototype = parent;
587
+ return new c;
588
+ };
589
+ function __objToStr(o) {
590
+ return Object.prototype.toString.call(o);
591
+ }
592
+ clone2.__objToStr = __objToStr;
593
+ function __isDate(o) {
594
+ return typeof o === "object" && __objToStr(o) === "[object Date]";
595
+ }
596
+ clone2.__isDate = __isDate;
597
+ function __isArray(o) {
598
+ return typeof o === "object" && __objToStr(o) === "[object Array]";
599
+ }
600
+ clone2.__isArray = __isArray;
601
+ function __isRegExp(o) {
602
+ return typeof o === "object" && __objToStr(o) === "[object RegExp]";
603
+ }
604
+ clone2.__isRegExp = __isRegExp;
605
+ function __getRegExpFlags(re) {
606
+ var flags = "";
607
+ if (re.global)
608
+ flags += "g";
609
+ if (re.ignoreCase)
610
+ flags += "i";
611
+ if (re.multiline)
612
+ flags += "m";
613
+ return flags;
614
+ }
615
+ clone2.__getRegExpFlags = __getRegExpFlags;
616
+ return clone2;
617
+ }();
618
+ if (typeof module === "object" && module.exports) {
619
+ module.exports = clone;
620
+ }
621
+ });
622
+
623
+ // node_modules/defaults/index.js
624
+ var require_defaults = __commonJS((exports, module) => {
625
+ var clone = require_clone();
626
+ module.exports = function(options, defaults) {
627
+ options = options || {};
628
+ Object.keys(defaults).forEach(function(key) {
629
+ if (typeof options[key] === "undefined") {
630
+ options[key] = clone(defaults[key]);
631
+ }
632
+ });
633
+ return options;
634
+ };
635
+ });
636
+
637
+ // node_modules/wcwidth/combining.js
638
+ var require_combining = __commonJS((exports, module) => {
639
+ module.exports = [
640
+ [768, 879],
641
+ [1155, 1158],
642
+ [1160, 1161],
643
+ [1425, 1469],
644
+ [1471, 1471],
645
+ [1473, 1474],
646
+ [1476, 1477],
647
+ [1479, 1479],
648
+ [1536, 1539],
649
+ [1552, 1557],
650
+ [1611, 1630],
651
+ [1648, 1648],
652
+ [1750, 1764],
653
+ [1767, 1768],
654
+ [1770, 1773],
655
+ [1807, 1807],
656
+ [1809, 1809],
657
+ [1840, 1866],
658
+ [1958, 1968],
659
+ [2027, 2035],
660
+ [2305, 2306],
661
+ [2364, 2364],
662
+ [2369, 2376],
663
+ [2381, 2381],
664
+ [2385, 2388],
665
+ [2402, 2403],
666
+ [2433, 2433],
667
+ [2492, 2492],
668
+ [2497, 2500],
669
+ [2509, 2509],
670
+ [2530, 2531],
671
+ [2561, 2562],
672
+ [2620, 2620],
673
+ [2625, 2626],
674
+ [2631, 2632],
675
+ [2635, 2637],
676
+ [2672, 2673],
677
+ [2689, 2690],
678
+ [2748, 2748],
679
+ [2753, 2757],
680
+ [2759, 2760],
681
+ [2765, 2765],
682
+ [2786, 2787],
683
+ [2817, 2817],
684
+ [2876, 2876],
685
+ [2879, 2879],
686
+ [2881, 2883],
687
+ [2893, 2893],
688
+ [2902, 2902],
689
+ [2946, 2946],
690
+ [3008, 3008],
691
+ [3021, 3021],
692
+ [3134, 3136],
693
+ [3142, 3144],
694
+ [3146, 3149],
695
+ [3157, 3158],
696
+ [3260, 3260],
697
+ [3263, 3263],
698
+ [3270, 3270],
699
+ [3276, 3277],
700
+ [3298, 3299],
701
+ [3393, 3395],
702
+ [3405, 3405],
703
+ [3530, 3530],
704
+ [3538, 3540],
705
+ [3542, 3542],
706
+ [3633, 3633],
707
+ [3636, 3642],
708
+ [3655, 3662],
709
+ [3761, 3761],
710
+ [3764, 3769],
711
+ [3771, 3772],
712
+ [3784, 3789],
713
+ [3864, 3865],
714
+ [3893, 3893],
715
+ [3895, 3895],
716
+ [3897, 3897],
717
+ [3953, 3966],
718
+ [3968, 3972],
719
+ [3974, 3975],
720
+ [3984, 3991],
721
+ [3993, 4028],
722
+ [4038, 4038],
723
+ [4141, 4144],
724
+ [4146, 4146],
725
+ [4150, 4151],
726
+ [4153, 4153],
727
+ [4184, 4185],
728
+ [4448, 4607],
729
+ [4959, 4959],
730
+ [5906, 5908],
731
+ [5938, 5940],
732
+ [5970, 5971],
733
+ [6002, 6003],
734
+ [6068, 6069],
735
+ [6071, 6077],
736
+ [6086, 6086],
737
+ [6089, 6099],
738
+ [6109, 6109],
739
+ [6155, 6157],
740
+ [6313, 6313],
741
+ [6432, 6434],
742
+ [6439, 6440],
743
+ [6450, 6450],
744
+ [6457, 6459],
745
+ [6679, 6680],
746
+ [6912, 6915],
747
+ [6964, 6964],
748
+ [6966, 6970],
749
+ [6972, 6972],
750
+ [6978, 6978],
751
+ [7019, 7027],
752
+ [7616, 7626],
753
+ [7678, 7679],
754
+ [8203, 8207],
755
+ [8234, 8238],
756
+ [8288, 8291],
757
+ [8298, 8303],
758
+ [8400, 8431],
759
+ [12330, 12335],
760
+ [12441, 12442],
761
+ [43014, 43014],
762
+ [43019, 43019],
763
+ [43045, 43046],
764
+ [64286, 64286],
765
+ [65024, 65039],
766
+ [65056, 65059],
767
+ [65279, 65279],
768
+ [65529, 65531],
769
+ [68097, 68099],
770
+ [68101, 68102],
771
+ [68108, 68111],
772
+ [68152, 68154],
773
+ [68159, 68159],
774
+ [119143, 119145],
775
+ [119155, 119170],
776
+ [119173, 119179],
777
+ [119210, 119213],
778
+ [119362, 119364],
779
+ [917505, 917505],
780
+ [917536, 917631],
781
+ [917760, 917999]
782
+ ];
783
+ });
784
+
785
+ // node_modules/wcwidth/index.js
786
+ var require_wcwidth = __commonJS((exports, module) => {
787
+ var defaults = require_defaults();
788
+ var combining = require_combining();
789
+ var DEFAULTS = {
790
+ nul: 0,
791
+ control: 0
792
+ };
793
+ module.exports = function wcwidth2(str) {
794
+ return wcswidth(str, DEFAULTS);
795
+ };
796
+ module.exports.config = function(opts) {
797
+ opts = defaults(opts || {}, DEFAULTS);
798
+ return function wcwidth2(str) {
799
+ return wcswidth(str, opts);
800
+ };
801
+ };
802
+ function wcswidth(str, opts) {
803
+ if (typeof str !== "string")
804
+ return wcwidth(str, opts);
805
+ var s = 0;
806
+ for (var i = 0;i < str.length; i++) {
807
+ var n = wcwidth(str.charCodeAt(i), opts);
808
+ if (n < 0)
809
+ return -1;
810
+ s += n;
811
+ }
812
+ return s;
813
+ }
814
+ function wcwidth(ucs, opts) {
815
+ if (ucs === 0)
816
+ return opts.nul;
817
+ if (ucs < 32 || ucs >= 127 && ucs < 160)
818
+ return opts.control;
819
+ if (bisearch(ucs))
820
+ return 0;
821
+ return 1 + (ucs >= 4352 && (ucs <= 4447 || ucs == 9001 || ucs == 9002 || ucs >= 11904 && ucs <= 42191 && ucs != 12351 || ucs >= 44032 && ucs <= 55203 || ucs >= 63744 && ucs <= 64255 || ucs >= 65040 && ucs <= 65049 || ucs >= 65072 && ucs <= 65135 || ucs >= 65280 && ucs <= 65376 || ucs >= 65504 && ucs <= 65510 || ucs >= 131072 && ucs <= 196605 || ucs >= 196608 && ucs <= 262141));
822
+ }
823
+ function bisearch(ucs) {
824
+ var min = 0;
825
+ var max = combining.length - 1;
826
+ var mid;
827
+ if (ucs < combining[0][0] || ucs > combining[max][1])
828
+ return false;
829
+ while (max >= min) {
830
+ mid = Math.floor((min + max) / 2);
831
+ if (ucs > combining[mid][1])
832
+ min = mid + 1;
833
+ else if (ucs < combining[mid][0])
834
+ max = mid - 1;
835
+ else
836
+ return true;
837
+ }
838
+ return false;
839
+ }
840
+ });
841
+
505
842
  // src/help.ts
506
843
  var exports_help = {};
507
844
  __export(exports_help, {
@@ -548,7 +885,7 @@ var init_help = __esm(() => {
548
885
  var require_package = __commonJS((exports, module) => {
549
886
  module.exports = {
550
887
  name: "numux",
551
- version: "2.16.2",
888
+ version: "2.17.1",
552
889
  description: "Terminal multiplexer with dependency orchestration",
553
890
  type: "module",
554
891
  license: "MIT",
@@ -593,8 +930,8 @@ var require_package = __commonJS((exports, module) => {
593
930
  "dist/"
594
931
  ],
595
932
  dependencies: {
596
- "@opentui/core": "^0.1.88",
597
- "ghostty-opentui": "^1.4.7"
933
+ "@opentui/core": "^0.4.1",
934
+ "ghostty-opentui": "^1.5.0"
598
935
  },
599
936
  devDependencies: {
600
937
  "@biomejs/biome": "^2.4.4",
@@ -602,9 +939,6 @@ var require_package = __commonJS((exports, module) => {
602
939
  "@commitlint/config-conventional": "^20.4.2",
603
940
  "@types/bun": "^1.3.9",
604
941
  "marked-man": "^2.1.0"
605
- },
606
- patchedDependencies: {
607
- "ghostty-opentui@1.4.7": "patches/ghostty-opentui@1.4.7.patch"
608
942
  }
609
943
  };
610
944
  });
@@ -876,6 +1210,20 @@ var FLAGS = [
876
1210
  valueName: "<path>",
877
1211
  completionHint: "directory"
878
1212
  },
1213
+ {
1214
+ type: "value",
1215
+ long: "--theme",
1216
+ key: "theme",
1217
+ description: "TUI theme (auto detects terminal background)",
1218
+ valueName: "<light|dark|auto>",
1219
+ completionHint: "none",
1220
+ parse(raw, flag) {
1221
+ if (raw !== "light" && raw !== "dark" && raw !== "auto") {
1222
+ throw new Error(`${flag} must be light, dark, or auto. Got "${raw}"`);
1223
+ }
1224
+ return raw;
1225
+ }
1226
+ },
879
1227
  {
880
1228
  type: "boolean",
881
1229
  long: "--debug",
@@ -1704,10 +2052,11 @@ function resolveColor(color) {
1704
2052
  return color[0];
1705
2053
  return;
1706
2054
  }
1707
- function buildProcessColorMap(names, config) {
2055
+ function buildProcessColorMap(names, config, palette = DEFAULT_PALETTE) {
1708
2056
  const map = new Map;
1709
2057
  if ("NO_COLOR" in process.env)
1710
2058
  return map;
2059
+ const ansiPalette = palette === DEFAULT_PALETTE ? DEFAULT_ANSI_COLORS : palette.map(hexToAnsi);
1711
2060
  let paletteIndex = 0;
1712
2061
  for (const name of names) {
1713
2062
  const explicit = resolveColor(config.processes[name]?.color);
@@ -1716,15 +2065,15 @@ function buildProcessColorMap(names, config) {
1716
2065
  if (hex)
1717
2066
  map.set(name, hexToAnsi(hex));
1718
2067
  else
1719
- map.set(name, DEFAULT_ANSI_COLORS[paletteIndex++ % DEFAULT_ANSI_COLORS.length]);
2068
+ map.set(name, ansiPalette[paletteIndex++ % ansiPalette.length]);
1720
2069
  } else {
1721
- map.set(name, DEFAULT_ANSI_COLORS[paletteIndex % DEFAULT_ANSI_COLORS.length]);
2070
+ map.set(name, ansiPalette[paletteIndex % ansiPalette.length]);
1722
2071
  paletteIndex++;
1723
2072
  }
1724
2073
  }
1725
2074
  return map;
1726
2075
  }
1727
- function buildProcessHexColorMap(names, config) {
2076
+ function buildProcessHexColorMap(names, config, palette = DEFAULT_PALETTE) {
1728
2077
  const map = new Map;
1729
2078
  if ("NO_COLOR" in process.env)
1730
2079
  return map;
@@ -1736,9 +2085,9 @@ function buildProcessHexColorMap(names, config) {
1736
2085
  if (hex)
1737
2086
  map.set(name, hex);
1738
2087
  else
1739
- map.set(name, DEFAULT_PALETTE[paletteIndex++ % DEFAULT_PALETTE.length]);
2088
+ map.set(name, palette[paletteIndex++ % palette.length]);
1740
2089
  } else {
1741
- map.set(name, DEFAULT_PALETTE[paletteIndex % DEFAULT_PALETTE.length]);
2090
+ map.set(name, palette[paletteIndex % palette.length]);
1742
2091
  paletteIndex++;
1743
2092
  }
1744
2093
  }
@@ -1783,6 +2132,7 @@ function validateConfig(raw, _warnings) {
1783
2132
  const killOthersOnFail = config.killOthersOnFail === true ? true : undefined;
1784
2133
  const noWatch = config.noWatch === true ? true : undefined;
1785
2134
  const logDir = typeof config.logDir === "string" && config.logDir.trim() ? config.logDir.trim() : undefined;
2135
+ const theme = validateTheme(config.theme);
1786
2136
  const validated = {};
1787
2137
  for (const name of names) {
1788
2138
  let proc = processes[name];
@@ -1881,9 +2231,19 @@ function validateConfig(raw, _warnings) {
1881
2231
  ...killOthersOnFail ? { killOthersOnFail } : {},
1882
2232
  ...noWatch ? { noWatch } : {},
1883
2233
  ...logDir ? { logDir } : {},
2234
+ ...theme ? { theme } : {},
1884
2235
  processes: validated
1885
2236
  };
1886
2237
  }
2238
+ var VALID_THEME_VALUES = new Set(["light", "dark", "auto"]);
2239
+ function validateTheme(value) {
2240
+ if (value === undefined)
2241
+ return;
2242
+ if (typeof value !== "string" || !VALID_THEME_VALUES.has(value)) {
2243
+ throw new Error(`theme must be one of: light, dark, auto. Got "${String(value)}"`);
2244
+ }
2245
+ return value;
2246
+ }
1887
2247
  function validateStringOrStringArray(value) {
1888
2248
  if (typeof value === "string")
1889
2249
  return value;
@@ -2949,6 +3309,178 @@ function setupShutdownHandlers(app, logWriter) {
2949
3309
  });
2950
3310
  }
2951
3311
 
3312
+ // src/utils/theme.ts
3313
+ var DARK_THEME = {
3314
+ mode: "dark",
3315
+ statusBarBg: "#1a1a1a",
3316
+ statusBarText: "#cccccc",
3317
+ helpBackdropBg: "#000000",
3318
+ helpBoxBg: "#1a1a2e",
3319
+ helpBorder: "#444444",
3320
+ helpText: "#cccccc",
3321
+ sidebarBg: "#1a1a1a",
3322
+ sidebarBorder: "#444444",
3323
+ tabSelectedBg: "#334455",
3324
+ tabSelectedText: "#ffffff",
3325
+ tabText: "#888888",
3326
+ tabDescriptionText: "#888888",
3327
+ tabSelectedDescriptionText: "#cccccc",
3328
+ scrollTrackBg: "#252527",
3329
+ scrollThumbBg: "#9a9ea3",
3330
+ searchCurrentBg: "#b58900",
3331
+ searchMatchBg: "#073642",
3332
+ palette: ["#00cccc", "#cccc00", "#cc00cc", "#5577ff", "#00cc00", "#ff5555", "#ffa500", "#cc88ff"],
3333
+ status: {
3334
+ ready: "#00cc00",
3335
+ failed: "#ff5555",
3336
+ stopped: "#888888",
3337
+ finished: "#66aa66",
3338
+ skipped: "#888888"
3339
+ },
3340
+ inputWaiting: "#ffaa00",
3341
+ errorIndicator: "#ff5555",
3342
+ searchMatchTab: "#b58900",
3343
+ iconDefault: "#888888"
3344
+ };
3345
+ var LIGHT_THEME = {
3346
+ mode: "light",
3347
+ statusBarBg: "#e8e8e8",
3348
+ statusBarText: "#000000",
3349
+ helpBackdropBg: "#ffffff",
3350
+ helpBoxBg: "#f5f5f5",
3351
+ helpBorder: "#aaaaaa",
3352
+ helpText: "#1a1a1a",
3353
+ sidebarBg: "#f0f0f0",
3354
+ sidebarBorder: "#aaaaaa",
3355
+ tabSelectedBg: "#7a9bbf",
3356
+ tabSelectedText: "#ffffff",
3357
+ tabText: "#444444",
3358
+ tabDescriptionText: "#666666",
3359
+ tabSelectedDescriptionText: "#e8e8e8",
3360
+ scrollTrackBg: "#d0d0d0",
3361
+ scrollThumbBg: "#888888",
3362
+ searchCurrentBg: "#ffaa33",
3363
+ searchMatchBg: "#d0e4b8",
3364
+ palette: ["#008888", "#886600", "#880088", "#0033aa", "#006600", "#aa0000", "#cc5500", "#6622aa"],
3365
+ status: {
3366
+ ready: "#006600",
3367
+ failed: "#aa0000",
3368
+ stopped: "#666666",
3369
+ finished: "#2a7a2a",
3370
+ skipped: "#666666"
3371
+ },
3372
+ inputWaiting: "#cc7a00",
3373
+ errorIndicator: "#aa0000",
3374
+ searchMatchTab: "#cc7a00",
3375
+ iconDefault: "#666666"
3376
+ };
3377
+ function themeFor(mode) {
3378
+ return mode === "light" ? LIGHT_THEME : DARK_THEME;
3379
+ }
3380
+ function relativeLuminance(r, g, b) {
3381
+ const norm = (c) => {
3382
+ const s = c / 255;
3383
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
3384
+ };
3385
+ return 0.2126 * norm(r) + 0.7152 * norm(g) + 0.0722 * norm(b);
3386
+ }
3387
+ function isLightRgb(r, g, b) {
3388
+ return relativeLuminance(r, g, b) > 0.5;
3389
+ }
3390
+ function parseOSC11Response(data) {
3391
+ const match = data.match(/rgb:([0-9a-f]+)\/([0-9a-f]+)\/([0-9a-f]+)/i);
3392
+ if (!match)
3393
+ return null;
3394
+ const scale = (hex) => {
3395
+ if (hex.length === 0 || hex.length > 4)
3396
+ return Number.NaN;
3397
+ const val = Number.parseInt(hex, 16);
3398
+ if (Number.isNaN(val))
3399
+ return Number.NaN;
3400
+ const max = 16 ** hex.length - 1;
3401
+ return Math.round(val / max * 255);
3402
+ };
3403
+ const r = scale(match[1]);
3404
+ const g = scale(match[2]);
3405
+ const b = scale(match[3]);
3406
+ if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b))
3407
+ return null;
3408
+ return { r, g, b };
3409
+ }
3410
+ function parseColorFgBg(value) {
3411
+ if (!value)
3412
+ return null;
3413
+ const parts = value.split(";");
3414
+ if (parts.length < 2)
3415
+ return null;
3416
+ const bgRaw = parts[parts.length - 1].trim();
3417
+ const bg = Number.parseInt(bgRaw, 10);
3418
+ if (Number.isNaN(bg))
3419
+ return null;
3420
+ return bg >= 7 && bg <= 15 ? "light" : "dark";
3421
+ }
3422
+ function queryOSC11(timeoutMs = 100) {
3423
+ const stdin = process.stdin;
3424
+ const stdout = process.stdout;
3425
+ if (!(stdin.isTTY && stdout.isTTY))
3426
+ return Promise.resolve(null);
3427
+ return new Promise((resolve8) => {
3428
+ let settled = false;
3429
+ let buf = "";
3430
+ let timer = null;
3431
+ const wasRaw = stdin.isRaw;
3432
+ const finish = (result) => {
3433
+ if (settled)
3434
+ return;
3435
+ settled = true;
3436
+ if (timer)
3437
+ clearTimeout(timer);
3438
+ stdin.off("data", onData);
3439
+ try {
3440
+ if (!wasRaw)
3441
+ stdin.setRawMode(false);
3442
+ } catch {}
3443
+ stdin.pause();
3444
+ resolve8(result);
3445
+ };
3446
+ const onData = (chunk) => {
3447
+ buf += chunk.toString("utf8");
3448
+ const match = buf.match(/\x1b\]1[01];rgb:[0-9a-f/]+(?:\x07|\x1b\\)/i);
3449
+ if (!match)
3450
+ return;
3451
+ const parsed = parseOSC11Response(match[0]);
3452
+ if (!parsed) {
3453
+ finish(null);
3454
+ return;
3455
+ }
3456
+ finish(isLightRgb(parsed.r, parsed.g, parsed.b) ? "light" : "dark");
3457
+ };
3458
+ try {
3459
+ stdin.setRawMode(true);
3460
+ stdin.resume();
3461
+ stdin.on("data", onData);
3462
+ timer = setTimeout(() => finish(null), timeoutMs);
3463
+ stdout.write("\x1B]11;?\x1B\\");
3464
+ } catch {
3465
+ finish(null);
3466
+ }
3467
+ });
3468
+ }
3469
+ async function detectThemeMode(timeoutMs = 100) {
3470
+ const osc = await queryOSC11(timeoutMs);
3471
+ if (osc)
3472
+ return osc;
3473
+ return parseColorFgBg(process.env.COLORFGBG);
3474
+ }
3475
+ async function resolveTheme(pref = "auto") {
3476
+ if (pref === "light")
3477
+ return LIGHT_THEME;
3478
+ if (pref === "dark")
3479
+ return DARK_THEME;
3480
+ const detected = await detectThemeMode();
3481
+ return themeFor(detected ?? "dark");
3482
+ }
3483
+
2952
3484
  // src/ui/help-overlay.ts
2953
3485
  import { BoxRenderable, TextRenderable } from "@opentui/core";
2954
3486
 
@@ -3002,7 +3534,7 @@ var STATUS_BAR_TEXT = STATUS_HINTS_COMPACT.map((h) => {
3002
3534
  class HelpOverlay {
3003
3535
  renderable;
3004
3536
  textRenderable;
3005
- constructor(renderer) {
3537
+ constructor(renderer, theme = DARK_THEME) {
3006
3538
  this.renderable = new BoxRenderable(renderer, {
3007
3539
  id: "help-overlay",
3008
3540
  position: "absolute",
@@ -3018,7 +3550,7 @@ class HelpOverlay {
3018
3550
  position: "absolute",
3019
3551
  width: "100%",
3020
3552
  height: "100%",
3021
- backgroundColor: "#000000",
3553
+ backgroundColor: theme.helpBackdropBg,
3022
3554
  opacity: 0.7
3023
3555
  });
3024
3556
  const box = new BoxRenderable(renderer, {
@@ -3026,9 +3558,9 @@ class HelpOverlay {
3026
3558
  flexDirection: "column",
3027
3559
  padding: 1,
3028
3560
  paddingX: 5,
3029
- backgroundColor: "#1a1a2e",
3561
+ backgroundColor: theme.helpBoxBg,
3030
3562
  border: true,
3031
- borderColor: "#444",
3563
+ borderColor: theme.helpBorder,
3032
3564
  zIndex: 101
3033
3565
  });
3034
3566
  const lines = [
@@ -3045,7 +3577,7 @@ class HelpOverlay {
3045
3577
  id: "help-text",
3046
3578
  content: lines.join(`
3047
3579
  `),
3048
- fg: "#cccccc"
3580
+ fg: theme.helpText
3049
3581
  });
3050
3582
  box.add(this.textRenderable);
3051
3583
  this.renderable.add(backdrop);
@@ -3085,6 +3617,7 @@ function resolveTimestampFormat(timestamps) {
3085
3617
  }
3086
3618
 
3087
3619
  // node_modules/ghostty-opentui/src/terminal-buffer.ts
3620
+ var import_wcwidth = __toESM(require_wcwidth(), 1);
3088
3621
  import {
3089
3622
  TextBufferRenderable,
3090
3623
  StyledText,
@@ -3093,6 +3626,22 @@ import {
3093
3626
  import { ptyToJson, PersistentTerminal, hasPersistentTerminalSupport, StyleFlags } from "ghostty-opentui";
3094
3627
  var DEFAULT_FG = RGBA.fromHex("#d4d4d4");
3095
3628
  var DEFAULT_BG = RGBA.fromHex("#1e1e1e");
3629
+ function getChunkCellWidth(chunk) {
3630
+ return "cellWidth" in chunk && typeof chunk.cellWidth === "number" ? chunk.cellWidth : import_wcwidth.default(chunk.text);
3631
+ }
3632
+ function cellColToStringIndex(text, cellCol) {
3633
+ if (cellCol <= 0)
3634
+ return 0;
3635
+ let col = 0;
3636
+ let strIdx = 0;
3637
+ for (const ch of text) {
3638
+ if (col >= cellCol)
3639
+ break;
3640
+ col += import_wcwidth.default(ch);
3641
+ strIdx += ch.length;
3642
+ }
3643
+ return strIdx;
3644
+ }
3096
3645
  var TextAttributes = {
3097
3646
  BOLD: 1 << 0,
3098
3647
  DIM: 1 << 1,
@@ -3103,8 +3652,11 @@ var TextAttributes = {
3103
3652
  HIDDEN: 1 << 6,
3104
3653
  STRIKETHROUGH: 1 << 7
3105
3654
  };
3655
+ function getLineStarts(lineInfo) {
3656
+ return lineInfo.lineStarts ?? lineInfo.lineStartCols ?? [];
3657
+ }
3106
3658
  function convertSpanToChunk(span) {
3107
- const { text, fg, bg, flags } = span;
3659
+ const { text, fg, bg, flags, width } = span;
3108
3660
  let fgColor = fg ? RGBA.fromHex(fg) : DEFAULT_FG;
3109
3661
  let bgColor = bg ? RGBA.fromHex(bg) : undefined;
3110
3662
  if (flags & StyleFlags.INVERSE) {
@@ -3123,7 +3675,7 @@ function convertSpanToChunk(span) {
3123
3675
  attributes |= TextAttributes.STRIKETHROUGH;
3124
3676
  if (flags & StyleFlags.FAINT)
3125
3677
  attributes |= TextAttributes.DIM;
3126
- return { __isChunk: true, text, fg: fgColor, bg: bgColor, attributes };
3678
+ return { __isChunk: true, text, fg: fgColor, bg: bgColor, attributes, cellWidth: width };
3127
3679
  }
3128
3680
  function applyHighlightsToLine(chunks, highlights) {
3129
3681
  if (highlights.length === 0)
@@ -3131,63 +3683,56 @@ function applyHighlightsToLine(chunks, highlights) {
3131
3683
  const result = [];
3132
3684
  let col = 0;
3133
3685
  for (const chunk of chunks) {
3686
+ const w = getChunkCellWidth(chunk);
3134
3687
  const chunkStart = col;
3135
- const chunkEnd = col + chunk.text.length;
3136
- const overlappingHighlights = highlights.filter((hl) => hl.start < chunkEnd && hl.end > chunkStart);
3137
- if (overlappingHighlights.length === 0) {
3688
+ const chunkEnd = col + w;
3689
+ const overlapping = highlights.filter((hl) => hl.start < chunkEnd && hl.end > chunkStart).sort((a, b) => a.start - b.start);
3690
+ if (overlapping.length === 0) {
3138
3691
  result.push(chunk);
3139
3692
  col = chunkEnd;
3140
3693
  continue;
3141
3694
  }
3142
- let pos = 0;
3143
- const text = chunk.text;
3144
- const sortedHighlights = [...overlappingHighlights].sort((a, b) => a.start - b.start);
3145
- for (const hl of sortedHighlights) {
3146
- const hlStartInChunk = Math.max(0, hl.start - chunkStart);
3147
- const hlEndInChunk = Math.min(text.length, hl.end - chunkStart);
3148
- if (pos < hlStartInChunk) {
3149
- result.push({
3150
- __isChunk: true,
3151
- text: text.slice(pos, hlStartInChunk),
3152
- fg: chunk.fg,
3153
- bg: chunk.bg,
3154
- attributes: chunk.attributes
3155
- });
3695
+ let cellPos = 0;
3696
+ for (const hl of overlapping) {
3697
+ const hlStartLocal = Math.max(0, hl.start - chunkStart);
3698
+ const hlEndLocal = Math.min(w, hl.end - chunkStart);
3699
+ if (cellPos < hlStartLocal) {
3700
+ const startStr = cellColToStringIndex(chunk.text, cellPos);
3701
+ const endStr = cellColToStringIndex(chunk.text, hlStartLocal);
3702
+ result.push({ ...chunk, text: chunk.text.slice(startStr, endStr), cellWidth: hlStartLocal - cellPos });
3156
3703
  }
3157
- if (hlStartInChunk < hlEndInChunk) {
3158
- const highlightedText = text.slice(hlStartInChunk, hlEndInChunk);
3159
- const displayText = hl.replaceWithX ? "x".repeat(highlightedText.length) : highlightedText;
3704
+ if (hlStartLocal < hlEndLocal) {
3705
+ const startStr = cellColToStringIndex(chunk.text, hlStartLocal);
3706
+ const endStr = cellColToStringIndex(chunk.text, hlEndLocal);
3707
+ const hlText = chunk.text.slice(startStr, endStr);
3708
+ const cellWidth = hlEndLocal - hlStartLocal;
3160
3709
  result.push({
3161
- __isChunk: true,
3162
- text: displayText,
3163
- fg: chunk.fg,
3710
+ ...chunk,
3711
+ text: hl.replaceWithX ? "x".repeat(cellWidth) : hlText,
3164
3712
  bg: RGBA.fromHex(hl.backgroundColor),
3165
- attributes: chunk.attributes
3713
+ cellWidth
3166
3714
  });
3167
3715
  }
3168
- pos = hlEndInChunk;
3169
- }
3170
- if (pos < text.length) {
3171
- result.push({
3172
- __isChunk: true,
3173
- text: text.slice(pos),
3174
- fg: chunk.fg,
3175
- bg: chunk.bg,
3176
- attributes: chunk.attributes
3177
- });
3716
+ cellPos = hlEndLocal;
3717
+ }
3718
+ if (cellPos < w) {
3719
+ const startStr = cellColToStringIndex(chunk.text, cellPos);
3720
+ result.push({ ...chunk, text: chunk.text.slice(startStr), cellWidth: w - cellPos });
3178
3721
  }
3179
3722
  col = chunkEnd;
3180
3723
  }
3181
3724
  return result;
3182
3725
  }
3183
3726
  function makeCursorChunk(char, style, original) {
3727
+ const cellWidth = Math.max(1, import_wcwidth.default(char));
3184
3728
  if (style === "block") {
3185
3729
  return {
3186
3730
  __isChunk: true,
3187
3731
  text: char,
3188
3732
  fg: original?.bg || RGBA.fromHex("#1e1e1e"),
3189
3733
  bg: original?.fg || DEFAULT_FG,
3190
- attributes: original?.attributes ?? 0
3734
+ attributes: original?.attributes ?? 0,
3735
+ cellWidth
3191
3736
  };
3192
3737
  }
3193
3738
  return {
@@ -3195,30 +3740,35 @@ function makeCursorChunk(char, style, original) {
3195
3740
  text: char,
3196
3741
  fg: original?.fg || DEFAULT_FG,
3197
3742
  bg: original?.bg,
3198
- attributes: (original?.attributes ?? 0) | TextAttributes.UNDERLINE
3743
+ attributes: (original?.attributes ?? 0) | TextAttributes.UNDERLINE,
3744
+ cellWidth
3199
3745
  };
3200
3746
  }
3201
3747
  function applyCursorToLine(chunks, cursorX, cursorStyle) {
3202
- const totalLen = chunks.reduce((sum, c) => sum + c.text.length, 0);
3748
+ const totalLen = chunks.reduce((sum, chunk) => sum + getChunkCellWidth(chunk), 0);
3203
3749
  if (cursorX >= totalLen) {
3204
3750
  const gap = cursorX - totalLen;
3205
3751
  if (gap > 0) {
3206
- return [...chunks, { __isChunk: true, text: " ".repeat(gap), attributes: 0 }, makeCursorChunk(" ", cursorStyle)];
3752
+ return [...chunks, { __isChunk: true, text: " ".repeat(gap), attributes: 0, cellWidth: gap }, makeCursorChunk(" ", cursorStyle)];
3207
3753
  }
3208
3754
  return [...chunks, makeCursorChunk(" ", cursorStyle)];
3209
3755
  }
3210
3756
  const result = [];
3211
3757
  let col = 0;
3212
3758
  for (const chunk of chunks) {
3213
- const chunkEnd = col + chunk.text.length;
3759
+ const w = getChunkCellWidth(chunk);
3760
+ const chunkEnd = col + w;
3214
3761
  if (cursorX >= col && cursorX < chunkEnd) {
3215
- const pos = cursorX - col;
3216
- if (pos > 0) {
3217
- result.push({ ...chunk, text: chunk.text.slice(0, pos) });
3762
+ const localCol = cursorX - col;
3763
+ const strIdx = cellColToStringIndex(chunk.text, localCol);
3764
+ const cursorChar = String.fromCodePoint(chunk.text.codePointAt(strIdx));
3765
+ const strEnd = strIdx + cursorChar.length;
3766
+ if (strIdx > 0) {
3767
+ result.push({ ...chunk, text: chunk.text.slice(0, strIdx) });
3218
3768
  }
3219
- result.push(makeCursorChunk(chunk.text[pos], cursorStyle, chunk));
3220
- if (pos + 1 < chunk.text.length) {
3221
- result.push({ ...chunk, text: chunk.text.slice(pos + 1) });
3769
+ result.push(makeCursorChunk(cursorChar, cursorStyle, chunk));
3770
+ if (strEnd < chunk.text.length) {
3771
+ result.push({ ...chunk, text: chunk.text.slice(strEnd) });
3222
3772
  }
3223
3773
  } else {
3224
3774
  result.push(chunk);
@@ -3273,7 +3823,13 @@ class GhosttyTerminalRenderable extends TextBufferRenderable {
3273
3823
  _ansiDirty = false;
3274
3824
  _lineCount = 0;
3275
3825
  _showCursor = false;
3276
- _cursorStyle = "block";
3826
+ _cursorStyle = undefined;
3827
+ _renderCursor = {
3828
+ x: 0,
3829
+ y: 0,
3830
+ visible: false,
3831
+ style: "default"
3832
+ };
3277
3833
  _persistent = false;
3278
3834
  _persistentTerminal = null;
3279
3835
  constructor(ctx, options) {
@@ -3290,7 +3846,10 @@ class GhosttyTerminalRenderable extends TextBufferRenderable {
3290
3846
  this._highlights = options.highlights;
3291
3847
  this._persistent = options.persistent ?? false;
3292
3848
  this._showCursor = options.showCursor ?? false;
3293
- this._cursorStyle = options.cursorStyle ?? "block";
3849
+ this._cursorStyle = options.cursorStyle;
3850
+ if (options.focusable) {
3851
+ this._focusable = true;
3852
+ }
3294
3853
  if (this._persistent && hasPersistentTerminalSupport()) {
3295
3854
  this._persistentTerminal = new PersistentTerminal({
3296
3855
  cols: this._cols,
@@ -3434,6 +3993,35 @@ class GhosttyTerminalRenderable extends TextBufferRenderable {
3434
3993
  }
3435
3994
  super.destroy();
3436
3995
  }
3996
+ onRemove() {
3997
+ if (this._focused || !this._focusable) {
3998
+ this.hideTerminalCursor();
3999
+ }
4000
+ }
4001
+ hideTerminalCursor() {
4002
+ this.ctx.setCursorPosition(0, 0, false);
4003
+ }
4004
+ renderTerminalCursor() {
4005
+ if (!this._renderCursor.visible || this._focusable && !this._focused) {
4006
+ this.hideTerminalCursor();
4007
+ return;
4008
+ }
4009
+ const style = this._cursorStyle ?? this._renderCursor.style;
4010
+ this.ctx.setCursorStyle({
4011
+ style,
4012
+ blinking: false
4013
+ });
4014
+ this.ctx.setCursorPosition(this.x + this._renderCursor.x + 1, this.y + this._renderCursor.y + 1, true);
4015
+ }
4016
+ focus() {
4017
+ super.focus();
4018
+ this.requestRender();
4019
+ }
4020
+ blur() {
4021
+ super.blur();
4022
+ this.hideTerminalCursor();
4023
+ this.requestRender();
4024
+ }
3437
4025
  renderSelf(buffer) {
3438
4026
  if (this._ansiDirty) {
3439
4027
  let data;
@@ -3457,26 +4045,29 @@ class GhosttyTerminalRenderable extends TextBufferRenderable {
3457
4045
  data.lines.pop();
3458
4046
  }
3459
4047
  }
3460
- const cursor = this._showCursor ? {
3461
- x: data.cursor[0],
3462
- y: Math.max(0, data.totalLines - data.rows + data.cursor[1] - data.offset),
3463
- style: this._cursorStyle
3464
- } : undefined;
3465
- const styledText = terminalDataToStyledText(data, this._highlights, cursor);
3466
- this.textBuffer.setStyledText(styledText);
4048
+ this.textBuffer.setStyledText(terminalDataToStyledText(data, this._highlights));
3467
4049
  this.updateTextInfo();
3468
- const lineInfo = this.textBufferView.logicalLineInfo;
3469
- if (lineInfo) {
3470
- this._lineCount = lineInfo.lineStartCols?.length ?? lineInfo.lineStarts?.length ?? this._lineCount;
4050
+ if (this._showCursor) {
4051
+ const cursorY = Math.max(0, data.totalLines - data.rows + data.cursor[1] - data.offset);
4052
+ this._renderCursor.x = data.cursor[0];
4053
+ this._renderCursor.y = cursorY;
4054
+ this._renderCursor.visible = data.cursorVisible && cursorY < data.lines.length;
4055
+ const ts = data.cursorStyle;
4056
+ this._renderCursor.style = ts === "default" ? "default" : ts === "bar" ? "line" : ts === "underline" ? "underline" : "block";
4057
+ } else {
4058
+ this._renderCursor.visible = false;
3471
4059
  }
4060
+ const lineInfo = this.textBufferView.logicalLineInfo;
4061
+ this._lineCount = getLineStarts(lineInfo).length;
3472
4062
  this._ansiDirty = false;
3473
4063
  }
3474
4064
  super.renderSelf(buffer);
4065
+ this.renderTerminalCursor();
3475
4066
  }
3476
4067
  getScrollPositionForLine(lineNumber) {
3477
4068
  const clampedLine = Math.max(0, Math.min(lineNumber, this._lineCount - 1));
3478
4069
  const lineInfo = this.textBufferView.logicalLineInfo;
3479
- const lineStarts = lineInfo?.lineStartCols ?? lineInfo?.lineStarts;
4070
+ const lineStarts = getLineStarts(lineInfo);
3480
4071
  let lineYOffset = clampedLine;
3481
4072
  if (lineStarts && lineStarts.length > clampedLine) {
3482
4073
  lineYOffset = lineStarts[clampedLine];
@@ -3484,13 +4075,6 @@ class GhosttyTerminalRenderable extends TextBufferRenderable {
3484
4075
  return this.y + lineYOffset;
3485
4076
  }
3486
4077
  }
3487
- var EMPTY_LINE_INFO = { lineStarts: [], lineStartCols: [], lineWidthColsMax: 0, lineSources: [] };
3488
- Object.defineProperty(GhosttyTerminalRenderable.prototype, "lineInfo", {
3489
- get() {
3490
- return this.textBufferView?.logicalLineInfo ?? EMPTY_LINE_INFO;
3491
- },
3492
- configurable: true
3493
- });
3494
4078
 
3495
4079
  // src/ui/tailing-terminal.ts
3496
4080
  class TailingTerminal extends GhosttyTerminalRenderable {
@@ -3576,8 +4160,10 @@ class Pane {
3576
4160
  _onScroll = null;
3577
4161
  _onCopy = null;
3578
4162
  _onLinkClick = null;
3579
- constructor(renderer, name, cols, rows, interactive = false) {
4163
+ theme;
4164
+ constructor(renderer, name, cols, rows, interactive = false, theme = DARK_THEME) {
3580
4165
  this.renderer = renderer;
4166
+ this.theme = theme;
3581
4167
  this.scrollBox = new ScrollBoxRenderable(renderer, {
3582
4168
  id: `pane-${name}`,
3583
4169
  flexGrow: 1,
@@ -3585,7 +4171,13 @@ class Pane {
3585
4171
  stickyScroll: true,
3586
4172
  stickyStart: "bottom",
3587
4173
  visible: false,
3588
- onMouseScroll: () => this._onScroll?.()
4174
+ onMouseScroll: () => this._onScroll?.(),
4175
+ scrollbarOptions: {
4176
+ trackOptions: {
4177
+ backgroundColor: theme.scrollTrackBg,
4178
+ foregroundColor: theme.scrollThumbBg
4179
+ }
4180
+ }
3589
4181
  });
3590
4182
  this.terminal = new TailingTerminal(renderer, {
3591
4183
  id: `term-${name}`,
@@ -3721,7 +4313,7 @@ class Pane {
3721
4313
  line: m.line,
3722
4314
  start: m.start,
3723
4315
  end: m.end,
3724
- backgroundColor: i === currentIndex ? "#b58900" : "#073642"
4316
+ backgroundColor: i === currentIndex ? this.theme.searchCurrentBg : this.theme.searchMatchBg
3725
4317
  });
3726
4318
  }
3727
4319
  this.terminal.highlights = regions;
@@ -4026,11 +4618,11 @@ class StatusBar {
4026
4618
  _tempMessage = null;
4027
4619
  _tempTimer = null;
4028
4620
  _inputMode = false;
4029
- constructor(renderer) {
4621
+ constructor(renderer, theme = DARK_THEME) {
4030
4622
  this.renderable = new BoxRenderable2(renderer, {
4031
4623
  id: "status-bar",
4032
4624
  width: "100%",
4033
- backgroundColor: "#1a1a1a",
4625
+ backgroundColor: theme.statusBarBg,
4034
4626
  paddingX: 1,
4035
4627
  minHeight: 1
4036
4628
  });
@@ -4038,6 +4630,7 @@ class StatusBar {
4038
4630
  id: "status-bar-text",
4039
4631
  width: "100%",
4040
4632
  wrapMode: "word",
4633
+ fg: theme.statusBarText,
4041
4634
  content: this.buildContent()
4042
4635
  });
4043
4636
  this.text.selectable = false;
@@ -4123,13 +4716,15 @@ var STATUS_ICONS = {
4123
4716
  failed: "\u2716",
4124
4717
  skipped: "\u2298"
4125
4718
  };
4126
- var STATUS_ICON_HEX = {
4127
- ready: "#00cc00",
4128
- finished: "#66aa66",
4129
- failed: "#ff5555",
4130
- stopped: "#888888",
4131
- skipped: "#888888"
4132
- };
4719
+ function getStatusIconHex(theme) {
4720
+ return {
4721
+ ready: theme.status.ready,
4722
+ finished: theme.status.finished,
4723
+ failed: theme.status.failed,
4724
+ stopped: theme.status.stopped,
4725
+ skipped: theme.status.skipped
4726
+ };
4727
+ }
4133
4728
  var TERMINAL_STATUSES = new Set(["finished", "stopped", "failed", "skipped"]);
4134
4729
  function getDisplayOrder(originalNames, statuses) {
4135
4730
  const active = originalNames.filter((n) => !TERMINAL_STATUSES.has(statuses.get(n)));
@@ -4149,16 +4744,17 @@ function formatDescription(status, exitCode, restartCount) {
4149
4744
  }
4150
4745
  return desc;
4151
4746
  }
4152
- function resolveOptionColors(names, statuses, processColors, inputWaiting, erroredProcesses, searchMatchProcesses) {
4747
+ function resolveOptionColors(names, statuses, processColors, inputWaiting, erroredProcesses, theme, searchMatchProcesses) {
4748
+ const statusIconHex = getStatusIconHex(theme);
4153
4749
  return names.map((name) => {
4154
4750
  const status = statuses.get(name);
4155
4751
  const waiting = inputWaiting.has(name);
4156
4752
  const errored = erroredProcesses.has(name);
4157
4753
  const hasSearchMatch = searchMatchProcesses?.has(name);
4158
- const statusHex = hasSearchMatch ? "#b58900" : waiting ? "#ffaa00" : errored ? "#ff5555" : STATUS_ICON_HEX[status];
4754
+ const statusHex = hasSearchMatch ? theme.searchMatchTab : waiting ? theme.inputWaiting : errored ? theme.errorIndicator : statusIconHex[status];
4159
4755
  const processHex = processColors.get(name);
4160
4756
  return {
4161
- iconHex: statusHex ?? processHex ?? "#888888",
4757
+ iconHex: statusHex ?? processHex ?? theme.iconDefault,
4162
4758
  nameHex: processHex ?? null
4163
4759
  };
4164
4760
  });
@@ -4242,13 +4838,15 @@ class TabBar {
4242
4838
  inputWaiting = new Set;
4243
4839
  erroredProcesses = new Set;
4244
4840
  searchMatchCounts = new Map;
4245
- constructor(renderer, names, colors, reorderByStatus = false) {
4841
+ theme;
4842
+ constructor(renderer, names, colors, theme = DARK_THEME, reorderByStatus = false) {
4246
4843
  this.originalNames = names;
4247
4844
  this.names = [...names];
4248
4845
  this.reorderByStatus = reorderByStatus;
4249
4846
  this.statuses = new Map(names.map((n) => [n, "pending"]));
4250
4847
  this.baseDescriptions = new Map(names.map((n) => [n, "pending"]));
4251
4848
  this.processColors = colors ?? new Map;
4849
+ this.theme = theme;
4252
4850
  this.renderable = new ColoredSelectRenderable(renderer, {
4253
4851
  id: "tab-bar",
4254
4852
  width: "100%",
@@ -4257,9 +4855,13 @@ class TabBar {
4257
4855
  name: formatTab(n, "pending"),
4258
4856
  description: "pending"
4259
4857
  })),
4260
- selectedBackgroundColor: "#334455",
4261
- selectedTextColor: "#fff",
4262
- textColor: "#888",
4858
+ backgroundColor: theme.sidebarBg,
4859
+ focusedBackgroundColor: theme.sidebarBg,
4860
+ selectedBackgroundColor: theme.tabSelectedBg,
4861
+ selectedTextColor: theme.tabSelectedText,
4862
+ textColor: theme.tabText,
4863
+ descriptionColor: theme.tabDescriptionText,
4864
+ selectedDescriptionColor: theme.tabSelectedDescriptionText,
4263
4865
  showDescription: true,
4264
4866
  wrapSelection: true
4265
4867
  });
@@ -4343,7 +4945,7 @@ class TabBar {
4343
4945
  }
4344
4946
  updateOptionColors() {
4345
4947
  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);
4948
+ const resolved = resolveOptionColors(this.names, this.statuses, this.processColors, this.inputWaiting, this.erroredProcesses, this.theme, searchProcesses);
4347
4949
  const colors = resolved.map((c) => ({
4348
4950
  icon: parseColor(c.iconHex),
4349
4951
  name: c.nameHex ? parseColor(c.nameHex) : null
@@ -4376,6 +4978,7 @@ class App {
4376
4978
  sidebarWidth = 20;
4377
4979
  config;
4378
4980
  logWriter;
4981
+ theme = DARK_THEME;
4379
4982
  resizeTimer = null;
4380
4983
  inputWaitTimers = new Map;
4381
4984
  awaitingInput = new Set;
@@ -4386,6 +4989,9 @@ class App {
4386
4989
  this.names = manager.getProcessNames();
4387
4990
  }
4388
4991
  async start() {
4992
+ log(`theme detect: pref=${this.config.theme ?? "auto"} stdin.isTTY=${process.stdin.isTTY} stdout.isTTY=${process.stdout.isTTY} COLORFGBG=${process.env.COLORFGBG ?? "(unset)"}`);
4993
+ this.theme = await resolveTheme(this.config.theme);
4994
+ log(`theme resolved: ${this.theme.mode}`);
4389
4995
  this.renderer = await createCliRenderer({
4390
4996
  exitOnCtrlC: false,
4391
4997
  useMouse: true,
@@ -4404,8 +5010,8 @@ class App {
4404
5010
  height: "100%",
4405
5011
  border: false
4406
5012
  });
4407
- const processHexColors = buildProcessHexColorMap(this.names, this.config);
4408
- this.tabBar = new TabBar(this.renderer, this.names, processHexColors, this.config.sort === "status");
5013
+ const processHexColors = buildProcessHexColorMap(this.names, this.config, this.theme.palette);
5014
+ this.tabBar = new TabBar(this.renderer, this.names, processHexColors, this.theme, this.config.sort === "status");
4409
5015
  const contentRow = new BoxRenderable3(this.renderer, {
4410
5016
  id: "content-row",
4411
5017
  flexDirection: "row",
@@ -4418,7 +5024,8 @@ class App {
4418
5024
  width: this.sidebarWidth,
4419
5025
  height: "100%",
4420
5026
  border: ["right"],
4421
- borderColor: "#444"
5027
+ borderColor: this.theme.sidebarBorder,
5028
+ backgroundColor: this.theme.sidebarBg
4422
5029
  });
4423
5030
  sidebar.add(this.tabBar.renderable);
4424
5031
  const paneContainer = new BoxRenderable3(this.renderer, {
@@ -4426,8 +5033,8 @@ class App {
4426
5033
  flexGrow: 1,
4427
5034
  border: false
4428
5035
  });
4429
- this.statusBar = new StatusBar(this.renderer);
4430
- this.helpOverlay = new HelpOverlay(this.renderer);
5036
+ this.statusBar = new StatusBar(this.renderer, this.theme);
5037
+ this.helpOverlay = new HelpOverlay(this.renderer, this.theme);
4431
5038
  this.search = new SearchController({
4432
5039
  logWriter: this.logWriter,
4433
5040
  statusBar: this.statusBar,
@@ -4437,7 +5044,7 @@ class App {
4437
5044
  });
4438
5045
  for (const name of this.names) {
4439
5046
  const interactive = this.config.processes[name].interactive === true;
4440
- const pane = new Pane(this.renderer, name, termCols, termRows, interactive);
5047
+ const pane = new Pane(this.renderer, name, termCols, termRows, interactive, this.theme);
4441
5048
  if (this.config.timestamps) {
4442
5049
  pane.setTimestamps(this.config.timestamps);
4443
5050
  }
@@ -5455,6 +6062,9 @@ async function main() {
5455
6062
  if (parsed.only || parsed.exclude) {
5456
6063
  config = filterConfig(config, parsed.only, parsed.exclude);
5457
6064
  }
6065
+ if (parsed.theme) {
6066
+ config.theme = parsed.theme;
6067
+ }
5458
6068
  if (parsed.autoColors) {
5459
6069
  for (const [name, proc] of Object.entries(config.processes)) {
5460
6070
  if (!proc.color) {