startall 0.0.2 → 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 +2162 -741
- package/package.json +29 -27
package/index.js
CHANGED
|
@@ -1,15 +1,261 @@
|
|
|
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
|
+
const APP_VERSION = 'v0.0.4';
|
|
14
|
+
|
|
15
|
+
// Detect if running inside VS Code's integrated terminal
|
|
16
|
+
const IS_VSCODE = process.env.TERM_PROGRAM === 'vscode';
|
|
17
|
+
|
|
18
|
+
// Pane ID generator
|
|
19
|
+
let paneIdCounter = 0;
|
|
20
|
+
function generatePaneId() {
|
|
21
|
+
return `pane-${++paneIdCounter}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Create a new pane node
|
|
25
|
+
function createPane(processes = []) {
|
|
26
|
+
return {
|
|
27
|
+
type: 'pane',
|
|
28
|
+
id: generatePaneId(),
|
|
29
|
+
name: '', // Custom name for the pane
|
|
30
|
+
processes: processes, // Array of process names shown in this pane (empty = all)
|
|
31
|
+
hidden: [], // Array of process names to hide from this pane
|
|
32
|
+
filter: '', // Text filter for this pane
|
|
33
|
+
colorFilter: null, // Color filter: 'red', 'yellow', 'green', 'blue', 'cyan', 'magenta', or null
|
|
34
|
+
isPaused: false,
|
|
35
|
+
scrollOffset: 0,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Create a split node
|
|
40
|
+
function createSplit(direction, children) {
|
|
41
|
+
return {
|
|
42
|
+
type: 'split',
|
|
43
|
+
direction: direction, // 'horizontal' (top/bottom) or 'vertical' (left/right)
|
|
44
|
+
children: children,
|
|
45
|
+
sizes: children.map(() => 1), // Equal sizes by default (flex ratios)
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Find a pane by ID in the tree
|
|
50
|
+
function findPaneById(node, id) {
|
|
51
|
+
if (!node) return null;
|
|
52
|
+
if (node.type === 'pane') {
|
|
53
|
+
return node.id === id ? node : null;
|
|
54
|
+
}
|
|
55
|
+
for (const child of node.children) {
|
|
56
|
+
const found = findPaneById(child, id);
|
|
57
|
+
if (found) return found;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Find all pane IDs in order (for navigation)
|
|
63
|
+
function getAllPaneIds(node, ids = []) {
|
|
64
|
+
if (!node) return ids;
|
|
65
|
+
if (node.type === 'pane') {
|
|
66
|
+
ids.push(node.id);
|
|
67
|
+
} else {
|
|
68
|
+
for (const child of node.children) {
|
|
69
|
+
getAllPaneIds(child, ids);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return ids;
|
|
73
|
+
}
|
|
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
|
+
|
|
103
|
+
// Find parent of a node
|
|
104
|
+
function findParent(root, targetId, parent = null) {
|
|
105
|
+
if (!root) return null;
|
|
106
|
+
if (root.type === 'pane') {
|
|
107
|
+
return root.id === targetId ? parent : null;
|
|
108
|
+
}
|
|
109
|
+
for (const child of root.children) {
|
|
110
|
+
if (child.type === 'pane' && child.id === targetId) {
|
|
111
|
+
return root;
|
|
112
|
+
}
|
|
113
|
+
const found = findParent(child, targetId, root);
|
|
114
|
+
if (found) return found;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Split a pane in a given direction
|
|
120
|
+
function splitPane(root, paneId, direction) {
|
|
121
|
+
if (!root) return root;
|
|
122
|
+
|
|
123
|
+
if (root.type === 'pane') {
|
|
124
|
+
if (root.id === paneId) {
|
|
125
|
+
// Split this pane - new pane inherits processes from current
|
|
126
|
+
const newPane = createPane([...root.processes]);
|
|
127
|
+
return createSplit(direction, [root, newPane]);
|
|
128
|
+
}
|
|
129
|
+
return root;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// It's a split node - recurse into children
|
|
133
|
+
const newChildren = root.children.map(child => splitPane(child, paneId, direction));
|
|
134
|
+
|
|
135
|
+
// Check if any child was replaced with a split of same direction - flatten it
|
|
136
|
+
const flattenedChildren = [];
|
|
137
|
+
const flattenedSizes = [];
|
|
138
|
+
newChildren.forEach((child, idx) => {
|
|
139
|
+
if (child.type === 'split' && child.direction === root.direction && child !== root.children[idx]) {
|
|
140
|
+
// Flatten: add the new split's children directly
|
|
141
|
+
flattenedChildren.push(...child.children);
|
|
142
|
+
const sizePerChild = root.sizes[idx] / child.children.length;
|
|
143
|
+
flattenedSizes.push(...child.children.map(() => sizePerChild));
|
|
144
|
+
} else {
|
|
145
|
+
flattenedChildren.push(child);
|
|
146
|
+
flattenedSizes.push(root.sizes[idx]);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
...root,
|
|
152
|
+
children: flattenedChildren,
|
|
153
|
+
sizes: flattenedSizes,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Close a pane (remove it from the tree)
|
|
158
|
+
function closePane(root, paneId) {
|
|
159
|
+
if (!root) return null;
|
|
160
|
+
|
|
161
|
+
if (root.type === 'pane') {
|
|
162
|
+
return root.id === paneId ? null : root;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Find and remove the pane
|
|
166
|
+
const newChildren = root.children
|
|
167
|
+
.map(child => closePane(child, paneId))
|
|
168
|
+
.filter(child => child !== null);
|
|
169
|
+
|
|
170
|
+
if (newChildren.length === 0) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
if (newChildren.length === 1) {
|
|
174
|
+
// Unwrap single child
|
|
175
|
+
return newChildren[0];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Recalculate sizes for remaining children
|
|
179
|
+
const originalIndices = [];
|
|
180
|
+
root.children.forEach((child, idx) => {
|
|
181
|
+
const closed = closePane(child, paneId);
|
|
182
|
+
if (closed !== null) {
|
|
183
|
+
originalIndices.push(idx);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const newSizes = originalIndices.map(idx => root.sizes[idx]);
|
|
188
|
+
// Normalize sizes
|
|
189
|
+
const total = newSizes.reduce((a, b) => a + b, 0);
|
|
190
|
+
const normalizedSizes = newSizes.map(s => s / total * newChildren.length);
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
...root,
|
|
194
|
+
children: newChildren,
|
|
195
|
+
sizes: normalizedSizes,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Serialize pane tree for saving to config (strip runtime state)
|
|
200
|
+
function serializePaneTree(node) {
|
|
201
|
+
if (!node) return null;
|
|
202
|
+
|
|
203
|
+
if (node.type === 'pane') {
|
|
204
|
+
return {
|
|
205
|
+
type: 'pane',
|
|
206
|
+
name: node.name || '',
|
|
207
|
+
processes: node.processes || [],
|
|
208
|
+
hidden: node.hidden || [],
|
|
209
|
+
filter: node.filter || '',
|
|
210
|
+
colorFilter: node.colorFilter || null,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
type: 'split',
|
|
216
|
+
direction: node.direction,
|
|
217
|
+
sizes: node.sizes,
|
|
218
|
+
children: node.children.map(child => serializePaneTree(child)),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Deserialize pane tree from config (restore with fresh IDs)
|
|
223
|
+
function deserializePaneTree(data) {
|
|
224
|
+
if (!data) return null;
|
|
225
|
+
|
|
226
|
+
if (data.type === 'pane') {
|
|
227
|
+
const pane = createPane(data.processes || []);
|
|
228
|
+
pane.name = data.name || '';
|
|
229
|
+
pane.hidden = data.hidden || [];
|
|
230
|
+
pane.filter = data.filter || '';
|
|
231
|
+
pane.colorFilter = data.colorFilter || null;
|
|
232
|
+
return pane;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
type: 'split',
|
|
237
|
+
direction: data.direction,
|
|
238
|
+
sizes: data.sizes || data.children.map(() => 1),
|
|
239
|
+
children: data.children.map(child => deserializePaneTree(child)),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Color palette (inspired by Tokyo Night theme)
|
|
244
|
+
const COLORS = {
|
|
245
|
+
border: '#3b4261',
|
|
246
|
+
borderFocused: '#7aa2f7',
|
|
247
|
+
bg: '#1a1b26',
|
|
248
|
+
bgLight: '#24283b',
|
|
249
|
+
bgHighlight: '#292e42',
|
|
250
|
+
text: '#c0caf5',
|
|
251
|
+
textDim: '#565f89',
|
|
252
|
+
accent: '#7aa2f7',
|
|
253
|
+
success: '#9ece6a',
|
|
254
|
+
error: '#f7768e',
|
|
255
|
+
warning: '#e0af68',
|
|
256
|
+
cyan: '#7dcfff',
|
|
257
|
+
magenta: '#bb9af7',
|
|
258
|
+
};
|
|
13
259
|
|
|
14
260
|
// Match string against pattern with wildcard support
|
|
15
261
|
function matchesPattern(str, pattern) {
|
|
@@ -25,26 +271,26 @@ function isIncluded(name, includePatterns) {
|
|
|
25
271
|
function isIgnored(name, ignorePatterns) {
|
|
26
272
|
return ignorePatterns.some(pattern => matchesPattern(name, pattern));
|
|
27
273
|
}
|
|
28
|
-
|
|
29
|
-
// Parse npm scripts from package.json
|
|
30
|
-
function parseNpmScripts(packageJsonPath) {
|
|
31
|
-
try {
|
|
32
|
-
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
33
|
-
const scripts = pkg.scripts || {};
|
|
34
|
-
|
|
35
|
-
return Object.entries(scripts)
|
|
36
|
-
.filter(([name]) => !name.startsWith('pre') && !name.startsWith('post') && name !== 'start')
|
|
37
|
-
.map(([name, command]) => ({
|
|
38
|
-
name,
|
|
39
|
-
command: `npm run ${name}`,
|
|
40
|
-
displayName: name,
|
|
41
|
-
}));
|
|
42
|
-
} catch (error) {
|
|
43
|
-
console.error('Error reading package.json:', error.message);
|
|
44
|
-
process.exit(1);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
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
|
+
|
|
48
294
|
// Load config
|
|
49
295
|
function loadConfig() {
|
|
50
296
|
if (existsSync(CONFIG_FILE)) {
|
|
@@ -65,338 +311,472 @@ function saveConfig(config) {
|
|
|
65
311
|
console.error('Error saving config:', error.message);
|
|
66
312
|
}
|
|
67
313
|
}
|
|
68
|
-
|
|
69
|
-
// Process Manager
|
|
314
|
+
|
|
315
|
+
// Process Manager
|
|
70
316
|
class ProcessManager {
|
|
71
317
|
constructor(renderer, scripts) {
|
|
72
318
|
this.renderer = renderer;
|
|
73
319
|
this.config = loadConfig();
|
|
320
|
+
this.allScripts = scripts; // Keep reference to all scripts (unfiltered)
|
|
74
321
|
this.scripts = scripts
|
|
75
322
|
.filter(s => isIncluded(s.name, this.config.include))
|
|
76
|
-
.filter(s => !isIgnored(s.name, this.config.ignore));
|
|
77
|
-
this.phase = 'selection'; // 'selection' | 'running'
|
|
323
|
+
.filter(s => !isIgnored(s.name, this.config.ignore || []));
|
|
324
|
+
this.phase = 'selection'; // 'selection' | 'running' | 'settings'
|
|
78
325
|
this.selectedScripts = new Set(this.config.defaultSelection);
|
|
79
|
-
this.countdown = COUNTDOWN_SECONDS;
|
|
80
|
-
this.selectedIndex = 0;
|
|
81
|
-
this.processes = new Map();
|
|
82
|
-
this.processRefs = new Map();
|
|
83
|
-
this.outputLines = [];
|
|
84
|
-
this.filter = '';
|
|
85
|
-
this.maxOutputLines = 1000;
|
|
86
|
-
this.maxVisibleLines =
|
|
87
|
-
this.isPaused = false; // Whether output scrolling is paused
|
|
88
|
-
this.wasPaused = false; // Track previous pause state to detect changes
|
|
89
|
-
this.isFilterMode = false; // Whether in filter input mode
|
|
90
|
-
this.
|
|
91
|
-
this.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
this.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
//
|
|
103
|
-
this.
|
|
104
|
-
this.
|
|
105
|
-
this.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
this.
|
|
110
|
-
this.
|
|
111
|
-
this.
|
|
112
|
-
this.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
this.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
326
|
+
this.countdown = COUNTDOWN_SECONDS;
|
|
327
|
+
this.selectedIndex = 0;
|
|
328
|
+
this.processes = new Map();
|
|
329
|
+
this.processRefs = new Map();
|
|
330
|
+
this.outputLines = [];
|
|
331
|
+
this.filter = '';
|
|
332
|
+
this.maxOutputLines = 1000;
|
|
333
|
+
this.maxVisibleLines = null; // Calculated dynamically based on screen height
|
|
334
|
+
this.isPaused = false; // Whether output scrolling is paused
|
|
335
|
+
this.wasPaused = false; // Track previous pause state to detect changes
|
|
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
|
|
339
|
+
|
|
340
|
+
// Settings menu state
|
|
341
|
+
this.settingsSection = 'ignore'; // 'ignore' | 'include' | 'scripts'
|
|
342
|
+
this.settingsIndex = 0; // Current selection index within section
|
|
343
|
+
this.isAddingPattern = false; // Whether typing a new pattern
|
|
344
|
+
this.newPatternText = ''; // Text being typed for new pattern
|
|
345
|
+
this.settingsContainer = null; // UI reference
|
|
346
|
+
this.previousPhase = 'selection'; // Track where we came from
|
|
347
|
+
this.outputBox = null; // Reference to the output container
|
|
348
|
+
this.destroyed = false; // Flag to prevent operations after cleanup
|
|
349
|
+
this.lastRenderedLineCount = 0; // Track how many lines we've rendered
|
|
350
|
+
this.headerRenderable = null; // Reference to header text in running UI
|
|
351
|
+
this.processListRenderable = null; // Reference to process list text in running UI
|
|
352
|
+
this.renderScheduled = false; // Throttle renders for CPU efficiency
|
|
353
|
+
|
|
354
|
+
// Split pane state
|
|
355
|
+
this.paneRoot = null; // Root of pane tree (initialized when running starts)
|
|
356
|
+
this.focusedPaneId = null; // ID of currently focused pane
|
|
357
|
+
this.splitMode = false; // Whether waiting for split command after Ctrl+b
|
|
358
|
+
this.showSplitMenu = false; // Whether to show the command palette
|
|
359
|
+
this.splitMenuIndex = 0; // Selected item in split menu
|
|
360
|
+
|
|
361
|
+
// Assign colors to each script
|
|
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
|
+
|
|
400
|
+
handleInput(keyName, keyEvent) {
|
|
401
|
+
if (this.phase === 'selection') {
|
|
402
|
+
if (keyName === 'enter' || keyName === 'return') {
|
|
403
|
+
clearInterval(this.countdownInterval);
|
|
404
|
+
this.startProcesses();
|
|
405
|
+
} else if (keyName === 'up') {
|
|
406
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
407
|
+
} else if (keyName === 'down') {
|
|
408
|
+
this.selectedIndex = Math.min(this.scripts.length - 1, this.selectedIndex + 1);
|
|
409
|
+
} else if (keyName === 'space') {
|
|
410
|
+
const scriptName = this.scripts[this.selectedIndex]?.name;
|
|
411
|
+
if (scriptName) {
|
|
412
|
+
if (this.selectedScripts.has(scriptName)) {
|
|
413
|
+
this.selectedScripts.delete(scriptName);
|
|
414
|
+
} else {
|
|
415
|
+
this.selectedScripts.add(scriptName);
|
|
416
|
+
}
|
|
417
|
+
// Reset countdown when selection changes
|
|
418
|
+
this.countdown = COUNTDOWN_SECONDS;
|
|
419
|
+
}
|
|
420
|
+
} else if (keyName === 'o') {
|
|
421
|
+
// Open settings menu (options)
|
|
422
|
+
clearInterval(this.countdownInterval);
|
|
423
|
+
this.previousPhase = 'selection';
|
|
424
|
+
this.phase = 'settings';
|
|
425
|
+
this.settingsSection = 'ignore';
|
|
426
|
+
this.settingsIndex = 0;
|
|
427
|
+
this.buildSettingsUI();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
} else if (this.phase === 'settings') {
|
|
431
|
+
this.handleSettingsInput(keyName, keyEvent);
|
|
432
|
+
return;
|
|
433
|
+
} else if (this.phase === 'running') {
|
|
434
|
+
// Handle split menu
|
|
435
|
+
if (this.showSplitMenu) {
|
|
436
|
+
this.handleSplitMenuInput(keyName, keyEvent);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
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
|
+
}
|
|
463
|
+
// If in filter mode, handle filter input
|
|
464
|
+
else if (this.isFilterMode) {
|
|
465
|
+
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
466
|
+
if (keyName === 'escape') {
|
|
467
|
+
this.isFilterMode = false;
|
|
468
|
+
if (pane) pane.filter = '';
|
|
469
|
+
this.buildRunningUI(); // Rebuild to clear filter
|
|
160
470
|
} else if (keyName === 'enter' || keyName === 'return') {
|
|
161
471
|
this.isFilterMode = false;
|
|
162
|
-
this.isPaused = true; // Pause when filter is applied
|
|
163
|
-
this.updateStreamPauseState();
|
|
164
472
|
this.buildRunningUI(); // Rebuild with filter
|
|
165
473
|
} else if (keyName === 'backspace') {
|
|
166
|
-
|
|
474
|
+
if (pane) pane.filter = (pane.filter || '').slice(0, -1);
|
|
167
475
|
this.buildRunningUI(); // Update UI to show filter change
|
|
168
476
|
} else if (keyName && keyName.length === 1 && !keyEvent.ctrl && !keyEvent.meta) {
|
|
169
|
-
|
|
477
|
+
if (pane) pane.filter = (pane.filter || '') + keyName;
|
|
170
478
|
this.buildRunningUI(); // Update UI to show filter change
|
|
171
479
|
}
|
|
172
480
|
} else {
|
|
173
481
|
// Normal mode - handle commands
|
|
174
482
|
if (keyName === 'q') {
|
|
483
|
+
this.savePaneLayout();
|
|
175
484
|
this.cleanup();
|
|
176
485
|
this.renderer.destroy();
|
|
486
|
+
} else if (keyName === '\\') {
|
|
487
|
+
// Open split/pane menu (VSCode-friendly alternative to Ctrl+b)
|
|
488
|
+
this.showSplitMenu = true;
|
|
489
|
+
this.splitMenuIndex = 0;
|
|
490
|
+
this.buildRunningUI();
|
|
491
|
+
} else if (keyName === '|') {
|
|
492
|
+
// Quick vertical split
|
|
493
|
+
this.splitCurrentPane('vertical');
|
|
494
|
+
this.buildRunningUI();
|
|
495
|
+
} else if (keyName === '_') {
|
|
496
|
+
// Quick horizontal split
|
|
497
|
+
this.splitCurrentPane('horizontal');
|
|
498
|
+
this.buildRunningUI();
|
|
499
|
+
} else if (keyName === 'x' && getAllPaneIds(this.paneRoot).length > 1) {
|
|
500
|
+
// Close current pane (only if more than one)
|
|
501
|
+
this.closeCurrentPane();
|
|
502
|
+
this.buildRunningUI();
|
|
177
503
|
} else if (keyName === 'space') {
|
|
178
|
-
// Toggle
|
|
504
|
+
// Toggle visibility of selected process in focused pane
|
|
505
|
+
this.toggleProcessVisibility();
|
|
506
|
+
this.buildRunningUI();
|
|
507
|
+
} else if (keyName === 'p') {
|
|
508
|
+
// Toggle pause output scrolling globally
|
|
179
509
|
this.isPaused = !this.isPaused;
|
|
180
510
|
this.updateStreamPauseState();
|
|
511
|
+
this.buildRunningUI();
|
|
181
512
|
} else if (keyName === 'f') {
|
|
182
|
-
// Filter to currently selected process
|
|
513
|
+
// Filter focused pane to currently selected process
|
|
183
514
|
const scriptName = this.scripts[this.selectedIndex]?.name;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
this.updateStreamPauseState();
|
|
515
|
+
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
516
|
+
if (scriptName && pane) {
|
|
517
|
+
pane.filter = scriptName;
|
|
188
518
|
this.buildRunningUI(); // Rebuild to apply filter
|
|
189
519
|
}
|
|
190
520
|
} else if (keyName === '/') {
|
|
191
|
-
// Enter filter mode
|
|
521
|
+
// Enter filter mode for focused pane
|
|
192
522
|
this.isFilterMode = true;
|
|
193
|
-
this.
|
|
523
|
+
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
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
|
|
194
531
|
} else if (keyName === 'escape') {
|
|
195
|
-
// Clear filter
|
|
196
|
-
this.
|
|
532
|
+
// Clear filter on focused pane
|
|
533
|
+
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
534
|
+
if (pane) pane.filter = '';
|
|
197
535
|
this.isPaused = false;
|
|
198
536
|
this.updateStreamPauseState();
|
|
199
537
|
this.buildRunningUI(); // Rebuild to clear filter
|
|
200
538
|
} else if (keyName === 'up' || keyName === 'k') {
|
|
201
|
-
// Navigate processes up
|
|
202
|
-
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
203
|
-
this.buildRunningUI(); // Rebuild to show selection change
|
|
204
|
-
} else if (keyName === 'down' || keyName === 'j') {
|
|
205
|
-
// Navigate processes down
|
|
206
|
-
this.selectedIndex = Math.min(this.scripts.length - 1, this.selectedIndex + 1);
|
|
207
|
-
this.buildRunningUI(); // Rebuild to show selection change
|
|
208
|
-
} else if (keyName === 'left' || keyName === 'h') {
|
|
209
|
-
// Navigate processes left
|
|
210
|
-
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
211
|
-
this.buildRunningUI(); // Rebuild to show selection change
|
|
212
|
-
} else if (keyName === 'right' || keyName === 'l') {
|
|
213
|
-
// Navigate processes right
|
|
214
|
-
this.selectedIndex = Math.min(this.scripts.length - 1, this.selectedIndex + 1);
|
|
215
|
-
this.buildRunningUI(); // Rebuild to show selection change
|
|
216
|
-
} else if (keyName === 'r') {
|
|
217
|
-
const scriptName = this.scripts[this.selectedIndex]?.name;
|
|
218
|
-
if (scriptName) {
|
|
219
|
-
this.restartProcess(scriptName);
|
|
220
|
-
}
|
|
221
|
-
} else if (keyName === 's') {
|
|
222
|
-
// Stop/start selected process
|
|
223
|
-
const scriptName = this.scripts[this.selectedIndex]?.name;
|
|
224
|
-
if (scriptName) {
|
|
225
|
-
this.toggleProcess(scriptName);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
539
|
+
// Navigate processes up
|
|
540
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
541
|
+
this.buildRunningUI(); // Rebuild to show selection change
|
|
542
|
+
} else if (keyName === 'down' || keyName === 'j') {
|
|
543
|
+
// Navigate processes down
|
|
544
|
+
this.selectedIndex = Math.min(this.scripts.length - 1, this.selectedIndex + 1);
|
|
545
|
+
this.buildRunningUI(); // Rebuild to show selection change
|
|
546
|
+
} else if (keyName === 'left' || keyName === 'h') {
|
|
547
|
+
// Navigate processes left
|
|
548
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
549
|
+
this.buildRunningUI(); // Rebuild to show selection change
|
|
550
|
+
} else if (keyName === 'right' || keyName === 'l') {
|
|
551
|
+
// Navigate processes right
|
|
552
|
+
this.selectedIndex = Math.min(this.scripts.length - 1, this.selectedIndex + 1);
|
|
553
|
+
this.buildRunningUI(); // Rebuild to show selection change
|
|
554
|
+
} else if (keyName === 'r') {
|
|
555
|
+
const scriptName = this.scripts[this.selectedIndex]?.name;
|
|
556
|
+
if (scriptName) {
|
|
557
|
+
this.restartProcess(scriptName);
|
|
558
|
+
}
|
|
559
|
+
} else if (keyName === 's') {
|
|
560
|
+
// Stop/start selected process
|
|
561
|
+
const scriptName = this.scripts[this.selectedIndex]?.name;
|
|
562
|
+
if (scriptName) {
|
|
563
|
+
this.toggleProcess(scriptName);
|
|
564
|
+
}
|
|
565
|
+
} else if (keyName === 'o') {
|
|
566
|
+
// Open settings (options)
|
|
567
|
+
this.previousPhase = 'running';
|
|
568
|
+
this.phase = 'settings';
|
|
569
|
+
this.settingsSection = 'ignore';
|
|
570
|
+
this.settingsIndex = 0;
|
|
571
|
+
this.buildSettingsUI();
|
|
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
|
+
}
|
|
582
|
+
} else if (keyName === 'tab') {
|
|
583
|
+
// Navigate to next pane
|
|
584
|
+
this.navigateToNextPane(1);
|
|
585
|
+
this.buildRunningUI();
|
|
586
|
+
} else if (keyEvent.shift && keyName === 'tab') {
|
|
587
|
+
// Navigate to previous pane
|
|
588
|
+
this.navigateToNextPane(-1);
|
|
589
|
+
this.buildRunningUI();
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
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
|
+
|
|
602
|
+
if (clickedIndex !== -1) {
|
|
603
|
+
const scriptName = this.scripts[clickedIndex]?.name;
|
|
604
|
+
if (scriptName) {
|
|
605
|
+
// Toggle selection
|
|
606
|
+
if (this.selectedScripts.has(scriptName)) {
|
|
607
|
+
this.selectedScripts.delete(scriptName);
|
|
608
|
+
} else {
|
|
609
|
+
this.selectedScripts.add(scriptName);
|
|
610
|
+
}
|
|
611
|
+
// Reset countdown when selection changes
|
|
612
|
+
this.countdown = COUNTDOWN_SECONDS;
|
|
613
|
+
// Update focused index
|
|
614
|
+
this.selectedIndex = clickedIndex;
|
|
615
|
+
this.render();
|
|
616
|
+
}
|
|
617
|
+
}
|
|
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
|
+
|
|
656
|
+
startProcesses() {
|
|
657
|
+
const selected = Array.from(this.selectedScripts);
|
|
658
|
+
|
|
659
|
+
if (selected.length === 0) {
|
|
660
|
+
console.log('No scripts selected.');
|
|
661
|
+
process.exit(0);
|
|
662
|
+
}
|
|
663
|
+
|
|
299
664
|
this.config.defaultSelection = selected;
|
|
300
|
-
saveConfig(this.config);
|
|
301
|
-
this.phase = 'running';
|
|
302
|
-
this.selectedIndex = 0;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
this.
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
this.
|
|
387
|
-
this.
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
665
|
+
saveConfig(this.config);
|
|
666
|
+
this.phase = 'running';
|
|
667
|
+
this.selectedIndex = 0;
|
|
668
|
+
|
|
669
|
+
// Load pane layout from config or create default
|
|
670
|
+
if (this.config.paneLayout) {
|
|
671
|
+
this.paneRoot = deserializePaneTree(this.config.paneLayout);
|
|
672
|
+
} else {
|
|
673
|
+
this.paneRoot = createPane([]); // Empty array means show all processes
|
|
674
|
+
}
|
|
675
|
+
this.focusedPaneId = this.paneRoot.id;
|
|
676
|
+
|
|
677
|
+
selected.forEach(scriptName => {
|
|
678
|
+
this.startProcess(scriptName);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
this.render();
|
|
682
|
+
}
|
|
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
|
+
|
|
697
|
+
proc.stdout.on('data', (data) => {
|
|
698
|
+
const text = data.toString();
|
|
699
|
+
const lines = text.split('\n');
|
|
700
|
+
lines.forEach(line => {
|
|
701
|
+
if (line.trim()) {
|
|
702
|
+
this.addOutputLine(scriptName, line);
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
proc.stderr.on('data', (data) => {
|
|
708
|
+
const text = data.toString();
|
|
709
|
+
const lines = text.split('\n');
|
|
710
|
+
lines.forEach(line => {
|
|
711
|
+
if (line.trim()) {
|
|
712
|
+
this.addOutputLine(scriptName, line);
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
proc.on('exit', (code) => {
|
|
718
|
+
const status = code === 0 ? 'exited' : 'crashed';
|
|
719
|
+
this.processes.set(scriptName, { status, exitCode: code });
|
|
720
|
+
this.addOutputLine(scriptName, `Process exited with code ${code}`);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
this.processRefs.set(scriptName, proc);
|
|
724
|
+
this.processes.set(scriptName, { status: 'running', pid: proc.pid });
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
addOutputLine(processName, text) {
|
|
728
|
+
// Always store the output line, even when paused
|
|
729
|
+
this.outputLines.push({
|
|
730
|
+
process: processName,
|
|
731
|
+
text,
|
|
732
|
+
timestamp: Date.now(),
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
if (this.outputLines.length > this.maxOutputLines) {
|
|
736
|
+
this.outputLines = this.outputLines.slice(-this.maxOutputLines);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Only render if not paused - this prevents new output from appearing
|
|
740
|
+
// when the user is reviewing history
|
|
741
|
+
if (!this.isPaused) {
|
|
742
|
+
this.scheduleRender();
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
scheduleRender() {
|
|
747
|
+
// Throttle renders to ~60fps to reduce CPU usage
|
|
748
|
+
if (this.renderScheduled) return;
|
|
749
|
+
this.renderScheduled = true;
|
|
750
|
+
setTimeout(() => {
|
|
751
|
+
this.renderScheduled = false;
|
|
752
|
+
this.render();
|
|
753
|
+
}, 16);
|
|
754
|
+
}
|
|
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
|
+
|
|
400
780
|
toggleProcess(scriptName) {
|
|
401
781
|
const proc = this.processes.get(scriptName);
|
|
402
782
|
if (proc?.status === 'running') {
|
|
@@ -406,6 +786,699 @@ class ProcessManager {
|
|
|
406
786
|
}
|
|
407
787
|
}
|
|
408
788
|
|
|
789
|
+
handleSettingsInput(keyName, keyEvent) {
|
|
790
|
+
// Handle text input mode for adding patterns
|
|
791
|
+
if (this.isAddingPattern) {
|
|
792
|
+
if (keyName === 'escape') {
|
|
793
|
+
this.isAddingPattern = false;
|
|
794
|
+
this.newPatternText = '';
|
|
795
|
+
this.buildSettingsUI();
|
|
796
|
+
} else if (keyName === 'enter' || keyName === 'return') {
|
|
797
|
+
if (this.newPatternText.trim()) {
|
|
798
|
+
// Add the pattern to the appropriate list
|
|
799
|
+
if (this.settingsSection === 'ignore') {
|
|
800
|
+
if (!this.config.ignore) this.config.ignore = [];
|
|
801
|
+
this.config.ignore.push(this.newPatternText.trim());
|
|
802
|
+
} else if (this.settingsSection === 'include') {
|
|
803
|
+
if (!this.config.include) this.config.include = [];
|
|
804
|
+
this.config.include.push(this.newPatternText.trim());
|
|
805
|
+
}
|
|
806
|
+
saveConfig(this.config);
|
|
807
|
+
this.applyFilters();
|
|
808
|
+
}
|
|
809
|
+
this.isAddingPattern = false;
|
|
810
|
+
this.newPatternText = '';
|
|
811
|
+
this.buildSettingsUI();
|
|
812
|
+
} else if (keyName === 'backspace') {
|
|
813
|
+
this.newPatternText = this.newPatternText.slice(0, -1);
|
|
814
|
+
this.buildSettingsUI();
|
|
815
|
+
} else if (keyName && keyName.length === 1 && !keyEvent.ctrl && !keyEvent.meta) {
|
|
816
|
+
this.newPatternText += keyName;
|
|
817
|
+
this.buildSettingsUI();
|
|
818
|
+
}
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Normal settings navigation
|
|
823
|
+
if (keyName === 'escape' || keyName === 'q') {
|
|
824
|
+
// Return to previous phase
|
|
825
|
+
if (this.previousPhase === 'running') {
|
|
826
|
+
this.phase = 'running';
|
|
827
|
+
this.buildRunningUI();
|
|
828
|
+
} else {
|
|
829
|
+
this.phase = 'selection';
|
|
830
|
+
this.buildSelectionUI();
|
|
831
|
+
this.countdown = COUNTDOWN_SECONDS;
|
|
832
|
+
this.startCountdown();
|
|
833
|
+
}
|
|
834
|
+
} else if (keyName === 'tab' || keyName === 'right') {
|
|
835
|
+
// Switch section
|
|
836
|
+
const sections = ['ignore', 'include', 'scripts'];
|
|
837
|
+
const idx = sections.indexOf(this.settingsSection);
|
|
838
|
+
this.settingsSection = sections[(idx + 1) % sections.length];
|
|
839
|
+
this.settingsIndex = 0;
|
|
840
|
+
this.buildSettingsUI();
|
|
841
|
+
} else if (keyEvent.shift && keyName === 'tab') {
|
|
842
|
+
// Switch section backwards
|
|
843
|
+
const sections = ['ignore', 'include', 'scripts'];
|
|
844
|
+
const idx = sections.indexOf(this.settingsSection);
|
|
845
|
+
this.settingsSection = sections[(idx - 1 + sections.length) % sections.length];
|
|
846
|
+
this.settingsIndex = 0;
|
|
847
|
+
this.buildSettingsUI();
|
|
848
|
+
} else if (keyName === 'left') {
|
|
849
|
+
// Switch section backwards
|
|
850
|
+
const sections = ['ignore', 'include', 'scripts'];
|
|
851
|
+
const idx = sections.indexOf(this.settingsSection);
|
|
852
|
+
this.settingsSection = sections[(idx - 1 + sections.length) % sections.length];
|
|
853
|
+
this.settingsIndex = 0;
|
|
854
|
+
this.buildSettingsUI();
|
|
855
|
+
} else if (keyName === 'up') {
|
|
856
|
+
this.settingsIndex = Math.max(0, this.settingsIndex - 1);
|
|
857
|
+
this.buildSettingsUI();
|
|
858
|
+
} else if (keyName === 'down') {
|
|
859
|
+
const maxIndex = this.getSettingsMaxIndex();
|
|
860
|
+
this.settingsIndex = Math.min(maxIndex, this.settingsIndex + 1);
|
|
861
|
+
this.buildSettingsUI();
|
|
862
|
+
} else if (keyName === 'a') {
|
|
863
|
+
// Add new pattern (only for ignore/include sections)
|
|
864
|
+
if (this.settingsSection === 'ignore' || this.settingsSection === 'include') {
|
|
865
|
+
this.isAddingPattern = true;
|
|
866
|
+
this.newPatternText = '';
|
|
867
|
+
this.buildSettingsUI();
|
|
868
|
+
}
|
|
869
|
+
} else if (keyName === 'd' || keyName === 'backspace') {
|
|
870
|
+
// Delete selected pattern or toggle script ignore
|
|
871
|
+
this.deleteSelectedItem();
|
|
872
|
+
this.buildSettingsUI();
|
|
873
|
+
} else if (keyName === 'space' || keyName === 'enter' || keyName === 'return') {
|
|
874
|
+
// Toggle for scripts section
|
|
875
|
+
if (this.settingsSection === 'scripts') {
|
|
876
|
+
this.toggleScriptIgnore();
|
|
877
|
+
this.buildSettingsUI();
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
getSettingsMaxIndex() {
|
|
883
|
+
if (this.settingsSection === 'ignore') {
|
|
884
|
+
return Math.max(0, (this.config.ignore?.length || 0) - 1);
|
|
885
|
+
} else if (this.settingsSection === 'include') {
|
|
886
|
+
return Math.max(0, (this.config.include?.length || 0) - 1);
|
|
887
|
+
} else if (this.settingsSection === 'scripts') {
|
|
888
|
+
return Math.max(0, this.allScripts.length - 1);
|
|
889
|
+
}
|
|
890
|
+
return 0;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
deleteSelectedItem() {
|
|
894
|
+
if (this.settingsSection === 'ignore' && this.config.ignore?.length > 0) {
|
|
895
|
+
this.config.ignore.splice(this.settingsIndex, 1);
|
|
896
|
+
if (this.config.ignore.length === 0) delete this.config.ignore;
|
|
897
|
+
saveConfig(this.config);
|
|
898
|
+
this.applyFilters();
|
|
899
|
+
this.settingsIndex = Math.max(0, Math.min(this.settingsIndex, (this.config.ignore?.length || 1) - 1));
|
|
900
|
+
} else if (this.settingsSection === 'include' && this.config.include?.length > 0) {
|
|
901
|
+
this.config.include.splice(this.settingsIndex, 1);
|
|
902
|
+
if (this.config.include.length === 0) delete this.config.include;
|
|
903
|
+
saveConfig(this.config);
|
|
904
|
+
this.applyFilters();
|
|
905
|
+
this.settingsIndex = Math.max(0, Math.min(this.settingsIndex, (this.config.include?.length || 1) - 1));
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
toggleScriptIgnore() {
|
|
910
|
+
const script = this.allScripts[this.settingsIndex];
|
|
911
|
+
if (!script) return;
|
|
912
|
+
|
|
913
|
+
if (!this.config.ignore) this.config.ignore = [];
|
|
914
|
+
|
|
915
|
+
const exactPattern = script.name;
|
|
916
|
+
const idx = this.config.ignore.indexOf(exactPattern);
|
|
917
|
+
|
|
918
|
+
if (idx >= 0) {
|
|
919
|
+
// Remove from ignore list
|
|
920
|
+
this.config.ignore.splice(idx, 1);
|
|
921
|
+
if (this.config.ignore.length === 0) delete this.config.ignore;
|
|
922
|
+
} else {
|
|
923
|
+
// Add to ignore list
|
|
924
|
+
this.config.ignore.push(exactPattern);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
saveConfig(this.config);
|
|
928
|
+
this.applyFilters();
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
applyFilters() {
|
|
932
|
+
// Re-filter scripts based on current config
|
|
933
|
+
this.scripts = this.allScripts
|
|
934
|
+
.filter(s => isIncluded(s.name, this.config.include))
|
|
935
|
+
.filter(s => !isIgnored(s.name, this.config.ignore || []));
|
|
936
|
+
|
|
937
|
+
// Clean up selected scripts that are no longer visible
|
|
938
|
+
const visibleNames = new Set(this.scripts.map(s => s.name));
|
|
939
|
+
this.selectedScripts = new Set([...this.selectedScripts].filter(name => visibleNames.has(name)));
|
|
940
|
+
|
|
941
|
+
// Update default selection in config
|
|
942
|
+
this.config.defaultSelection = Array.from(this.selectedScripts);
|
|
943
|
+
saveConfig(this.config);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Handle split mode commands (after Ctrl+b prefix)
|
|
947
|
+
handleSplitModeInput(keyName, keyEvent) {
|
|
948
|
+
this.splitMode = false; // Exit split mode after any key
|
|
949
|
+
|
|
950
|
+
if (keyName === 'escape') {
|
|
951
|
+
// Just cancel split mode
|
|
952
|
+
this.buildRunningUI();
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// % or | = vertical split (left/right)
|
|
957
|
+
if (keyName === '5' && keyEvent.shift) { // % key
|
|
958
|
+
this.splitCurrentPane('vertical');
|
|
959
|
+
} else if (keyName === '\\' && keyEvent.shift) { // | key
|
|
960
|
+
this.splitCurrentPane('vertical');
|
|
961
|
+
}
|
|
962
|
+
// " or - = horizontal split (top/bottom)
|
|
963
|
+
else if (keyName === "'" && keyEvent.shift) { // " key
|
|
964
|
+
this.splitCurrentPane('horizontal');
|
|
965
|
+
} else if (keyName === '-') {
|
|
966
|
+
this.splitCurrentPane('horizontal');
|
|
967
|
+
}
|
|
968
|
+
// Arrow keys = navigate between panes
|
|
969
|
+
else if (keyName === 'up' || keyName === 'down' || keyName === 'left' || keyName === 'right') {
|
|
970
|
+
this.navigatePaneByDirection(keyName);
|
|
971
|
+
}
|
|
972
|
+
// x = close current pane
|
|
973
|
+
else if (keyName === 'x') {
|
|
974
|
+
this.closeCurrentPane();
|
|
975
|
+
}
|
|
976
|
+
// m = move selected process to current pane
|
|
977
|
+
else if (keyName === 'm') {
|
|
978
|
+
this.moveProcessToCurrentPane();
|
|
979
|
+
}
|
|
980
|
+
// o = cycle through panes
|
|
981
|
+
else if (keyName === 'o') {
|
|
982
|
+
this.navigateToNextPane(1);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
this.buildRunningUI();
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Handle command palette input
|
|
989
|
+
handleSplitMenuInput(keyName, keyEvent) {
|
|
990
|
+
const menuItems = this.getSplitMenuItems();
|
|
991
|
+
|
|
992
|
+
if (keyName === 'escape' || keyName === 'q') {
|
|
993
|
+
this.showSplitMenu = false;
|
|
994
|
+
this.buildRunningUI();
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if (keyName === 'up' || keyName === 'k') {
|
|
999
|
+
this.splitMenuIndex = Math.max(0, this.splitMenuIndex - 1);
|
|
1000
|
+
this.buildRunningUI();
|
|
1001
|
+
} else if (keyName === 'down' || keyName === 'j') {
|
|
1002
|
+
this.splitMenuIndex = Math.min(menuItems.length - 1, this.splitMenuIndex + 1);
|
|
1003
|
+
this.buildRunningUI();
|
|
1004
|
+
} else if (keyName === 'enter' || keyName === 'return') {
|
|
1005
|
+
const selectedItem = menuItems[this.splitMenuIndex];
|
|
1006
|
+
if (selectedItem) {
|
|
1007
|
+
selectedItem.action();
|
|
1008
|
+
}
|
|
1009
|
+
this.showSplitMenu = false;
|
|
1010
|
+
this.buildRunningUI();
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
getSplitMenuItems() {
|
|
1015
|
+
const allPanes = getAllPaneIds(this.paneRoot);
|
|
1016
|
+
const items = [
|
|
1017
|
+
{ label: 'Split Vertical (left/right)', shortcut: '|', action: () => this.splitCurrentPane('vertical') },
|
|
1018
|
+
{ label: 'Split Horizontal (top/bottom)', shortcut: '_', action: () => this.splitCurrentPane('horizontal') },
|
|
1019
|
+
];
|
|
1020
|
+
|
|
1021
|
+
if (allPanes.length > 1) {
|
|
1022
|
+
items.push({ label: 'Close Pane', shortcut: 'x', action: () => this.closeCurrentPane() });
|
|
1023
|
+
items.push({ label: 'Next Pane', shortcut: 'Tab', action: () => this.navigateToNextPane(1) });
|
|
1024
|
+
items.push({ label: 'Previous Pane', shortcut: 'Shift+Tab', action: () => this.navigateToNextPane(-1) });
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
return items;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Split the currently focused pane
|
|
1031
|
+
splitCurrentPane(direction) {
|
|
1032
|
+
if (!this.focusedPaneId) return;
|
|
1033
|
+
|
|
1034
|
+
this.paneRoot = splitPane(this.paneRoot, this.focusedPaneId, direction);
|
|
1035
|
+
|
|
1036
|
+
// Focus the new pane (second child of the split)
|
|
1037
|
+
const allPanes = getAllPaneIds(this.paneRoot);
|
|
1038
|
+
const currentIdx = allPanes.indexOf(this.focusedPaneId);
|
|
1039
|
+
if (currentIdx >= 0 && currentIdx + 1 < allPanes.length) {
|
|
1040
|
+
this.focusedPaneId = allPanes[currentIdx + 1];
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
this.savePaneLayout();
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// Close the currently focused pane
|
|
1047
|
+
closeCurrentPane() {
|
|
1048
|
+
if (!this.focusedPaneId) return;
|
|
1049
|
+
|
|
1050
|
+
const allPanes = getAllPaneIds(this.paneRoot);
|
|
1051
|
+
if (allPanes.length <= 1) {
|
|
1052
|
+
// Don't close the last pane
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Find the next pane to focus
|
|
1057
|
+
const currentIdx = allPanes.indexOf(this.focusedPaneId);
|
|
1058
|
+
const nextIdx = currentIdx > 0 ? currentIdx - 1 : 1;
|
|
1059
|
+
const nextPaneId = allPanes[nextIdx];
|
|
1060
|
+
|
|
1061
|
+
this.paneRoot = closePane(this.paneRoot, this.focusedPaneId);
|
|
1062
|
+
this.focusedPaneId = nextPaneId;
|
|
1063
|
+
|
|
1064
|
+
this.savePaneLayout();
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Navigate to next/previous pane
|
|
1068
|
+
navigateToNextPane(direction) {
|
|
1069
|
+
const allPanes = getAllPaneIds(this.paneRoot);
|
|
1070
|
+
if (allPanes.length <= 1) return;
|
|
1071
|
+
|
|
1072
|
+
const currentIdx = allPanes.indexOf(this.focusedPaneId);
|
|
1073
|
+
let nextIdx = (currentIdx + direction + allPanes.length) % allPanes.length;
|
|
1074
|
+
this.focusedPaneId = allPanes[nextIdx];
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Navigate pane by direction (up/down/left/right)
|
|
1078
|
+
navigatePaneByDirection(direction) {
|
|
1079
|
+
// For now, just cycle through panes
|
|
1080
|
+
// A more sophisticated implementation would use pane positions
|
|
1081
|
+
if (direction === 'right' || direction === 'down') {
|
|
1082
|
+
this.navigateToNextPane(1);
|
|
1083
|
+
} else {
|
|
1084
|
+
this.navigateToNextPane(-1);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// Move the currently selected process to the focused pane
|
|
1089
|
+
moveProcessToCurrentPane() {
|
|
1090
|
+
const scriptName = this.scripts[this.selectedIndex]?.name;
|
|
1091
|
+
if (!scriptName || !this.focusedPaneId) return;
|
|
1092
|
+
|
|
1093
|
+
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
1094
|
+
if (!pane) return;
|
|
1095
|
+
|
|
1096
|
+
// If pane shows all processes (empty array), make it show only this one
|
|
1097
|
+
if (pane.processes.length === 0) {
|
|
1098
|
+
pane.processes = [scriptName];
|
|
1099
|
+
} else if (!pane.processes.includes(scriptName)) {
|
|
1100
|
+
pane.processes.push(scriptName);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// Remove from other panes
|
|
1104
|
+
const allPanes = getAllPaneIds(this.paneRoot);
|
|
1105
|
+
for (const paneId of allPanes) {
|
|
1106
|
+
if (paneId !== this.focusedPaneId) {
|
|
1107
|
+
const otherPane = findPaneById(this.paneRoot, paneId);
|
|
1108
|
+
if (otherPane && otherPane.processes.length > 0) {
|
|
1109
|
+
otherPane.processes = otherPane.processes.filter(p => p !== scriptName);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// Hide the selected process from the focused pane
|
|
1116
|
+
hideProcessFromCurrentPane() {
|
|
1117
|
+
const scriptName = this.scripts[this.selectedIndex]?.name;
|
|
1118
|
+
if (!scriptName || !this.focusedPaneId) return;
|
|
1119
|
+
|
|
1120
|
+
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
1121
|
+
if (!pane) return;
|
|
1122
|
+
|
|
1123
|
+
// Initialize hidden array if needed
|
|
1124
|
+
if (!pane.hidden) pane.hidden = [];
|
|
1125
|
+
|
|
1126
|
+
// Add to hidden list if not already there
|
|
1127
|
+
if (!pane.hidden.includes(scriptName)) {
|
|
1128
|
+
pane.hidden.push(scriptName);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Also remove from processes list if it was explicitly added
|
|
1132
|
+
if (pane.processes.length > 0) {
|
|
1133
|
+
pane.processes = pane.processes.filter(p => p !== scriptName);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// Unhide/show the selected process in the focused pane
|
|
1138
|
+
unhideProcessInCurrentPane() {
|
|
1139
|
+
const scriptName = this.scripts[this.selectedIndex]?.name;
|
|
1140
|
+
if (!scriptName || !this.focusedPaneId) return;
|
|
1141
|
+
|
|
1142
|
+
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
1143
|
+
if (!pane) return;
|
|
1144
|
+
|
|
1145
|
+
// Remove from hidden list
|
|
1146
|
+
if (pane.hidden) {
|
|
1147
|
+
pane.hidden = pane.hidden.filter(p => p !== scriptName);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// Toggle visibility of selected process in focused pane
|
|
1152
|
+
toggleProcessVisibility() {
|
|
1153
|
+
const scriptName = this.scripts[this.selectedIndex]?.name;
|
|
1154
|
+
if (!scriptName || !this.focusedPaneId) return;
|
|
1155
|
+
|
|
1156
|
+
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
1157
|
+
if (!pane) return;
|
|
1158
|
+
|
|
1159
|
+
// Initialize hidden array if needed
|
|
1160
|
+
if (!pane.hidden) pane.hidden = [];
|
|
1161
|
+
|
|
1162
|
+
// Toggle: if hidden, show it; if visible, hide it
|
|
1163
|
+
if (pane.hidden.includes(scriptName)) {
|
|
1164
|
+
pane.hidden = pane.hidden.filter(p => p !== scriptName);
|
|
1165
|
+
} else {
|
|
1166
|
+
pane.hidden.push(scriptName);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
this.savePaneLayout();
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// Save the current pane layout to config
|
|
1173
|
+
savePaneLayout() {
|
|
1174
|
+
this.config.paneLayout = serializePaneTree(this.paneRoot);
|
|
1175
|
+
saveConfig(this.config);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// Check if a process is visible in the focused pane
|
|
1179
|
+
isProcessVisibleInPane(scriptName, pane) {
|
|
1180
|
+
if (!pane) return true;
|
|
1181
|
+
|
|
1182
|
+
// If pane has specific processes, check if this one is included
|
|
1183
|
+
if (pane.processes.length > 0 && !pane.processes.includes(scriptName)) {
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Check if hidden
|
|
1188
|
+
if (pane.hidden && pane.hidden.includes(scriptName)) {
|
|
1189
|
+
return false;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
return true;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Count horizontal splits (which reduce available height per pane)
|
|
1196
|
+
// Get output lines for a specific pane
|
|
1197
|
+
getOutputLinesForPane(pane) {
|
|
1198
|
+
let lines = this.outputLines;
|
|
1199
|
+
|
|
1200
|
+
// Filter by processes assigned to this pane
|
|
1201
|
+
if (pane.processes.length > 0) {
|
|
1202
|
+
lines = lines.filter(line => pane.processes.includes(line.process));
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Exclude hidden processes
|
|
1206
|
+
if (pane.hidden && pane.hidden.length > 0) {
|
|
1207
|
+
lines = lines.filter(line => !pane.hidden.includes(line.process));
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Apply pane-specific text filter (from / or f command)
|
|
1211
|
+
if (pane.filter) {
|
|
1212
|
+
lines = lines.filter(line =>
|
|
1213
|
+
line.process.toLowerCase().includes(pane.filter.toLowerCase()) ||
|
|
1214
|
+
line.text.toLowerCase().includes(pane.filter.toLowerCase())
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
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
|
+
|
|
1223
|
+
return lines;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
buildSettingsUI() {
|
|
1227
|
+
// Remove old containers - use destroyRecursively to clean up all children
|
|
1228
|
+
if (this.selectionContainer) {
|
|
1229
|
+
this.renderer.root.remove(this.selectionContainer);
|
|
1230
|
+
this.selectionContainer.destroyRecursively();
|
|
1231
|
+
this.selectionContainer = null;
|
|
1232
|
+
this.scriptLines = null;
|
|
1233
|
+
this.headerText = null;
|
|
1234
|
+
}
|
|
1235
|
+
if (this.settingsContainer) {
|
|
1236
|
+
this.renderer.root.remove(this.settingsContainer);
|
|
1237
|
+
this.settingsContainer.destroyRecursively();
|
|
1238
|
+
this.settingsContainer = null;
|
|
1239
|
+
}
|
|
1240
|
+
if (this.runningContainer) {
|
|
1241
|
+
this.renderer.root.remove(this.runningContainer);
|
|
1242
|
+
this.runningContainer.destroyRecursively();
|
|
1243
|
+
this.runningContainer = null;
|
|
1244
|
+
this.outputBox = null;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// Create main container - full screen with dark background
|
|
1248
|
+
this.settingsContainer = new BoxRenderable(this.renderer, {
|
|
1249
|
+
id: 'settings-container',
|
|
1250
|
+
flexDirection: 'column',
|
|
1251
|
+
width: '100%',
|
|
1252
|
+
height: '100%',
|
|
1253
|
+
backgroundColor: COLORS.bg,
|
|
1254
|
+
padding: 1,
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
// Header bar with title
|
|
1258
|
+
const headerBar = new BoxRenderable(this.renderer, {
|
|
1259
|
+
id: 'header-bar',
|
|
1260
|
+
flexDirection: 'row',
|
|
1261
|
+
justifyContent: 'space-between',
|
|
1262
|
+
width: '100%',
|
|
1263
|
+
border: ['bottom'],
|
|
1264
|
+
borderStyle: 'single',
|
|
1265
|
+
borderColor: COLORS.border,
|
|
1266
|
+
paddingBottom: 1,
|
|
1267
|
+
marginBottom: 1,
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
const titleText = new TextRenderable(this.renderer, {
|
|
1271
|
+
id: 'title',
|
|
1272
|
+
content: t`${fg(COLORS.accent)('# Settings')}`,
|
|
1273
|
+
});
|
|
1274
|
+
headerBar.add(titleText);
|
|
1275
|
+
|
|
1276
|
+
const versionText = new TextRenderable(this.renderer, {
|
|
1277
|
+
id: 'version',
|
|
1278
|
+
content: t`${fg(COLORS.textDim)(APP_VERSION)}`,
|
|
1279
|
+
});
|
|
1280
|
+
headerBar.add(versionText);
|
|
1281
|
+
|
|
1282
|
+
this.settingsContainer.add(headerBar);
|
|
1283
|
+
|
|
1284
|
+
// Input prompt if adding pattern
|
|
1285
|
+
if (this.isAddingPattern) {
|
|
1286
|
+
const inputBar = new BoxRenderable(this.renderer, {
|
|
1287
|
+
id: 'input-bar',
|
|
1288
|
+
border: ['left'],
|
|
1289
|
+
borderStyle: 'single',
|
|
1290
|
+
borderColor: COLORS.accent,
|
|
1291
|
+
paddingLeft: 1,
|
|
1292
|
+
marginBottom: 1,
|
|
1293
|
+
});
|
|
1294
|
+
const inputText = new TextRenderable(this.renderer, {
|
|
1295
|
+
id: 'input-text',
|
|
1296
|
+
content: t`${fg(COLORS.textDim)('Add ' + this.settingsSection + ' pattern:')} ${fg(COLORS.text)(this.newPatternText)}${fg(COLORS.accent)('_')}`,
|
|
1297
|
+
});
|
|
1298
|
+
inputBar.add(inputText);
|
|
1299
|
+
this.settingsContainer.add(inputBar);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// Section tabs
|
|
1303
|
+
const tabsContainer = new BoxRenderable(this.renderer, {
|
|
1304
|
+
id: 'tabs-container',
|
|
1305
|
+
flexDirection: 'row',
|
|
1306
|
+
gap: 2,
|
|
1307
|
+
marginBottom: 1,
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
const sections = [
|
|
1311
|
+
{ id: 'ignore', label: 'IGNORE' },
|
|
1312
|
+
{ id: 'include', label: 'INCLUDE' },
|
|
1313
|
+
{ id: 'scripts', label: 'SCRIPTS' },
|
|
1314
|
+
];
|
|
1315
|
+
|
|
1316
|
+
sections.forEach(({ id, label }) => {
|
|
1317
|
+
const isActive = this.settingsSection === id;
|
|
1318
|
+
const tab = new TextRenderable(this.renderer, {
|
|
1319
|
+
id: `tab-${id}`,
|
|
1320
|
+
content: isActive
|
|
1321
|
+
? t`${fg(COLORS.accent)('[' + label + ']')}`
|
|
1322
|
+
: t`${fg(COLORS.textDim)(' ' + label + ' ')}`,
|
|
1323
|
+
});
|
|
1324
|
+
tabsContainer.add(tab);
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
this.settingsContainer.add(tabsContainer);
|
|
1328
|
+
|
|
1329
|
+
// Content panel with border
|
|
1330
|
+
const contentPanel = new BoxRenderable(this.renderer, {
|
|
1331
|
+
id: 'content-panel',
|
|
1332
|
+
flexDirection: 'column',
|
|
1333
|
+
border: true,
|
|
1334
|
+
borderStyle: 'rounded',
|
|
1335
|
+
borderColor: COLORS.border,
|
|
1336
|
+
title: ` ${this.settingsSection.charAt(0).toUpperCase() + this.settingsSection.slice(1)} `,
|
|
1337
|
+
titleAlignment: 'left',
|
|
1338
|
+
flexGrow: 1,
|
|
1339
|
+
padding: 1,
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
// Section content
|
|
1343
|
+
if (this.settingsSection === 'ignore') {
|
|
1344
|
+
this.buildIgnoreSectionContent(contentPanel);
|
|
1345
|
+
} else if (this.settingsSection === 'include') {
|
|
1346
|
+
this.buildIncludeSectionContent(contentPanel);
|
|
1347
|
+
} else if (this.settingsSection === 'scripts') {
|
|
1348
|
+
this.buildScriptsSectionContent(contentPanel);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
this.settingsContainer.add(contentPanel);
|
|
1352
|
+
|
|
1353
|
+
// Footer bar with keyboard shortcuts
|
|
1354
|
+
const footerBar = new BoxRenderable(this.renderer, {
|
|
1355
|
+
id: 'footer-bar',
|
|
1356
|
+
flexDirection: 'row',
|
|
1357
|
+
width: '100%',
|
|
1358
|
+
border: ['top'],
|
|
1359
|
+
borderStyle: 'single',
|
|
1360
|
+
borderColor: COLORS.border,
|
|
1361
|
+
paddingTop: 1,
|
|
1362
|
+
marginTop: 1,
|
|
1363
|
+
gap: 2,
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
const shortcuts = this.isAddingPattern
|
|
1367
|
+
? [
|
|
1368
|
+
{ key: 'enter', desc: 'save' },
|
|
1369
|
+
{ key: 'esc', desc: 'cancel' },
|
|
1370
|
+
]
|
|
1371
|
+
: [
|
|
1372
|
+
{ key: 'tab', desc: 'section' },
|
|
1373
|
+
{ key: 'a', desc: 'add' },
|
|
1374
|
+
{ key: 'd', desc: 'delete' },
|
|
1375
|
+
{ key: 'space', desc: 'toggle' },
|
|
1376
|
+
{ key: 'esc', desc: 'back' },
|
|
1377
|
+
];
|
|
1378
|
+
|
|
1379
|
+
shortcuts.forEach(({ key, desc }) => {
|
|
1380
|
+
const shortcut = new TextRenderable(this.renderer, {
|
|
1381
|
+
id: `shortcut-${key}`,
|
|
1382
|
+
content: t`${fg(COLORS.textDim)(key)} ${fg(COLORS.text)(desc)}`,
|
|
1383
|
+
});
|
|
1384
|
+
footerBar.add(shortcut);
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
this.settingsContainer.add(footerBar);
|
|
1388
|
+
|
|
1389
|
+
this.renderer.root.add(this.settingsContainer);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
buildIgnoreSectionContent(container) {
|
|
1393
|
+
const desc = new TextRenderable(this.renderer, {
|
|
1394
|
+
id: 'ignore-desc',
|
|
1395
|
+
content: t`${fg(COLORS.textDim)('Patterns to exclude from script list. Use * as wildcard.')}`,
|
|
1396
|
+
});
|
|
1397
|
+
container.add(desc);
|
|
1398
|
+
|
|
1399
|
+
container.add(new TextRenderable(this.renderer, { id: 'spacer', content: '' }));
|
|
1400
|
+
|
|
1401
|
+
const patterns = this.config.ignore || [];
|
|
1402
|
+
|
|
1403
|
+
if (patterns.length === 0) {
|
|
1404
|
+
const empty = new TextRenderable(this.renderer, {
|
|
1405
|
+
id: 'ignore-empty',
|
|
1406
|
+
content: t`${fg(COLORS.textDim)('No ignore patterns defined. Press A to add.')}`,
|
|
1407
|
+
});
|
|
1408
|
+
container.add(empty);
|
|
1409
|
+
} else {
|
|
1410
|
+
patterns.forEach((pattern, idx) => {
|
|
1411
|
+
const isFocused = idx === this.settingsIndex;
|
|
1412
|
+
const indicator = isFocused ? '>' : ' ';
|
|
1413
|
+
|
|
1414
|
+
const line = new TextRenderable(this.renderer, {
|
|
1415
|
+
id: `ignore-pattern-${idx}`,
|
|
1416
|
+
content: t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(COLORS.error)(pattern)}`,
|
|
1417
|
+
});
|
|
1418
|
+
container.add(line);
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
buildIncludeSectionContent(container) {
|
|
1424
|
+
const desc = new TextRenderable(this.renderer, {
|
|
1425
|
+
id: 'include-desc',
|
|
1426
|
+
content: t`${fg(COLORS.textDim)('Only show scripts matching these patterns. Use * as wildcard.')}`,
|
|
1427
|
+
});
|
|
1428
|
+
container.add(desc);
|
|
1429
|
+
|
|
1430
|
+
container.add(new TextRenderable(this.renderer, { id: 'spacer', content: '' }));
|
|
1431
|
+
|
|
1432
|
+
const patterns = this.config.include || [];
|
|
1433
|
+
|
|
1434
|
+
if (patterns.length === 0) {
|
|
1435
|
+
const empty = new TextRenderable(this.renderer, {
|
|
1436
|
+
id: 'include-empty',
|
|
1437
|
+
content: t`${fg(COLORS.textDim)('No include patterns (all scripts shown). Press A to add.')}`,
|
|
1438
|
+
});
|
|
1439
|
+
container.add(empty);
|
|
1440
|
+
} else {
|
|
1441
|
+
patterns.forEach((pattern, idx) => {
|
|
1442
|
+
const isFocused = idx === this.settingsIndex;
|
|
1443
|
+
const indicator = isFocused ? '>' : ' ';
|
|
1444
|
+
|
|
1445
|
+
const line = new TextRenderable(this.renderer, {
|
|
1446
|
+
id: `include-pattern-${idx}`,
|
|
1447
|
+
content: t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(COLORS.success)(pattern)}`,
|
|
1448
|
+
});
|
|
1449
|
+
container.add(line);
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
buildScriptsSectionContent(container) {
|
|
1455
|
+
const desc = new TextRenderable(this.renderer, {
|
|
1456
|
+
id: 'scripts-desc',
|
|
1457
|
+
content: t`${fg(COLORS.textDim)('Toggle individual scripts. Ignored scripts are hidden from selection.')}`,
|
|
1458
|
+
});
|
|
1459
|
+
container.add(desc);
|
|
1460
|
+
|
|
1461
|
+
container.add(new TextRenderable(this.renderer, { id: 'spacer', content: '' }));
|
|
1462
|
+
|
|
1463
|
+
const ignorePatterns = this.config.ignore || [];
|
|
1464
|
+
|
|
1465
|
+
this.allScripts.forEach((script, idx) => {
|
|
1466
|
+
const isIgnored = ignorePatterns.includes(script.name);
|
|
1467
|
+
const isFocused = idx === this.settingsIndex;
|
|
1468
|
+
const indicator = isFocused ? '>' : ' ';
|
|
1469
|
+
const checkbox = isIgnored ? '[x]' : '[ ]';
|
|
1470
|
+
const checkColor = isIgnored ? COLORS.error : COLORS.success;
|
|
1471
|
+
const processColor = this.processColors.get(script.name) || COLORS.text;
|
|
1472
|
+
const nameColor = isIgnored ? COLORS.textDim : processColor;
|
|
1473
|
+
|
|
1474
|
+
const line = new TextRenderable(this.renderer, {
|
|
1475
|
+
id: `script-toggle-${idx}`,
|
|
1476
|
+
content: t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(checkColor)(checkbox)} ${fg(nameColor)(script.displayName)}${isIgnored ? t` ${fg(COLORS.textDim)('(ignored)')}` : ''}`,
|
|
1477
|
+
});
|
|
1478
|
+
container.add(line);
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
|
|
409
1482
|
updateStreamPauseState() {
|
|
410
1483
|
// Pause or resume all process stdout/stderr streams
|
|
411
1484
|
for (const proc of this.processRefs.values()) {
|
|
@@ -420,381 +1493,352 @@ class ProcessManager {
|
|
|
420
1493
|
}
|
|
421
1494
|
}
|
|
422
1495
|
}
|
|
423
|
-
|
|
424
|
-
cleanup() {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
//
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
this.
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
//
|
|
650
|
-
this.
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
this.
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
// Create
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
})
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
const
|
|
708
|
-
const
|
|
709
|
-
const
|
|
710
|
-
const
|
|
711
|
-
const
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
// Add output lines to scrollbox in reverse order (newest first)
|
|
768
|
-
const filteredLines = this.filter
|
|
769
|
-
? this.outputLines.filter(line =>
|
|
770
|
-
line.process.toLowerCase().includes(this.filter.toLowerCase()) ||
|
|
771
|
-
line.text.toLowerCase().includes(this.filter.toLowerCase())
|
|
772
|
-
)
|
|
773
|
-
: this.outputLines;
|
|
774
|
-
|
|
775
|
-
// Decide which lines to show
|
|
776
|
-
let linesToShow;
|
|
777
|
-
if (this.isPaused) {
|
|
778
|
-
// When paused, show all lines (scrollable)
|
|
779
|
-
linesToShow = filteredLines;
|
|
780
|
-
} else {
|
|
781
|
-
// When not paused, only show most recent N lines
|
|
782
|
-
linesToShow = filteredLines.slice(-this.maxVisibleLines);
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// Add lines in reverse order (newest first)
|
|
786
|
-
for (let i = linesToShow.length - 1; i >= 0; i--) {
|
|
787
|
-
const line = linesToShow[i];
|
|
788
|
-
const processColor = this.processColors.get(line.process) || '#FFFFFF';
|
|
789
|
-
|
|
790
|
-
// Truncate long lines to prevent wrapping (terminal width - prefix length - padding)
|
|
791
|
-
const maxWidth = Math.max(40, this.renderer.width - line.process.length - 10);
|
|
1496
|
+
|
|
1497
|
+
cleanup() {
|
|
1498
|
+
this.destroyed = true;
|
|
1499
|
+
|
|
1500
|
+
// Stop the countdown interval
|
|
1501
|
+
if (this.countdownInterval) {
|
|
1502
|
+
clearInterval(this.countdownInterval);
|
|
1503
|
+
this.countdownInterval = null;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
for (const [scriptName, proc] of this.processRefs.entries()) {
|
|
1507
|
+
try {
|
|
1508
|
+
if (proc.pid) {
|
|
1509
|
+
kill(proc.pid, 'SIGKILL');
|
|
1510
|
+
}
|
|
1511
|
+
} catch (err) {
|
|
1512
|
+
// Ignore
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
buildSelectionUI() {
|
|
1518
|
+
// Remove old containers if they exist - use destroyRecursively to clean up all children
|
|
1519
|
+
if (this.selectionContainer) {
|
|
1520
|
+
this.renderer.root.remove(this.selectionContainer);
|
|
1521
|
+
this.selectionContainer.destroyRecursively();
|
|
1522
|
+
this.selectionContainer = null;
|
|
1523
|
+
this.scriptLines = null;
|
|
1524
|
+
this.headerText = null;
|
|
1525
|
+
}
|
|
1526
|
+
if (this.settingsContainer) {
|
|
1527
|
+
this.renderer.root.remove(this.settingsContainer);
|
|
1528
|
+
this.settingsContainer.destroyRecursively();
|
|
1529
|
+
this.settingsContainer = null;
|
|
1530
|
+
}
|
|
1531
|
+
if (this.runningContainer) {
|
|
1532
|
+
this.renderer.root.remove(this.runningContainer);
|
|
1533
|
+
this.runningContainer.destroyRecursively();
|
|
1534
|
+
this.runningContainer = null;
|
|
1535
|
+
this.outputBox = null;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// Create main container - full screen with dark background
|
|
1539
|
+
this.selectionContainer = new BoxRenderable(this.renderer, {
|
|
1540
|
+
id: 'selection-container',
|
|
1541
|
+
flexDirection: 'column',
|
|
1542
|
+
width: '100%',
|
|
1543
|
+
height: '100%',
|
|
1544
|
+
backgroundColor: COLORS.bg,
|
|
1545
|
+
});
|
|
1546
|
+
|
|
1547
|
+
// Scripts panel - compact with background for focused item
|
|
1548
|
+
const scriptsPanel = new BoxRenderable(this.renderer, {
|
|
1549
|
+
id: 'scripts-panel',
|
|
1550
|
+
flexDirection: 'column',
|
|
1551
|
+
flexGrow: 1,
|
|
1552
|
+
paddingLeft: 1,
|
|
1553
|
+
paddingTop: 1,
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
// Track Y positions for mouse clicks
|
|
1557
|
+
let currentY = 1; // start of scripts
|
|
1558
|
+
this.scriptLinePositions = [];
|
|
1559
|
+
|
|
1560
|
+
this.scriptLines = this.scripts.map((script, index) => {
|
|
1561
|
+
const isSelected = this.selectedScripts.has(script.name);
|
|
1562
|
+
const isFocused = index === this.selectedIndex;
|
|
1563
|
+
const checkIcon = isSelected ? '●' : '○';
|
|
1564
|
+
const checkColor = isSelected ? COLORS.success : COLORS.textDim;
|
|
1565
|
+
const processColor = this.processColors.get(script.name) || COLORS.text;
|
|
1566
|
+
const nameColor = isFocused ? COLORS.text : processColor;
|
|
1567
|
+
const bgColor = isFocused ? COLORS.bgHighlight : null;
|
|
1568
|
+
|
|
1569
|
+
// Build styled content - all in one template, no nesting
|
|
1570
|
+
const content = t`${fg(checkColor)(checkIcon)} ${fg(nameColor)(script.displayName)}`;
|
|
1571
|
+
|
|
1572
|
+
const lineContainer = new BoxRenderable(this.renderer, {
|
|
1573
|
+
id: `script-box-${index}`,
|
|
1574
|
+
backgroundColor: bgColor,
|
|
1575
|
+
paddingLeft: 1,
|
|
1576
|
+
width: '100%',
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
const line = new TextRenderable(this.renderer, {
|
|
1580
|
+
id: `script-${index}`,
|
|
1581
|
+
content: content,
|
|
1582
|
+
});
|
|
1583
|
+
lineContainer.add(line);
|
|
1584
|
+
scriptsPanel.add(lineContainer);
|
|
1585
|
+
this.scriptLinePositions.push(currentY);
|
|
1586
|
+
currentY++;
|
|
1587
|
+
return lineContainer;
|
|
1588
|
+
});
|
|
1589
|
+
|
|
1590
|
+
this.selectionContainer.add(scriptsPanel);
|
|
1591
|
+
|
|
1592
|
+
// Footer bar with title, countdown, and shortcuts
|
|
1593
|
+
const footerBar = new BoxRenderable(this.renderer, {
|
|
1594
|
+
id: 'footer-bar',
|
|
1595
|
+
flexDirection: 'row',
|
|
1596
|
+
justifyContent: 'space-between',
|
|
1597
|
+
width: '100%',
|
|
1598
|
+
backgroundColor: COLORS.bgLight,
|
|
1599
|
+
paddingLeft: 1,
|
|
1600
|
+
paddingRight: 1,
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
// Left side: title and countdown
|
|
1604
|
+
const leftSide = new BoxRenderable(this.renderer, {
|
|
1605
|
+
id: 'footer-left',
|
|
1606
|
+
flexDirection: 'row',
|
|
1607
|
+
gap: 2,
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
const titleText = new TextRenderable(this.renderer, {
|
|
1611
|
+
id: 'title',
|
|
1612
|
+
content: t`${fg(COLORS.accent)('startall')} ${fg(COLORS.warning)(this.countdown + 's')}`,
|
|
1613
|
+
});
|
|
1614
|
+
leftSide.add(titleText);
|
|
1615
|
+
this.headerText = titleText; // Save reference for countdown updates
|
|
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
|
+
|
|
1626
|
+
footerBar.add(leftSide);
|
|
1627
|
+
|
|
1628
|
+
// Right side: shortcuts
|
|
1629
|
+
const rightSide = new BoxRenderable(this.renderer, {
|
|
1630
|
+
id: 'footer-right',
|
|
1631
|
+
flexDirection: 'row',
|
|
1632
|
+
gap: 2,
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
const shortcuts = [
|
|
1636
|
+
{ key: 'spc', desc: 'sel', color: COLORS.success },
|
|
1637
|
+
{ key: 'ret', desc: 'go', color: COLORS.accent },
|
|
1638
|
+
{ key: 'o', desc: 'cfg', color: COLORS.magenta },
|
|
1639
|
+
];
|
|
1640
|
+
|
|
1641
|
+
shortcuts.forEach(({ key, desc, color }) => {
|
|
1642
|
+
const shortcut = new TextRenderable(this.renderer, {
|
|
1643
|
+
id: `shortcut-${key}`,
|
|
1644
|
+
content: t`${fg(color)(key)}${fg(COLORS.textDim)(':' + desc)}`,
|
|
1645
|
+
});
|
|
1646
|
+
rightSide.add(shortcut);
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1649
|
+
footerBar.add(rightSide);
|
|
1650
|
+
this.selectionContainer.add(footerBar);
|
|
1651
|
+
|
|
1652
|
+
this.renderer.root.add(this.selectionContainer);
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
getHeaderText() {
|
|
1656
|
+
return `Starting in ${this.countdown}s...`;
|
|
1657
|
+
}
|
|
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
|
+
|
|
1675
|
+
updateSelectionUI() {
|
|
1676
|
+
// Rebuild UI each time - simpler and more reliable with the new structure
|
|
1677
|
+
this.buildSelectionUI();
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
render() {
|
|
1681
|
+
// Don't render if destroyed
|
|
1682
|
+
if (this.destroyed) return;
|
|
1683
|
+
|
|
1684
|
+
if (this.phase === 'selection') {
|
|
1685
|
+
// For selection phase, just update the text content
|
|
1686
|
+
this.updateSelectionUI();
|
|
1687
|
+
} else if (this.phase === 'settings') {
|
|
1688
|
+
// Settings UI is rebuilt on each input
|
|
1689
|
+
// No-op here as buildSettingsUI handles everything
|
|
1690
|
+
} else if (this.phase === 'running') {
|
|
1691
|
+
// For running phase, only update output, don't rebuild entire UI
|
|
1692
|
+
this.updateRunningUI();
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
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
|
+
|
|
1819
|
+
// Build a single pane's output area
|
|
1820
|
+
buildPaneOutput(pane, container, height) {
|
|
1821
|
+
const isFocused = pane.id === this.focusedPaneId;
|
|
1822
|
+
const lines = this.getOutputLinesForPane(pane);
|
|
1823
|
+
|
|
1824
|
+
// Calculate visible lines - use global pause state
|
|
1825
|
+
const outputHeight = Math.max(3, height - 2);
|
|
1826
|
+
let linesToShow = this.isPaused ? lines : lines.slice(-outputHeight);
|
|
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
|
+
|
|
1832
|
+
// Add lines in reverse order (newest first)
|
|
1833
|
+
for (let i = linesToShow.length - 1; i >= 0; i--) {
|
|
1834
|
+
const line = linesToShow[i];
|
|
1835
|
+
const processColor = this.processColors.get(line.process) || COLORS.text;
|
|
1836
|
+
|
|
1837
|
+
const maxWidth = Math.max(20, this.renderer.width / 2 - line.process.length - 10);
|
|
792
1838
|
const visibleLength = stripAnsi(line.text).length;
|
|
793
1839
|
let truncatedText = line.text;
|
|
794
1840
|
if (visibleLength > maxWidth) {
|
|
795
|
-
// Truncate by visible characters, preserving ANSI codes
|
|
796
1841
|
let visible = 0;
|
|
797
|
-
let i = 0;
|
|
798
1842
|
const ansiRegex = /\x1b\[[0-9;]*m/g;
|
|
799
1843
|
let lastIndex = 0;
|
|
800
1844
|
let result = '';
|
|
@@ -808,7 +1852,7 @@ class ProcessManager {
|
|
|
808
1852
|
visible++;
|
|
809
1853
|
}
|
|
810
1854
|
if (visible >= maxWidth - 3) break;
|
|
811
|
-
result += match[0];
|
|
1855
|
+
result += match[0];
|
|
812
1856
|
lastIndex = ansiRegex.lastIndex;
|
|
813
1857
|
}
|
|
814
1858
|
if (visible < maxWidth - 3) {
|
|
@@ -819,54 +1863,431 @@ class ProcessManager {
|
|
|
819
1863
|
visible++;
|
|
820
1864
|
}
|
|
821
1865
|
}
|
|
822
|
-
truncatedText = result + '\x1b[0m...';
|
|
1866
|
+
truncatedText = result + '\x1b[0m...';
|
|
1867
|
+
}
|
|
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
|
+
|
|
1877
|
+
const outputLine = new TextRenderable(this.renderer, {
|
|
1878
|
+
id: `output-${pane.id}-${i}`,
|
|
1879
|
+
content: t`${fg(processColor)(`[${line.process}]`)} ${truncatedText}${padding}`,
|
|
1880
|
+
bg: '#000000', // Black background for pane content
|
|
1881
|
+
});
|
|
1882
|
+
|
|
1883
|
+
container.add(outputLine);
|
|
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
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
// Build a pane panel with title bar
|
|
1913
|
+
buildPanePanel(pane, flexGrow = 1, availableHeight = null) {
|
|
1914
|
+
const isFocused = pane.id === this.focusedPaneId;
|
|
1915
|
+
const borderColor = isFocused ? COLORS.borderFocused : COLORS.border;
|
|
1916
|
+
|
|
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
|
|
1920
|
+
? pane.processes.join(', ')
|
|
1921
|
+
: 'All');
|
|
1922
|
+
const focusLabel = isFocused ? '*' : '';
|
|
1923
|
+
const hiddenCount = pane.hidden?.length || 0;
|
|
1924
|
+
const hiddenLabel = hiddenCount > 0 ? ` -${hiddenCount}` : '';
|
|
1925
|
+
const filterLabel = pane.filter ? ` /${pane.filter}` : '';
|
|
1926
|
+
const namingInputLabel = (isFocused && this.isNamingMode) ? `Name: ${this.namingModeText}_` : '';
|
|
1927
|
+
const filterInputLabel = (isFocused && this.isFilterMode) ? `/${pane.filter || ''}_` : '';
|
|
1928
|
+
const title = ` ${focusLabel}${namingInputLabel || processLabel}${hiddenLabel}${filterInputLabel || filterLabel} `;
|
|
1929
|
+
|
|
1930
|
+
const paneContainer = new BoxRenderable(this.renderer, {
|
|
1931
|
+
id: `pane-${pane.id}`,
|
|
1932
|
+
flexDirection: 'column',
|
|
1933
|
+
flexGrow: flexGrow,
|
|
1934
|
+
flexShrink: 0, // Prevent shrinking - maintain 50/50 split
|
|
1935
|
+
flexBasis: 0, // Use flexGrow ratio for sizing, not content size
|
|
1936
|
+
border: true,
|
|
1937
|
+
borderStyle: 'rounded',
|
|
1938
|
+
borderColor: borderColor,
|
|
1939
|
+
title: title,
|
|
1940
|
+
titleAlignment: 'left',
|
|
1941
|
+
padding: 0,
|
|
1942
|
+
overflow: 'hidden',
|
|
1943
|
+
backgroundColor: '#000000', // Black background for pane container
|
|
1944
|
+
});
|
|
1945
|
+
|
|
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
|
+
|
|
1950
|
+
const outputBox = new BoxRenderable(this.renderer, {
|
|
1951
|
+
id: `pane-output-${pane.id}`,
|
|
1952
|
+
flexDirection: 'column',
|
|
1953
|
+
flexGrow: 1,
|
|
1954
|
+
flexShrink: 1,
|
|
1955
|
+
flexBasis: 0,
|
|
1956
|
+
overflow: 'hidden',
|
|
1957
|
+
paddingLeft: 1,
|
|
1958
|
+
backgroundColor: '#000000', // Black background for pane
|
|
1959
|
+
});
|
|
1960
|
+
|
|
1961
|
+
this.buildPaneOutput(pane, outputBox, height);
|
|
1962
|
+
|
|
1963
|
+
paneContainer.add(outputBox);
|
|
1964
|
+
return paneContainer;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
// Recursively build the pane layout, passing available height down
|
|
1968
|
+
buildPaneLayout(node, flexGrow = 1, availableHeight = null) {
|
|
1969
|
+
if (!node) return null;
|
|
1970
|
+
|
|
1971
|
+
// Default available height (screen minus header/footer)
|
|
1972
|
+
if (availableHeight === null) {
|
|
1973
|
+
availableHeight = this.renderer.height - 2;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
if (node.type === 'pane') {
|
|
1977
|
+
return this.buildPanePanel(node, flexGrow, availableHeight);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
// It's a split node
|
|
1981
|
+
const container = new BoxRenderable(this.renderer, {
|
|
1982
|
+
id: `split-${node.direction}`,
|
|
1983
|
+
flexDirection: node.direction === 'vertical' ? 'row' : 'column',
|
|
1984
|
+
flexGrow: flexGrow,
|
|
1985
|
+
flexShrink: 0,
|
|
1986
|
+
flexBasis: 0,
|
|
1987
|
+
gap: 0,
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
// Calculate child heights - only horizontal splits divide height
|
|
1991
|
+
const childCount = node.children.length;
|
|
1992
|
+
const childHeight = node.direction === 'horizontal'
|
|
1993
|
+
? Math.floor(availableHeight / childCount)
|
|
1994
|
+
: availableHeight; // vertical splits don't reduce height
|
|
1995
|
+
|
|
1996
|
+
node.children.forEach((child, idx) => {
|
|
1997
|
+
const childElement = this.buildPaneLayout(child, node.sizes[idx], childHeight);
|
|
1998
|
+
if (childElement) {
|
|
1999
|
+
container.add(childElement);
|
|
823
2000
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
}
|
|
2001
|
+
});
|
|
2002
|
+
|
|
2003
|
+
return container;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
// Build command palette overlay
|
|
2007
|
+
buildSplitMenuOverlay(parent) {
|
|
2008
|
+
const menuItems = this.getSplitMenuItems();
|
|
2009
|
+
|
|
2010
|
+
// Create centered overlay
|
|
2011
|
+
const overlay = new BoxRenderable(this.renderer, {
|
|
2012
|
+
id: 'split-menu-overlay',
|
|
2013
|
+
position: 'absolute',
|
|
2014
|
+
top: '30%',
|
|
2015
|
+
left: '30%',
|
|
2016
|
+
width: '40%',
|
|
2017
|
+
backgroundColor: COLORS.bgLight,
|
|
2018
|
+
border: true,
|
|
2019
|
+
borderStyle: 'rounded',
|
|
2020
|
+
borderColor: COLORS.accent,
|
|
2021
|
+
title: ' Command Palette ',
|
|
2022
|
+
padding: 1,
|
|
2023
|
+
flexDirection: 'column',
|
|
2024
|
+
});
|
|
2025
|
+
|
|
2026
|
+
menuItems.forEach((item, idx) => {
|
|
2027
|
+
const isFocused = idx === this.splitMenuIndex;
|
|
2028
|
+
const indicator = isFocused ? '>' : ' ';
|
|
2029
|
+
const bgColor = isFocused ? COLORS.bgHighlight : null;
|
|
2030
|
+
|
|
2031
|
+
const itemContainer = new BoxRenderable(this.renderer, {
|
|
2032
|
+
id: `menu-item-${idx}`,
|
|
2033
|
+
backgroundColor: bgColor,
|
|
2034
|
+
paddingLeft: 1,
|
|
2035
|
+
});
|
|
2036
|
+
|
|
2037
|
+
const itemText = new TextRenderable(this.renderer, {
|
|
2038
|
+
id: `menu-text-${idx}`,
|
|
2039
|
+
content: t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(COLORS.text)(item.label)} ${fg(COLORS.textDim)(`(${item.shortcut})`)}`,
|
|
2040
|
+
});
|
|
2041
|
+
|
|
2042
|
+
itemContainer.add(itemText);
|
|
2043
|
+
overlay.add(itemContainer);
|
|
2044
|
+
});
|
|
2045
|
+
|
|
2046
|
+
// Footer hint
|
|
2047
|
+
const hint = new TextRenderable(this.renderer, {
|
|
2048
|
+
id: 'menu-hint',
|
|
2049
|
+
content: t`${fg(COLORS.textDim)('Enter to select, Esc to close')}`,
|
|
2050
|
+
});
|
|
2051
|
+
overlay.add(hint);
|
|
2052
|
+
|
|
2053
|
+
parent.add(overlay);
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
buildRunningUI() {
|
|
2057
|
+
// Remove old containers if they exist - use destroyRecursively to clean up all children
|
|
2058
|
+
if (this.selectionContainer) {
|
|
2059
|
+
this.renderer.root.remove(this.selectionContainer);
|
|
2060
|
+
this.selectionContainer.destroyRecursively();
|
|
2061
|
+
this.selectionContainer = null;
|
|
2062
|
+
this.scriptLines = null;
|
|
2063
|
+
this.headerText = null;
|
|
2064
|
+
}
|
|
2065
|
+
if (this.settingsContainer) {
|
|
2066
|
+
this.renderer.root.remove(this.settingsContainer);
|
|
2067
|
+
this.settingsContainer.destroyRecursively();
|
|
2068
|
+
this.settingsContainer = null;
|
|
2069
|
+
}
|
|
2070
|
+
if (this.runningContainer) {
|
|
2071
|
+
this.renderer.root.remove(this.runningContainer);
|
|
2072
|
+
this.runningContainer.destroyRecursively();
|
|
2073
|
+
this.runningContainer = null;
|
|
2074
|
+
}
|
|
2075
|
+
// Clear outputBox reference since it was destroyed with runningContainer
|
|
2076
|
+
this.outputBox = null;
|
|
2077
|
+
|
|
2078
|
+
// Create main container - full screen with black background
|
|
2079
|
+
const mainContainer = new BoxRenderable(this.renderer, {
|
|
2080
|
+
id: 'running-container',
|
|
2081
|
+
flexDirection: 'column',
|
|
2082
|
+
width: '100%',
|
|
2083
|
+
height: '100%',
|
|
2084
|
+
backgroundColor: '#000000',
|
|
2085
|
+
});
|
|
2086
|
+
|
|
2087
|
+
// Process tabs at top
|
|
2088
|
+
const processBar = new BoxRenderable(this.renderer, {
|
|
2089
|
+
id: 'process-bar',
|
|
2090
|
+
flexDirection: 'row',
|
|
2091
|
+
width: '100%',
|
|
2092
|
+
backgroundColor: COLORS.bgLight,
|
|
2093
|
+
paddingLeft: 1,
|
|
2094
|
+
});
|
|
2095
|
+
|
|
2096
|
+
// Pane count indicator
|
|
2097
|
+
const allPanes = getAllPaneIds(this.paneRoot);
|
|
2098
|
+
if (allPanes.length > 1) {
|
|
2099
|
+
const paneIndicator = new TextRenderable(this.renderer, {
|
|
2100
|
+
id: 'pane-indicator',
|
|
2101
|
+
content: t`${fg(COLORS.cyan)(`[${allPanes.length} panes]`)} `,
|
|
2102
|
+
});
|
|
2103
|
+
processBar.add(paneIndicator);
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
// Add each process with checkbox showing visibility in focused pane
|
|
2107
|
+
const focusedPane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
2108
|
+
|
|
2109
|
+
this.scripts.forEach((script, index) => {
|
|
2110
|
+
const proc = this.processes.get(script.name);
|
|
2111
|
+
const status = proc?.status || 'stopped';
|
|
2112
|
+
const statusIcon = status === 'running' ? '●' : status === 'crashed' ? '!' : '○';
|
|
2113
|
+
const statusColor = status === 'running' ? COLORS.success : status === 'crashed' ? COLORS.error : COLORS.textDim;
|
|
2114
|
+
const processColor = this.processColors.get(script.name) || COLORS.text;
|
|
2115
|
+
const isSelected = this.selectedIndex === index;
|
|
2116
|
+
const isVisible = this.isProcessVisibleInPane(script.name, focusedPane);
|
|
2117
|
+
const checkbox = isVisible ? '[x]' : '[ ]';
|
|
2118
|
+
const nameColor = isSelected ? COLORS.accent : (isVisible ? processColor : COLORS.textDim);
|
|
2119
|
+
const indicator = isSelected ? '>' : ' ';
|
|
2120
|
+
|
|
2121
|
+
const processItem = new TextRenderable(this.renderer, {
|
|
2122
|
+
id: `process-item-${index}`,
|
|
2123
|
+
content: t`${fg(isSelected ? COLORS.accent : COLORS.textDim)(indicator)}${fg(isVisible ? COLORS.text : COLORS.textDim)(checkbox)} ${fg(nameColor)(script.displayName)} ${fg(statusColor)(statusIcon)}`,
|
|
2124
|
+
});
|
|
2125
|
+
processBar.add(processItem);
|
|
2126
|
+
});
|
|
2127
|
+
|
|
2128
|
+
this.processListRenderable = processBar;
|
|
2129
|
+
mainContainer.add(processBar);
|
|
2130
|
+
|
|
2131
|
+
// Build pane layout
|
|
2132
|
+
const paneArea = new BoxRenderable(this.renderer, {
|
|
2133
|
+
id: 'pane-area',
|
|
2134
|
+
flexDirection: 'column',
|
|
2135
|
+
flexGrow: 1,
|
|
2136
|
+
flexShrink: 0,
|
|
2137
|
+
flexBasis: 0,
|
|
2138
|
+
backgroundColor: '#000000',
|
|
2139
|
+
});
|
|
2140
|
+
|
|
2141
|
+
const paneLayout = this.buildPaneLayout(this.paneRoot);
|
|
2142
|
+
if (paneLayout) {
|
|
2143
|
+
paneArea.add(paneLayout);
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
mainContainer.add(paneArea);
|
|
2147
|
+
|
|
2148
|
+
// Footer bar - compact style matching selection UI
|
|
2149
|
+
const footerBar = new BoxRenderable(this.renderer, {
|
|
2150
|
+
id: 'footer-bar',
|
|
2151
|
+
flexDirection: 'row',
|
|
2152
|
+
width: '100%',
|
|
2153
|
+
backgroundColor: COLORS.bgLight,
|
|
2154
|
+
paddingLeft: 1,
|
|
2155
|
+
paddingRight: 1,
|
|
2156
|
+
justifyContent: 'space-between',
|
|
2157
|
+
});
|
|
2158
|
+
|
|
2159
|
+
// Left side: status indicator and filter
|
|
2160
|
+
const leftSide = new BoxRenderable(this.renderer, {
|
|
2161
|
+
id: 'footer-left',
|
|
2162
|
+
flexDirection: 'row',
|
|
2163
|
+
gap: 2,
|
|
2164
|
+
});
|
|
2165
|
+
|
|
2166
|
+
// Status (LIVE/PAUSED)
|
|
2167
|
+
const statusText = this.isPaused ? 'PAUSED' : 'LIVE';
|
|
2168
|
+
const statusColor = this.isPaused ? COLORS.warning : COLORS.success;
|
|
2169
|
+
const statusIndicator = new TextRenderable(this.renderer, {
|
|
2170
|
+
id: 'status-indicator',
|
|
2171
|
+
content: t`${fg(statusColor)(statusText)}`,
|
|
2172
|
+
});
|
|
2173
|
+
leftSide.add(statusIndicator);
|
|
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
|
+
|
|
2184
|
+
// Filter indicator if active
|
|
2185
|
+
if (this.filter || this.isFilterMode) {
|
|
2186
|
+
const filterText = this.isFilterMode ? `/${this.filter}_` : `/${this.filter}`;
|
|
2187
|
+
const filterIndicator = new TextRenderable(this.renderer, {
|
|
2188
|
+
id: 'filter-indicator',
|
|
2189
|
+
content: t`${fg(COLORS.cyan)(filterText)}`,
|
|
2190
|
+
});
|
|
2191
|
+
leftSide.add(filterIndicator);
|
|
2192
|
+
}
|
|
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
|
+
|
|
2211
|
+
footerBar.add(leftSide);
|
|
2212
|
+
|
|
2213
|
+
// Right side: shortcuts and title
|
|
2214
|
+
const rightSide = new BoxRenderable(this.renderer, {
|
|
2215
|
+
id: 'footer-right',
|
|
2216
|
+
flexDirection: 'row',
|
|
2217
|
+
gap: 2,
|
|
2218
|
+
});
|
|
2219
|
+
|
|
2220
|
+
const shortcuts = [
|
|
2221
|
+
{ key: '\\', desc: 'panes', color: COLORS.cyan },
|
|
2222
|
+
{ key: 'spc', desc: 'toggle', color: COLORS.success },
|
|
2223
|
+
{ key: 'n', desc: 'name', color: COLORS.accent },
|
|
2224
|
+
{ key: 'p', desc: 'pause', color: COLORS.warning },
|
|
2225
|
+
{ key: '/', desc: 'filter', color: COLORS.cyan },
|
|
2226
|
+
{ key: 'c', desc: 'color', color: COLORS.magenta },
|
|
2227
|
+
{ key: 's', desc: 'stop', color: COLORS.error },
|
|
2228
|
+
{ key: 'r', desc: 'restart', color: COLORS.success },
|
|
2229
|
+
{ key: 'q', desc: 'quit', color: COLORS.error },
|
|
2230
|
+
];
|
|
2231
|
+
|
|
2232
|
+
shortcuts.forEach(({ key, desc, color }) => {
|
|
2233
|
+
const shortcut = new TextRenderable(this.renderer, {
|
|
2234
|
+
id: `shortcut-${key}`,
|
|
2235
|
+
content: t`${fg(color)(key)}${fg(COLORS.textDim)(':' + desc)}`,
|
|
2236
|
+
});
|
|
2237
|
+
rightSide.add(shortcut);
|
|
2238
|
+
});
|
|
2239
|
+
|
|
2240
|
+
// Title and version on far right
|
|
2241
|
+
const titleText = new TextRenderable(this.renderer, {
|
|
2242
|
+
id: 'footer-title',
|
|
2243
|
+
content: t`${fg(COLORS.accent)('running')} ${fg(COLORS.textDim)(APP_VERSION)}`,
|
|
2244
|
+
});
|
|
2245
|
+
rightSide.add(titleText);
|
|
2246
|
+
|
|
2247
|
+
footerBar.add(rightSide);
|
|
2248
|
+
mainContainer.add(footerBar);
|
|
2249
|
+
|
|
2250
|
+
// Add command palette overlay if active
|
|
2251
|
+
if (this.showSplitMenu) {
|
|
2252
|
+
this.buildSplitMenuOverlay(mainContainer);
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
this.renderer.root.add(mainContainer);
|
|
2256
|
+
this.runningContainer = mainContainer;
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
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
|
+
|
|
2280
|
+
// Handle cleanup on exit
|
|
2281
|
+
const handleExit = () => {
|
|
2282
|
+
manager.cleanup();
|
|
2283
|
+
renderer.destroy();
|
|
2284
|
+
};
|
|
2285
|
+
|
|
2286
|
+
process.on('SIGINT', handleExit);
|
|
2287
|
+
process.on('SIGTERM', handleExit);
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
main().catch(err => {
|
|
2291
|
+
console.error('Error:', err);
|
|
2292
|
+
process.exit(1);
|
|
2293
|
+
});
|