numux 1.23.2 → 1.24.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 +18 -1
- package/dist/numux.js +328 -127
- package/dist/types.d.ts +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -167,7 +167,7 @@ Template properties (color, env, dependsOn, etc.) are inherited by all matched p
|
|
|
167
167
|
| `-s, --sort <mode>` | Tab display order: `config` (default), `alphabetical`, `topological` |
|
|
168
168
|
| `--no-watch` | Disable file watching even if config has `watch` patterns |
|
|
169
169
|
| `-t, --timestamps` | Add `[HH:MM:SS]` timestamps to prefixed output |
|
|
170
|
-
| `--log-dir <path>` |
|
|
170
|
+
| `--log-dir <path>` | Persist logs to timestamped subdirs (`<path>/<timestamp>/<name>.log`) with a `latest` symlink. Path is printed on exit |
|
|
171
171
|
| `--debug` | Log to `.numux/debug.log` |
|
|
172
172
|
| `-h, --help` | Show help |
|
|
173
173
|
| `-v, --version` | Show version |
|
|
@@ -347,6 +347,23 @@ Unmatched references are left as-is (the shell will expand `$db` as empty + `.po
|
|
|
347
347
|
|
|
348
348
|
Keybindings are shown in the status bar at the bottom of the app. Panes are readonly by default — keyboard input is not forwarded to processes. Set `interactive: true` on processes that need stdin (REPLs, shells, etc.).
|
|
349
349
|
|
|
350
|
+
| Key | Action |
|
|
351
|
+
|-----|--------|
|
|
352
|
+
| `←`/`→` or `1`-`9` | Switch tabs |
|
|
353
|
+
| `F` | Search current pane |
|
|
354
|
+
| `Tab` (in search) | Toggle between single-pane and all-process search |
|
|
355
|
+
| `Enter`/`Shift+Enter` | Next/previous match |
|
|
356
|
+
| `Esc` | Exit search |
|
|
357
|
+
| `R` | Restart current process |
|
|
358
|
+
| `Shift+R` | Restart all processes |
|
|
359
|
+
| `S` | Stop/start current process |
|
|
360
|
+
| `Y` | Copy all output |
|
|
361
|
+
| `L` | Clear pane |
|
|
362
|
+
| `G`/`Shift+G` | Scroll to top/bottom |
|
|
363
|
+
| `PageUp`/`PageDown` | Scroll by page |
|
|
364
|
+
| `Ctrl+Click` | Open link |
|
|
365
|
+
| `Ctrl+C` | Quit |
|
|
366
|
+
|
|
350
367
|
## Tab icons
|
|
351
368
|
|
|
352
369
|
| Icon | Status |
|
package/dist/numux.js
CHANGED
|
@@ -36,7 +36,7 @@ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports,
|
|
|
36
36
|
var require_package = __commonJS((exports, module) => {
|
|
37
37
|
module.exports = {
|
|
38
38
|
name: "numux",
|
|
39
|
-
version: "1.
|
|
39
|
+
version: "1.24.0",
|
|
40
40
|
description: "Terminal multiplexer with dependency orchestration",
|
|
41
41
|
type: "module",
|
|
42
42
|
license: "MIT",
|
|
@@ -2320,6 +2320,197 @@ class Pane {
|
|
|
2320
2320
|
}
|
|
2321
2321
|
}
|
|
2322
2322
|
|
|
2323
|
+
// src/ui/search.ts
|
|
2324
|
+
class SearchController {
|
|
2325
|
+
mode = false;
|
|
2326
|
+
allMode = false;
|
|
2327
|
+
query = "";
|
|
2328
|
+
matches = [];
|
|
2329
|
+
index = -1;
|
|
2330
|
+
crossMatches = [];
|
|
2331
|
+
crossCounts = new Map;
|
|
2332
|
+
timer = null;
|
|
2333
|
+
logWriter;
|
|
2334
|
+
statusBar;
|
|
2335
|
+
tabBar;
|
|
2336
|
+
getActivePane;
|
|
2337
|
+
getPane;
|
|
2338
|
+
constructor(opts) {
|
|
2339
|
+
this.logWriter = opts.logWriter;
|
|
2340
|
+
this.statusBar = opts.statusBar;
|
|
2341
|
+
this.tabBar = opts.tabBar;
|
|
2342
|
+
this.getActivePane = opts.getActivePane;
|
|
2343
|
+
this.getPane = opts.getPane;
|
|
2344
|
+
}
|
|
2345
|
+
get isActive() {
|
|
2346
|
+
return this.mode;
|
|
2347
|
+
}
|
|
2348
|
+
get isAllMode() {
|
|
2349
|
+
return this.allMode;
|
|
2350
|
+
}
|
|
2351
|
+
get currentMatches() {
|
|
2352
|
+
return this.matches;
|
|
2353
|
+
}
|
|
2354
|
+
enter() {
|
|
2355
|
+
this.mode = true;
|
|
2356
|
+
this.query = "";
|
|
2357
|
+
this.matches = [];
|
|
2358
|
+
this.index = -1;
|
|
2359
|
+
this.allMode = false;
|
|
2360
|
+
this.crossMatches = [];
|
|
2361
|
+
this.crossCounts.clear();
|
|
2362
|
+
this.statusBar.setSearchMode(true);
|
|
2363
|
+
}
|
|
2364
|
+
exit() {
|
|
2365
|
+
this.mode = false;
|
|
2366
|
+
this.query = "";
|
|
2367
|
+
this.matches = [];
|
|
2368
|
+
this.index = -1;
|
|
2369
|
+
this.allMode = false;
|
|
2370
|
+
this.crossMatches = [];
|
|
2371
|
+
this.crossCounts.clear();
|
|
2372
|
+
if (this.timer) {
|
|
2373
|
+
clearTimeout(this.timer);
|
|
2374
|
+
this.timer = null;
|
|
2375
|
+
}
|
|
2376
|
+
const active = this.getActivePane();
|
|
2377
|
+
if (active) {
|
|
2378
|
+
this.getPane(active)?.clearHighlights();
|
|
2379
|
+
}
|
|
2380
|
+
this.tabBar.clearSearchMatches();
|
|
2381
|
+
this.statusBar.setSearchMode(false);
|
|
2382
|
+
}
|
|
2383
|
+
onPaneSwitch() {
|
|
2384
|
+
if (this.mode && this.allMode) {
|
|
2385
|
+
this.applyPaneMatches();
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
refreshHighlights() {
|
|
2389
|
+
if (this.matches.length > 0) {
|
|
2390
|
+
this.updateHighlights();
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
handleInput(key) {
|
|
2394
|
+
if (key.name === "escape") {
|
|
2395
|
+
this.exit();
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
if (key.name === "tab") {
|
|
2399
|
+
this.allMode = !this.allMode;
|
|
2400
|
+
if (!this.allMode) {
|
|
2401
|
+
this.crossMatches = [];
|
|
2402
|
+
this.crossCounts.clear();
|
|
2403
|
+
this.tabBar.clearSearchMatches();
|
|
2404
|
+
}
|
|
2405
|
+
this.scheduleSearch();
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2408
|
+
if (key.name === "return") {
|
|
2409
|
+
if (this.matches.length === 0)
|
|
2410
|
+
return;
|
|
2411
|
+
if (key.shift) {
|
|
2412
|
+
this.index = (this.index - 1 + this.matches.length) % this.matches.length;
|
|
2413
|
+
} else {
|
|
2414
|
+
this.index = (this.index + 1) % this.matches.length;
|
|
2415
|
+
}
|
|
2416
|
+
this.scrollToMatch();
|
|
2417
|
+
this.updateHighlights();
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
if (key.name === "backspace") {
|
|
2421
|
+
if (this.query.length > 0) {
|
|
2422
|
+
this.query = this.query.slice(0, -1);
|
|
2423
|
+
this.scheduleSearch();
|
|
2424
|
+
}
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
|
|
2428
|
+
this.query += key.sequence;
|
|
2429
|
+
this.scheduleSearch();
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
dispose() {
|
|
2433
|
+
if (this.timer) {
|
|
2434
|
+
clearTimeout(this.timer);
|
|
2435
|
+
this.timer = null;
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
scheduleSearch() {
|
|
2439
|
+
this.statusBar.setSearchMode(true, this.query, this.matches.length, this.index, this.crossInfo());
|
|
2440
|
+
if (this.timer)
|
|
2441
|
+
clearTimeout(this.timer);
|
|
2442
|
+
this.timer = setTimeout(() => {
|
|
2443
|
+
this.timer = null;
|
|
2444
|
+
this.runSearch();
|
|
2445
|
+
}, 100);
|
|
2446
|
+
}
|
|
2447
|
+
async runSearch() {
|
|
2448
|
+
const active = this.getActivePane();
|
|
2449
|
+
if (!active)
|
|
2450
|
+
return;
|
|
2451
|
+
const query = this.query;
|
|
2452
|
+
const allMode = this.allMode;
|
|
2453
|
+
if (allMode) {
|
|
2454
|
+
const allMatches = await this.logWriter.searchAll(query);
|
|
2455
|
+
if (!this.mode || this.query !== query || !this.allMode)
|
|
2456
|
+
return;
|
|
2457
|
+
this.crossMatches = allMatches;
|
|
2458
|
+
this.crossCounts.clear();
|
|
2459
|
+
for (const m of allMatches) {
|
|
2460
|
+
this.crossCounts.set(m.process, (this.crossCounts.get(m.process) ?? 0) + 1);
|
|
2461
|
+
}
|
|
2462
|
+
this.tabBar.setSearchMatches(this.crossCounts);
|
|
2463
|
+
this.applyPaneMatches();
|
|
2464
|
+
} else {
|
|
2465
|
+
const matches = await this.logWriter.search(active, query);
|
|
2466
|
+
if (!this.mode || this.query !== query || this.getActivePane() !== active)
|
|
2467
|
+
return;
|
|
2468
|
+
this.matches = matches;
|
|
2469
|
+
this.index = matches.length > 0 ? 0 : -1;
|
|
2470
|
+
this.updateHighlights();
|
|
2471
|
+
if (this.index >= 0)
|
|
2472
|
+
this.scrollToMatch();
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
applyPaneMatches() {
|
|
2476
|
+
const active = this.getActivePane();
|
|
2477
|
+
if (!active)
|
|
2478
|
+
return;
|
|
2479
|
+
const paneMatches = this.crossMatches.filter((m) => m.process === active).map(({ line, start, end }) => ({ line, start, end }));
|
|
2480
|
+
this.matches = paneMatches;
|
|
2481
|
+
this.index = paneMatches.length > 0 ? 0 : -1;
|
|
2482
|
+
this.updateHighlights();
|
|
2483
|
+
if (this.index >= 0)
|
|
2484
|
+
this.scrollToMatch();
|
|
2485
|
+
}
|
|
2486
|
+
updateHighlights() {
|
|
2487
|
+
const active = this.getActivePane();
|
|
2488
|
+
if (!active)
|
|
2489
|
+
return;
|
|
2490
|
+
const pane = this.getPane(active);
|
|
2491
|
+
if (!pane)
|
|
2492
|
+
return;
|
|
2493
|
+
if (this.matches.length > 0) {
|
|
2494
|
+
pane.setHighlights(this.matches, this.index);
|
|
2495
|
+
} else {
|
|
2496
|
+
pane.clearHighlights();
|
|
2497
|
+
}
|
|
2498
|
+
this.statusBar.setSearchMode(true, this.query, this.matches.length, this.index, this.crossInfo());
|
|
2499
|
+
}
|
|
2500
|
+
scrollToMatch() {
|
|
2501
|
+
const active = this.getActivePane();
|
|
2502
|
+
if (!active || this.index < 0)
|
|
2503
|
+
return;
|
|
2504
|
+
const pane = this.getPane(active);
|
|
2505
|
+
if (!pane)
|
|
2506
|
+
return;
|
|
2507
|
+
pane.scrollToLine(this.matches[this.index].line);
|
|
2508
|
+
}
|
|
2509
|
+
crossInfo() {
|
|
2510
|
+
return this.allMode ? { totalMatches: this.crossMatches.length, processCount: this.crossCounts.size } : undefined;
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2323
2514
|
// src/ui/status-bar.ts
|
|
2324
2515
|
import { cyan, red, reverse, StyledText, TextRenderable, yellow } from "@opentui/core";
|
|
2325
2516
|
function plain(text) {
|
|
@@ -2332,6 +2523,7 @@ class StatusBar {
|
|
|
2332
2523
|
_searchQuery = "";
|
|
2333
2524
|
_searchMatchCount = 0;
|
|
2334
2525
|
_searchCurrentIndex = -1;
|
|
2526
|
+
_crossProcessInfo;
|
|
2335
2527
|
_tempMessage = null;
|
|
2336
2528
|
_tempTimer = null;
|
|
2337
2529
|
constructor(renderer) {
|
|
@@ -2344,11 +2536,12 @@ class StatusBar {
|
|
|
2344
2536
|
paddingX: 1
|
|
2345
2537
|
});
|
|
2346
2538
|
}
|
|
2347
|
-
setSearchMode(active, query = "", matchCount = 0, currentIndex = -1) {
|
|
2539
|
+
setSearchMode(active, query = "", matchCount = 0, currentIndex = -1, crossProcessInfo) {
|
|
2348
2540
|
this._searchMode = active;
|
|
2349
2541
|
this._searchQuery = query;
|
|
2350
2542
|
this._searchMatchCount = matchCount;
|
|
2351
2543
|
this._searchCurrentIndex = currentIndex;
|
|
2544
|
+
this._crossProcessInfo = crossProcessInfo;
|
|
2352
2545
|
this.renderable.content = this.buildContent();
|
|
2353
2546
|
}
|
|
2354
2547
|
showTemporaryMessage(message, duration = 2000) {
|
|
@@ -2373,6 +2566,7 @@ class StatusBar {
|
|
|
2373
2566
|
}
|
|
2374
2567
|
buildSearchContent() {
|
|
2375
2568
|
const chunks = [];
|
|
2569
|
+
const isAllMode = !!this._crossProcessInfo;
|
|
2376
2570
|
chunks.push(yellow("/"));
|
|
2377
2571
|
if (this._searchQuery)
|
|
2378
2572
|
chunks.push(plain(this._searchQuery));
|
|
@@ -2380,14 +2574,20 @@ class StatusBar {
|
|
|
2380
2574
|
if (this._searchMatchCount === 0 && this._searchQuery) {
|
|
2381
2575
|
chunks.push(plain(" "));
|
|
2382
2576
|
chunks.push(red("no matches"));
|
|
2383
|
-
chunks.push(plain(" Esc: close"));
|
|
2384
2577
|
} else if (this._searchMatchCount > 0) {
|
|
2385
2578
|
chunks.push(plain(" "));
|
|
2386
|
-
|
|
2387
|
-
|
|
2579
|
+
if (isAllMode) {
|
|
2580
|
+
const { totalMatches, processCount } = this._crossProcessInfo;
|
|
2581
|
+
chunks.push(cyan(`${this._searchCurrentIndex + 1}/${this._searchMatchCount}`));
|
|
2582
|
+
chunks.push(plain(` ${totalMatches} in ${processCount} process${processCount === 1 ? "" : "es"}`));
|
|
2583
|
+
} else {
|
|
2584
|
+
chunks.push(cyan(`${this._searchCurrentIndex + 1}/${this._searchMatchCount}`));
|
|
2585
|
+
}
|
|
2586
|
+
chunks.push(plain(" Enter: next"));
|
|
2388
2587
|
} else {
|
|
2389
|
-
chunks.push(plain(" Enter: next
|
|
2588
|
+
chunks.push(plain(" Enter: next"));
|
|
2390
2589
|
}
|
|
2590
|
+
chunks.push(plain(` Tab: ${isAllMode ? "single" : "all"} Esc: close`));
|
|
2391
2591
|
return new StyledText(chunks);
|
|
2392
2592
|
}
|
|
2393
2593
|
}
|
|
@@ -2435,12 +2635,13 @@ function getDisplayOrder(originalNames, statuses) {
|
|
|
2435
2635
|
const terminal = originalNames.filter((n) => TERMINAL_STATUSES.has(statuses.get(n)));
|
|
2436
2636
|
return [...active, ...terminal];
|
|
2437
2637
|
}
|
|
2438
|
-
function resolveOptionColors(names, statuses, processColors, inputWaiting, erroredProcesses) {
|
|
2638
|
+
function resolveOptionColors(names, statuses, processColors, inputWaiting, erroredProcesses, searchMatchProcesses) {
|
|
2439
2639
|
return names.map((name) => {
|
|
2440
2640
|
const status = statuses.get(name);
|
|
2441
2641
|
const waiting = inputWaiting.has(name);
|
|
2442
2642
|
const errored = erroredProcesses.has(name);
|
|
2443
|
-
const
|
|
2643
|
+
const hasSearchMatch = searchMatchProcesses?.has(name);
|
|
2644
|
+
const statusHex = hasSearchMatch ? "#b58900" : waiting ? "#ffaa00" : errored ? "#ff5555" : STATUS_ICON_HEX[status];
|
|
2444
2645
|
const processHex = processColors.get(name);
|
|
2445
2646
|
return {
|
|
2446
2647
|
iconHex: statusHex ?? processHex ?? "#888888",
|
|
@@ -2525,6 +2726,7 @@ class TabBar {
|
|
|
2525
2726
|
processColors;
|
|
2526
2727
|
inputWaiting = new Set;
|
|
2527
2728
|
erroredProcesses = new Set;
|
|
2729
|
+
searchMatchCounts = new Map;
|
|
2528
2730
|
constructor(renderer, names, colors) {
|
|
2529
2731
|
this.originalNames = names;
|
|
2530
2732
|
this.names = [...names];
|
|
@@ -2582,6 +2784,14 @@ class TabBar {
|
|
|
2582
2784
|
this.erroredProcesses.delete(name);
|
|
2583
2785
|
this.refreshOptions();
|
|
2584
2786
|
}
|
|
2787
|
+
setSearchMatches(counts) {
|
|
2788
|
+
this.searchMatchCounts = counts;
|
|
2789
|
+
this.refreshOptions();
|
|
2790
|
+
}
|
|
2791
|
+
clearSearchMatches() {
|
|
2792
|
+
this.searchMatchCounts.clear();
|
|
2793
|
+
this.refreshOptions();
|
|
2794
|
+
}
|
|
2585
2795
|
getNameAtIndex(index) {
|
|
2586
2796
|
return this.names[index];
|
|
2587
2797
|
}
|
|
@@ -2603,6 +2813,10 @@ class TabBar {
|
|
|
2603
2813
|
this.updateOptionColors();
|
|
2604
2814
|
}
|
|
2605
2815
|
getDescription(name) {
|
|
2816
|
+
const matchCount = this.searchMatchCounts.get(name);
|
|
2817
|
+
if (matchCount != null && matchCount > 0) {
|
|
2818
|
+
return `${matchCount} match${matchCount === 1 ? "" : "es"}`;
|
|
2819
|
+
}
|
|
2606
2820
|
if (this.inputWaiting.has(name))
|
|
2607
2821
|
return "awaiting input";
|
|
2608
2822
|
if (this.erroredProcesses.has(name))
|
|
@@ -2610,7 +2824,8 @@ class TabBar {
|
|
|
2610
2824
|
return this.baseDescriptions.get(name) ?? "pending";
|
|
2611
2825
|
}
|
|
2612
2826
|
updateOptionColors() {
|
|
2613
|
-
const
|
|
2827
|
+
const searchProcesses = this.searchMatchCounts.size > 0 ? new Set(this.searchMatchCounts.keys()) : undefined;
|
|
2828
|
+
const resolved = resolveOptionColors(this.names, this.statuses, this.processColors, this.inputWaiting, this.erroredProcesses, searchProcesses);
|
|
2614
2829
|
const colors = resolved.map((c) => ({
|
|
2615
2830
|
icon: parseColor(c.iconHex),
|
|
2616
2831
|
name: c.nameHex ? parseColor(c.nameHex) : null
|
|
@@ -2635,6 +2850,7 @@ class App {
|
|
|
2635
2850
|
panes = new Map;
|
|
2636
2851
|
tabBar;
|
|
2637
2852
|
statusBar;
|
|
2853
|
+
search;
|
|
2638
2854
|
activePane = null;
|
|
2639
2855
|
destroyed = false;
|
|
2640
2856
|
names;
|
|
@@ -2644,11 +2860,6 @@ class App {
|
|
|
2644
2860
|
config;
|
|
2645
2861
|
logWriter;
|
|
2646
2862
|
resizeTimer = null;
|
|
2647
|
-
searchTimer = null;
|
|
2648
|
-
searchMode = false;
|
|
2649
|
-
searchQuery = "";
|
|
2650
|
-
searchMatches = [];
|
|
2651
|
-
searchIndex = -1;
|
|
2652
2863
|
inputWaitTimers = new Map;
|
|
2653
2864
|
awaitingInput = new Set;
|
|
2654
2865
|
constructor(manager, config, logWriter) {
|
|
@@ -2698,6 +2909,14 @@ class App {
|
|
|
2698
2909
|
flexGrow: 1,
|
|
2699
2910
|
border: false
|
|
2700
2911
|
});
|
|
2912
|
+
this.statusBar = new StatusBar(this.renderer);
|
|
2913
|
+
this.search = new SearchController({
|
|
2914
|
+
logWriter: this.logWriter,
|
|
2915
|
+
statusBar: this.statusBar,
|
|
2916
|
+
tabBar: this.tabBar,
|
|
2917
|
+
getActivePane: () => this.activePane,
|
|
2918
|
+
getPane: (name) => this.panes.get(name)
|
|
2919
|
+
});
|
|
2701
2920
|
for (const name of this.names) {
|
|
2702
2921
|
const interactive = this.config.processes[name].interactive === true;
|
|
2703
2922
|
const pane = new Pane(this.renderer, name, termCols, termRows, interactive);
|
|
@@ -2710,14 +2929,13 @@ class App {
|
|
|
2710
2929
|
this.statusBar.showTemporaryMessage(`Opening ${link.url}`);
|
|
2711
2930
|
});
|
|
2712
2931
|
pane.onScroll(() => {
|
|
2713
|
-
if (this.
|
|
2714
|
-
this.
|
|
2932
|
+
if (this.search.isActive && this.search.currentMatches.length > 0 && this.activePane === name) {
|
|
2933
|
+
this.search.refreshHighlights();
|
|
2715
2934
|
}
|
|
2716
2935
|
});
|
|
2717
2936
|
this.panes.set(name, pane);
|
|
2718
2937
|
paneContainer.add(pane.scrollBox);
|
|
2719
2938
|
}
|
|
2720
|
-
this.statusBar = new StatusBar(this.renderer);
|
|
2721
2939
|
contentRow.add(sidebar);
|
|
2722
2940
|
contentRow.add(paneContainer);
|
|
2723
2941
|
layout.add(contentRow);
|
|
@@ -2759,8 +2977,8 @@ class App {
|
|
|
2759
2977
|
this.renderer.keyInput.on("keypress", (key) => {
|
|
2760
2978
|
log(key);
|
|
2761
2979
|
if (key.ctrl && key.name === "c") {
|
|
2762
|
-
if (this.
|
|
2763
|
-
this.
|
|
2980
|
+
if (this.search.isActive) {
|
|
2981
|
+
this.search.exit();
|
|
2764
2982
|
return;
|
|
2765
2983
|
}
|
|
2766
2984
|
this.shutdown().then(() => {
|
|
@@ -2768,8 +2986,8 @@ class App {
|
|
|
2768
2986
|
});
|
|
2769
2987
|
return;
|
|
2770
2988
|
}
|
|
2771
|
-
if (this.
|
|
2772
|
-
this.
|
|
2989
|
+
if (this.search.isActive) {
|
|
2990
|
+
this.search.handleInput(key);
|
|
2773
2991
|
return;
|
|
2774
2992
|
}
|
|
2775
2993
|
if (!this.activePane)
|
|
@@ -2794,7 +3012,7 @@ class App {
|
|
|
2794
3012
|
return;
|
|
2795
3013
|
}
|
|
2796
3014
|
if (name === SHORTCUTS.search.key) {
|
|
2797
|
-
this.
|
|
3015
|
+
this.search.enter();
|
|
2798
3016
|
return;
|
|
2799
3017
|
}
|
|
2800
3018
|
if (name === SHORTCUTS.restart.key) {
|
|
@@ -2858,14 +3076,16 @@ class App {
|
|
|
2858
3076
|
switchPane(name) {
|
|
2859
3077
|
if (this.activePane === name)
|
|
2860
3078
|
return;
|
|
2861
|
-
if (this.
|
|
2862
|
-
this.
|
|
3079
|
+
if (this.search.isActive && !this.search.isAllMode) {
|
|
3080
|
+
this.search.exit();
|
|
2863
3081
|
}
|
|
2864
3082
|
if (this.activePane) {
|
|
3083
|
+
this.panes.get(this.activePane)?.clearHighlights();
|
|
2865
3084
|
this.panes.get(this.activePane)?.hide();
|
|
2866
3085
|
}
|
|
2867
3086
|
this.activePane = name;
|
|
2868
3087
|
this.panes.get(name)?.show();
|
|
3088
|
+
this.search.onPaneSwitch();
|
|
2869
3089
|
}
|
|
2870
3090
|
checkInputWaiting(name, data) {
|
|
2871
3091
|
const existing = this.inputWaitTimers.get(name);
|
|
@@ -2926,102 +3146,6 @@ class App {
|
|
|
2926
3146
|
this.copyToClipboard(text);
|
|
2927
3147
|
this.statusBar.showTemporaryMessage("Copied all output!");
|
|
2928
3148
|
}
|
|
2929
|
-
enterSearch() {
|
|
2930
|
-
this.searchMode = true;
|
|
2931
|
-
this.searchQuery = "";
|
|
2932
|
-
this.searchMatches = [];
|
|
2933
|
-
this.searchIndex = -1;
|
|
2934
|
-
this.statusBar.setSearchMode(true);
|
|
2935
|
-
}
|
|
2936
|
-
exitSearch() {
|
|
2937
|
-
this.searchMode = false;
|
|
2938
|
-
this.searchQuery = "";
|
|
2939
|
-
this.searchMatches = [];
|
|
2940
|
-
this.searchIndex = -1;
|
|
2941
|
-
if (this.searchTimer) {
|
|
2942
|
-
clearTimeout(this.searchTimer);
|
|
2943
|
-
this.searchTimer = null;
|
|
2944
|
-
}
|
|
2945
|
-
if (this.activePane) {
|
|
2946
|
-
this.panes.get(this.activePane)?.clearHighlights();
|
|
2947
|
-
}
|
|
2948
|
-
this.statusBar.setSearchMode(false);
|
|
2949
|
-
}
|
|
2950
|
-
handleSearchInput(key) {
|
|
2951
|
-
if (key.name === "escape") {
|
|
2952
|
-
this.exitSearch();
|
|
2953
|
-
return;
|
|
2954
|
-
}
|
|
2955
|
-
if (key.name === "return") {
|
|
2956
|
-
if (this.searchMatches.length === 0)
|
|
2957
|
-
return;
|
|
2958
|
-
if (key.shift) {
|
|
2959
|
-
this.searchIndex = (this.searchIndex - 1 + this.searchMatches.length) % this.searchMatches.length;
|
|
2960
|
-
} else {
|
|
2961
|
-
this.searchIndex = (this.searchIndex + 1) % this.searchMatches.length;
|
|
2962
|
-
}
|
|
2963
|
-
this.scrollToCurrentMatch();
|
|
2964
|
-
this.updateSearchHighlights();
|
|
2965
|
-
return;
|
|
2966
|
-
}
|
|
2967
|
-
if (key.name === "backspace") {
|
|
2968
|
-
if (this.searchQuery.length > 0) {
|
|
2969
|
-
this.searchQuery = this.searchQuery.slice(0, -1);
|
|
2970
|
-
this.scheduleSearch();
|
|
2971
|
-
}
|
|
2972
|
-
return;
|
|
2973
|
-
}
|
|
2974
|
-
if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
|
|
2975
|
-
this.searchQuery += key.sequence;
|
|
2976
|
-
this.scheduleSearch();
|
|
2977
|
-
}
|
|
2978
|
-
}
|
|
2979
|
-
scheduleSearch() {
|
|
2980
|
-
this.statusBar.setSearchMode(true, this.searchQuery, this.searchMatches.length, this.searchIndex);
|
|
2981
|
-
if (this.searchTimer)
|
|
2982
|
-
clearTimeout(this.searchTimer);
|
|
2983
|
-
this.searchTimer = setTimeout(() => {
|
|
2984
|
-
this.searchTimer = null;
|
|
2985
|
-
this.runSearch();
|
|
2986
|
-
}, 100);
|
|
2987
|
-
}
|
|
2988
|
-
async runSearch() {
|
|
2989
|
-
if (!this.activePane)
|
|
2990
|
-
return;
|
|
2991
|
-
const query = this.searchQuery;
|
|
2992
|
-
const activeName = this.activePane;
|
|
2993
|
-
const matches = await this.logWriter.search(activeName, query);
|
|
2994
|
-
if (!this.searchMode || this.searchQuery !== query || this.activePane !== activeName)
|
|
2995
|
-
return;
|
|
2996
|
-
this.searchMatches = matches;
|
|
2997
|
-
this.searchIndex = matches.length > 0 ? 0 : -1;
|
|
2998
|
-
this.updateSearchHighlights();
|
|
2999
|
-
if (this.searchIndex >= 0) {
|
|
3000
|
-
this.scrollToCurrentMatch();
|
|
3001
|
-
}
|
|
3002
|
-
}
|
|
3003
|
-
updateSearchHighlights() {
|
|
3004
|
-
if (!this.activePane)
|
|
3005
|
-
return;
|
|
3006
|
-
const pane = this.panes.get(this.activePane);
|
|
3007
|
-
if (!pane)
|
|
3008
|
-
return;
|
|
3009
|
-
if (this.searchMatches.length > 0) {
|
|
3010
|
-
pane.setHighlights(this.searchMatches, this.searchIndex);
|
|
3011
|
-
} else {
|
|
3012
|
-
pane.clearHighlights();
|
|
3013
|
-
}
|
|
3014
|
-
this.statusBar.setSearchMode(true, this.searchQuery, this.searchMatches.length, this.searchIndex);
|
|
3015
|
-
}
|
|
3016
|
-
scrollToCurrentMatch() {
|
|
3017
|
-
if (!this.activePane || this.searchIndex < 0)
|
|
3018
|
-
return;
|
|
3019
|
-
const pane = this.panes.get(this.activePane);
|
|
3020
|
-
if (!pane)
|
|
3021
|
-
return;
|
|
3022
|
-
const match = this.searchMatches[this.searchIndex];
|
|
3023
|
-
pane.scrollToLine(match.line);
|
|
3024
|
-
}
|
|
3025
3149
|
async shutdown() {
|
|
3026
3150
|
if (this.destroyed)
|
|
3027
3151
|
return;
|
|
@@ -3030,10 +3154,7 @@ class App {
|
|
|
3030
3154
|
clearTimeout(this.resizeTimer);
|
|
3031
3155
|
this.resizeTimer = null;
|
|
3032
3156
|
}
|
|
3033
|
-
|
|
3034
|
-
clearTimeout(this.searchTimer);
|
|
3035
|
-
this.searchTimer = null;
|
|
3036
|
-
}
|
|
3157
|
+
this.search.dispose();
|
|
3037
3158
|
for (const timer of this.inputWaitTimers.values()) {
|
|
3038
3159
|
clearTimeout(timer);
|
|
3039
3160
|
}
|
|
@@ -3237,9 +3358,9 @@ ${DIM}Done in ${elapsed}${RESET}
|
|
|
3237
3358
|
}
|
|
3238
3359
|
|
|
3239
3360
|
// src/utils/log-writer.ts
|
|
3240
|
-
import { closeSync, mkdirSync as mkdirSync2, openSync, rmSync, writeSync } from "fs";
|
|
3361
|
+
import { closeSync, mkdirSync as mkdirSync2, openSync, rmSync, symlinkSync, unlinkSync, writeSync } from "fs";
|
|
3241
3362
|
import { tmpdir } from "os";
|
|
3242
|
-
import { join } from "path";
|
|
3363
|
+
import { basename as basename2, join } from "path";
|
|
3243
3364
|
class LogWriter {
|
|
3244
3365
|
dir;
|
|
3245
3366
|
isTemp;
|
|
@@ -3255,6 +3376,30 @@ class LogWriter {
|
|
|
3255
3376
|
const dir = join(tmpdir(), `numux-${process.pid}`);
|
|
3256
3377
|
return new LogWriter(dir, true);
|
|
3257
3378
|
}
|
|
3379
|
+
static createPersistent(baseDir) {
|
|
3380
|
+
mkdirSync2(baseDir, { recursive: true });
|
|
3381
|
+
const now = new Date;
|
|
3382
|
+
const ts = now.toISOString().replace(/:/g, "-").replace(/\.\d+Z$/, "");
|
|
3383
|
+
const sessionDir = join(baseDir, ts);
|
|
3384
|
+
mkdirSync2(sessionDir, { recursive: true });
|
|
3385
|
+
const latestLink = join(baseDir, "latest");
|
|
3386
|
+
try {
|
|
3387
|
+
unlinkSync(latestLink);
|
|
3388
|
+
} catch {}
|
|
3389
|
+
try {
|
|
3390
|
+
symlinkSync(sessionDir, latestLink);
|
|
3391
|
+
} catch {}
|
|
3392
|
+
return new LogWriter(sessionDir, false);
|
|
3393
|
+
}
|
|
3394
|
+
get isTemporary() {
|
|
3395
|
+
return this.isTemp;
|
|
3396
|
+
}
|
|
3397
|
+
getDirectory() {
|
|
3398
|
+
return this.dir;
|
|
3399
|
+
}
|
|
3400
|
+
getProcessNames() {
|
|
3401
|
+
return [...this.files.keys()];
|
|
3402
|
+
}
|
|
3258
3403
|
errored = false;
|
|
3259
3404
|
handleEvent = (event) => {
|
|
3260
3405
|
if (event.type !== "output" || this.errored)
|
|
@@ -3326,6 +3471,58 @@ class LogWriter {
|
|
|
3326
3471
|
return [];
|
|
3327
3472
|
}
|
|
3328
3473
|
}
|
|
3474
|
+
async searchAll(query) {
|
|
3475
|
+
if (!query)
|
|
3476
|
+
return [];
|
|
3477
|
+
const paths = [...this.files.keys()].map((name) => join(this.dir, `${name}.log`));
|
|
3478
|
+
if (paths.length === 0)
|
|
3479
|
+
return [];
|
|
3480
|
+
try {
|
|
3481
|
+
const proc = Bun.spawn(["grep", "-inFH", query, ...paths], {
|
|
3482
|
+
stdout: "pipe",
|
|
3483
|
+
stderr: "ignore"
|
|
3484
|
+
});
|
|
3485
|
+
const output = await new Response(proc.stdout).text();
|
|
3486
|
+
await proc.exited;
|
|
3487
|
+
const matches = [];
|
|
3488
|
+
const lowerQuery = query.toLowerCase();
|
|
3489
|
+
for (const line of output.split(`
|
|
3490
|
+
`)) {
|
|
3491
|
+
if (!line)
|
|
3492
|
+
continue;
|
|
3493
|
+
const firstColon = line.indexOf(":");
|
|
3494
|
+
if (firstColon === -1)
|
|
3495
|
+
continue;
|
|
3496
|
+
const filePath = line.slice(0, firstColon);
|
|
3497
|
+
const rest = line.slice(firstColon + 1);
|
|
3498
|
+
const secondColon = rest.indexOf(":");
|
|
3499
|
+
if (secondColon === -1)
|
|
3500
|
+
continue;
|
|
3501
|
+
const lineNumber = Number.parseInt(rest.slice(0, secondColon), 10);
|
|
3502
|
+
if (Number.isNaN(lineNumber))
|
|
3503
|
+
continue;
|
|
3504
|
+
const fileName = basename2(filePath);
|
|
3505
|
+
const processName = fileName.replace(/\.log$/, "");
|
|
3506
|
+
const lineText = rest.slice(secondColon + 1).toLowerCase();
|
|
3507
|
+
let pos = 0;
|
|
3508
|
+
while (true) {
|
|
3509
|
+
const idx = lineText.indexOf(lowerQuery, pos);
|
|
3510
|
+
if (idx === -1)
|
|
3511
|
+
break;
|
|
3512
|
+
matches.push({
|
|
3513
|
+
process: processName,
|
|
3514
|
+
line: lineNumber - 1,
|
|
3515
|
+
start: idx,
|
|
3516
|
+
end: idx + query.length
|
|
3517
|
+
});
|
|
3518
|
+
pos = idx + 1;
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
return matches;
|
|
3522
|
+
} catch {
|
|
3523
|
+
return [];
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3329
3526
|
truncate(name) {
|
|
3330
3527
|
const fd = this.files.get(name);
|
|
3331
3528
|
if (fd === undefined)
|
|
@@ -3362,6 +3559,10 @@ function setupShutdownHandlers(app, logWriter) {
|
|
|
3362
3559
|
}
|
|
3363
3560
|
shuttingDown = true;
|
|
3364
3561
|
app.shutdown().finally(() => {
|
|
3562
|
+
if (logWriter && !logWriter.isTemporary) {
|
|
3563
|
+
process.stderr.write(`Logs saved to: ${logWriter.getDirectory()}
|
|
3564
|
+
`);
|
|
3565
|
+
}
|
|
3365
3566
|
logWriter?.cleanup();
|
|
3366
3567
|
process.exit(app.hasFailures() ? 1 : 0);
|
|
3367
3568
|
});
|
|
@@ -3591,7 +3792,7 @@ async function main() {
|
|
|
3591
3792
|
}
|
|
3592
3793
|
const manager = new ProcessManager(config);
|
|
3593
3794
|
const logDir = parsed.logDir ?? config.logDir;
|
|
3594
|
-
const logWriter = logDir ?
|
|
3795
|
+
const logWriter = logDir ? LogWriter.createPersistent(logDir) : LogWriter.createTemp();
|
|
3595
3796
|
printWarnings(warnings);
|
|
3596
3797
|
const usePrefix = parsed.prefix || config.prefix;
|
|
3597
3798
|
if (usePrefix) {
|
package/dist/types.d.ts
CHANGED