numux 1.10.0 → 1.10.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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/numux.js +407 -181
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -226,6 +226,7 @@ Each process accepts:
226
226
  | `color` | `string \| string[]` | auto | Hex (e.g. `"#ff6600"`) or basic name: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple |
227
227
  | `watch` | `string \| string[]` | — | Glob patterns — restart process when matching files change |
228
228
  | `interactive` | `boolean` | `false` | When `true`, keyboard input is forwarded to the process |
229
+ | `errorMatcher` | `boolean \| string` | — | `true` detects ANSI red output, string = regex pattern — shows error indicator on tab |
229
230
 
230
231
  ### File watching
231
232
 
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.10.0",
25
+ version: "1.10.1",
26
26
  description: "Terminal multiplexer with dependency orchestration",
27
27
  type: "module",
28
28
  license: "MIT",
@@ -78,7 +78,227 @@ var require_package = __commonJS((exports, module) => {
78
78
  import { existsSync as existsSync5, writeFileSync } from "fs";
79
79
  import { resolve as resolve8 } from "path";
80
80
 
81
+ // src/cli-flags.ts
82
+ var commaSplit = (raw) => raw.split(",").map((s) => s.trim()).filter(Boolean);
83
+ var FLAGS = [
84
+ {
85
+ type: "value",
86
+ long: "--workspace",
87
+ short: "-w",
88
+ key: "workspace",
89
+ description: "Run a package.json script across all workspaces",
90
+ valueName: "<script>",
91
+ completionHint: "none"
92
+ },
93
+ {
94
+ type: "value",
95
+ long: "--name",
96
+ short: "-n",
97
+ key: "named",
98
+ description: "Add a named process",
99
+ valueName: "<name=command>",
100
+ completionHint: "none",
101
+ parse(raw) {
102
+ const eq = raw.indexOf("=");
103
+ if (eq < 1) {
104
+ throw new Error(`Invalid --name value: expected "name=command", got "${raw}"`);
105
+ }
106
+ return { name: raw.slice(0, eq), command: raw.slice(eq + 1) };
107
+ }
108
+ },
109
+ {
110
+ type: "value",
111
+ long: "--color",
112
+ short: "-c",
113
+ key: "colors",
114
+ description: "Comma-separated colors (hex or names: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple)",
115
+ valueName: "<colors>",
116
+ completionHint: "none",
117
+ parse: commaSplit
118
+ },
119
+ {
120
+ type: "boolean",
121
+ long: "--colors",
122
+ key: "autoColors",
123
+ description: "Auto-assign colors to processes based on their name"
124
+ },
125
+ {
126
+ type: "value",
127
+ long: "--config",
128
+ key: "configPath",
129
+ description: "Config file path (default: auto-detect)",
130
+ valueName: "<path>",
131
+ completionHint: "file"
132
+ },
133
+ {
134
+ type: "boolean",
135
+ long: "--prefix",
136
+ short: "-p",
137
+ key: "prefix",
138
+ description: "Prefixed output mode (no TUI, for CI/scripts)"
139
+ },
140
+ {
141
+ type: "value",
142
+ long: "--only",
143
+ key: "only",
144
+ description: "Only run these processes (+ their dependencies)",
145
+ valueName: "<a,b,...>",
146
+ completionHint: "none",
147
+ parse: commaSplit
148
+ },
149
+ {
150
+ type: "value",
151
+ long: "--exclude",
152
+ key: "exclude",
153
+ description: "Exclude these processes",
154
+ valueName: "<a,b,...>",
155
+ completionHint: "none",
156
+ parse: commaSplit
157
+ },
158
+ {
159
+ type: "boolean",
160
+ long: "--kill-others",
161
+ key: "killOthers",
162
+ description: "Kill all processes when any exits"
163
+ },
164
+ {
165
+ type: "boolean",
166
+ long: "--no-restart",
167
+ key: "noRestart",
168
+ description: "Disable auto-restart for crashed processes"
169
+ },
170
+ {
171
+ type: "boolean",
172
+ long: "--no-watch",
173
+ key: "noWatch",
174
+ description: "Disable file watching even if config has watch patterns"
175
+ },
176
+ {
177
+ type: "boolean",
178
+ long: "--timestamps",
179
+ short: "-t",
180
+ key: "timestamps",
181
+ description: "Add timestamps to prefixed output lines"
182
+ },
183
+ {
184
+ type: "value",
185
+ long: "--log-dir",
186
+ key: "logDir",
187
+ description: "Write per-process logs to directory",
188
+ valueName: "<path>",
189
+ completionHint: "directory"
190
+ },
191
+ {
192
+ type: "boolean",
193
+ long: "--debug",
194
+ key: "debug",
195
+ description: "Enable debug logging to .numux/debug.log"
196
+ },
197
+ {
198
+ type: "boolean",
199
+ long: "--help",
200
+ short: "-h",
201
+ key: "help",
202
+ description: "Show this help"
203
+ },
204
+ {
205
+ type: "boolean",
206
+ long: "--version",
207
+ short: "-v",
208
+ key: "version",
209
+ description: "Show version"
210
+ }
211
+ ];
212
+ var SUBCOMMANDS = [
213
+ {
214
+ name: "init",
215
+ description: "Create a starter config file",
216
+ parse: (_args, i, result) => {
217
+ result.init = true;
218
+ return i;
219
+ }
220
+ },
221
+ {
222
+ name: "validate",
223
+ description: "Validate config and show process graph",
224
+ parse: (_args, i, result) => {
225
+ result.validate = true;
226
+ return i;
227
+ }
228
+ },
229
+ {
230
+ name: "exec",
231
+ description: "Run a command in a process's environment",
232
+ usage: "exec <name> [--] <cmd>",
233
+ parse: (args, i, result) => {
234
+ result.exec = true;
235
+ const name = args[++i];
236
+ if (!name)
237
+ throw new Error("exec requires a process name");
238
+ result.execName = name;
239
+ if (args[i + 1] === "--")
240
+ i++;
241
+ const rest = args.slice(i + 1);
242
+ if (rest.length === 0)
243
+ throw new Error("exec requires a command to run");
244
+ result.execCommand = rest.join(" ");
245
+ return "break";
246
+ }
247
+ },
248
+ {
249
+ name: "completions",
250
+ description: "Generate shell completions (bash, zsh, fish)",
251
+ usage: "completions <shell>",
252
+ parse: (args, i, result) => {
253
+ const next = args[++i];
254
+ if (next === undefined)
255
+ throw new Error("Missing value for completions");
256
+ result.completions = next;
257
+ return i;
258
+ }
259
+ }
260
+ ];
261
+ function generateHelp() {
262
+ const lines = [
263
+ "numux \u2014 terminal multiplexer with dependency orchestration",
264
+ "",
265
+ "Usage:",
266
+ " numux Run processes from config file",
267
+ " numux <cmd1> <cmd2> ... Run ad-hoc commands in parallel",
268
+ " numux -n name1=cmd1 -n name2=cmd2 Named ad-hoc commands",
269
+ " numux -w <script> Run a script across all workspaces"
270
+ ];
271
+ for (const sub of SUBCOMMANDS) {
272
+ const label = ` numux ${sub.usage ?? sub.name}`;
273
+ lines.push(`${label.padEnd(33)}${sub.description}`);
274
+ }
275
+ lines.push("", "Options:");
276
+ for (const f of FLAGS) {
277
+ const parts = [];
278
+ if (f.short)
279
+ parts.push(`${f.short},`);
280
+ parts.push(f.long);
281
+ if (f.type === "value")
282
+ parts.push(f.valueName);
283
+ const left = ` ${parts.join(" ")}`;
284
+ lines.push(`${left.padEnd(29)}${f.description}`);
285
+ }
286
+ lines.push("", "Config files (auto-detected):", " numux.config.ts, numux.config.js");
287
+ return lines.join(`
288
+ `);
289
+ }
290
+
81
291
  // src/cli.ts
292
+ var flagByName = new Map;
293
+ for (const f of FLAGS) {
294
+ flagByName.set(f.long, f);
295
+ if (f.short)
296
+ flagByName.set(f.short, f);
297
+ }
298
+ var subcommandByName = new Map;
299
+ for (const s of SUBCOMMANDS) {
300
+ subcommandByName.set(s.name, s);
301
+ }
82
302
  function parseArgs(argv) {
83
303
  const result = {
84
304
  help: false,
@@ -99,76 +319,35 @@ function parseArgs(argv) {
99
319
  };
100
320
  const args = argv.slice(2);
101
321
  let i = 0;
102
- const consumeValue = (flag) => {
103
- const next = args[++i];
104
- if (next === undefined) {
105
- throw new Error(`Missing value for ${flag}`);
106
- }
107
- return next;
108
- };
109
322
  while (i < args.length) {
110
323
  const arg = args[i];
111
- if (arg === "-h" || arg === "--help") {
112
- result.help = true;
113
- } else if (arg === "-v" || arg === "--version") {
114
- result.version = true;
115
- } else if (arg === "--debug") {
116
- result.debug = true;
117
- } else if (arg === "-p" || arg === "--prefix") {
118
- result.prefix = true;
119
- } else if (arg === "--kill-others") {
120
- result.killOthers = true;
121
- } else if (arg === "-t" || arg === "--timestamps") {
122
- result.timestamps = true;
123
- } else if (arg === "--no-restart") {
124
- result.noRestart = true;
125
- } else if (arg === "-w" || arg === "--workspace") {
126
- result.workspace = consumeValue(arg);
127
- } else if (arg === "--no-watch") {
128
- result.noWatch = true;
129
- } else if (arg === "--colors") {
130
- result.autoColors = true;
131
- } else if (arg === "--config") {
132
- result.configPath = consumeValue(arg);
133
- } else if (arg === "-c" || arg === "--color") {
134
- result.colors = consumeValue(arg).split(",").map((s) => s.trim()).filter(Boolean);
135
- } else if (arg === "--log-dir") {
136
- result.logDir = consumeValue(arg);
137
- } else if (arg === "--only") {
138
- result.only = consumeValue(arg).split(",").map((s) => s.trim()).filter(Boolean);
139
- } else if (arg === "--exclude") {
140
- result.exclude = consumeValue(arg).split(",").map((s) => s.trim()).filter(Boolean);
141
- } else if (arg === "-n" || arg === "--name") {
142
- const value = consumeValue(arg);
143
- const eq = value.indexOf("=");
144
- if (eq < 1) {
145
- throw new Error(`Invalid --name value: expected "name=command", got "${value}"`);
324
+ const flag = flagByName.get(arg);
325
+ if (flag) {
326
+ if (flag.type === "boolean") {
327
+ result[flag.key] = true;
328
+ } else {
329
+ const next = args[++i];
330
+ if (next === undefined) {
331
+ throw new Error(`Missing value for ${arg}`);
332
+ }
333
+ const value = flag.parse ? flag.parse(next, arg) : next;
334
+ const current = result[flag.key];
335
+ if (Array.isArray(current)) {
336
+ current.push(value);
337
+ } else {
338
+ result[flag.key] = value;
339
+ }
146
340
  }
147
- result.named.push({
148
- name: value.slice(0, eq),
149
- command: value.slice(eq + 1)
150
- });
151
- } else if (arg === "init" && result.commands.length === 0) {
152
- result.init = true;
153
- } else if (arg === "validate" && result.commands.length === 0) {
154
- result.validate = true;
155
- } else if (arg === "exec" && result.commands.length === 0) {
156
- result.exec = true;
157
- const name = args[++i];
158
- if (!name)
159
- throw new Error("exec requires a process name");
160
- result.execName = name;
161
- if (args[i + 1] === "--")
162
- i++;
163
- const rest = args.slice(i + 1);
164
- if (rest.length === 0)
165
- throw new Error("exec requires a command to run");
166
- result.execCommand = rest.join(" ");
167
- break;
168
- } else if (arg === "completions" && result.commands.length === 0) {
169
- result.completions = consumeValue(arg);
170
341
  } else if (!arg.startsWith("-")) {
171
- result.commands.push(arg);
342
+ const sub = result.commands.length === 0 ? subcommandByName.get(arg) : undefined;
343
+ if (sub) {
344
+ const ret = sub.parse(args, i, result);
345
+ if (ret === "break")
346
+ break;
347
+ i = ret;
348
+ } else {
349
+ result.commands.push(arg);
350
+ }
172
351
  } else {
173
352
  throw new Error(`Unknown option: ${arg}`);
174
353
  }
@@ -259,7 +438,36 @@ function generateCompletions(shell) {
259
438
  throw new Error(`Unknown shell: "${shell}". Supported: ${SUPPORTED_SHELLS.join(", ")}`);
260
439
  }
261
440
  }
441
+ function longName(f) {
442
+ return f.long.replace(/^-+/, "");
443
+ }
444
+ function sq(s) {
445
+ return s.replace(/'/g, "'\\''");
446
+ }
262
447
  function bashCompletions() {
448
+ const caseEntries = [];
449
+ for (const f of FLAGS) {
450
+ if (f.type !== "value")
451
+ continue;
452
+ const names = f.short ? `${f.short}|${f.long}` : f.long;
453
+ if (f.completionHint === "file") {
454
+ caseEntries.push(` ${names})
455
+ COMPREPLY=( $(compgen -f -- "$cur") )
456
+ return ;;`);
457
+ } else if (f.completionHint === "directory") {
458
+ caseEntries.push(` ${names})
459
+ COMPREPLY=( $(compgen -d -- "$cur") )
460
+ return ;;`);
461
+ } else {
462
+ caseEntries.push(` ${names})
463
+ return ;;`);
464
+ }
465
+ }
466
+ caseEntries.push(` completions)
467
+ COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
468
+ return ;;`);
469
+ const allFlags = FLAGS.flatMap((f) => f.short ? [f.short, f.long] : [f.long]);
470
+ const subcmds = SUBCOMMANDS.map((s) => s.name);
263
471
  return `# numux bash completions
264
472
  # Add to ~/.bashrc: eval "$(numux completions bash)"
265
473
  _numux() {
@@ -268,60 +476,66 @@ _numux() {
268
476
  prev="\${COMP_WORDS[COMP_CWORD-1]}"
269
477
 
270
478
  case "$prev" in
271
- --config)
272
- COMPREPLY=( $(compgen -f -- "$cur") )
273
- return ;;
274
- --log-dir)
275
- COMPREPLY=( $(compgen -d -- "$cur") )
276
- return ;;
277
- --only|--exclude)
278
- return ;;
279
- -n|--name|-w|--workspace)
280
- return ;;
281
- completions)
282
- COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
283
- return ;;
479
+ ${caseEntries.join(`
480
+ `)}
284
481
  esac
285
482
 
286
483
  if [[ "$cur" == -* ]]; then
287
- COMPREPLY=( $(compgen -W "-h --help -v --version -w --workspace -c --color --colors --config -n --name -p --prefix --only --exclude --kill-others --no-restart --no-watch -t --timestamps --log-dir --debug" -- "$cur") )
484
+ COMPREPLY=( $(compgen -W "${allFlags.join(" ")}" -- "$cur") )
288
485
  else
289
- local subcmds="init validate exec completions"
486
+ local subcmds="${subcmds.join(" ")}"
290
487
  COMPREPLY=( $(compgen -W "$subcmds" -- "$cur") )
291
488
  fi
292
489
  }
293
490
  complete -F _numux numux`;
294
491
  }
295
492
  function zshCompletions() {
493
+ const subcmdLines = SUBCOMMANDS.map((s) => ` '${s.name}:${sq(s.description)}'`).join(`
494
+ `);
495
+ const argLines = [];
496
+ for (const f of FLAGS) {
497
+ const desc = sq(f.description);
498
+ if (f.short) {
499
+ if (f.type === "value") {
500
+ let suffix = "";
501
+ if (f.completionHint === "file")
502
+ suffix = ":file:_files";
503
+ else if (f.completionHint === "directory")
504
+ suffix = ":directory:_directories";
505
+ else
506
+ suffix = `:${longName(f)}`;
507
+ argLines.push(` '(${f.short} ${f.long})'{${f.short},${f.long}}'[${desc}]${suffix}'`);
508
+ } else {
509
+ argLines.push(` '(${f.short} ${f.long})'{${f.short},${f.long}}'[${desc}]'`);
510
+ }
511
+ } else {
512
+ if (f.type === "value") {
513
+ let suffix = "";
514
+ if (f.completionHint === "file")
515
+ suffix = ":file:_files";
516
+ else if (f.completionHint === "directory")
517
+ suffix = ":directory:_directories";
518
+ else
519
+ suffix = `:${longName(f)}`;
520
+ argLines.push(` '${f.long}[${desc}]${suffix}'`);
521
+ } else {
522
+ argLines.push(` '${f.long}[${desc}]'`);
523
+ }
524
+ }
525
+ }
526
+ const argsBlock = argLines.map((l) => `${l} \\`).join(`
527
+ `);
296
528
  return `#compdef numux
297
529
  # numux zsh completions
298
530
  # Add to ~/.zshrc: eval "$(numux completions zsh)"
299
531
  _numux() {
300
532
  local -a subcmds
301
533
  subcmds=(
302
- 'init:Create a starter config file'
303
- 'validate:Validate config and show process graph'
304
- 'exec:Run a command in a process environment'
305
- 'completions:Generate shell completions'
534
+ ${subcmdLines}
306
535
  )
307
536
 
308
537
  _arguments -s \\
309
- '(-h --help)'{-h,--help}'[Show help]' \\
310
- '(-v --version)'{-v,--version}'[Show version]' \\
311
- '(-w --workspace)'{-w,--workspace}'[Run script across all workspaces]:script' \\
312
- '(-c --color)'{-c,--color}'[Comma-separated colors for processes]' \\
313
- '--colors[Auto-assign colors based on process name]' \\
314
- '--config[Config file path]:file:_files' \\
315
- '(-n --name)'{-n,--name}'[Named process (name=command)]:named process' \\
316
- '(-p --prefix)'{-p,--prefix}'[Prefixed output mode]' \\
317
- '--only[Only run these processes]:processes' \\
318
- '--exclude[Exclude these processes]:processes' \\
319
- '--kill-others[Kill all when any exits]' \\
320
- '--no-restart[Disable auto-restart]' \\
321
- '--no-watch[Disable file watching]' \\
322
- '(-t --timestamps)'{-t,--timestamps}'[Add timestamps to output]' \\
323
- '--log-dir[Log directory]:directory:_directories' \\
324
- '--debug[Enable debug logging]' \\
538
+ ${argsBlock}
325
539
  '1:subcommand:->subcmd' \\
326
540
  '*:command' \\
327
541
  && return
@@ -335,37 +549,37 @@ _numux() {
335
549
  _numux`;
336
550
  }
337
551
  function fishCompletions() {
338
- return `# numux fish completions
339
- # Add to fish: numux completions fish | source
340
- # Or save to: ~/.config/fish/completions/numux.fish
341
- complete -c numux -f
342
-
343
- # Subcommands
344
- complete -c numux -n __fish_use_subcommand -a init -d 'Create a starter config file'
345
- complete -c numux -n __fish_use_subcommand -a validate -d 'Validate config and show process graph'
346
- complete -c numux -n __fish_use_subcommand -a exec -d 'Run a command in a process environment'
347
- complete -c numux -n __fish_use_subcommand -a completions -d 'Generate shell completions'
348
-
349
- # Completions subcommand
350
- complete -c numux -n '__fish_seen_subcommand_from completions' -a 'bash zsh fish'
351
-
352
- # Options
353
- complete -c numux -s h -l help -d 'Show help'
354
- complete -c numux -s v -l version -d 'Show version'
355
- complete -c numux -s c -l color -r -d 'Comma-separated colors for processes'
356
- complete -c numux -l colors -d 'Auto-assign colors based on process name'
357
- complete -c numux -l config -rF -d 'Config file path'
358
- complete -c numux -s w -l workspace -r -d 'Run script across all workspaces'
359
- complete -c numux -s n -l name -r -d 'Named process (name=command)'
360
- complete -c numux -s p -l prefix -d 'Prefixed output mode'
361
- complete -c numux -l only -r -d 'Only run these processes'
362
- complete -c numux -l exclude -r -d 'Exclude these processes'
363
- complete -c numux -l kill-others -d 'Kill all when any exits'
364
- complete -c numux -l no-restart -d 'Disable auto-restart'
365
- complete -c numux -l no-watch -d 'Disable file watching'
366
- complete -c numux -s t -l timestamps -d 'Add timestamps to output'
367
- complete -c numux -l log-dir -ra '(__fish_complete_directories)' -d 'Log directory'
368
- complete -c numux -l debug -d 'Enable debug logging'`;
552
+ const lines = [
553
+ "# numux fish completions",
554
+ "# Add to fish: numux completions fish | source",
555
+ "# Or save to: ~/.config/fish/completions/numux.fish",
556
+ "complete -c numux -f",
557
+ "",
558
+ "# Subcommands"
559
+ ];
560
+ for (const s of SUBCOMMANDS) {
561
+ lines.push(`complete -c numux -n __fish_use_subcommand -a ${s.name} -d '${sq(s.description)}'`);
562
+ }
563
+ lines.push("", "# Completions subcommand", "complete -c numux -n '__fish_seen_subcommand_from completions' -a 'bash zsh fish'", "", "# Options");
564
+ for (const f of FLAGS) {
565
+ const parts = ["complete -c numux"];
566
+ if (f.short)
567
+ parts.push(`-s ${f.short.replace("-", "")}`);
568
+ parts.push(`-l ${longName(f)}`);
569
+ if (f.type === "value") {
570
+ if (f.completionHint === "file") {
571
+ parts.push("-rF");
572
+ } else if (f.completionHint === "directory") {
573
+ parts.push("-ra '(__fish_complete_directories)'");
574
+ } else {
575
+ parts.push("-r");
576
+ }
577
+ }
578
+ parts.push(`-d '${sq(f.description)}'`);
579
+ lines.push(parts.join(" "));
580
+ }
581
+ return lines.join(`
582
+ `);
369
583
  }
370
584
 
371
585
  // src/config/expand-scripts.ts
@@ -1653,6 +1867,8 @@ class Pane {
1653
1867
  decoder = new TextDecoder;
1654
1868
  _onScroll = null;
1655
1869
  _onCopy = null;
1870
+ _textLines = null;
1871
+ _textLinesLower = null;
1656
1872
  constructor(renderer, name, cols, rows, interactive = false) {
1657
1873
  this.scrollBox = new ScrollBoxRenderable(renderer, {
1658
1874
  id: `pane-${name}`,
@@ -1688,10 +1904,14 @@ class Pane {
1688
1904
  feed(data) {
1689
1905
  const text = this.decoder.decode(data, { stream: true });
1690
1906
  this.terminal.feed(text);
1907
+ this._textLines = null;
1908
+ this._textLinesLower = null;
1691
1909
  }
1692
1910
  resize(cols, rows) {
1693
1911
  this.terminal.cols = cols;
1694
1912
  this.terminal.rows = rows;
1913
+ this._textLines = null;
1914
+ this._textLinesLower = null;
1695
1915
  }
1696
1916
  get isAtBottom() {
1697
1917
  const { scrollTop, scrollHeight, viewport } = this.scrollBox;
@@ -1723,16 +1943,19 @@ class Pane {
1723
1943
  search(query) {
1724
1944
  if (!query)
1725
1945
  return [];
1726
- const text = this.terminal.getText();
1727
- const lines = text.split(`
1946
+ if (!this._textLines) {
1947
+ const text = this.terminal.getText();
1948
+ this._textLines = text.split(`
1728
1949
  `);
1950
+ this._textLinesLower = this._textLines.map((l) => l.toLowerCase());
1951
+ }
1952
+ const lines = this._textLinesLower;
1729
1953
  const matches = [];
1730
1954
  const lowerQuery = query.toLowerCase();
1731
1955
  for (let line = 0;line < lines.length; line++) {
1732
- const lowerLine = lines[line].toLowerCase();
1733
1956
  let pos = 0;
1734
1957
  while (true) {
1735
- const idx = lowerLine.indexOf(lowerQuery, pos);
1958
+ const idx = lines[line].indexOf(lowerQuery, pos);
1736
1959
  if (idx === -1)
1737
1960
  break;
1738
1961
  matches.push({ line, start: idx, end: idx + query.length });
@@ -1742,12 +1965,22 @@ class Pane {
1742
1965
  return matches;
1743
1966
  }
1744
1967
  setHighlights(matches, currentIndex) {
1745
- const regions = matches.map((m, i) => ({
1746
- line: m.line,
1747
- start: m.start,
1748
- end: m.end,
1749
- backgroundColor: i === currentIndex ? "#b58900" : "#073642"
1750
- }));
1968
+ const firstVisible = Math.max(0, Math.floor(this.scrollBox.scrollTop) - 2);
1969
+ const lastVisible = Math.ceil(this.scrollBox.scrollTop + this.scrollBox.viewport.height) + 2;
1970
+ const regions = [];
1971
+ for (let i = 0;i < matches.length; i++) {
1972
+ const m = matches[i];
1973
+ if (m.line < firstVisible || m.line > lastVisible) {
1974
+ if (i !== currentIndex)
1975
+ continue;
1976
+ }
1977
+ regions.push({
1978
+ line: m.line,
1979
+ start: m.start,
1980
+ end: m.end,
1981
+ backgroundColor: i === currentIndex ? "#b58900" : "#073642"
1982
+ });
1983
+ }
1751
1984
  this.terminal.highlights = regions;
1752
1985
  }
1753
1986
  clearHighlights() {
@@ -1759,6 +1992,8 @@ class Pane {
1759
1992
  }
1760
1993
  clear() {
1761
1994
  this.terminal.reset();
1995
+ this._textLines = null;
1996
+ this._textLinesLower = null;
1762
1997
  }
1763
1998
  destroy() {
1764
1999
  this.terminal.destroy();
@@ -1897,6 +2132,7 @@ class ColoredSelectRenderable extends SelectRenderable {
1897
2132
  const visibleCount = Math.min(maxVisibleItems, options.length - scrollOffset);
1898
2133
  const baseTextColor = this._focused ? this._focusedTextColor : this._textColor;
1899
2134
  const selectedTextColor = this._selectedTextColor;
2135
+ const lineWidth = fb.width;
1900
2136
  for (let i = 0;i < visibleCount; i++) {
1901
2137
  const actualIndex = scrollOffset + i;
1902
2138
  const itemY = i * linesPerItem;
@@ -1904,13 +2140,11 @@ class ColoredSelectRenderable extends SelectRenderable {
1904
2140
  const isSelected = actualIndex === selectedIndex;
1905
2141
  const defaultColor = isSelected ? selectedTextColor : baseTextColor;
1906
2142
  const colors = this._optionColors[actualIndex];
1907
- fb.drawText(`${optName} `, 1, itemY, defaultColor);
2143
+ const textColor = colors?.name ?? defaultColor;
2144
+ fb.drawText(optName.padEnd(lineWidth), 1, itemY, textColor);
1908
2145
  if (colors?.icon) {
1909
2146
  fb.drawText(optName.charAt(0), 1, itemY, colors.icon);
1910
2147
  }
1911
- if (colors?.name) {
1912
- fb.drawText(optName.slice(2), 3, itemY, colors.name);
1913
- }
1914
2148
  }
1915
2149
  }
1916
2150
  }
@@ -2067,6 +2301,7 @@ class App {
2067
2301
  sidebarWidth = 20;
2068
2302
  config;
2069
2303
  resizeTimer = null;
2304
+ searchTimer = null;
2070
2305
  searchMode = false;
2071
2306
  searchQuery = "";
2072
2307
  searchMatches = [];
@@ -2122,6 +2357,11 @@ class App {
2122
2357
  const interactive = this.config.processes[name].interactive === true;
2123
2358
  const pane = new Pane(this.renderer, name, termCols, termRows, interactive);
2124
2359
  pane.onCopy(() => this.statusBar.showTemporaryMessage("Copied!"));
2360
+ pane.onScroll(() => {
2361
+ if (this.searchMode && this.searchMatches.length > 0 && this.activePane === name) {
2362
+ this.updateSearchHighlights();
2363
+ }
2364
+ });
2125
2365
  this.panes.set(name, pane);
2126
2366
  paneContainer.add(pane.scrollBox);
2127
2367
  }
@@ -2322,6 +2562,10 @@ class App {
2322
2562
  this.searchQuery = "";
2323
2563
  this.searchMatches = [];
2324
2564
  this.searchIndex = -1;
2565
+ if (this.searchTimer) {
2566
+ clearTimeout(this.searchTimer);
2567
+ this.searchTimer = null;
2568
+ }
2325
2569
  if (this.activePane) {
2326
2570
  this.panes.get(this.activePane)?.clearHighlights();
2327
2571
  }
@@ -2347,15 +2591,24 @@ class App {
2347
2591
  if (key.name === "backspace") {
2348
2592
  if (this.searchQuery.length > 0) {
2349
2593
  this.searchQuery = this.searchQuery.slice(0, -1);
2350
- this.runSearch();
2594
+ this.scheduleSearch();
2351
2595
  }
2352
2596
  return;
2353
2597
  }
2354
2598
  if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
2355
2599
  this.searchQuery += key.sequence;
2356
- this.runSearch();
2600
+ this.scheduleSearch();
2357
2601
  }
2358
2602
  }
2603
+ scheduleSearch() {
2604
+ this.statusBar.setSearchMode(true, this.searchQuery, this.searchMatches.length, this.searchIndex);
2605
+ if (this.searchTimer)
2606
+ clearTimeout(this.searchTimer);
2607
+ this.searchTimer = setTimeout(() => {
2608
+ this.searchTimer = null;
2609
+ this.runSearch();
2610
+ }, 100);
2611
+ }
2359
2612
  runSearch() {
2360
2613
  if (!this.activePane)
2361
2614
  return;
@@ -2399,6 +2652,10 @@ class App {
2399
2652
  clearTimeout(this.resizeTimer);
2400
2653
  this.resizeTimer = null;
2401
2654
  }
2655
+ if (this.searchTimer) {
2656
+ clearTimeout(this.searchTimer);
2657
+ this.searchTimer = null;
2658
+ }
2402
2659
  for (const timer of this.inputWaitTimers.values()) {
2403
2660
  clearTimeout(timer);
2404
2661
  }
@@ -2665,38 +2922,7 @@ function setupShutdownHandlers(app, logWriter) {
2665
2922
  }
2666
2923
 
2667
2924
  // src/index.ts
2668
- var HELP = `numux \u2014 terminal multiplexer with dependency orchestration
2669
-
2670
- Usage:
2671
- numux Run processes from config file
2672
- numux <cmd1> <cmd2> ... Run ad-hoc commands in parallel
2673
- numux -n name1=cmd1 -n name2=cmd2 Named ad-hoc commands
2674
- numux -w <script> Run a script across all workspaces
2675
- numux init Create a starter config file
2676
- numux validate Validate config and show process graph
2677
- numux exec <name> [--] <cmd> Run a command in a process's environment
2678
- numux completions <shell> Generate shell completions (bash, zsh, fish)
2679
-
2680
- Options:
2681
- -w, --workspace <script> Run a package.json script across all workspaces
2682
- -n, --name <name=command> Add a named process
2683
- -c, --color <colors> Comma-separated colors (hex or names: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple)
2684
- --colors Auto-assign colors to processes based on their name
2685
- --config <path> Config file path (default: auto-detect)
2686
- -p, --prefix Prefixed output mode (no TUI, for CI/scripts)
2687
- --only <a,b,...> Only run these processes (+ their dependencies)
2688
- --exclude <a,b,...> Exclude these processes
2689
- --kill-others Kill all processes when any exits
2690
- --no-restart Disable auto-restart for crashed processes
2691
- --no-watch Disable file watching even if config has watch patterns
2692
- -t, --timestamps Add timestamps to prefixed output lines
2693
- --log-dir <path> Write per-process logs to directory
2694
- --debug Enable debug logging to .numux/debug.log
2695
- -h, --help Show this help
2696
- -v, --version Show version
2697
-
2698
- Config files (auto-detected):
2699
- numux.config.ts, numux.config.js`;
2925
+ var HELP = generateHelp();
2700
2926
  var INIT_TEMPLATE = `import { defineConfig } from 'numux'
2701
2927
 
2702
2928
  export default defineConfig({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "numux",
3
- "version": "1.10.0",
3
+ "version": "1.10.1",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",