startall 0.0.3 → 0.0.8
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/.github/workflows/publish.yml +42 -6
- package/README.md +122 -122
- package/index.js +514 -340
- package/package.json +29 -27
package/index.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import { createCliRenderer, TextRenderable, BoxRenderable, ScrollBoxRenderable, t, fg } from '@opentui/core';
|
|
4
4
|
import { spawn } from 'child_process';
|
|
5
5
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
6
6
|
import { join } from 'path';
|
|
7
7
|
import kill from 'tree-kill';
|
|
8
8
|
import stripAnsi from 'strip-ansi';
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
// Configuration
|
|
11
11
|
const CONFIG_FILE = process.argv[2] || 'startall.json';
|
|
12
12
|
const COUNTDOWN_SECONDS = 10;
|
|
13
13
|
const APP_VERSION = 'v0.0.4';
|
|
14
14
|
|
|
15
|
+
// Detect if running inside VS Code's integrated terminal
|
|
16
|
+
const IS_VSCODE = process.env.TERM_PROGRAM === 'vscode';
|
|
17
|
+
|
|
15
18
|
// Pane ID generator
|
|
16
19
|
let paneIdCounter = 0;
|
|
17
20
|
function generatePaneId() {
|
|
@@ -23,9 +26,11 @@ function createPane(processes = []) {
|
|
|
23
26
|
return {
|
|
24
27
|
type: 'pane',
|
|
25
28
|
id: generatePaneId(),
|
|
29
|
+
name: '', // Custom name for the pane
|
|
26
30
|
processes: processes, // Array of process names shown in this pane (empty = all)
|
|
27
31
|
hidden: [], // Array of process names to hide from this pane
|
|
28
32
|
filter: '', // Text filter for this pane
|
|
33
|
+
colorFilter: null, // Color filter: 'red', 'yellow', 'green', 'blue', 'cyan', 'magenta', or null
|
|
29
34
|
isPaused: false,
|
|
30
35
|
scrollOffset: 0,
|
|
31
36
|
};
|
|
@@ -67,6 +72,34 @@ function getAllPaneIds(node, ids = []) {
|
|
|
67
72
|
return ids;
|
|
68
73
|
}
|
|
69
74
|
|
|
75
|
+
// Get ANSI color codes for a color name (includes normal and bright variants)
|
|
76
|
+
function getAnsiColorCodes(colorName) {
|
|
77
|
+
const colorMap = {
|
|
78
|
+
red: [31, 91], // normal red, bright red
|
|
79
|
+
yellow: [33, 93], // normal yellow, bright yellow (warnings)
|
|
80
|
+
green: [32, 92], // normal green, bright green
|
|
81
|
+
blue: [34, 94], // normal blue, bright blue
|
|
82
|
+
cyan: [36, 96], // normal cyan, bright cyan
|
|
83
|
+
magenta: [35, 95], // normal magenta, bright magenta
|
|
84
|
+
};
|
|
85
|
+
return colorMap[colorName] || [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check if a line contains a specific ANSI color
|
|
89
|
+
function lineHasColor(text, colorName) {
|
|
90
|
+
const codes = getAnsiColorCodes(colorName);
|
|
91
|
+
// Match ANSI escape sequences like \x1b[31m, \x1b[91m, \x1b[1;31m, etc.
|
|
92
|
+
for (const code of codes) {
|
|
93
|
+
// Check for direct color code: \x1b[31m
|
|
94
|
+
if (text.includes(`\x1b[${code}m`)) return true;
|
|
95
|
+
// Check for color with modifiers: \x1b[1;31m, \x1b[0;31m, etc.
|
|
96
|
+
if (text.includes(`;${code}m`)) return true;
|
|
97
|
+
// Check for color at start of sequence: \x1b[31;1m
|
|
98
|
+
if (text.includes(`\x1b[${code};`)) return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
70
103
|
// Find parent of a node
|
|
71
104
|
function findParent(root, targetId, parent = null) {
|
|
72
105
|
if (!root) return null;
|
|
@@ -170,8 +203,11 @@ function serializePaneTree(node) {
|
|
|
170
203
|
if (node.type === 'pane') {
|
|
171
204
|
return {
|
|
172
205
|
type: 'pane',
|
|
206
|
+
name: node.name || '',
|
|
173
207
|
processes: node.processes || [],
|
|
174
208
|
hidden: node.hidden || [],
|
|
209
|
+
filter: node.filter || '',
|
|
210
|
+
colorFilter: node.colorFilter || null,
|
|
175
211
|
};
|
|
176
212
|
}
|
|
177
213
|
|
|
@@ -189,7 +225,10 @@ function deserializePaneTree(data) {
|
|
|
189
225
|
|
|
190
226
|
if (data.type === 'pane') {
|
|
191
227
|
const pane = createPane(data.processes || []);
|
|
228
|
+
pane.name = data.name || '';
|
|
192
229
|
pane.hidden = data.hidden || [];
|
|
230
|
+
pane.filter = data.filter || '';
|
|
231
|
+
pane.colorFilter = data.colorFilter || null;
|
|
193
232
|
return pane;
|
|
194
233
|
}
|
|
195
234
|
|
|
@@ -232,26 +271,26 @@ function isIncluded(name, includePatterns) {
|
|
|
232
271
|
function isIgnored(name, ignorePatterns) {
|
|
233
272
|
return ignorePatterns.some(pattern => matchesPattern(name, pattern));
|
|
234
273
|
}
|
|
235
|
-
|
|
236
|
-
// Parse npm scripts from package.json
|
|
237
|
-
function parseNpmScripts(packageJsonPath) {
|
|
238
|
-
try {
|
|
239
|
-
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
240
|
-
const scripts = pkg.scripts || {};
|
|
241
|
-
|
|
242
|
-
return Object.entries(scripts)
|
|
243
|
-
.filter(([name]) => !name.startsWith('pre') && !name.startsWith('post') && name !== 'start')
|
|
244
|
-
.map(([name, command]) => ({
|
|
245
|
-
name,
|
|
246
|
-
command: `npm run ${name}`,
|
|
247
|
-
displayName: name,
|
|
248
|
-
}));
|
|
249
|
-
} catch (error) {
|
|
250
|
-
console.error('Error reading package.json:', error.message);
|
|
251
|
-
process.exit(1);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
274
|
+
|
|
275
|
+
// Parse npm scripts from package.json
|
|
276
|
+
function parseNpmScripts(packageJsonPath) {
|
|
277
|
+
try {
|
|
278
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
279
|
+
const scripts = pkg.scripts || {};
|
|
280
|
+
|
|
281
|
+
return Object.entries(scripts)
|
|
282
|
+
.filter(([name]) => !name.startsWith('pre') && !name.startsWith('post') && name !== 'start')
|
|
283
|
+
.map(([name, command]) => ({
|
|
284
|
+
name,
|
|
285
|
+
command: `npm run ${name}`,
|
|
286
|
+
displayName: name,
|
|
287
|
+
}));
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error('Error reading package.json:', error.message);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
255
294
|
// Load config
|
|
256
295
|
function loadConfig() {
|
|
257
296
|
if (existsSync(CONFIG_FILE)) {
|
|
@@ -272,7 +311,7 @@ function saveConfig(config) {
|
|
|
272
311
|
console.error('Error saving config:', error.message);
|
|
273
312
|
}
|
|
274
313
|
}
|
|
275
|
-
|
|
314
|
+
|
|
276
315
|
// Process Manager
|
|
277
316
|
class ProcessManager {
|
|
278
317
|
constructor(renderer, scripts) {
|
|
@@ -295,6 +334,8 @@ class ProcessManager {
|
|
|
295
334
|
this.isPaused = false; // Whether output scrolling is paused
|
|
296
335
|
this.wasPaused = false; // Track previous pause state to detect changes
|
|
297
336
|
this.isFilterMode = false; // Whether in filter input mode
|
|
337
|
+
this.isNamingMode = false; // Whether in pane naming input mode
|
|
338
|
+
this.namingModeText = ''; // Text being typed for pane name
|
|
298
339
|
|
|
299
340
|
// Settings menu state
|
|
300
341
|
this.settingsSection = 'ignore'; // 'ignore' | 'include' | 'scripts'
|
|
@@ -305,7 +346,7 @@ class ProcessManager {
|
|
|
305
346
|
this.previousPhase = 'selection'; // Track where we came from
|
|
306
347
|
this.outputBox = null; // Reference to the output container
|
|
307
348
|
this.destroyed = false; // Flag to prevent operations after cleanup
|
|
308
|
-
this.lastRenderedLineCount = 0; // Track how many lines we've rendered
|
|
349
|
+
this.lastRenderedLineCount = 0; // Track how many lines we've rendered
|
|
309
350
|
this.headerRenderable = null; // Reference to header text in running UI
|
|
310
351
|
this.processListRenderable = null; // Reference to process list text in running UI
|
|
311
352
|
this.renderScheduled = false; // Throttle renders for CPU efficiency
|
|
@@ -318,44 +359,44 @@ class ProcessManager {
|
|
|
318
359
|
this.splitMenuIndex = 0; // Selected item in split menu
|
|
319
360
|
|
|
320
361
|
// Assign colors to each script
|
|
321
|
-
this.processColors = new Map();
|
|
322
|
-
const colors = ['#7aa2f7', '#bb9af7', '#9ece6a', '#f7768e', '#e0af68', '#73daca'];
|
|
323
|
-
scripts.forEach((script, index) => {
|
|
324
|
-
this.processColors.set(script.name, colors[index % colors.length]);
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
// UI references
|
|
328
|
-
this.headerText = null;
|
|
329
|
-
this.scriptLines = [];
|
|
330
|
-
this.scriptLinePositions = []; // Track Y positions of script lines for mouse clicks
|
|
331
|
-
this.selectionContainer = null;
|
|
332
|
-
this.runningContainer = null;
|
|
333
|
-
|
|
334
|
-
this.setupKeyboardHandlers();
|
|
335
|
-
this.setupMouseHandlers();
|
|
336
|
-
this.buildSelectionUI();
|
|
337
|
-
this.startCountdown();
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
setupKeyboardHandlers() {
|
|
341
|
-
this.renderer.keyInput.on('keypress', (key) => {
|
|
342
|
-
// Handle Ctrl+C (if exitOnCtrlC is false)
|
|
343
|
-
if (key.ctrl && key.name === 'c') {
|
|
344
|
-
this.cleanup();
|
|
345
|
-
this.renderer.destroy();
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
this.handleInput(key.name, key);
|
|
350
|
-
this.render();
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
setupMouseHandlers() {
|
|
355
|
-
// Mouse events are handled via BoxRenderable properties, not a global handler
|
|
356
|
-
// We'll add onMouseDown to individual script lines in buildSelectionUI
|
|
357
|
-
}
|
|
358
|
-
|
|
362
|
+
this.processColors = new Map();
|
|
363
|
+
const colors = ['#7aa2f7', '#bb9af7', '#9ece6a', '#f7768e', '#e0af68', '#73daca'];
|
|
364
|
+
scripts.forEach((script, index) => {
|
|
365
|
+
this.processColors.set(script.name, colors[index % colors.length]);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// UI references
|
|
369
|
+
this.headerText = null;
|
|
370
|
+
this.scriptLines = [];
|
|
371
|
+
this.scriptLinePositions = []; // Track Y positions of script lines for mouse clicks
|
|
372
|
+
this.selectionContainer = null;
|
|
373
|
+
this.runningContainer = null;
|
|
374
|
+
|
|
375
|
+
this.setupKeyboardHandlers();
|
|
376
|
+
this.setupMouseHandlers();
|
|
377
|
+
this.buildSelectionUI();
|
|
378
|
+
this.startCountdown();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
setupKeyboardHandlers() {
|
|
382
|
+
this.renderer.keyInput.on('keypress', (key) => {
|
|
383
|
+
// Handle Ctrl+C (if exitOnCtrlC is false)
|
|
384
|
+
if (key.ctrl && key.name === 'c') {
|
|
385
|
+
this.cleanup();
|
|
386
|
+
this.renderer.destroy();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
this.handleInput(key.name, key);
|
|
391
|
+
this.render();
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
setupMouseHandlers() {
|
|
396
|
+
// Mouse events are handled via BoxRenderable properties, not a global handler
|
|
397
|
+
// We'll add onMouseDown to individual script lines in buildSelectionUI
|
|
398
|
+
}
|
|
399
|
+
|
|
359
400
|
handleInput(keyName, keyEvent) {
|
|
360
401
|
if (this.phase === 'selection') {
|
|
361
402
|
if (keyName === 'enter' || keyName === 'return') {
|
|
@@ -376,8 +417,8 @@ class ProcessManager {
|
|
|
376
417
|
// Reset countdown when selection changes
|
|
377
418
|
this.countdown = COUNTDOWN_SECONDS;
|
|
378
419
|
}
|
|
379
|
-
} else if (keyName === '
|
|
380
|
-
// Open settings menu
|
|
420
|
+
} else if (keyName === 'o') {
|
|
421
|
+
// Open settings menu (options)
|
|
381
422
|
clearInterval(this.countdownInterval);
|
|
382
423
|
this.previousPhase = 'selection';
|
|
383
424
|
this.phase = 'settings';
|
|
@@ -396,8 +437,31 @@ class ProcessManager {
|
|
|
396
437
|
return;
|
|
397
438
|
}
|
|
398
439
|
|
|
440
|
+
// If in naming mode, handle name input
|
|
441
|
+
if (this.isNamingMode) {
|
|
442
|
+
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
443
|
+
if (keyName === 'escape') {
|
|
444
|
+
this.isNamingMode = false;
|
|
445
|
+
this.namingModeText = '';
|
|
446
|
+
this.buildRunningUI(); // Rebuild to cancel naming
|
|
447
|
+
} else if (keyName === 'enter' || keyName === 'return') {
|
|
448
|
+
if (pane) {
|
|
449
|
+
pane.name = this.namingModeText;
|
|
450
|
+
this.savePaneLayout(); // Save the new name
|
|
451
|
+
}
|
|
452
|
+
this.isNamingMode = false;
|
|
453
|
+
this.namingModeText = '';
|
|
454
|
+
this.buildRunningUI(); // Rebuild with new name
|
|
455
|
+
} else if (keyName === 'backspace') {
|
|
456
|
+
this.namingModeText = this.namingModeText.slice(0, -1);
|
|
457
|
+
this.buildRunningUI(); // Update UI to show name change
|
|
458
|
+
} else if (keyName && keyName.length === 1 && !keyEvent.ctrl && !keyEvent.meta) {
|
|
459
|
+
this.namingModeText += keyName;
|
|
460
|
+
this.buildRunningUI(); // Update UI to show name change
|
|
461
|
+
}
|
|
462
|
+
}
|
|
399
463
|
// If in filter mode, handle filter input
|
|
400
|
-
if (this.isFilterMode) {
|
|
464
|
+
else if (this.isFilterMode) {
|
|
401
465
|
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
402
466
|
if (keyName === 'escape') {
|
|
403
467
|
this.isFilterMode = false;
|
|
@@ -416,6 +480,7 @@ class ProcessManager {
|
|
|
416
480
|
} else {
|
|
417
481
|
// Normal mode - handle commands
|
|
418
482
|
if (keyName === 'q') {
|
|
483
|
+
this.savePaneLayout();
|
|
419
484
|
this.cleanup();
|
|
420
485
|
this.renderer.destroy();
|
|
421
486
|
} else if (keyName === '\\') {
|
|
@@ -457,6 +522,12 @@ class ProcessManager {
|
|
|
457
522
|
this.isFilterMode = true;
|
|
458
523
|
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
459
524
|
if (pane) pane.filter = '';
|
|
525
|
+
} else if (keyName === 'n') {
|
|
526
|
+
// Enter naming mode for focused pane
|
|
527
|
+
this.isNamingMode = true;
|
|
528
|
+
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
529
|
+
this.namingModeText = pane?.name || '';
|
|
530
|
+
this.buildRunningUI(); // Rebuild to show naming input
|
|
460
531
|
} else if (keyName === 'escape') {
|
|
461
532
|
// Clear filter on focused pane
|
|
462
533
|
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
@@ -491,14 +562,23 @@ class ProcessManager {
|
|
|
491
562
|
if (scriptName) {
|
|
492
563
|
this.toggleProcess(scriptName);
|
|
493
564
|
}
|
|
494
|
-
} else if (keyName === '
|
|
495
|
-
// Open settings
|
|
565
|
+
} else if (keyName === 'o') {
|
|
566
|
+
// Open settings (options)
|
|
496
567
|
this.previousPhase = 'running';
|
|
497
568
|
this.phase = 'settings';
|
|
498
569
|
this.settingsSection = 'ignore';
|
|
499
570
|
this.settingsIndex = 0;
|
|
500
571
|
this.buildSettingsUI();
|
|
501
572
|
return;
|
|
573
|
+
} else if (keyName === 'c') {
|
|
574
|
+
// Cycle color filter on focused pane
|
|
575
|
+
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
576
|
+
if (pane) {
|
|
577
|
+
const colors = [null, 'red', 'yellow', 'green', 'blue', 'cyan', 'magenta'];
|
|
578
|
+
const currentIndex = colors.indexOf(pane.colorFilter);
|
|
579
|
+
pane.colorFilter = colors[(currentIndex + 1) % colors.length];
|
|
580
|
+
this.buildRunningUI();
|
|
581
|
+
}
|
|
502
582
|
} else if (keyName === 'tab') {
|
|
503
583
|
// Navigate to next pane
|
|
504
584
|
this.navigateToNextPane(1);
|
|
@@ -511,14 +591,14 @@ class ProcessManager {
|
|
|
511
591
|
}
|
|
512
592
|
}
|
|
513
593
|
}
|
|
514
|
-
|
|
515
|
-
handleMouse(mouse) {
|
|
516
|
-
if (this.phase === 'selection') {
|
|
517
|
-
// Left click or scroll wheel click
|
|
518
|
-
if (mouse.type === 'mousedown' && (mouse.button === 'left' || mouse.button === 'middle')) {
|
|
519
|
-
// Check if click is on a script line
|
|
520
|
-
const clickedIndex = this.scriptLinePositions.findIndex(pos => pos === mouse.y);
|
|
521
|
-
|
|
594
|
+
|
|
595
|
+
handleMouse(mouse) {
|
|
596
|
+
if (this.phase === 'selection') {
|
|
597
|
+
// Left click or scroll wheel click
|
|
598
|
+
if (mouse.type === 'mousedown' && (mouse.button === 'left' || mouse.button === 'middle')) {
|
|
599
|
+
// Check if click is on a script line
|
|
600
|
+
const clickedIndex = this.scriptLinePositions.findIndex(pos => pos === mouse.y);
|
|
601
|
+
|
|
522
602
|
if (clickedIndex !== -1) {
|
|
523
603
|
const scriptName = this.scripts[clickedIndex]?.name;
|
|
524
604
|
if (scriptName) {
|
|
@@ -535,44 +615,44 @@ class ProcessManager {
|
|
|
535
615
|
this.render();
|
|
536
616
|
}
|
|
537
617
|
}
|
|
538
|
-
} else if (mouse.type === 'wheeldown') {
|
|
539
|
-
this.selectedIndex = Math.min(this.scripts.length - 1, this.selectedIndex + 1);
|
|
540
|
-
this.render();
|
|
541
|
-
} else if (mouse.type === 'wheelup') {
|
|
542
|
-
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
543
|
-
this.render();
|
|
544
|
-
}
|
|
545
|
-
} else if (this.phase === 'running') {
|
|
546
|
-
// Mouse support for running phase
|
|
547
|
-
if (mouse.type === 'mousedown' && mouse.button === 'left') {
|
|
548
|
-
const clickedIndex = this.scriptLinePositions.findIndex(pos => pos === mouse.y);
|
|
549
|
-
|
|
550
|
-
if (clickedIndex !== -1) {
|
|
551
|
-
this.selectedIndex = clickedIndex;
|
|
552
|
-
this.render();
|
|
553
|
-
}
|
|
554
|
-
} else if (mouse.type === 'wheeldown') {
|
|
555
|
-
this.selectedIndex = Math.min(this.scripts.length - 1, this.selectedIndex + 1);
|
|
556
|
-
this.render();
|
|
557
|
-
} else if (mouse.type === 'wheelup') {
|
|
558
|
-
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
559
|
-
this.render();
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
startCountdown() {
|
|
565
|
-
this.countdownInterval = setInterval(() => {
|
|
566
|
-
this.countdown--;
|
|
567
|
-
this.render();
|
|
568
|
-
|
|
569
|
-
if (this.countdown <= 0) {
|
|
570
|
-
clearInterval(this.countdownInterval);
|
|
571
|
-
this.startProcesses();
|
|
572
|
-
}
|
|
573
|
-
}, 1000);
|
|
574
|
-
}
|
|
575
|
-
|
|
618
|
+
} else if (mouse.type === 'wheeldown') {
|
|
619
|
+
this.selectedIndex = Math.min(this.scripts.length - 1, this.selectedIndex + 1);
|
|
620
|
+
this.render();
|
|
621
|
+
} else if (mouse.type === 'wheelup') {
|
|
622
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
623
|
+
this.render();
|
|
624
|
+
}
|
|
625
|
+
} else if (this.phase === 'running') {
|
|
626
|
+
// Mouse support for running phase
|
|
627
|
+
if (mouse.type === 'mousedown' && mouse.button === 'left') {
|
|
628
|
+
const clickedIndex = this.scriptLinePositions.findIndex(pos => pos === mouse.y);
|
|
629
|
+
|
|
630
|
+
if (clickedIndex !== -1) {
|
|
631
|
+
this.selectedIndex = clickedIndex;
|
|
632
|
+
this.render();
|
|
633
|
+
}
|
|
634
|
+
} else if (mouse.type === 'wheeldown') {
|
|
635
|
+
this.selectedIndex = Math.min(this.scripts.length - 1, this.selectedIndex + 1);
|
|
636
|
+
this.render();
|
|
637
|
+
} else if (mouse.type === 'wheelup') {
|
|
638
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
639
|
+
this.render();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
startCountdown() {
|
|
645
|
+
this.countdownInterval = setInterval(() => {
|
|
646
|
+
this.countdown--;
|
|
647
|
+
this.render();
|
|
648
|
+
|
|
649
|
+
if (this.countdown <= 0) {
|
|
650
|
+
clearInterval(this.countdownInterval);
|
|
651
|
+
this.startProcesses();
|
|
652
|
+
}
|
|
653
|
+
}, 1000);
|
|
654
|
+
}
|
|
655
|
+
|
|
576
656
|
startProcesses() {
|
|
577
657
|
const selected = Array.from(this.selectedScripts);
|
|
578
658
|
|
|
@@ -600,20 +680,20 @@ class ProcessManager {
|
|
|
600
680
|
|
|
601
681
|
this.render();
|
|
602
682
|
}
|
|
603
|
-
|
|
604
|
-
startProcess(scriptName) {
|
|
605
|
-
const script = this.scripts.find(s => s.name === scriptName);
|
|
606
|
-
if (!script) return;
|
|
607
|
-
|
|
608
|
-
const proc = spawn('npm', ['run', scriptName], {
|
|
609
|
-
env: {
|
|
610
|
-
...process.env,
|
|
611
|
-
FORCE_COLOR: '1',
|
|
612
|
-
COLORTERM: 'truecolor',
|
|
613
|
-
},
|
|
614
|
-
shell: true,
|
|
615
|
-
});
|
|
616
|
-
|
|
683
|
+
|
|
684
|
+
startProcess(scriptName) {
|
|
685
|
+
const script = this.scripts.find(s => s.name === scriptName);
|
|
686
|
+
if (!script) return;
|
|
687
|
+
|
|
688
|
+
const proc = spawn('npm', ['run', scriptName], {
|
|
689
|
+
env: {
|
|
690
|
+
...process.env,
|
|
691
|
+
FORCE_COLOR: '1',
|
|
692
|
+
COLORTERM: 'truecolor',
|
|
693
|
+
},
|
|
694
|
+
shell: true,
|
|
695
|
+
});
|
|
696
|
+
|
|
617
697
|
proc.stdout.on('data', (data) => {
|
|
618
698
|
const text = data.toString();
|
|
619
699
|
const lines = text.split('\n');
|
|
@@ -639,11 +719,11 @@ class ProcessManager {
|
|
|
639
719
|
this.processes.set(scriptName, { status, exitCode: code });
|
|
640
720
|
this.addOutputLine(scriptName, `Process exited with code ${code}`);
|
|
641
721
|
});
|
|
642
|
-
|
|
643
|
-
this.processRefs.set(scriptName, proc);
|
|
644
|
-
this.processes.set(scriptName, { status: 'running', pid: proc.pid });
|
|
645
|
-
}
|
|
646
|
-
|
|
722
|
+
|
|
723
|
+
this.processRefs.set(scriptName, proc);
|
|
724
|
+
this.processes.set(scriptName, { status: 'running', pid: proc.pid });
|
|
725
|
+
}
|
|
726
|
+
|
|
647
727
|
addOutputLine(processName, text) {
|
|
648
728
|
// Always store the output line, even when paused
|
|
649
729
|
this.outputLines.push({
|
|
@@ -672,31 +752,31 @@ class ProcessManager {
|
|
|
672
752
|
this.render();
|
|
673
753
|
}, 16);
|
|
674
754
|
}
|
|
675
|
-
|
|
676
|
-
stopProcess(scriptName) {
|
|
677
|
-
const proc = this.processRefs.get(scriptName);
|
|
678
|
-
if (proc && proc.pid) {
|
|
679
|
-
// Use tree-kill to kill the entire process tree
|
|
680
|
-
kill(proc.pid, 'SIGTERM', (err) => {
|
|
681
|
-
if (err) {
|
|
682
|
-
// If SIGTERM fails, try SIGKILL
|
|
683
|
-
kill(proc.pid, 'SIGKILL');
|
|
684
|
-
}
|
|
685
|
-
});
|
|
686
|
-
this.processRefs.delete(scriptName);
|
|
687
|
-
this.processes.set(scriptName, { status: 'stopped' });
|
|
688
|
-
this.addOutputLine(scriptName, 'Process stopped');
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
restartProcess(scriptName) {
|
|
693
|
-
this.stopProcess(scriptName);
|
|
694
|
-
setTimeout(() => {
|
|
695
|
-
this.startProcess(scriptName);
|
|
696
|
-
this.render();
|
|
697
|
-
}, 100);
|
|
698
|
-
}
|
|
699
|
-
|
|
755
|
+
|
|
756
|
+
stopProcess(scriptName) {
|
|
757
|
+
const proc = this.processRefs.get(scriptName);
|
|
758
|
+
if (proc && proc.pid) {
|
|
759
|
+
// Use tree-kill to kill the entire process tree
|
|
760
|
+
kill(proc.pid, 'SIGTERM', (err) => {
|
|
761
|
+
if (err) {
|
|
762
|
+
// If SIGTERM fails, try SIGKILL
|
|
763
|
+
kill(proc.pid, 'SIGKILL');
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
this.processRefs.delete(scriptName);
|
|
767
|
+
this.processes.set(scriptName, { status: 'stopped' });
|
|
768
|
+
this.addOutputLine(scriptName, 'Process stopped');
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
restartProcess(scriptName) {
|
|
773
|
+
this.stopProcess(scriptName);
|
|
774
|
+
setTimeout(() => {
|
|
775
|
+
this.startProcess(scriptName);
|
|
776
|
+
this.render();
|
|
777
|
+
}, 100);
|
|
778
|
+
}
|
|
779
|
+
|
|
700
780
|
toggleProcess(scriptName) {
|
|
701
781
|
const proc = this.processes.get(scriptName);
|
|
702
782
|
if (proc?.status === 'running') {
|
|
@@ -1135,6 +1215,11 @@ class ProcessManager {
|
|
|
1135
1215
|
);
|
|
1136
1216
|
}
|
|
1137
1217
|
|
|
1218
|
+
// Apply color filter (filter lines containing specific ANSI color codes)
|
|
1219
|
+
if (pane.colorFilter) {
|
|
1220
|
+
lines = lines.filter(line => lineHasColor(line.text, pane.colorFilter));
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1138
1223
|
return lines;
|
|
1139
1224
|
}
|
|
1140
1225
|
|
|
@@ -1408,7 +1493,7 @@ class ProcessManager {
|
|
|
1408
1493
|
}
|
|
1409
1494
|
}
|
|
1410
1495
|
}
|
|
1411
|
-
|
|
1496
|
+
|
|
1412
1497
|
cleanup() {
|
|
1413
1498
|
this.destroyed = true;
|
|
1414
1499
|
|
|
@@ -1428,7 +1513,7 @@ class ProcessManager {
|
|
|
1428
1513
|
}
|
|
1429
1514
|
}
|
|
1430
1515
|
}
|
|
1431
|
-
|
|
1516
|
+
|
|
1432
1517
|
buildSelectionUI() {
|
|
1433
1518
|
// Remove old containers if they exist - use destroyRecursively to clean up all children
|
|
1434
1519
|
if (this.selectionContainer) {
|
|
@@ -1529,6 +1614,15 @@ class ProcessManager {
|
|
|
1529
1614
|
leftSide.add(titleText);
|
|
1530
1615
|
this.headerText = titleText; // Save reference for countdown updates
|
|
1531
1616
|
|
|
1617
|
+
// VS Code hint
|
|
1618
|
+
if (IS_VSCODE) {
|
|
1619
|
+
const vscodeHint = new TextRenderable(this.renderer, {
|
|
1620
|
+
id: 'vscode-hint',
|
|
1621
|
+
content: t`${fg(COLORS.textDim)('(vscode)')}`,
|
|
1622
|
+
});
|
|
1623
|
+
leftSide.add(vscodeHint);
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1532
1626
|
footerBar.add(leftSide);
|
|
1533
1627
|
|
|
1534
1628
|
// Right side: shortcuts
|
|
@@ -1541,7 +1635,7 @@ class ProcessManager {
|
|
|
1541
1635
|
const shortcuts = [
|
|
1542
1636
|
{ key: 'spc', desc: 'sel', color: COLORS.success },
|
|
1543
1637
|
{ key: 'ret', desc: 'go', color: COLORS.accent },
|
|
1544
|
-
{ key: '
|
|
1638
|
+
{ key: 'o', desc: 'cfg', color: COLORS.magenta },
|
|
1545
1639
|
];
|
|
1546
1640
|
|
|
1547
1641
|
shortcuts.forEach(({ key, desc, color }) => {
|
|
@@ -1557,32 +1651,32 @@ class ProcessManager {
|
|
|
1557
1651
|
|
|
1558
1652
|
this.renderer.root.add(this.selectionContainer);
|
|
1559
1653
|
}
|
|
1560
|
-
|
|
1654
|
+
|
|
1561
1655
|
getHeaderText() {
|
|
1562
1656
|
return `Starting in ${this.countdown}s...`;
|
|
1563
1657
|
}
|
|
1564
|
-
|
|
1565
|
-
getScriptLineText(script, index) {
|
|
1566
|
-
const isSelected = this.selectedScripts.has(script.name);
|
|
1567
|
-
const isFocused = index === this.selectedIndex;
|
|
1568
|
-
const prefix = isFocused ? '▶' : ' ';
|
|
1569
|
-
const checkbox = isSelected ? '✓' : ' ';
|
|
1570
|
-
const processColor = this.processColors.get(script.name) || '#FFFFFF';
|
|
1571
|
-
|
|
1572
|
-
// Use colored text for script name
|
|
1573
|
-
return t`${prefix} [${checkbox}] ${fg(processColor)(script.displayName)}`;
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
getScriptLineColor(index) {
|
|
1577
|
-
// Return base color for the line (prefix will be cyan when focused)
|
|
1578
|
-
return index === this.selectedIndex ? '#00FFFF' : '#FFFFFF';
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1658
|
+
|
|
1659
|
+
getScriptLineText(script, index) {
|
|
1660
|
+
const isSelected = this.selectedScripts.has(script.name);
|
|
1661
|
+
const isFocused = index === this.selectedIndex;
|
|
1662
|
+
const prefix = isFocused ? '▶' : ' ';
|
|
1663
|
+
const checkbox = isSelected ? '✓' : ' ';
|
|
1664
|
+
const processColor = this.processColors.get(script.name) || '#FFFFFF';
|
|
1665
|
+
|
|
1666
|
+
// Use colored text for script name
|
|
1667
|
+
return t`${prefix} [${checkbox}] ${fg(processColor)(script.displayName)}`;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
getScriptLineColor(index) {
|
|
1671
|
+
// Return base color for the line (prefix will be cyan when focused)
|
|
1672
|
+
return index === this.selectedIndex ? '#00FFFF' : '#FFFFFF';
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1581
1675
|
updateSelectionUI() {
|
|
1582
1676
|
// Rebuild UI each time - simpler and more reliable with the new structure
|
|
1583
1677
|
this.buildSelectionUI();
|
|
1584
1678
|
}
|
|
1585
|
-
|
|
1679
|
+
|
|
1586
1680
|
render() {
|
|
1587
1681
|
// Don't render if destroyed
|
|
1588
1682
|
if (this.destroyed) return;
|
|
@@ -1598,130 +1692,130 @@ class ProcessManager {
|
|
|
1598
1692
|
this.updateRunningUI();
|
|
1599
1693
|
}
|
|
1600
1694
|
}
|
|
1601
|
-
|
|
1602
|
-
getProcessListContent() {
|
|
1603
|
-
// Build process list content dynamically for any number of processes
|
|
1604
|
-
let contentString = '';
|
|
1605
|
-
|
|
1606
|
-
this.scripts.forEach((script, index) => {
|
|
1607
|
-
const proc = this.processes.get(script.name);
|
|
1608
|
-
const status = proc?.status || 'stopped';
|
|
1609
|
-
const icon = status === 'running' ? '●' : status === 'crashed' ? '✖' : '○';
|
|
1610
|
-
const statusColor = status === 'running' ? '#00FF00' : status === 'crashed' ? '#FF0000' : '#666666';
|
|
1611
|
-
const processColor = this.processColors.get(script.name) || '#FFFFFF';
|
|
1612
|
-
const prefix = this.selectedIndex === index ? '▶' : '';
|
|
1613
|
-
|
|
1614
|
-
// Build the colored string for this process
|
|
1615
|
-
if (index > 0) contentString += ' ';
|
|
1616
|
-
contentString += prefix + script.displayName + ' ' + icon;
|
|
1617
|
-
});
|
|
1618
|
-
|
|
1619
|
-
return contentString;
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
updateRunningHeader() {
|
|
1623
|
-
// Update only the header and process list without rebuilding everything
|
|
1624
|
-
if (!this.headerRenderable || !this.processListRenderable || !this.runningContainer) {
|
|
1625
|
-
return;
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
// Update header (plain text works)
|
|
1629
|
-
const selectedScript = this.scripts[this.selectedIndex];
|
|
1630
|
-
const selectedName = selectedScript ? selectedScript.displayName : '';
|
|
1631
|
-
const pauseIndicator = this.isPaused ? ' [PAUSED]' : '';
|
|
1632
|
-
const filterIndicator = this.isFilterMode ? ` [FILTER: ${this.filter}_]` : (this.filter ? ` [FILTER: ${this.filter}]` : '');
|
|
1633
|
-
const headerText = `[←→: Navigate | Space: Pause | S: Stop | R: Restart | F: Filter Selected | /: Filter Text | Q: Quit] ${selectedName}${pauseIndicator}${filterIndicator}`;
|
|
1634
|
-
|
|
1635
|
-
if (this.headerRenderable.setContent) {
|
|
1636
|
-
this.headerRenderable.setContent(headerText);
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
// For process list with styled text, we need to recreate it
|
|
1640
|
-
// Remove old one
|
|
1641
|
-
this.runningContainer.remove(this.processListRenderable);
|
|
1642
|
-
this.processListRenderable.destroy();
|
|
1643
|
-
|
|
1644
|
-
// Create new process list with current selection
|
|
1645
|
-
let processContent;
|
|
1646
|
-
if (this.scripts.length === 1) {
|
|
1647
|
-
const script = this.scripts[0];
|
|
1648
|
-
const proc = this.processes.get(script.name);
|
|
1649
|
-
const status = proc?.status || 'stopped';
|
|
1650
|
-
const statusIcon = status === 'running' ? '●' : status === 'crashed' ? '✖' : '○';
|
|
1651
|
-
const statusColor = status === 'running' ? '#00FF00' : status === 'crashed' ? '#FF0000' : '#666666';
|
|
1652
|
-
const processColor = this.processColors.get(script.name) || '#FFFFFF';
|
|
1653
|
-
processContent = t`▶${fg(processColor)(script.displayName)} ${fg(statusColor)(statusIcon)}`;
|
|
1654
|
-
} else if (this.scripts.length === 2) {
|
|
1655
|
-
const s0 = this.scripts[0];
|
|
1656
|
-
const s1 = this.scripts[1];
|
|
1657
|
-
const proc0 = this.processes.get(s0.name);
|
|
1658
|
-
const proc1 = this.processes.get(s1.name);
|
|
1659
|
-
const status0 = proc0?.status || 'stopped';
|
|
1660
|
-
const status1 = proc1?.status || 'stopped';
|
|
1661
|
-
const icon0 = status0 === 'running' ? '●' : status0 === 'crashed' ? '✖' : '○';
|
|
1662
|
-
const icon1 = status1 === 'running' ? '●' : status1 === 'crashed' ? '✖' : '○';
|
|
1663
|
-
const color0 = status0 === 'running' ? '#00FF00' : status0 === 'crashed' ? '#FF0000' : '#666666';
|
|
1664
|
-
const color1 = status1 === 'running' ? '#00FF00' : status1 === 'crashed' ? '#FF0000' : '#666666';
|
|
1665
|
-
const pcolor0 = this.processColors.get(s0.name) || '#FFFFFF';
|
|
1666
|
-
const pcolor1 = this.processColors.get(s1.name) || '#FFFFFF';
|
|
1667
|
-
const prefix0 = this.selectedIndex === 0 ? '▶' : '';
|
|
1668
|
-
const prefix1 = this.selectedIndex === 1 ? '▶' : '';
|
|
1669
|
-
processContent = t`${prefix0}${fg(pcolor0)(s0.displayName)} ${fg(color0)(icon0)} ${prefix1}${fg(pcolor1)(s1.displayName)} ${fg(color1)(icon1)}`;
|
|
1670
|
-
} else if (this.scripts.length === 3) {
|
|
1671
|
-
const s0 = this.scripts[0];
|
|
1672
|
-
const s1 = this.scripts[1];
|
|
1673
|
-
const s2 = this.scripts[2];
|
|
1674
|
-
const proc0 = this.processes.get(s0.name);
|
|
1675
|
-
const proc1 = this.processes.get(s1.name);
|
|
1676
|
-
const proc2 = this.processes.get(s2.name);
|
|
1677
|
-
const status0 = proc0?.status || 'stopped';
|
|
1678
|
-
const status1 = proc1?.status || 'stopped';
|
|
1679
|
-
const status2 = proc2?.status || 'stopped';
|
|
1680
|
-
const icon0 = status0 === 'running' ? '●' : status0 === 'crashed' ? '✖' : '○';
|
|
1681
|
-
const icon1 = status1 === 'running' ? '●' : status1 === 'crashed' ? '✖' : '○';
|
|
1682
|
-
const icon2 = status2 === 'running' ? '●' : status2 === 'crashed' ? '✖' : '○';
|
|
1683
|
-
const color0 = status0 === 'running' ? '#00FF00' : status0 === 'crashed' ? '#FF0000' : '#666666';
|
|
1684
|
-
const color1 = status1 === 'running' ? '#00FF00' : status1 === 'crashed' ? '#FF0000' : '#666666';
|
|
1685
|
-
const color2 = status2 === 'running' ? '#00FF00' : status2 === 'crashed' ? '#FF0000' : '#666666';
|
|
1686
|
-
const pcolor0 = this.processColors.get(s0.name) || '#FFFFFF';
|
|
1687
|
-
const pcolor1 = this.processColors.get(s1.name) || '#FFFFFF';
|
|
1688
|
-
const pcolor2 = this.processColors.get(s2.name) || '#FFFFFF';
|
|
1689
|
-
const prefix0 = this.selectedIndex === 0 ? '▶' : '';
|
|
1690
|
-
const prefix1 = this.selectedIndex === 1 ? '▶' : '';
|
|
1691
|
-
const prefix2 = this.selectedIndex === 2 ? '▶' : '';
|
|
1692
|
-
processContent = t`${prefix0}${fg(pcolor0)(s0.displayName)} ${fg(color0)(icon0)} ${prefix1}${fg(pcolor1)(s1.displayName)} ${fg(color1)(icon1)} ${prefix2}${fg(pcolor2)(s2.displayName)} ${fg(color2)(icon2)}`;
|
|
1693
|
-
} else {
|
|
1694
|
-
// 4+ processes - for now hardcode to 4, but should be dynamic
|
|
1695
|
-
const parts = this.scripts.slice(0, 4).map((script, idx) => {
|
|
1696
|
-
const proc = this.processes.get(script.name);
|
|
1697
|
-
const status = proc?.status || 'stopped';
|
|
1698
|
-
const icon = status === 'running' ? '●' : status === 'crashed' ? '✖' : '○';
|
|
1699
|
-
const color = status === 'running' ? '#00FF00' : status === 'crashed' ? '#FF0000' : '#666666';
|
|
1700
|
-
const pcolor = this.processColors.get(script.name) || '#FFFFFF';
|
|
1701
|
-
const prefix = this.selectedIndex === idx ? '▶' : '';
|
|
1702
|
-
return { prefix, name: script.displayName, icon, color, pcolor };
|
|
1703
|
-
});
|
|
1704
|
-
processContent = t`${parts[0].prefix}${fg(parts[0].pcolor)(parts[0].name)} ${fg(parts[0].color)(parts[0].icon)} ${parts[1].prefix}${fg(parts[1].pcolor)(parts[1].name)} ${fg(parts[1].color)(parts[1].icon)} ${parts[2].prefix}${fg(parts[2].pcolor)(parts[2].name)} ${fg(parts[2].color)(parts[2].icon)} ${parts[3].prefix}${fg(parts[3].pcolor)(parts[3].name)} ${fg(parts[3].color)(parts[3].icon)}`;
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
// Create new process list renderable
|
|
1708
|
-
this.processListRenderable = new TextRenderable(this.renderer, {
|
|
1709
|
-
id: 'process-list',
|
|
1710
|
-
content: processContent,
|
|
1711
|
-
});
|
|
1712
|
-
|
|
1713
|
-
// Insert it back in the right position (after header and spacer)
|
|
1714
|
-
// This is tricky - we need to insert at position 2
|
|
1715
|
-
// For now, just rebuild the whole UI since we can't easily insert
|
|
1716
|
-
this.buildRunningUI();
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
updateRunningUI() {
|
|
1720
|
-
// Just rebuild the entire UI - simpler and more reliable
|
|
1721
|
-
// OpenTUI doesn't have great incremental update support anyway
|
|
1722
|
-
this.buildRunningUI();
|
|
1723
|
-
}
|
|
1724
|
-
|
|
1695
|
+
|
|
1696
|
+
getProcessListContent() {
|
|
1697
|
+
// Build process list content dynamically for any number of processes
|
|
1698
|
+
let contentString = '';
|
|
1699
|
+
|
|
1700
|
+
this.scripts.forEach((script, index) => {
|
|
1701
|
+
const proc = this.processes.get(script.name);
|
|
1702
|
+
const status = proc?.status || 'stopped';
|
|
1703
|
+
const icon = status === 'running' ? '●' : status === 'crashed' ? '✖' : '○';
|
|
1704
|
+
const statusColor = status === 'running' ? '#00FF00' : status === 'crashed' ? '#FF0000' : '#666666';
|
|
1705
|
+
const processColor = this.processColors.get(script.name) || '#FFFFFF';
|
|
1706
|
+
const prefix = this.selectedIndex === index ? '▶' : '';
|
|
1707
|
+
|
|
1708
|
+
// Build the colored string for this process
|
|
1709
|
+
if (index > 0) contentString += ' ';
|
|
1710
|
+
contentString += prefix + script.displayName + ' ' + icon;
|
|
1711
|
+
});
|
|
1712
|
+
|
|
1713
|
+
return contentString;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
updateRunningHeader() {
|
|
1717
|
+
// Update only the header and process list without rebuilding everything
|
|
1718
|
+
if (!this.headerRenderable || !this.processListRenderable || !this.runningContainer) {
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
// Update header (plain text works)
|
|
1723
|
+
const selectedScript = this.scripts[this.selectedIndex];
|
|
1724
|
+
const selectedName = selectedScript ? selectedScript.displayName : '';
|
|
1725
|
+
const pauseIndicator = this.isPaused ? ' [PAUSED]' : '';
|
|
1726
|
+
const filterIndicator = this.isFilterMode ? ` [FILTER: ${this.filter}_]` : (this.filter ? ` [FILTER: ${this.filter}]` : '');
|
|
1727
|
+
const headerText = `[←→: Navigate | Space: Pause | S: Stop | R: Restart | F: Filter Selected | /: Filter Text | Q: Quit] ${selectedName}${pauseIndicator}${filterIndicator}`;
|
|
1728
|
+
|
|
1729
|
+
if (this.headerRenderable.setContent) {
|
|
1730
|
+
this.headerRenderable.setContent(headerText);
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// For process list with styled text, we need to recreate it
|
|
1734
|
+
// Remove old one
|
|
1735
|
+
this.runningContainer.remove(this.processListRenderable);
|
|
1736
|
+
this.processListRenderable.destroy();
|
|
1737
|
+
|
|
1738
|
+
// Create new process list with current selection
|
|
1739
|
+
let processContent;
|
|
1740
|
+
if (this.scripts.length === 1) {
|
|
1741
|
+
const script = this.scripts[0];
|
|
1742
|
+
const proc = this.processes.get(script.name);
|
|
1743
|
+
const status = proc?.status || 'stopped';
|
|
1744
|
+
const statusIcon = status === 'running' ? '●' : status === 'crashed' ? '✖' : '○';
|
|
1745
|
+
const statusColor = status === 'running' ? '#00FF00' : status === 'crashed' ? '#FF0000' : '#666666';
|
|
1746
|
+
const processColor = this.processColors.get(script.name) || '#FFFFFF';
|
|
1747
|
+
processContent = t`▶${fg(processColor)(script.displayName)} ${fg(statusColor)(statusIcon)}`;
|
|
1748
|
+
} else if (this.scripts.length === 2) {
|
|
1749
|
+
const s0 = this.scripts[0];
|
|
1750
|
+
const s1 = this.scripts[1];
|
|
1751
|
+
const proc0 = this.processes.get(s0.name);
|
|
1752
|
+
const proc1 = this.processes.get(s1.name);
|
|
1753
|
+
const status0 = proc0?.status || 'stopped';
|
|
1754
|
+
const status1 = proc1?.status || 'stopped';
|
|
1755
|
+
const icon0 = status0 === 'running' ? '●' : status0 === 'crashed' ? '✖' : '○';
|
|
1756
|
+
const icon1 = status1 === 'running' ? '●' : status1 === 'crashed' ? '✖' : '○';
|
|
1757
|
+
const color0 = status0 === 'running' ? '#00FF00' : status0 === 'crashed' ? '#FF0000' : '#666666';
|
|
1758
|
+
const color1 = status1 === 'running' ? '#00FF00' : status1 === 'crashed' ? '#FF0000' : '#666666';
|
|
1759
|
+
const pcolor0 = this.processColors.get(s0.name) || '#FFFFFF';
|
|
1760
|
+
const pcolor1 = this.processColors.get(s1.name) || '#FFFFFF';
|
|
1761
|
+
const prefix0 = this.selectedIndex === 0 ? '▶' : '';
|
|
1762
|
+
const prefix1 = this.selectedIndex === 1 ? '▶' : '';
|
|
1763
|
+
processContent = t`${prefix0}${fg(pcolor0)(s0.displayName)} ${fg(color0)(icon0)} ${prefix1}${fg(pcolor1)(s1.displayName)} ${fg(color1)(icon1)}`;
|
|
1764
|
+
} else if (this.scripts.length === 3) {
|
|
1765
|
+
const s0 = this.scripts[0];
|
|
1766
|
+
const s1 = this.scripts[1];
|
|
1767
|
+
const s2 = this.scripts[2];
|
|
1768
|
+
const proc0 = this.processes.get(s0.name);
|
|
1769
|
+
const proc1 = this.processes.get(s1.name);
|
|
1770
|
+
const proc2 = this.processes.get(s2.name);
|
|
1771
|
+
const status0 = proc0?.status || 'stopped';
|
|
1772
|
+
const status1 = proc1?.status || 'stopped';
|
|
1773
|
+
const status2 = proc2?.status || 'stopped';
|
|
1774
|
+
const icon0 = status0 === 'running' ? '●' : status0 === 'crashed' ? '✖' : '○';
|
|
1775
|
+
const icon1 = status1 === 'running' ? '●' : status1 === 'crashed' ? '✖' : '○';
|
|
1776
|
+
const icon2 = status2 === 'running' ? '●' : status2 === 'crashed' ? '✖' : '○';
|
|
1777
|
+
const color0 = status0 === 'running' ? '#00FF00' : status0 === 'crashed' ? '#FF0000' : '#666666';
|
|
1778
|
+
const color1 = status1 === 'running' ? '#00FF00' : status1 === 'crashed' ? '#FF0000' : '#666666';
|
|
1779
|
+
const color2 = status2 === 'running' ? '#00FF00' : status2 === 'crashed' ? '#FF0000' : '#666666';
|
|
1780
|
+
const pcolor0 = this.processColors.get(s0.name) || '#FFFFFF';
|
|
1781
|
+
const pcolor1 = this.processColors.get(s1.name) || '#FFFFFF';
|
|
1782
|
+
const pcolor2 = this.processColors.get(s2.name) || '#FFFFFF';
|
|
1783
|
+
const prefix0 = this.selectedIndex === 0 ? '▶' : '';
|
|
1784
|
+
const prefix1 = this.selectedIndex === 1 ? '▶' : '';
|
|
1785
|
+
const prefix2 = this.selectedIndex === 2 ? '▶' : '';
|
|
1786
|
+
processContent = t`${prefix0}${fg(pcolor0)(s0.displayName)} ${fg(color0)(icon0)} ${prefix1}${fg(pcolor1)(s1.displayName)} ${fg(color1)(icon1)} ${prefix2}${fg(pcolor2)(s2.displayName)} ${fg(color2)(icon2)}`;
|
|
1787
|
+
} else {
|
|
1788
|
+
// 4+ processes - for now hardcode to 4, but should be dynamic
|
|
1789
|
+
const parts = this.scripts.slice(0, 4).map((script, idx) => {
|
|
1790
|
+
const proc = this.processes.get(script.name);
|
|
1791
|
+
const status = proc?.status || 'stopped';
|
|
1792
|
+
const icon = status === 'running' ? '●' : status === 'crashed' ? '✖' : '○';
|
|
1793
|
+
const color = status === 'running' ? '#00FF00' : status === 'crashed' ? '#FF0000' : '#666666';
|
|
1794
|
+
const pcolor = this.processColors.get(script.name) || '#FFFFFF';
|
|
1795
|
+
const prefix = this.selectedIndex === idx ? '▶' : '';
|
|
1796
|
+
return { prefix, name: script.displayName, icon, color, pcolor };
|
|
1797
|
+
});
|
|
1798
|
+
processContent = t`${parts[0].prefix}${fg(parts[0].pcolor)(parts[0].name)} ${fg(parts[0].color)(parts[0].icon)} ${parts[1].prefix}${fg(parts[1].pcolor)(parts[1].name)} ${fg(parts[1].color)(parts[1].icon)} ${parts[2].prefix}${fg(parts[2].pcolor)(parts[2].name)} ${fg(parts[2].color)(parts[2].icon)} ${parts[3].prefix}${fg(parts[3].pcolor)(parts[3].name)} ${fg(parts[3].color)(parts[3].icon)}`;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// Create new process list renderable
|
|
1802
|
+
this.processListRenderable = new TextRenderable(this.renderer, {
|
|
1803
|
+
id: 'process-list',
|
|
1804
|
+
content: processContent,
|
|
1805
|
+
});
|
|
1806
|
+
|
|
1807
|
+
// Insert it back in the right position (after header and spacer)
|
|
1808
|
+
// This is tricky - we need to insert at position 2
|
|
1809
|
+
// For now, just rebuild the whole UI since we can't easily insert
|
|
1810
|
+
this.buildRunningUI();
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
updateRunningUI() {
|
|
1814
|
+
// Just rebuild the entire UI - simpler and more reliable
|
|
1815
|
+
// OpenTUI doesn't have great incremental update support anyway
|
|
1816
|
+
this.buildRunningUI();
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1725
1819
|
// Build a single pane's output area
|
|
1726
1820
|
buildPaneOutput(pane, container, height) {
|
|
1727
1821
|
const isFocused = pane.id === this.focusedPaneId;
|
|
@@ -1731,6 +1825,10 @@ class ProcessManager {
|
|
|
1731
1825
|
const outputHeight = Math.max(3, height - 2);
|
|
1732
1826
|
let linesToShow = this.isPaused ? lines : lines.slice(-outputHeight);
|
|
1733
1827
|
|
|
1828
|
+
// Use full terminal width for padding - simpler and ensures complete clearing
|
|
1829
|
+
// Each pane will pad to full width, excess will be clipped by container
|
|
1830
|
+
const approxPaneWidth = this.renderer.width;
|
|
1831
|
+
|
|
1734
1832
|
// Add lines in reverse order (newest first)
|
|
1735
1833
|
for (let i = linesToShow.length - 1; i >= 0; i--) {
|
|
1736
1834
|
const line = linesToShow[i];
|
|
@@ -1768,12 +1866,47 @@ class ProcessManager {
|
|
|
1768
1866
|
truncatedText = result + '\x1b[0m...';
|
|
1769
1867
|
}
|
|
1770
1868
|
|
|
1869
|
+
// Get visible length of current line and pad to fill width
|
|
1870
|
+
const linePrefix = `[${line.process}] `;
|
|
1871
|
+
const currentLength = linePrefix.length + stripAnsi(truncatedText).length;
|
|
1872
|
+
|
|
1873
|
+
// Pad with spaces to fill the width
|
|
1874
|
+
const paddingNeeded = Math.max(0, approxPaneWidth - currentLength);
|
|
1875
|
+
const padding = ' '.repeat(paddingNeeded);
|
|
1876
|
+
|
|
1771
1877
|
const outputLine = new TextRenderable(this.renderer, {
|
|
1772
1878
|
id: `output-${pane.id}-${i}`,
|
|
1773
|
-
content: t`${fg(processColor)(`[${line.process}]`)} ${truncatedText}`,
|
|
1879
|
+
content: t`${fg(processColor)(`[${line.process}]`)} ${truncatedText}${padding}`,
|
|
1880
|
+
bg: '#000000', // Black background for pane content
|
|
1774
1881
|
});
|
|
1882
|
+
|
|
1775
1883
|
container.add(outputLine);
|
|
1776
1884
|
}
|
|
1885
|
+
|
|
1886
|
+
// Fill remaining vertical space with blank lines
|
|
1887
|
+
const emptyLinesNeeded = Math.max(0, outputHeight - linesToShow.length);
|
|
1888
|
+
for (let j = 0; j < emptyLinesNeeded; j++) {
|
|
1889
|
+
const emptyLine = new TextRenderable(this.renderer, {
|
|
1890
|
+
id: `empty-${pane.id}-${j}`,
|
|
1891
|
+
content: ' '.repeat(approxPaneWidth), // Fill entire width with spaces
|
|
1892
|
+
bg: '#000000', // Black background for empty lines
|
|
1893
|
+
});
|
|
1894
|
+
|
|
1895
|
+
container.add(emptyLine);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// Count how many vertical panes exist (for width calculation)
|
|
1900
|
+
countVerticalPanes(node) {
|
|
1901
|
+
if (!node) return 1;
|
|
1902
|
+
if (node.type === 'pane') return 1;
|
|
1903
|
+
if (node.direction === 'vertical') {
|
|
1904
|
+
// Vertical split means panes side by side
|
|
1905
|
+
return node.children.reduce((sum, child) => sum + this.countVerticalPanes(child), 0);
|
|
1906
|
+
} else {
|
|
1907
|
+
// Horizontal split means panes stacked, count the max
|
|
1908
|
+
return Math.max(...node.children.map(child => this.countVerticalPanes(child)));
|
|
1909
|
+
}
|
|
1777
1910
|
}
|
|
1778
1911
|
|
|
1779
1912
|
// Build a pane panel with title bar
|
|
@@ -1781,21 +1914,25 @@ class ProcessManager {
|
|
|
1781
1914
|
const isFocused = pane.id === this.focusedPaneId;
|
|
1782
1915
|
const borderColor = isFocused ? COLORS.borderFocused : COLORS.border;
|
|
1783
1916
|
|
|
1784
|
-
// Title shows assigned processes or "All", plus filter and hidden count
|
|
1785
|
-
const
|
|
1917
|
+
// Title shows custom name (if set), or assigned processes or "All", plus filter and hidden count
|
|
1918
|
+
const customName = pane.name ? pane.name : null;
|
|
1919
|
+
const processLabel = customName || (pane.processes.length > 0
|
|
1786
1920
|
? pane.processes.join(', ')
|
|
1787
|
-
: 'All';
|
|
1921
|
+
: 'All');
|
|
1788
1922
|
const focusLabel = isFocused ? '*' : '';
|
|
1789
1923
|
const hiddenCount = pane.hidden?.length || 0;
|
|
1790
1924
|
const hiddenLabel = hiddenCount > 0 ? ` -${hiddenCount}` : '';
|
|
1791
1925
|
const filterLabel = pane.filter ? ` /${pane.filter}` : '';
|
|
1926
|
+
const namingInputLabel = (isFocused && this.isNamingMode) ? `Name: ${this.namingModeText}_` : '';
|
|
1792
1927
|
const filterInputLabel = (isFocused && this.isFilterMode) ? `/${pane.filter || ''}_` : '';
|
|
1793
|
-
const title = ` ${focusLabel}${processLabel}${hiddenLabel}${filterInputLabel || filterLabel} `;
|
|
1928
|
+
const title = ` ${focusLabel}${namingInputLabel || processLabel}${hiddenLabel}${filterInputLabel || filterLabel} `;
|
|
1794
1929
|
|
|
1795
1930
|
const paneContainer = new BoxRenderable(this.renderer, {
|
|
1796
1931
|
id: `pane-${pane.id}`,
|
|
1797
1932
|
flexDirection: 'column',
|
|
1798
1933
|
flexGrow: flexGrow,
|
|
1934
|
+
flexShrink: 0, // Prevent shrinking - maintain 50/50 split
|
|
1935
|
+
flexBasis: 0, // Use flexGrow ratio for sizing, not content size
|
|
1799
1936
|
border: true,
|
|
1800
1937
|
borderStyle: 'rounded',
|
|
1801
1938
|
borderColor: borderColor,
|
|
@@ -1803,19 +1940,24 @@ class ProcessManager {
|
|
|
1803
1940
|
titleAlignment: 'left',
|
|
1804
1941
|
padding: 0,
|
|
1805
1942
|
overflow: 'hidden',
|
|
1943
|
+
backgroundColor: '#000000', // Black background for pane container
|
|
1806
1944
|
});
|
|
1807
1945
|
|
|
1808
|
-
// Output content -
|
|
1946
|
+
// Output content - use BoxRenderable that fills remaining space
|
|
1947
|
+
// Use passed height or calculate default for line count calculation
|
|
1948
|
+
const height = availableHeight ? Math.max(5, availableHeight - 2) : Math.max(5, this.renderer.height - 6);
|
|
1949
|
+
|
|
1809
1950
|
const outputBox = new BoxRenderable(this.renderer, {
|
|
1810
1951
|
id: `pane-output-${pane.id}`,
|
|
1811
1952
|
flexDirection: 'column',
|
|
1812
1953
|
flexGrow: 1,
|
|
1954
|
+
flexShrink: 1,
|
|
1955
|
+
flexBasis: 0,
|
|
1813
1956
|
overflow: 'hidden',
|
|
1814
1957
|
paddingLeft: 1,
|
|
1958
|
+
backgroundColor: '#000000', // Black background for pane
|
|
1815
1959
|
});
|
|
1816
1960
|
|
|
1817
|
-
// Use passed height or calculate default
|
|
1818
|
-
const height = availableHeight ? Math.max(5, availableHeight - 2) : Math.max(5, this.renderer.height - 6);
|
|
1819
1961
|
this.buildPaneOutput(pane, outputBox, height);
|
|
1820
1962
|
|
|
1821
1963
|
paneContainer.add(outputBox);
|
|
@@ -1840,6 +1982,8 @@ class ProcessManager {
|
|
|
1840
1982
|
id: `split-${node.direction}`,
|
|
1841
1983
|
flexDirection: node.direction === 'vertical' ? 'row' : 'column',
|
|
1842
1984
|
flexGrow: flexGrow,
|
|
1985
|
+
flexShrink: 0,
|
|
1986
|
+
flexBasis: 0,
|
|
1843
1987
|
gap: 0,
|
|
1844
1988
|
});
|
|
1845
1989
|
|
|
@@ -1931,13 +2075,13 @@ class ProcessManager {
|
|
|
1931
2075
|
// Clear outputBox reference since it was destroyed with runningContainer
|
|
1932
2076
|
this.outputBox = null;
|
|
1933
2077
|
|
|
1934
|
-
// Create main container - full screen with
|
|
2078
|
+
// Create main container - full screen with black background
|
|
1935
2079
|
const mainContainer = new BoxRenderable(this.renderer, {
|
|
1936
2080
|
id: 'running-container',
|
|
1937
2081
|
flexDirection: 'column',
|
|
1938
2082
|
width: '100%',
|
|
1939
2083
|
height: '100%',
|
|
1940
|
-
backgroundColor:
|
|
2084
|
+
backgroundColor: '#000000',
|
|
1941
2085
|
});
|
|
1942
2086
|
|
|
1943
2087
|
// Process tabs at top
|
|
@@ -1989,7 +2133,9 @@ class ProcessManager {
|
|
|
1989
2133
|
id: 'pane-area',
|
|
1990
2134
|
flexDirection: 'column',
|
|
1991
2135
|
flexGrow: 1,
|
|
1992
|
-
|
|
2136
|
+
flexShrink: 0,
|
|
2137
|
+
flexBasis: 0,
|
|
2138
|
+
backgroundColor: '#000000',
|
|
1993
2139
|
});
|
|
1994
2140
|
|
|
1995
2141
|
const paneLayout = this.buildPaneLayout(this.paneRoot);
|
|
@@ -2026,6 +2172,15 @@ class ProcessManager {
|
|
|
2026
2172
|
});
|
|
2027
2173
|
leftSide.add(statusIndicator);
|
|
2028
2174
|
|
|
2175
|
+
// VS Code hint
|
|
2176
|
+
if (IS_VSCODE) {
|
|
2177
|
+
const vscodeHint = new TextRenderable(this.renderer, {
|
|
2178
|
+
id: 'vscode-hint',
|
|
2179
|
+
content: t`${fg(COLORS.textDim)('(vscode)')}`,
|
|
2180
|
+
});
|
|
2181
|
+
leftSide.add(vscodeHint);
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2029
2184
|
// Filter indicator if active
|
|
2030
2185
|
if (this.filter || this.isFilterMode) {
|
|
2031
2186
|
const filterText = this.isFilterMode ? `/${this.filter}_` : `/${this.filter}`;
|
|
@@ -2036,6 +2191,23 @@ class ProcessManager {
|
|
|
2036
2191
|
leftSide.add(filterIndicator);
|
|
2037
2192
|
}
|
|
2038
2193
|
|
|
2194
|
+
// Color filter indicator if active on focused pane
|
|
2195
|
+
if (focusedPane?.colorFilter) {
|
|
2196
|
+
const colorMap = {
|
|
2197
|
+
red: COLORS.error,
|
|
2198
|
+
yellow: COLORS.warning,
|
|
2199
|
+
green: COLORS.success,
|
|
2200
|
+
blue: COLORS.accent,
|
|
2201
|
+
cyan: COLORS.cyan,
|
|
2202
|
+
magenta: COLORS.magenta,
|
|
2203
|
+
};
|
|
2204
|
+
const colorIndicator = new TextRenderable(this.renderer, {
|
|
2205
|
+
id: 'color-filter-indicator',
|
|
2206
|
+
content: t`${fg(colorMap[focusedPane.colorFilter] || COLORS.text)(`[${focusedPane.colorFilter}]`)}`,
|
|
2207
|
+
});
|
|
2208
|
+
leftSide.add(colorIndicator);
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2039
2211
|
footerBar.add(leftSide);
|
|
2040
2212
|
|
|
2041
2213
|
// Right side: shortcuts and title
|
|
@@ -2048,8 +2220,10 @@ class ProcessManager {
|
|
|
2048
2220
|
const shortcuts = [
|
|
2049
2221
|
{ key: '\\', desc: 'panes', color: COLORS.cyan },
|
|
2050
2222
|
{ key: 'spc', desc: 'toggle', color: COLORS.success },
|
|
2223
|
+
{ key: 'n', desc: 'name', color: COLORS.accent },
|
|
2051
2224
|
{ key: 'p', desc: 'pause', color: COLORS.warning },
|
|
2052
2225
|
{ key: '/', desc: 'filter', color: COLORS.cyan },
|
|
2226
|
+
{ key: 'c', desc: 'color', color: COLORS.magenta },
|
|
2053
2227
|
{ key: 's', desc: 'stop', color: COLORS.error },
|
|
2054
2228
|
{ key: 'r', desc: 'restart', color: COLORS.success },
|
|
2055
2229
|
{ key: 'q', desc: 'quit', color: COLORS.error },
|
|
@@ -2082,27 +2256,27 @@ class ProcessManager {
|
|
|
2082
2256
|
this.runningContainer = mainContainer;
|
|
2083
2257
|
}
|
|
2084
2258
|
}
|
|
2085
|
-
|
|
2086
|
-
// Main
|
|
2087
|
-
async function main() {
|
|
2088
|
-
const cwd = process.cwd();
|
|
2089
|
-
const packageJsonPath = join(cwd, 'package.json');
|
|
2090
|
-
|
|
2091
|
-
if (!existsSync(packageJsonPath)) {
|
|
2092
|
-
console.error(`Error: No package.json found in ${cwd}`);
|
|
2093
|
-
process.exit(1);
|
|
2094
|
-
}
|
|
2095
|
-
|
|
2096
|
-
const scripts = parseNpmScripts(packageJsonPath);
|
|
2097
|
-
|
|
2098
|
-
if (scripts.length === 0) {
|
|
2099
|
-
console.error('No npm scripts found in package.json');
|
|
2100
|
-
process.exit(1);
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
const renderer = await createCliRenderer();
|
|
2104
|
-
const manager = new ProcessManager(renderer, scripts);
|
|
2105
|
-
|
|
2259
|
+
|
|
2260
|
+
// Main
|
|
2261
|
+
async function main() {
|
|
2262
|
+
const cwd = process.cwd();
|
|
2263
|
+
const packageJsonPath = join(cwd, 'package.json');
|
|
2264
|
+
|
|
2265
|
+
if (!existsSync(packageJsonPath)) {
|
|
2266
|
+
console.error(`Error: No package.json found in ${cwd}`);
|
|
2267
|
+
process.exit(1);
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
const scripts = parseNpmScripts(packageJsonPath);
|
|
2271
|
+
|
|
2272
|
+
if (scripts.length === 0) {
|
|
2273
|
+
console.error('No npm scripts found in package.json');
|
|
2274
|
+
process.exit(1);
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
const renderer = await createCliRenderer();
|
|
2278
|
+
const manager = new ProcessManager(renderer, scripts);
|
|
2279
|
+
|
|
2106
2280
|
// Handle cleanup on exit
|
|
2107
2281
|
const handleExit = () => {
|
|
2108
2282
|
manager.cleanup();
|
|
@@ -2112,8 +2286,8 @@ async function main() {
|
|
|
2112
2286
|
process.on('SIGINT', handleExit);
|
|
2113
2287
|
process.on('SIGTERM', handleExit);
|
|
2114
2288
|
}
|
|
2115
|
-
|
|
2116
|
-
main().catch(err => {
|
|
2117
|
-
console.error('Error:', err);
|
|
2118
|
-
process.exit(1);
|
|
2119
|
-
});
|
|
2289
|
+
|
|
2290
|
+
main().catch(err => {
|
|
2291
|
+
console.error('Error:', err);
|
|
2292
|
+
process.exit(1);
|
|
2293
|
+
});
|