startall 0.0.11 → 0.0.13
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 +88 -43
- package/index.js +428 -194
- package/package.json +3 -2
- package/screenshot.png +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
# 🚀
|
|
1
|
+
# 🚀 startall
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> A powerful, interactive terminal UI for managing multiple npm scripts with tmux-style panes, filtering, and real-time control
|
|
4
|
+
|
|
5
|
+

|
|
4
6
|
|
|
5
7
|
## The Problem
|
|
6
8
|
|
|
@@ -20,47 +22,47 @@ Traditional solutions fall short:
|
|
|
20
22
|
|
|
21
23
|
## The Solution
|
|
22
24
|
|
|
23
|
-
**
|
|
24
|
-
|
|
25
|
-
```
|
|
26
|
-
┌─ Starting in 7s... [Enter to start now] ─────────────┐
|
|
27
|
-
│ [x] frontend (npm run start:frontend) │
|
|
28
|
-
│ [x] backend (npm run start:backend) │
|
|
29
|
-
│ [ ] worker (npm run start:worker) │
|
|
30
|
-
│ [x] db (npm run start:db) │
|
|
31
|
-
│ │
|
|
32
|
-
│ ↑/↓ Navigate | Space: Toggle | Enter: Start │
|
|
33
|
-
└───────────────────────────────────────────────────────┘
|
|
34
|
-
|
|
35
|
-
After starting:
|
|
36
|
-
┌─ Processes ──────────────┬─ Output (filter: error) ───┐
|
|
37
|
-
│ [f] frontend ● Running │ [backend] Error: ECONNREF │
|
|
38
|
-
│ [b] backend ✖ Crashed │ [backend] Retrying... │
|
|
39
|
-
│ [w] worker ⏸ Stopped │ [frontend] Started on 3000 │
|
|
40
|
-
│ [d] db ● Running │ │
|
|
41
|
-
│ │ │
|
|
42
|
-
│ Space: Start/Stop │ │
|
|
43
|
-
│ r: Restart │ │
|
|
44
|
-
│ /: Filter output │ │
|
|
45
|
-
└──────────────────────────┴────────────────────────────┘
|
|
46
|
-
```
|
|
25
|
+
**startall** is a sophisticated TUI that combines the power of tmux with the simplicity of npm scripts, giving you complete control over your development processes with split panes, filtering, and interactive controls.
|
|
47
26
|
|
|
48
27
|
## Features
|
|
49
28
|
|
|
50
|
-
###
|
|
51
|
-
- **Auto-discovery**:
|
|
52
|
-
- **Smart defaults**: Remembers your last selection
|
|
53
|
-
- **10-second countdown**:
|
|
29
|
+
### 🎯 Core Features
|
|
30
|
+
- **Auto-discovery**: Automatically reads all scripts from `package.json`
|
|
31
|
+
- **Smart defaults**: Remembers your last selection in `startall.json`
|
|
32
|
+
- **10-second countdown**: Review selections before starting
|
|
54
33
|
- **Parallel execution**: Run multiple npm scripts simultaneously
|
|
55
|
-
- **
|
|
56
|
-
|
|
57
|
-
### 🚧 Planned
|
|
58
|
-
- **Live status monitoring**: See which processes are running/crashed/stopped at a glance
|
|
59
|
-
- **Interactive controls**: Start, stop, and restart individual processes with keyboard shortcuts
|
|
60
|
-
- **Output filtering**: Search/filter logs across all processes in real-time
|
|
34
|
+
- **Live status monitoring**: Real-time status indicators (● running, ✖ crashed, ○ stopped)
|
|
35
|
+
- **Interactive controls**: Start, stop, and restart individual processes on the fly
|
|
61
36
|
- **Cross-platform**: Works identically on Windows, Linux, and macOS
|
|
62
|
-
|
|
63
|
-
|
|
37
|
+
|
|
38
|
+
### 🎨 Advanced UI
|
|
39
|
+
- **Multi-pane layout**: tmux-inspired split panes (vertical & horizontal)
|
|
40
|
+
- **Flexible filtering**:
|
|
41
|
+
- Text search across all output (`/`)
|
|
42
|
+
- Filter by ANSI color (red/yellow/green/blue/cyan/magenta) (`c`)
|
|
43
|
+
- Per-process visibility toggles (`Space` or `1-9`)
|
|
44
|
+
- Per-pane filters (different views in each pane)
|
|
45
|
+
- **Custom pane naming**: Label panes for easier identification (`n`)
|
|
46
|
+
- **Persistent layouts**: Your pane configuration is saved between sessions
|
|
47
|
+
- **Process-specific views**: Show/hide specific processes in each pane
|
|
48
|
+
- **Colored output**: Each process gets unique color-coded output
|
|
49
|
+
- **Pause/resume**: Freeze output to review logs (`p`)
|
|
50
|
+
- **Scrollable history**: 1000-line buffer with mouse wheel support
|
|
51
|
+
- **Enhanced navigation**: Home/End/PageUp/PageDown keys
|
|
52
|
+
|
|
53
|
+
### ⚙️ Display Options
|
|
54
|
+
- **Toggleable line numbers**: Show/hide line numbers (`#`)
|
|
55
|
+
- **Timestamps**: Show/hide timestamps for each log line (`t`)
|
|
56
|
+
- **Quick process toggle**: Use number keys `1-9` for instant visibility control
|
|
57
|
+
|
|
58
|
+
### 🔧 Advanced Controls
|
|
59
|
+
- **Interactive input mode**: Send commands to running processes via stdin (`i`)
|
|
60
|
+
- Perfect for dev servers that accept commands (Vite, Rust watch, etc.)
|
|
61
|
+
- **Settings panel**: Configure ignore/include patterns (`o`)
|
|
62
|
+
- Wildcard support (`*`) for pattern matching
|
|
63
|
+
- Per-script visibility toggles
|
|
64
|
+
- **Keyboard & mouse support**: Full keyboard navigation + mouse clicking/scrolling
|
|
65
|
+
- **VSCode integration**: Optimized for VSCode integrated terminal
|
|
64
66
|
|
|
65
67
|
## Installation
|
|
66
68
|
|
|
@@ -90,14 +92,57 @@ That's it! The TUI will:
|
|
|
90
92
|
- `↑`/`↓` - Navigate scripts
|
|
91
93
|
- `Space` - Toggle selection
|
|
92
94
|
- `Enter` - Start immediately (skip countdown)
|
|
95
|
+
- `o` - Open settings
|
|
93
96
|
- `Ctrl+C` - Exit
|
|
94
97
|
|
|
95
|
-
**Running Screen
|
|
96
|
-
|
|
98
|
+
**Running Screen:**
|
|
99
|
+
|
|
100
|
+
*Process Control:*
|
|
101
|
+
- `1-9` - Quick toggle process visibility in focused pane
|
|
102
|
+
- `Space` - Toggle visibility of selected process
|
|
103
|
+
- `s` - Stop/start selected process
|
|
97
104
|
- `r` - Restart selected process
|
|
98
|
-
-
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
- `i` - Send input to selected process (interactive mode)
|
|
106
|
+
|
|
107
|
+
*Pane Management:*
|
|
108
|
+
- `\` - Open command palette
|
|
109
|
+
- `|` - Split pane vertically (left/right)
|
|
110
|
+
- `_` - Split pane horizontally (top/bottom)
|
|
111
|
+
- `x` - Close current pane (if >1 pane exists)
|
|
112
|
+
- `Tab` - Next pane
|
|
113
|
+
- `Shift+Tab` - Previous pane
|
|
114
|
+
- `n` - Name current pane
|
|
115
|
+
|
|
116
|
+
*Filtering & View:*
|
|
117
|
+
- `/` - Enter text filter mode
|
|
118
|
+
- `c` - Cycle color filter (red/yellow/green/blue/cyan/magenta/none)
|
|
119
|
+
- `f` - Filter to selected process only
|
|
120
|
+
- `Esc` - Clear filters
|
|
121
|
+
- `p` - Pause/resume output scrolling
|
|
122
|
+
- `#` - Toggle line numbers
|
|
123
|
+
- `t` - Toggle timestamps
|
|
124
|
+
|
|
125
|
+
*Navigation:*
|
|
126
|
+
- `↑`/`↓` or `k`/`j` - Select process (vim-style)
|
|
127
|
+
- `←`/`→` or `h`/`l` - Select process (vim-style)
|
|
128
|
+
- `Home` - Scroll to top of pane
|
|
129
|
+
- `End` - Scroll to bottom of pane
|
|
130
|
+
- `Page Up` - Scroll up one page
|
|
131
|
+
- `Page Down` - Scroll down one page
|
|
132
|
+
- `Mouse wheel` - Scroll output
|
|
133
|
+
|
|
134
|
+
*Other:*
|
|
135
|
+
- `o` - Open settings
|
|
136
|
+
- `q` - Quit (stops all processes)
|
|
137
|
+
- `Ctrl+C` - Force quit
|
|
138
|
+
|
|
139
|
+
**Settings Screen:**
|
|
140
|
+
- `Tab`/`←`/`→` - Switch sections (Ignore/Include/Scripts)
|
|
141
|
+
- `↑`/`↓` - Navigate items
|
|
142
|
+
- `a` - Add new pattern (Ignore/Include sections)
|
|
143
|
+
- `d` or `Backspace` - Delete pattern
|
|
144
|
+
- `Space` or `Enter` - Toggle script (Scripts section)
|
|
145
|
+
- `Esc` or `q` - Return to previous screen
|
|
101
146
|
|
|
102
147
|
## Why Build This?
|
|
103
148
|
|
package/index.js
CHANGED
|
@@ -15,6 +15,16 @@ const APP_VERSION = 'v0.0.4';
|
|
|
15
15
|
// Detect if running inside VS Code's integrated terminal
|
|
16
16
|
const IS_VSCODE = process.env.TERM_PROGRAM === 'vscode';
|
|
17
17
|
|
|
18
|
+
// VSCode-specific optimizations
|
|
19
|
+
const VSCODE_CONFIG = {
|
|
20
|
+
// VSCode terminal has better mouse support
|
|
21
|
+
enhancedMouse: IS_VSCODE,
|
|
22
|
+
// VSCode can detect and linkify file paths (file:///path/to/file.js:line:col)
|
|
23
|
+
fileLinking: IS_VSCODE,
|
|
24
|
+
// Some key combinations are captured by VSCode
|
|
25
|
+
remapKeys: IS_VSCODE,
|
|
26
|
+
};
|
|
27
|
+
|
|
18
28
|
// Pane ID generator
|
|
19
29
|
let paneIdCounter = 0;
|
|
20
30
|
function generatePaneId() {
|
|
@@ -328,6 +338,7 @@ class ProcessManager {
|
|
|
328
338
|
this.processes = new Map();
|
|
329
339
|
this.processRefs = new Map();
|
|
330
340
|
this.outputLines = [];
|
|
341
|
+
this.totalLinesReceived = 0; // Track total lines ever received (never resets)
|
|
331
342
|
this.filter = '';
|
|
332
343
|
this.maxOutputLines = 1000;
|
|
333
344
|
this.maxVisibleLines = null; // Calculated dynamically based on screen height
|
|
@@ -336,9 +347,13 @@ class ProcessManager {
|
|
|
336
347
|
this.isFilterMode = false; // Whether in filter input mode
|
|
337
348
|
this.isNamingMode = false; // Whether in pane naming input mode
|
|
338
349
|
this.namingModeText = ''; // Text being typed for pane name
|
|
350
|
+
this.showLineNumbers = this.config.showLineNumbers !== undefined ? this.config.showLineNumbers : true; // Whether to show line numbers
|
|
351
|
+
this.showTimestamps = this.config.showTimestamps !== undefined ? this.config.showTimestamps : false; // Whether to show timestamps
|
|
352
|
+
this.isInputMode = false; // Whether in stdin input mode
|
|
353
|
+
this.inputModeText = ''; // Text being typed for stdin
|
|
339
354
|
|
|
340
355
|
// Settings menu state
|
|
341
|
-
this.settingsSection = '
|
|
356
|
+
this.settingsSection = 'display'; // 'display' | 'ignore' | 'include' | 'scripts'
|
|
342
357
|
this.settingsIndex = 0; // Current selection index within section
|
|
343
358
|
this.isAddingPattern = false; // Whether typing a new pattern
|
|
344
359
|
this.newPatternText = ''; // Text being typed for new pattern
|
|
@@ -426,10 +441,25 @@ class ProcessManager {
|
|
|
426
441
|
clearInterval(this.countdownInterval);
|
|
427
442
|
this.previousPhase = 'selection';
|
|
428
443
|
this.phase = 'settings';
|
|
429
|
-
this.settingsSection = '
|
|
444
|
+
this.settingsSection = 'display';
|
|
430
445
|
this.settingsIndex = 0;
|
|
431
446
|
this.buildSettingsUI();
|
|
432
447
|
return;
|
|
448
|
+
} else if (keyName >= '1' && keyName <= '9') {
|
|
449
|
+
// Toggle script by number (1-9)
|
|
450
|
+
const index = parseInt(keyName) - 1;
|
|
451
|
+
if (index >= 0 && index < this.scripts.length) {
|
|
452
|
+
const scriptName = this.scripts[index]?.name;
|
|
453
|
+
if (scriptName) {
|
|
454
|
+
if (this.selectedScripts.has(scriptName)) {
|
|
455
|
+
this.selectedScripts.delete(scriptName);
|
|
456
|
+
} else {
|
|
457
|
+
this.selectedScripts.add(scriptName);
|
|
458
|
+
}
|
|
459
|
+
// Reset countdown when selection changes
|
|
460
|
+
this.countdown = COUNTDOWN_SECONDS;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
433
463
|
}
|
|
434
464
|
} else if (this.phase === 'settings') {
|
|
435
465
|
this.handleSettingsInput(keyName, keyEvent);
|
|
@@ -441,8 +471,34 @@ class ProcessManager {
|
|
|
441
471
|
return;
|
|
442
472
|
}
|
|
443
473
|
|
|
474
|
+
// If in input mode (stdin), handle stdin input
|
|
475
|
+
if (this.isInputMode) {
|
|
476
|
+
const scriptName = this.scripts[this.selectedIndex]?.name;
|
|
477
|
+
if (keyName === 'escape') {
|
|
478
|
+
this.isInputMode = false;
|
|
479
|
+
this.inputModeText = '';
|
|
480
|
+
this.buildRunningUI();
|
|
481
|
+
} else if (keyName === 'enter' || keyName === 'return') {
|
|
482
|
+
// Send the input to the selected process
|
|
483
|
+
if (scriptName && this.inputModeText.trim()) {
|
|
484
|
+
this.sendInputToProcess(scriptName, this.inputModeText + '\n');
|
|
485
|
+
}
|
|
486
|
+
this.isInputMode = false;
|
|
487
|
+
this.inputModeText = '';
|
|
488
|
+
this.buildRunningUI();
|
|
489
|
+
} else if (keyName === 'backspace') {
|
|
490
|
+
this.inputModeText = this.inputModeText.slice(0, -1);
|
|
491
|
+
this.buildRunningUI();
|
|
492
|
+
} else if (keyName === 'space') {
|
|
493
|
+
this.inputModeText += ' ';
|
|
494
|
+
this.buildRunningUI();
|
|
495
|
+
} else if (keyName && keyName.length === 1 && !keyEvent.ctrl && !keyEvent.meta) {
|
|
496
|
+
this.inputModeText += keyName;
|
|
497
|
+
this.buildRunningUI();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
444
500
|
// If in naming mode, handle name input
|
|
445
|
-
if (this.isNamingMode) {
|
|
501
|
+
else if (this.isNamingMode) {
|
|
446
502
|
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
447
503
|
if (keyName === 'escape') {
|
|
448
504
|
this.isNamingMode = false;
|
|
@@ -548,12 +604,15 @@ class ProcessManager {
|
|
|
548
604
|
this.selectedIndex = Math.min(this.scripts.length - 1, this.selectedIndex + 1);
|
|
549
605
|
this.buildRunningUI(); // Rebuild to show selection change
|
|
550
606
|
} else if (keyName === 'left' || keyName === 'h') {
|
|
551
|
-
// Navigate processes left
|
|
552
|
-
this.selectedIndex =
|
|
607
|
+
// Navigate processes left with wrapping
|
|
608
|
+
this.selectedIndex = this.selectedIndex - 1;
|
|
609
|
+
if (this.selectedIndex < 0) {
|
|
610
|
+
this.selectedIndex = this.scripts.length - 1;
|
|
611
|
+
}
|
|
553
612
|
this.buildRunningUI(); // Rebuild to show selection change
|
|
554
613
|
} else if (keyName === 'right' || keyName === 'l') {
|
|
555
|
-
// Navigate processes right
|
|
556
|
-
this.selectedIndex =
|
|
614
|
+
// Navigate processes right with wrapping
|
|
615
|
+
this.selectedIndex = (this.selectedIndex + 1) % this.scripts.length;
|
|
557
616
|
this.buildRunningUI(); // Rebuild to show selection change
|
|
558
617
|
} else if (keyName === 'r') {
|
|
559
618
|
const scriptName = this.scripts[this.selectedIndex]?.name;
|
|
@@ -567,13 +626,13 @@ class ProcessManager {
|
|
|
567
626
|
this.toggleProcess(scriptName);
|
|
568
627
|
}
|
|
569
628
|
} else if (keyName === 'o') {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
629
|
+
// Open settings (options)
|
|
630
|
+
this.previousPhase = 'running';
|
|
631
|
+
this.phase = 'settings';
|
|
632
|
+
this.settingsSection = 'display';
|
|
633
|
+
this.settingsIndex = 0;
|
|
634
|
+
this.buildSettingsUI();
|
|
635
|
+
return;
|
|
577
636
|
} else if (keyName === 'c') {
|
|
578
637
|
// Cycle color filter on focused pane
|
|
579
638
|
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
@@ -591,6 +650,35 @@ class ProcessManager {
|
|
|
591
650
|
// Navigate to previous pane
|
|
592
651
|
this.navigateToNextPane(-1);
|
|
593
652
|
this.buildRunningUI();
|
|
653
|
+
} else if (keyName === 'home') {
|
|
654
|
+
// Scroll to top of focused pane
|
|
655
|
+
this.scrollFocusedPane('home');
|
|
656
|
+
} else if (keyName === 'end') {
|
|
657
|
+
// Scroll to bottom of focused pane
|
|
658
|
+
this.scrollFocusedPane('end');
|
|
659
|
+
} else if (keyName === 'pageup') {
|
|
660
|
+
// Scroll up one page in focused pane
|
|
661
|
+
this.scrollFocusedPane('pageup');
|
|
662
|
+
} else if (keyName === 'pagedown') {
|
|
663
|
+
// Scroll down one page in focused pane
|
|
664
|
+
this.scrollFocusedPane('pagedown');
|
|
665
|
+
} else if (keyName >= '1' && keyName <= '9') {
|
|
666
|
+
// Toggle process by number (1-9)
|
|
667
|
+
const index = parseInt(keyName) - 1;
|
|
668
|
+
if (index >= 0 && index < this.scripts.length) {
|
|
669
|
+
this.selectedIndex = index;
|
|
670
|
+
this.toggleProcessVisibility();
|
|
671
|
+
this.buildRunningUI();
|
|
672
|
+
}
|
|
673
|
+
} else if (keyName === 'i') {
|
|
674
|
+
// Enter input mode to send stdin to selected process
|
|
675
|
+
const scriptName = this.scripts[this.selectedIndex]?.name;
|
|
676
|
+
const proc = this.processes.get(scriptName);
|
|
677
|
+
if (scriptName && proc?.status === 'running') {
|
|
678
|
+
this.isInputMode = true;
|
|
679
|
+
this.inputModeText = '';
|
|
680
|
+
this.buildRunningUI();
|
|
681
|
+
}
|
|
594
682
|
}
|
|
595
683
|
}
|
|
596
684
|
}
|
|
@@ -734,6 +822,7 @@ class ProcessManager {
|
|
|
734
822
|
process: processName,
|
|
735
823
|
text,
|
|
736
824
|
timestamp: Date.now(),
|
|
825
|
+
lineNumber: ++this.totalLinesReceived, // Track absolute line number
|
|
737
826
|
});
|
|
738
827
|
|
|
739
828
|
if (this.outputLines.length > this.maxOutputLines) {
|
|
@@ -748,13 +837,10 @@ class ProcessManager {
|
|
|
748
837
|
}
|
|
749
838
|
|
|
750
839
|
scheduleRender() {
|
|
751
|
-
//
|
|
752
|
-
if (this.
|
|
753
|
-
this.renderScheduled = true;
|
|
754
|
-
setTimeout(() => {
|
|
755
|
-
this.renderScheduled = false;
|
|
840
|
+
// Update the DOM - OpenTUI's render loop will pick up changes automatically
|
|
841
|
+
if (!this.destroyed) {
|
|
756
842
|
this.render();
|
|
757
|
-
}
|
|
843
|
+
}
|
|
758
844
|
}
|
|
759
845
|
|
|
760
846
|
stopProcess(scriptName) {
|
|
@@ -789,6 +875,19 @@ class ProcessManager {
|
|
|
789
875
|
this.startProcess(scriptName);
|
|
790
876
|
}
|
|
791
877
|
}
|
|
878
|
+
|
|
879
|
+
sendInputToProcess(scriptName, input) {
|
|
880
|
+
const proc = this.processRefs.get(scriptName);
|
|
881
|
+
if (proc && proc.stdin && proc.stdin.writable) {
|
|
882
|
+
try {
|
|
883
|
+
proc.stdin.write(input);
|
|
884
|
+
// Echo the input in the output for visibility
|
|
885
|
+
this.addOutputLine(scriptName, `> ${input.trim()}`);
|
|
886
|
+
} catch (err) {
|
|
887
|
+
this.addOutputLine(scriptName, `Error sending input: ${err.message}`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
792
891
|
|
|
793
892
|
handleSettingsInput(keyName, keyEvent) {
|
|
794
893
|
// Handle text input mode for adding patterns
|
|
@@ -825,6 +924,14 @@ class ProcessManager {
|
|
|
825
924
|
|
|
826
925
|
// Normal settings navigation
|
|
827
926
|
if (keyName === 'escape' || keyName === 'q') {
|
|
927
|
+
// Apply filters before returning (updates this.scripts)
|
|
928
|
+
this.applyFilters();
|
|
929
|
+
|
|
930
|
+
// Ensure selectedIndex is within bounds after filter changes
|
|
931
|
+
if (this.selectedIndex >= this.scripts.length) {
|
|
932
|
+
this.selectedIndex = Math.max(0, this.scripts.length - 1);
|
|
933
|
+
}
|
|
934
|
+
|
|
828
935
|
// Return to previous phase
|
|
829
936
|
if (this.previousPhase === 'running') {
|
|
830
937
|
this.phase = 'running';
|
|
@@ -837,46 +944,76 @@ class ProcessManager {
|
|
|
837
944
|
}
|
|
838
945
|
} else if (keyName === 'tab' || keyName === 'right') {
|
|
839
946
|
// Switch section
|
|
840
|
-
const sections = ['ignore', 'include', 'scripts'];
|
|
947
|
+
const sections = ['display', 'ignore', 'include', 'scripts'];
|
|
841
948
|
const idx = sections.indexOf(this.settingsSection);
|
|
842
949
|
this.settingsSection = sections[(idx + 1) % sections.length];
|
|
843
950
|
this.settingsIndex = 0;
|
|
844
951
|
this.buildSettingsUI();
|
|
845
952
|
} else if (keyEvent.shift && keyName === 'tab') {
|
|
846
953
|
// Switch section backwards
|
|
847
|
-
const sections = ['ignore', 'include', 'scripts'];
|
|
954
|
+
const sections = ['display', 'ignore', 'include', 'scripts'];
|
|
848
955
|
const idx = sections.indexOf(this.settingsSection);
|
|
849
956
|
this.settingsSection = sections[(idx - 1 + sections.length) % sections.length];
|
|
850
957
|
this.settingsIndex = 0;
|
|
851
958
|
this.buildSettingsUI();
|
|
852
959
|
} else if (keyName === 'left') {
|
|
853
960
|
// Switch section backwards
|
|
854
|
-
const sections = ['ignore', 'include', 'scripts'];
|
|
961
|
+
const sections = ['display', 'ignore', 'include', 'scripts'];
|
|
855
962
|
const idx = sections.indexOf(this.settingsSection);
|
|
856
963
|
this.settingsSection = sections[(idx - 1 + sections.length) % sections.length];
|
|
857
964
|
this.settingsIndex = 0;
|
|
858
965
|
this.buildSettingsUI();
|
|
859
966
|
} else if (keyName === 'up') {
|
|
860
|
-
|
|
861
|
-
|
|
967
|
+
if (this.settingsIndex > 0) {
|
|
968
|
+
this.settingsIndex--;
|
|
969
|
+
this.buildSettingsUI();
|
|
970
|
+
} else {
|
|
971
|
+
// Move to previous section
|
|
972
|
+
const sections = ['display', 'ignore', 'include', 'scripts'];
|
|
973
|
+
const idx = sections.indexOf(this.settingsSection);
|
|
974
|
+
if (idx > 0) {
|
|
975
|
+
this.settingsSection = sections[idx - 1];
|
|
976
|
+
this.settingsIndex = this.getSettingsMaxIndex();
|
|
977
|
+
this.buildSettingsUI();
|
|
978
|
+
}
|
|
979
|
+
}
|
|
862
980
|
} else if (keyName === 'down') {
|
|
863
981
|
const maxIndex = this.getSettingsMaxIndex();
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
} else if (keyName === 'a') {
|
|
867
|
-
// Add new pattern (only for ignore/include sections)
|
|
868
|
-
if (this.settingsSection === 'ignore' || this.settingsSection === 'include') {
|
|
869
|
-
this.isAddingPattern = true;
|
|
870
|
-
this.newPatternText = '';
|
|
982
|
+
if (this.settingsIndex < maxIndex) {
|
|
983
|
+
this.settingsIndex++;
|
|
871
984
|
this.buildSettingsUI();
|
|
985
|
+
} else {
|
|
986
|
+
// Move to next section
|
|
987
|
+
const sections = ['display', 'ignore', 'include', 'scripts'];
|
|
988
|
+
const idx = sections.indexOf(this.settingsSection);
|
|
989
|
+
if (idx < sections.length - 1) {
|
|
990
|
+
this.settingsSection = sections[idx + 1];
|
|
991
|
+
this.settingsIndex = 0;
|
|
992
|
+
this.buildSettingsUI();
|
|
993
|
+
}
|
|
872
994
|
}
|
|
995
|
+
} else if (keyName === 'i') {
|
|
996
|
+
// Add new ignore pattern
|
|
997
|
+
this.settingsSection = 'ignore';
|
|
998
|
+
this.isAddingPattern = true;
|
|
999
|
+
this.newPatternText = '';
|
|
1000
|
+
this.buildSettingsUI();
|
|
1001
|
+
} else if (keyName === 'n') {
|
|
1002
|
+
// Add new include pattern
|
|
1003
|
+
this.settingsSection = 'include';
|
|
1004
|
+
this.isAddingPattern = true;
|
|
1005
|
+
this.newPatternText = '';
|
|
1006
|
+
this.buildSettingsUI();
|
|
873
1007
|
} else if (keyName === 'd' || keyName === 'backspace') {
|
|
874
|
-
// Delete selected pattern
|
|
1008
|
+
// Delete selected pattern
|
|
875
1009
|
this.deleteSelectedItem();
|
|
876
1010
|
this.buildSettingsUI();
|
|
877
1011
|
} else if (keyName === 'space' || keyName === 'enter' || keyName === 'return') {
|
|
878
|
-
// Toggle
|
|
879
|
-
if (this.settingsSection === '
|
|
1012
|
+
// Toggle display options, script visibility
|
|
1013
|
+
if (this.settingsSection === 'display') {
|
|
1014
|
+
this.toggleDisplayOption();
|
|
1015
|
+
this.buildSettingsUI();
|
|
1016
|
+
} else if (this.settingsSection === 'scripts') {
|
|
880
1017
|
this.toggleScriptIgnore();
|
|
881
1018
|
this.buildSettingsUI();
|
|
882
1019
|
}
|
|
@@ -884,16 +1021,31 @@ class ProcessManager {
|
|
|
884
1021
|
}
|
|
885
1022
|
|
|
886
1023
|
getSettingsMaxIndex() {
|
|
887
|
-
if (this.settingsSection === '
|
|
888
|
-
return
|
|
1024
|
+
if (this.settingsSection === 'display') {
|
|
1025
|
+
return 1; // 2 display options (line numbers, timestamps)
|
|
1026
|
+
} else if (this.settingsSection === 'ignore') {
|
|
1027
|
+
const count = this.config.ignore?.length || 0;
|
|
1028
|
+
return count > 0 ? count - 1 : 0;
|
|
889
1029
|
} else if (this.settingsSection === 'include') {
|
|
890
|
-
|
|
1030
|
+
const count = this.config.include?.length || 0;
|
|
1031
|
+
return count > 0 ? count - 1 : 0;
|
|
891
1032
|
} else if (this.settingsSection === 'scripts') {
|
|
892
1033
|
return Math.max(0, this.allScripts.length - 1);
|
|
893
1034
|
}
|
|
894
1035
|
return 0;
|
|
895
1036
|
}
|
|
896
1037
|
|
|
1038
|
+
toggleDisplayOption() {
|
|
1039
|
+
if (this.settingsIndex === 0) {
|
|
1040
|
+
this.showLineNumbers = !this.showLineNumbers;
|
|
1041
|
+
this.config.showLineNumbers = this.showLineNumbers;
|
|
1042
|
+
} else if (this.settingsIndex === 1) {
|
|
1043
|
+
this.showTimestamps = !this.showTimestamps;
|
|
1044
|
+
this.config.showTimestamps = this.showTimestamps;
|
|
1045
|
+
}
|
|
1046
|
+
saveConfig(this.config);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
897
1049
|
deleteSelectedItem() {
|
|
898
1050
|
if (this.settingsSection === 'ignore' && this.config.ignore?.length > 0) {
|
|
899
1051
|
this.config.ignore.splice(this.settingsIndex, 1);
|
|
@@ -1028,6 +1180,9 @@ class ProcessManager {
|
|
|
1028
1180
|
items.push({ label: 'Previous Pane', shortcut: 'Shift+Tab', action: () => this.navigateToNextPane(-1) });
|
|
1029
1181
|
}
|
|
1030
1182
|
|
|
1183
|
+
items.push({ label: 'Toggle Line Numbers', shortcut: '#', action: () => { this.showLineNumbers = !this.showLineNumbers; } });
|
|
1184
|
+
items.push({ label: 'Toggle Timestamps', shortcut: 't', action: () => { this.showTimestamps = !this.showTimestamps; } });
|
|
1185
|
+
|
|
1031
1186
|
return items;
|
|
1032
1187
|
}
|
|
1033
1188
|
|
|
@@ -1179,6 +1334,42 @@ class ProcessManager {
|
|
|
1179
1334
|
saveConfig(this.config);
|
|
1180
1335
|
}
|
|
1181
1336
|
|
|
1337
|
+
// Scroll the focused pane
|
|
1338
|
+
scrollFocusedPane(direction) {
|
|
1339
|
+
if (!this.focusedPaneId) return;
|
|
1340
|
+
|
|
1341
|
+
const scrollBox = this.paneScrollBoxes.get(this.focusedPaneId);
|
|
1342
|
+
if (!scrollBox || !scrollBox.scrollTo) return;
|
|
1343
|
+
|
|
1344
|
+
const currentY = scrollBox.scrollTop || 0;
|
|
1345
|
+
const viewportHeight = scrollBox.height || 20;
|
|
1346
|
+
const contentHeight = scrollBox.contentHeight || 0;
|
|
1347
|
+
|
|
1348
|
+
let newY = currentY;
|
|
1349
|
+
|
|
1350
|
+
if (direction === 'home') {
|
|
1351
|
+
newY = 0;
|
|
1352
|
+
} else if (direction === 'end') {
|
|
1353
|
+
newY = Number.MAX_SAFE_INTEGER;
|
|
1354
|
+
} else if (direction === 'pageup') {
|
|
1355
|
+
newY = Math.max(0, currentY - viewportHeight);
|
|
1356
|
+
} else if (direction === 'pagedown') {
|
|
1357
|
+
newY = Math.min(contentHeight - viewportHeight, currentY + viewportHeight);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
scrollBox.scrollTo({ x: 0, y: newY });
|
|
1361
|
+
|
|
1362
|
+
// Save the new scroll position
|
|
1363
|
+
this.paneScrollPositions.set(this.focusedPaneId, { x: 0, y: newY });
|
|
1364
|
+
|
|
1365
|
+
// Auto-pause when manually scrolling (unless going to end)
|
|
1366
|
+
if (direction !== 'end' && !this.isPaused) {
|
|
1367
|
+
this.isPaused = true;
|
|
1368
|
+
this.updateStreamPauseState();
|
|
1369
|
+
this.buildRunningUI();
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1182
1373
|
// Check if a process is visible in the focused pane
|
|
1183
1374
|
isProcessVisibleInPane(scriptName, pane) {
|
|
1184
1375
|
if (!pane) return true;
|
|
@@ -1303,54 +1494,82 @@ class ProcessManager {
|
|
|
1303
1494
|
this.settingsContainer.add(inputBar);
|
|
1304
1495
|
}
|
|
1305
1496
|
|
|
1306
|
-
//
|
|
1307
|
-
const
|
|
1308
|
-
id: '
|
|
1497
|
+
// Combined content panel with all sections
|
|
1498
|
+
const contentPanel = new BoxRenderable(this.renderer, {
|
|
1499
|
+
id: 'content-panel',
|
|
1309
1500
|
flexDirection: 'row',
|
|
1310
|
-
|
|
1311
|
-
|
|
1501
|
+
flexGrow: 1,
|
|
1502
|
+
gap: 1,
|
|
1312
1503
|
});
|
|
1313
1504
|
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
sections.forEach(({ id, label }) => {
|
|
1321
|
-
const isActive = this.settingsSection === id;
|
|
1322
|
-
const tab = new TextRenderable(this.renderer, {
|
|
1323
|
-
id: `tab-${id}`,
|
|
1324
|
-
content: isActive
|
|
1325
|
-
? t`${fg(COLORS.accent)('[' + label + ']')}`
|
|
1326
|
-
: t`${fg(COLORS.textDim)(' ' + label + ' ')}`,
|
|
1327
|
-
});
|
|
1328
|
-
tabsContainer.add(tab);
|
|
1505
|
+
// Left column - Display options, Ignore, Include
|
|
1506
|
+
const leftColumn = new BoxRenderable(this.renderer, {
|
|
1507
|
+
id: 'left-column',
|
|
1508
|
+
flexDirection: 'column',
|
|
1509
|
+
flexGrow: 1,
|
|
1510
|
+
gap: 1,
|
|
1329
1511
|
});
|
|
1330
1512
|
|
|
1331
|
-
|
|
1513
|
+
// Display options section
|
|
1514
|
+
const displayBox = new BoxRenderable(this.renderer, {
|
|
1515
|
+
id: 'display-box',
|
|
1516
|
+
flexDirection: 'column',
|
|
1517
|
+
border: true,
|
|
1518
|
+
borderStyle: 'rounded',
|
|
1519
|
+
borderColor: this.settingsSection === 'display' ? COLORS.borderFocused : COLORS.border,
|
|
1520
|
+
title: ' Display Options ',
|
|
1521
|
+
titleAlignment: 'left',
|
|
1522
|
+
padding: 1,
|
|
1523
|
+
});
|
|
1524
|
+
this.buildDisplaySectionContent(displayBox);
|
|
1525
|
+
leftColumn.add(displayBox);
|
|
1332
1526
|
|
|
1333
|
-
//
|
|
1334
|
-
const
|
|
1335
|
-
id: '
|
|
1527
|
+
// Ignore patterns section
|
|
1528
|
+
const ignoreBox = new BoxRenderable(this.renderer, {
|
|
1529
|
+
id: 'ignore-box',
|
|
1336
1530
|
flexDirection: 'column',
|
|
1337
1531
|
border: true,
|
|
1338
1532
|
borderStyle: 'rounded',
|
|
1339
|
-
borderColor: COLORS.border,
|
|
1340
|
-
title:
|
|
1533
|
+
borderColor: this.settingsSection === 'ignore' ? COLORS.borderFocused : COLORS.border,
|
|
1534
|
+
title: ' Ignore Patterns (i) ',
|
|
1341
1535
|
titleAlignment: 'left',
|
|
1536
|
+
padding: 1,
|
|
1342
1537
|
flexGrow: 1,
|
|
1538
|
+
});
|
|
1539
|
+
this.buildIgnoreSectionContent(ignoreBox);
|
|
1540
|
+
leftColumn.add(ignoreBox);
|
|
1541
|
+
|
|
1542
|
+
// Include patterns section
|
|
1543
|
+
const includeBox = new BoxRenderable(this.renderer, {
|
|
1544
|
+
id: 'include-box',
|
|
1545
|
+
flexDirection: 'column',
|
|
1546
|
+
border: true,
|
|
1547
|
+
borderStyle: 'rounded',
|
|
1548
|
+
borderColor: this.settingsSection === 'include' ? COLORS.borderFocused : COLORS.border,
|
|
1549
|
+
title: ' Include Patterns (n) ',
|
|
1550
|
+
titleAlignment: 'left',
|
|
1343
1551
|
padding: 1,
|
|
1552
|
+
flexGrow: 1,
|
|
1344
1553
|
});
|
|
1554
|
+
this.buildIncludeSectionContent(includeBox);
|
|
1555
|
+
leftColumn.add(includeBox);
|
|
1345
1556
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1557
|
+
contentPanel.add(leftColumn);
|
|
1558
|
+
|
|
1559
|
+
// Right column - Scripts list
|
|
1560
|
+
const scriptsBox = new BoxRenderable(this.renderer, {
|
|
1561
|
+
id: 'scripts-box',
|
|
1562
|
+
flexDirection: 'column',
|
|
1563
|
+
border: true,
|
|
1564
|
+
borderStyle: 'rounded',
|
|
1565
|
+
borderColor: this.settingsSection === 'scripts' ? COLORS.borderFocused : COLORS.border,
|
|
1566
|
+
title: ' Scripts ',
|
|
1567
|
+
titleAlignment: 'left',
|
|
1568
|
+
flexGrow: 2,
|
|
1569
|
+
padding: 1,
|
|
1570
|
+
});
|
|
1571
|
+
this.buildScriptsSectionContent(scriptsBox);
|
|
1572
|
+
contentPanel.add(scriptsBox);
|
|
1354
1573
|
|
|
1355
1574
|
this.settingsContainer.add(contentPanel);
|
|
1356
1575
|
|
|
@@ -1374,9 +1593,10 @@ class ProcessManager {
|
|
|
1374
1593
|
]
|
|
1375
1594
|
: [
|
|
1376
1595
|
{ key: 'tab', desc: 'section' },
|
|
1377
|
-
{ key: 'a', desc: 'add' },
|
|
1378
|
-
{ key: 'd', desc: 'delete' },
|
|
1379
1596
|
{ key: 'space', desc: 'toggle' },
|
|
1597
|
+
{ key: 'i', desc: 'add ignore' },
|
|
1598
|
+
{ key: 'n', desc: 'add include' },
|
|
1599
|
+
{ key: 'd', desc: 'delete' },
|
|
1380
1600
|
{ key: 'esc', desc: 'back' },
|
|
1381
1601
|
];
|
|
1382
1602
|
|
|
@@ -1393,26 +1613,38 @@ class ProcessManager {
|
|
|
1393
1613
|
this.renderer.root.add(this.settingsContainer);
|
|
1394
1614
|
}
|
|
1395
1615
|
|
|
1396
|
-
|
|
1397
|
-
const
|
|
1398
|
-
id: '
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
container.add(desc);
|
|
1402
|
-
|
|
1403
|
-
container.add(new TextRenderable(this.renderer, { id: 'spacer', content: '' }));
|
|
1616
|
+
buildDisplaySectionContent(container) {
|
|
1617
|
+
const options = [
|
|
1618
|
+
{ id: 'lineNumbers', label: 'Show Line Numbers', value: this.showLineNumbers },
|
|
1619
|
+
{ id: 'timestamps', label: 'Show Timestamps', value: this.showTimestamps },
|
|
1620
|
+
];
|
|
1404
1621
|
|
|
1622
|
+
options.forEach((option, idx) => {
|
|
1623
|
+
const isFocused = this.settingsSection === 'display' && idx === this.settingsIndex;
|
|
1624
|
+
const indicator = isFocused ? '>' : ' ';
|
|
1625
|
+
const checkbox = option.value ? '[x]' : '[ ]';
|
|
1626
|
+
const checkColor = option.value ? COLORS.success : COLORS.textDim;
|
|
1627
|
+
|
|
1628
|
+
const line = new TextRenderable(this.renderer, {
|
|
1629
|
+
id: `display-option-${idx}`,
|
|
1630
|
+
content: t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(checkColor)(checkbox)} ${fg(COLORS.text)(option.label)}`,
|
|
1631
|
+
});
|
|
1632
|
+
container.add(line);
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
buildIgnoreSectionContent(container) {
|
|
1405
1637
|
const patterns = this.config.ignore || [];
|
|
1406
1638
|
|
|
1407
1639
|
if (patterns.length === 0) {
|
|
1408
1640
|
const empty = new TextRenderable(this.renderer, {
|
|
1409
1641
|
id: 'ignore-empty',
|
|
1410
|
-
content: t`${fg(COLORS.textDim)('
|
|
1642
|
+
content: t`${fg(COLORS.textDim)('Press i to add')}`,
|
|
1411
1643
|
});
|
|
1412
1644
|
container.add(empty);
|
|
1413
1645
|
} else {
|
|
1414
1646
|
patterns.forEach((pattern, idx) => {
|
|
1415
|
-
const isFocused = idx === this.settingsIndex;
|
|
1647
|
+
const isFocused = this.settingsSection === 'ignore' && idx === this.settingsIndex;
|
|
1416
1648
|
const indicator = isFocused ? '>' : ' ';
|
|
1417
1649
|
|
|
1418
1650
|
const line = new TextRenderable(this.renderer, {
|
|
@@ -1425,25 +1657,17 @@ class ProcessManager {
|
|
|
1425
1657
|
}
|
|
1426
1658
|
|
|
1427
1659
|
buildIncludeSectionContent(container) {
|
|
1428
|
-
const desc = new TextRenderable(this.renderer, {
|
|
1429
|
-
id: 'include-desc',
|
|
1430
|
-
content: t`${fg(COLORS.textDim)('Only show scripts matching these patterns. Use * as wildcard.')}`,
|
|
1431
|
-
});
|
|
1432
|
-
container.add(desc);
|
|
1433
|
-
|
|
1434
|
-
container.add(new TextRenderable(this.renderer, { id: 'spacer', content: '' }));
|
|
1435
|
-
|
|
1436
1660
|
const patterns = this.config.include || [];
|
|
1437
1661
|
|
|
1438
1662
|
if (patterns.length === 0) {
|
|
1439
1663
|
const empty = new TextRenderable(this.renderer, {
|
|
1440
1664
|
id: 'include-empty',
|
|
1441
|
-
content: t`${fg(COLORS.textDim)('
|
|
1665
|
+
content: t`${fg(COLORS.textDim)('Press n to add')}`,
|
|
1442
1666
|
});
|
|
1443
1667
|
container.add(empty);
|
|
1444
1668
|
} else {
|
|
1445
1669
|
patterns.forEach((pattern, idx) => {
|
|
1446
|
-
const isFocused = idx === this.settingsIndex;
|
|
1670
|
+
const isFocused = this.settingsSection === 'include' && idx === this.settingsIndex;
|
|
1447
1671
|
const indicator = isFocused ? '>' : ' ';
|
|
1448
1672
|
|
|
1449
1673
|
const line = new TextRenderable(this.renderer, {
|
|
@@ -1456,19 +1680,11 @@ class ProcessManager {
|
|
|
1456
1680
|
}
|
|
1457
1681
|
|
|
1458
1682
|
buildScriptsSectionContent(container) {
|
|
1459
|
-
const desc = new TextRenderable(this.renderer, {
|
|
1460
|
-
id: 'scripts-desc',
|
|
1461
|
-
content: t`${fg(COLORS.textDim)('Toggle individual scripts. Ignored scripts are hidden from selection.')}`,
|
|
1462
|
-
});
|
|
1463
|
-
container.add(desc);
|
|
1464
|
-
|
|
1465
|
-
container.add(new TextRenderable(this.renderer, { id: 'spacer', content: '' }));
|
|
1466
|
-
|
|
1467
1683
|
const ignorePatterns = this.config.ignore || [];
|
|
1468
1684
|
|
|
1469
1685
|
this.allScripts.forEach((script, idx) => {
|
|
1470
1686
|
const isIgnored = ignorePatterns.includes(script.name);
|
|
1471
|
-
const isFocused = idx === this.settingsIndex;
|
|
1687
|
+
const isFocused = this.settingsSection === 'scripts' && idx === this.settingsIndex;
|
|
1472
1688
|
const indicator = isFocused ? '>' : ' ';
|
|
1473
1689
|
const checkbox = isIgnored ? '[x]' : '[ ]';
|
|
1474
1690
|
const checkColor = isIgnored ? COLORS.error : COLORS.success;
|
|
@@ -1564,14 +1780,22 @@ class ProcessManager {
|
|
|
1564
1780
|
this.scriptLines = this.scripts.map((script, index) => {
|
|
1565
1781
|
const isSelected = this.selectedScripts.has(script.name);
|
|
1566
1782
|
const isFocused = index === this.selectedIndex;
|
|
1567
|
-
const checkIcon = isSelected ? '●' : '○';
|
|
1568
|
-
const checkColor = isSelected ? COLORS.success : COLORS.textDim;
|
|
1569
1783
|
const processColor = this.processColors.get(script.name) || COLORS.text;
|
|
1570
1784
|
const nameColor = isFocused ? COLORS.text : processColor;
|
|
1785
|
+
const numberColor = processColor;
|
|
1786
|
+
const bracketColor = processColor;
|
|
1571
1787
|
const bgColor = isFocused ? COLORS.bgHighlight : null;
|
|
1572
1788
|
|
|
1573
|
-
//
|
|
1574
|
-
const
|
|
1789
|
+
// Show number for first 9 scripts
|
|
1790
|
+
const numberLabel = index < 9 ? ` ${index + 1}` : ' ';
|
|
1791
|
+
|
|
1792
|
+
// Build checkbox with colored brackets and white x (like running screen)
|
|
1793
|
+
let content;
|
|
1794
|
+
if (isSelected) {
|
|
1795
|
+
content = t`${fg(numberColor)(numberLabel)} ${fg(bracketColor)('[')}${fg(COLORS.text)('x')}${fg(bracketColor)(']')} ${fg(nameColor)(script.displayName)}`;
|
|
1796
|
+
} else {
|
|
1797
|
+
content = t`${fg(numberColor)(numberLabel)} ${fg(bracketColor)('[ ]')} ${fg(nameColor)(script.displayName)}`;
|
|
1798
|
+
}
|
|
1575
1799
|
|
|
1576
1800
|
const lineContainer = new BoxRenderable(this.renderer, {
|
|
1577
1801
|
id: `script-box-${index}`,
|
|
@@ -1815,87 +2039,65 @@ class ProcessManager {
|
|
|
1815
2039
|
}
|
|
1816
2040
|
|
|
1817
2041
|
updateRunningUI() {
|
|
1818
|
-
// Update existing panes
|
|
2042
|
+
// Update existing panes incrementally, or rebuild if needed
|
|
1819
2043
|
if (this.paneScrollBoxes.size > 0) {
|
|
1820
|
-
//
|
|
2044
|
+
// Incremental update - just append new lines to existing panes
|
|
2045
|
+
let hasNewContent = false;
|
|
2046
|
+
|
|
1821
2047
|
for (const [paneId, scrollBox] of this.paneScrollBoxes.entries()) {
|
|
1822
2048
|
const pane = findPaneById(this.paneRoot, paneId);
|
|
1823
2049
|
if (pane && scrollBox && scrollBox.content) {
|
|
1824
|
-
|
|
1825
|
-
const
|
|
1826
|
-
filter: pane.filter || '',
|
|
1827
|
-
hidden: pane.hidden || [],
|
|
1828
|
-
processes: pane.processes || [],
|
|
1829
|
-
colorFilter: pane.colorFilter || null,
|
|
1830
|
-
});
|
|
1831
|
-
const previousFilterState = this.paneFilterState.get(paneId);
|
|
1832
|
-
const filterChanged = currentFilterState !== previousFilterState;
|
|
1833
|
-
const needsRebuild = filterChanged || this.isPaused;
|
|
2050
|
+
const lines = this.getOutputLinesForPane(pane);
|
|
2051
|
+
const lastRenderedLineNumber = this.paneLineCount.get(paneId) || 0;
|
|
1834
2052
|
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
if (scrollBox.content.children) {
|
|
1841
|
-
while (scrollBox.content.children.length > 0) {
|
|
1842
|
-
const child = scrollBox.content.children[0];
|
|
1843
|
-
if (child && child.id) {
|
|
1844
|
-
scrollBox.content.remove(child.id);
|
|
1845
|
-
} else {
|
|
1846
|
-
break;
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
// Rebuild all content
|
|
1852
|
-
const height = scrollBox.height || this.renderer.height - 6;
|
|
1853
|
-
this.buildPaneOutput(pane, scrollBox.content, height);
|
|
1854
|
-
|
|
1855
|
-
// Update line count after rebuild
|
|
1856
|
-
const lines = this.getOutputLinesForPane(pane);
|
|
1857
|
-
this.paneLineCount.set(paneId, lines.length);
|
|
1858
|
-
|
|
1859
|
-
// Auto-scroll to bottom after filter change
|
|
1860
|
-
if (!this.isPaused) {
|
|
1861
|
-
scrollBox.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER });
|
|
1862
|
-
}
|
|
1863
|
-
} else {
|
|
1864
|
-
// No filter change - just append new lines
|
|
1865
|
-
const lines = this.getOutputLinesForPane(pane);
|
|
1866
|
-
const lastRenderedCount = this.paneLineCount.get(paneId) || 0;
|
|
2053
|
+
// Find lines that haven't been rendered yet (based on absolute line number)
|
|
2054
|
+
const newLines = lines.filter(line => line.lineNumber > lastRenderedLineNumber);
|
|
2055
|
+
|
|
2056
|
+
if (newLines.length > 0) {
|
|
2057
|
+
hasNewContent = true;
|
|
1867
2058
|
|
|
1868
|
-
|
|
1869
|
-
const
|
|
2059
|
+
for (let i = 0; i < newLines.length; i++) {
|
|
2060
|
+
const line = newLines[i];
|
|
2061
|
+
const processColor = this.processColors.get(line.process) || COLORS.text;
|
|
2062
|
+
const trimmedText = line.text.trim();
|
|
2063
|
+
|
|
2064
|
+
// Build content with proper template literal
|
|
2065
|
+
const lineNumber = this.showLineNumbers ? String(line.lineNumber).padStart(4, ' ') : '';
|
|
2066
|
+
const timestamp = this.showTimestamps ? new Date(line.timestamp).toLocaleTimeString('en-US', { hour12: false }) : '';
|
|
1870
2067
|
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
bg: '#000000',
|
|
1881
|
-
});
|
|
1882
|
-
|
|
1883
|
-
scrollBox.content.add(outputLine);
|
|
2068
|
+
let content;
|
|
2069
|
+
if (this.showLineNumbers && this.showTimestamps) {
|
|
2070
|
+
content = t`${fg(COLORS.textDim)(lineNumber)} ${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
|
|
2071
|
+
} else if (this.showLineNumbers) {
|
|
2072
|
+
content = t`${fg(COLORS.textDim)(lineNumber)} ${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
|
|
2073
|
+
} else if (this.showTimestamps) {
|
|
2074
|
+
content = t`${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
|
|
2075
|
+
} else {
|
|
2076
|
+
content = t`${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
|
|
1884
2077
|
}
|
|
1885
2078
|
|
|
1886
|
-
|
|
1887
|
-
|
|
2079
|
+
const outputLine = new TextRenderable(this.renderer, {
|
|
2080
|
+
id: `output-${pane.id}-${line.lineNumber}`,
|
|
2081
|
+
content: content,
|
|
2082
|
+
bg: '#000000',
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
scrollBox.content.add(outputLine);
|
|
1888
2086
|
|
|
1889
|
-
//
|
|
1890
|
-
if (
|
|
1891
|
-
|
|
2087
|
+
// Remove oldest line if we exceed maxOutputLines to maintain rolling window
|
|
2088
|
+
if (scrollBox.content.children && scrollBox.content.children.length > this.maxOutputLines) {
|
|
2089
|
+
const oldestChild = scrollBox.content.children[0];
|
|
2090
|
+
scrollBox.content.remove(oldestChild);
|
|
1892
2091
|
}
|
|
1893
2092
|
}
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
2093
|
+
|
|
2094
|
+
// Update to track the last absolute line number we rendered
|
|
2095
|
+
this.paneLineCount.set(paneId, newLines[newLines.length - 1].lineNumber);
|
|
2096
|
+
|
|
2097
|
+
// Auto-scroll to bottom if not paused
|
|
2098
|
+
if (!this.isPaused && scrollBox.scrollTo) {
|
|
2099
|
+
scrollBox.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER });
|
|
2100
|
+
}
|
|
1899
2101
|
}
|
|
1900
2102
|
}
|
|
1901
2103
|
}
|
|
@@ -1920,9 +2122,25 @@ class ProcessManager {
|
|
|
1920
2122
|
|
|
1921
2123
|
// Trim whitespace and let text wrap naturally - ScrollBox will handle overflow
|
|
1922
2124
|
const trimmedText = line.text.trim();
|
|
2125
|
+
|
|
2126
|
+
// Build content with proper template literal
|
|
2127
|
+
const lineNumber = this.showLineNumbers ? String(line.lineNumber).padStart(4, ' ') : '';
|
|
2128
|
+
const timestamp = this.showTimestamps ? new Date(line.timestamp).toLocaleTimeString('en-US', { hour12: false }) : '';
|
|
2129
|
+
|
|
2130
|
+
let content;
|
|
2131
|
+
if (this.showLineNumbers && this.showTimestamps) {
|
|
2132
|
+
content = t`${fg(COLORS.textDim)(lineNumber)} ${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
|
|
2133
|
+
} else if (this.showLineNumbers) {
|
|
2134
|
+
content = t`${fg(COLORS.textDim)(lineNumber)} ${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
|
|
2135
|
+
} else if (this.showTimestamps) {
|
|
2136
|
+
content = t`${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
|
|
2137
|
+
} else {
|
|
2138
|
+
content = t`${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
|
|
2139
|
+
}
|
|
2140
|
+
|
|
1923
2141
|
const outputLine = new TextRenderable(this.renderer, {
|
|
1924
2142
|
id: `output-${pane.id}-${i}`,
|
|
1925
|
-
content:
|
|
2143
|
+
content: content,
|
|
1926
2144
|
bg: '#000000', // Black background for pane content
|
|
1927
2145
|
});
|
|
1928
2146
|
|
|
@@ -2012,10 +2230,8 @@ class ProcessManager {
|
|
|
2012
2230
|
|
|
2013
2231
|
this.buildPaneOutput(pane, outputBox.content, height);
|
|
2014
2232
|
|
|
2015
|
-
// Restore or set scroll position
|
|
2016
|
-
|
|
2017
|
-
if (!outputBox || !outputBox.scrollTo) return;
|
|
2018
|
-
|
|
2233
|
+
// Restore or set scroll position immediately
|
|
2234
|
+
if (outputBox && outputBox.scrollTo) {
|
|
2019
2235
|
if (this.isPaused && this.paneScrollPositions.has(pane.id)) {
|
|
2020
2236
|
// Restore saved scroll position when paused
|
|
2021
2237
|
const savedPos = this.paneScrollPositions.get(pane.id);
|
|
@@ -2024,7 +2240,7 @@ class ProcessManager {
|
|
|
2024
2240
|
// Auto-scroll to bottom when not paused
|
|
2025
2241
|
outputBox.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER });
|
|
2026
2242
|
}
|
|
2027
|
-
}
|
|
2243
|
+
}
|
|
2028
2244
|
|
|
2029
2245
|
paneContainer.add(outputBox);
|
|
2030
2246
|
return paneContainer;
|
|
@@ -2170,15 +2386,7 @@ class ProcessManager {
|
|
|
2170
2386
|
paddingLeft: 1,
|
|
2171
2387
|
});
|
|
2172
2388
|
|
|
2173
|
-
|
|
2174
|
-
const allPanes = getAllPaneIds(this.paneRoot);
|
|
2175
|
-
if (allPanes.length > 1) {
|
|
2176
|
-
const paneIndicator = new TextRenderable(this.renderer, {
|
|
2177
|
-
id: 'pane-indicator',
|
|
2178
|
-
content: t`${fg(COLORS.cyan)(`[${allPanes.length} panes]`)} `,
|
|
2179
|
-
});
|
|
2180
|
-
processBar.add(paneIndicator);
|
|
2181
|
-
}
|
|
2389
|
+
|
|
2182
2390
|
|
|
2183
2391
|
// Add each process with checkbox showing visibility in focused pane
|
|
2184
2392
|
const focusedPane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
@@ -2191,13 +2399,25 @@ class ProcessManager {
|
|
|
2191
2399
|
const processColor = this.processColors.get(script.name) || COLORS.text;
|
|
2192
2400
|
const isSelected = this.selectedIndex === index;
|
|
2193
2401
|
const isVisible = this.isProcessVisibleInPane(script.name, focusedPane);
|
|
2194
|
-
const checkbox = isVisible ? '[x]' : '[ ]';
|
|
2195
2402
|
const nameColor = isSelected ? COLORS.accent : (isVisible ? processColor : COLORS.textDim);
|
|
2403
|
+
const numberColor = isVisible ? processColor : COLORS.textDim;
|
|
2196
2404
|
const indicator = isSelected ? '>' : ' ';
|
|
2405
|
+
const bracketColor = isVisible ? processColor : COLORS.textDim;
|
|
2406
|
+
|
|
2407
|
+
// Show number for first 9 processes
|
|
2408
|
+
const numberLabel = index < 9 ? `${index + 1}` : ' ';
|
|
2409
|
+
|
|
2410
|
+
// Build content - can't nest template literals, so build entire thing at once
|
|
2411
|
+
let content;
|
|
2412
|
+
if (isVisible) {
|
|
2413
|
+
content = t`${fg(numberColor)(numberLabel)} ${fg(isSelected ? COLORS.accent : COLORS.textDim)(indicator)}${fg(bracketColor)('[')}${fg(COLORS.text)('x')}${fg(bracketColor)(']')} ${fg(statusColor)(statusIcon)} ${fg(nameColor)(script.displayName)}`;
|
|
2414
|
+
} else {
|
|
2415
|
+
content = t`${fg(numberColor)(numberLabel)} ${fg(isSelected ? COLORS.accent : COLORS.textDim)(indicator)}${fg(bracketColor)('[ ]')} ${fg(statusColor)(statusIcon)} ${fg(nameColor)(script.displayName)}`;
|
|
2416
|
+
}
|
|
2197
2417
|
|
|
2198
2418
|
const processItem = new TextRenderable(this.renderer, {
|
|
2199
2419
|
id: `process-item-${index}`,
|
|
2200
|
-
content:
|
|
2420
|
+
content: content,
|
|
2201
2421
|
});
|
|
2202
2422
|
processBar.add(processItem);
|
|
2203
2423
|
});
|
|
@@ -2268,6 +2488,17 @@ class ProcessManager {
|
|
|
2268
2488
|
leftSide.add(filterIndicator);
|
|
2269
2489
|
}
|
|
2270
2490
|
|
|
2491
|
+
// Input mode indicator if active
|
|
2492
|
+
if (this.isInputMode) {
|
|
2493
|
+
const scriptName = this.scripts[this.selectedIndex]?.displayName || '';
|
|
2494
|
+
const inputText = `[${scriptName}]> ${this.inputModeText}_`;
|
|
2495
|
+
const inputIndicator = new TextRenderable(this.renderer, {
|
|
2496
|
+
id: 'input-indicator',
|
|
2497
|
+
content: t`${fg(COLORS.success)(inputText)}`,
|
|
2498
|
+
});
|
|
2499
|
+
leftSide.add(inputIndicator);
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2271
2502
|
// Color filter indicator if active on focused pane
|
|
2272
2503
|
if (focusedPane?.colorFilter) {
|
|
2273
2504
|
const colorMap = {
|
|
@@ -2296,13 +2527,15 @@ class ProcessManager {
|
|
|
2296
2527
|
|
|
2297
2528
|
const shortcuts = [
|
|
2298
2529
|
{ key: '\\', desc: 'panes', color: COLORS.cyan },
|
|
2299
|
-
{ key: '
|
|
2530
|
+
{ key: '1-9', desc: 'toggle', color: COLORS.success },
|
|
2531
|
+
{ key: 'i', desc: 'input', color: COLORS.success },
|
|
2300
2532
|
{ key: 'n', desc: 'name', color: COLORS.accent },
|
|
2301
2533
|
{ key: 'p', desc: 'pause', color: COLORS.warning },
|
|
2302
2534
|
{ key: '/', desc: 'filter', color: COLORS.cyan },
|
|
2303
2535
|
{ key: 'c', desc: 'color', color: COLORS.magenta },
|
|
2304
2536
|
{ key: 's', desc: 'stop', color: COLORS.error },
|
|
2305
2537
|
{ key: 'r', desc: 'restart', color: COLORS.success },
|
|
2538
|
+
{ key: 'o', desc: 'settings', color: COLORS.magenta },
|
|
2306
2539
|
{ key: 'q', desc: 'quit', color: COLORS.error },
|
|
2307
2540
|
];
|
|
2308
2541
|
|
|
@@ -2352,6 +2585,7 @@ async function main() {
|
|
|
2352
2585
|
}
|
|
2353
2586
|
|
|
2354
2587
|
const renderer = await createCliRenderer();
|
|
2588
|
+
renderer.start(); // Start the automatic render loop
|
|
2355
2589
|
const manager = new ProcessManager(renderer, scripts);
|
|
2356
2590
|
|
|
2357
2591
|
// Handle cleanup on exit
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "startall",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"demo:worker2": "node -e \"setInterval(() => console.log('Processing jobs...'), 100)\"",
|
|
16
16
|
"demo:errors": "node -e \"setInterval(() => { console.log('\\x1b[32mOK: All good\\x1b[0m'); console.log('\\x1b[33mWARN: Deprecated API used\\x1b[0m'); console.log('\\x1b[31mERROR: Connection failed\\x1b[0m'); console.log('Normal log message'); }, 2000)\"",
|
|
17
17
|
"demo:build": "node -e \"let i=0; setInterval(() => { i++; if(i%5===0) console.log('\\x1b[31mError: Type mismatch in file.ts:42\\x1b[0m'); else if(i%3===0) console.log('\\x1b[33mWarning: Unused variable\\x1b[0m'); else console.log('\\x1b[36mCompiling module ' + i + '...\\x1b[0m'); }, 800)\"",
|
|
18
|
-
"demo:longtext": "node -e \"setInterval(() => { console.log('\\x1b[36m[2024-01-20T12:34:56.789Z] Executing SQL query: SELECT users.id, users.name, users.email, orders.order_id, orders.total, products.name FROM users INNER JOIN orders ON users.id = orders.user_id LEFT JOIN products ON orders.product_id = products.id WHERE users.created_at > NOW() - INTERVAL 30 DAY AND orders.status = \\'completed\\' ORDER BY orders.total DESC LIMIT 100\\x1b[0m'); console.log('Fetched 42 records in 127ms'); }, 1500)\""
|
|
18
|
+
"demo:longtext": "node -e \"setInterval(() => { console.log('\\x1b[36m[2024-01-20T12:34:56.789Z] Executing SQL query: SELECT users.id, users.name, users.email, orders.order_id, orders.total, products.name FROM users INNER JOIN orders ON users.id = orders.user_id LEFT JOIN products ON orders.product_id = products.id WHERE users.created_at > NOW() - INTERVAL 30 DAY AND orders.status = \\'completed\\' ORDER BY orders.total DESC LIMIT 100\\x1b[0m'); console.log('Fetched 42 records in 127ms'); }, 1500)\"",
|
|
19
|
+
"demo:cmd": "cmd"
|
|
19
20
|
},
|
|
20
21
|
"keywords": [],
|
|
21
22
|
"author": "",
|
package/screenshot.png
ADDED
|
Binary file
|