startall 0.0.24 → 0.0.25
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/index.js +644 -339
- package/package.json +4 -4
package/index.js
CHANGED
|
@@ -1,11 +1,183 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import { createCliRenderer, TextRenderable, BoxRenderable, ScrollBoxRenderable, ASCIIFontRenderable, t, fg, bold, dim, RGBA } from '@opentui/core';
|
|
3
|
+
import { createCliRenderer, TextRenderable, BoxRenderable, ScrollBoxRenderable, ASCIIFontRenderable, TextareaRenderable, SyntaxStyle, parseColor, t, fg, bg, bold, dim, RGBA, StyledText, OptimizedBuffer } from '@opentui/core';
|
|
4
4
|
import { spawn, execSync, spawnSync } from 'child_process';
|
|
5
5
|
import { readFileSync, writeFileSync, writeSync, existsSync } from 'fs';
|
|
6
6
|
import { join } from 'path';
|
|
7
7
|
import kill from 'tree-kill';
|
|
8
8
|
import stripAnsi from 'strip-ansi';
|
|
9
|
+
import { Database } from 'bun:sqlite';
|
|
10
|
+
|
|
11
|
+
// SQLite-backed output line storage
|
|
12
|
+
class OutputDatabase {
|
|
13
|
+
constructor(maxLines = 1000) {
|
|
14
|
+
this.db = new Database(':memory:');
|
|
15
|
+
this.maxLines = maxLines;
|
|
16
|
+
|
|
17
|
+
// Enable WAL mode for better concurrent read/write performance
|
|
18
|
+
this.db.exec('PRAGMA journal_mode = WAL');
|
|
19
|
+
|
|
20
|
+
// Create the output_lines table with colors bitmask for efficient filtering
|
|
21
|
+
this.db.exec(`
|
|
22
|
+
CREATE TABLE output_lines (
|
|
23
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
24
|
+
line_number INTEGER NOT NULL,
|
|
25
|
+
process TEXT NOT NULL,
|
|
26
|
+
process_lower TEXT NOT NULL,
|
|
27
|
+
text TEXT NOT NULL,
|
|
28
|
+
text_lower TEXT NOT NULL,
|
|
29
|
+
timestamp INTEGER NOT NULL,
|
|
30
|
+
colors INTEGER NOT NULL DEFAULT 0
|
|
31
|
+
)
|
|
32
|
+
`);
|
|
33
|
+
this.db.exec('CREATE INDEX idx_line_number ON output_lines(line_number)');
|
|
34
|
+
this.db.exec('CREATE INDEX idx_process ON output_lines(process)');
|
|
35
|
+
this.db.exec('CREATE INDEX idx_colors ON output_lines(colors)');
|
|
36
|
+
|
|
37
|
+
// Column list with aliases to match existing JS property names
|
|
38
|
+
this._columns = 'id, line_number AS lineNumber, process, process_lower AS processLower, text, text_lower AS textLower, timestamp, colors';
|
|
39
|
+
|
|
40
|
+
// Prepare reusable statements for performance
|
|
41
|
+
this._insertStmt = this.db.prepare(
|
|
42
|
+
'INSERT INTO output_lines (line_number, process, process_lower, text, text_lower, timestamp, colors) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
|
43
|
+
);
|
|
44
|
+
this._countStmt = this.db.prepare('SELECT COUNT(*) AS cnt FROM output_lines');
|
|
45
|
+
this._deleteOldStmt = this.db.prepare(
|
|
46
|
+
'DELETE FROM output_lines WHERE id NOT IN (SELECT id FROM output_lines ORDER BY id DESC LIMIT ?)'
|
|
47
|
+
);
|
|
48
|
+
this._selectAllStmt = this.db.prepare(`SELECT ${this._columns} FROM output_lines ORDER BY id ASC`);
|
|
49
|
+
this._clearStmt = this.db.prepare('DELETE FROM output_lines');
|
|
50
|
+
// Fast path for queryVisible with no filters
|
|
51
|
+
this._queryVisibleNoFilterStmt = this.db.prepare(`SELECT ${this._columns} FROM (
|
|
52
|
+
SELECT * FROM output_lines ORDER BY id DESC LIMIT ?
|
|
53
|
+
) ORDER BY id ASC`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
insert(lineNumber, process, text, timestamp, colorBitmask) {
|
|
57
|
+
this._insertStmt.run(lineNumber, process, process.toLowerCase(), text, text.toLowerCase(), timestamp, colorBitmask);
|
|
58
|
+
|
|
59
|
+
// Enforce max lines limit
|
|
60
|
+
const { cnt } = this._countStmt.get();
|
|
61
|
+
if (cnt > this.maxLines) {
|
|
62
|
+
this._deleteOldStmt.run(this.maxLines);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getAll() {
|
|
67
|
+
return this._selectAllStmt.all();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
count() {
|
|
71
|
+
return this._countStmt.get().cnt;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
clear() {
|
|
75
|
+
this._clearStmt.run();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Build SQL conditions and params for pane filters
|
|
79
|
+
_buildPaneFilters(pane) {
|
|
80
|
+
const conditions = [];
|
|
81
|
+
const params = [];
|
|
82
|
+
|
|
83
|
+
if (pane.processes && pane.processes.length > 0) {
|
|
84
|
+
const placeholders = pane.processes.map(() => '?').join(', ');
|
|
85
|
+
conditions.push(`process IN (${placeholders})`);
|
|
86
|
+
params.push(...pane.processes);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (pane.hidden && pane.hidden.length > 0) {
|
|
90
|
+
const placeholders = pane.hidden.map(() => '?').join(', ');
|
|
91
|
+
conditions.push(`process NOT IN (${placeholders})`);
|
|
92
|
+
params.push(...pane.hidden);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (pane.filter) {
|
|
96
|
+
const filterLower = pane.filter.toLowerCase();
|
|
97
|
+
conditions.push('(process_lower LIKE ? OR text_lower LIKE ?)');
|
|
98
|
+
params.push(`%${filterLower}%`, `%${filterLower}%`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (pane.colorFilter) {
|
|
102
|
+
// Use bitmask check: (colors & bit) != 0
|
|
103
|
+
const colorBit = this._getColorBit(pane.colorFilter);
|
|
104
|
+
if (colorBit > 0) {
|
|
105
|
+
conditions.push('(colors & ?) != 0');
|
|
106
|
+
params.push(colorBit);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { conditions, params };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Get color bitmask value for a color name
|
|
114
|
+
_getColorBit(colorName) {
|
|
115
|
+
const bits = { red: 1, yellow: 2, green: 4, blue: 8, cyan: 16, magenta: 32 };
|
|
116
|
+
return bits[colorName] || 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Query with pane filters applied via SQL
|
|
120
|
+
queryForPane(pane) {
|
|
121
|
+
const { conditions, params } = this._buildPaneFilters(pane);
|
|
122
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
123
|
+
const sql = `SELECT ${this._columns} FROM output_lines ${where} ORDER BY id ASC`;
|
|
124
|
+
return this.db.prepare(sql).all(...params);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Query lines newer than a given line_number, with pane filters
|
|
128
|
+
queryNewLines(afterLineNumber, pane, limit) {
|
|
129
|
+
const { conditions, params } = this._buildPaneFilters(pane);
|
|
130
|
+
conditions.unshift('line_number > ?');
|
|
131
|
+
params.unshift(afterLineNumber);
|
|
132
|
+
|
|
133
|
+
const where = `WHERE ${conditions.join(' AND ')}`;
|
|
134
|
+
// Get newest lines up to limit, but return them in ascending order
|
|
135
|
+
const sql = `SELECT ${this._columns} FROM (SELECT * FROM output_lines ${where} ORDER BY id DESC LIMIT ?) ORDER BY id ASC`;
|
|
136
|
+
params.push(limit);
|
|
137
|
+
|
|
138
|
+
return this.db.prepare(sql).all(...params);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Count lines matching pane filters (for calculating max scroll)
|
|
142
|
+
countForPane(pane) {
|
|
143
|
+
const { conditions, params } = this._buildPaneFilters(pane);
|
|
144
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
145
|
+
const sql = `SELECT COUNT(*) AS cnt FROM output_lines ${where}`;
|
|
146
|
+
return this.db.prepare(sql).get(...params).cnt;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Query visible lines for virtual scrolling: get `limit` lines starting from `offset` from the end
|
|
150
|
+
// offset=0 means the most recent lines, offset=10 means 10 lines back from the end
|
|
151
|
+
queryVisible(pane, limit, offsetFromEnd) {
|
|
152
|
+
const { conditions, params } = this._buildPaneFilters(pane);
|
|
153
|
+
const totalToFetch = limit + offsetFromEnd;
|
|
154
|
+
|
|
155
|
+
let rows;
|
|
156
|
+
if (conditions.length === 0) {
|
|
157
|
+
// Fast path: no filters, use prepared statement
|
|
158
|
+
rows = this._queryVisibleNoFilterStmt.all(totalToFetch);
|
|
159
|
+
} else {
|
|
160
|
+
// Slow path: build query with filters
|
|
161
|
+
const where = `WHERE ${conditions.join(' AND ')}`;
|
|
162
|
+
const sql = `SELECT ${this._columns} FROM (
|
|
163
|
+
SELECT * FROM output_lines ${where} ORDER BY id DESC LIMIT ?
|
|
164
|
+
) ORDER BY id ASC`;
|
|
165
|
+
params.push(totalToFetch);
|
|
166
|
+
rows = this.db.prepare(sql).all(...params);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// If offset > 0, trim the newest lines
|
|
170
|
+
if (offsetFromEnd > 0 && rows.length > limit) {
|
|
171
|
+
rows = rows.slice(0, limit);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return rows;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
close() {
|
|
178
|
+
this.db.close();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
9
181
|
|
|
10
182
|
// Configuration
|
|
11
183
|
const CONFIG_FILE = process.argv[2] || 'startall.json';
|
|
@@ -93,34 +265,65 @@ function getAllPaneIds(node, ids = []) {
|
|
|
93
265
|
return ids;
|
|
94
266
|
}
|
|
95
267
|
|
|
268
|
+
// Color bitmask values for efficient SQL filtering
|
|
269
|
+
const COLOR_BITS = {
|
|
270
|
+
red: 1, // 0b000001
|
|
271
|
+
yellow: 2, // 0b000010
|
|
272
|
+
green: 4, // 0b000100
|
|
273
|
+
blue: 8, // 0b001000
|
|
274
|
+
cyan: 16, // 0b010000
|
|
275
|
+
magenta: 32, // 0b100000
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// ANSI color codes (normal and bright variants)
|
|
279
|
+
const ANSI_COLOR_CODES = {
|
|
280
|
+
red: [31, 91],
|
|
281
|
+
yellow: [33, 93],
|
|
282
|
+
green: [32, 92],
|
|
283
|
+
blue: [34, 94],
|
|
284
|
+
cyan: [36, 96],
|
|
285
|
+
magenta: [35, 95],
|
|
286
|
+
};
|
|
287
|
+
|
|
96
288
|
// Get ANSI color codes for a color name (includes normal and bright variants)
|
|
97
289
|
function getAnsiColorCodes(colorName) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
290
|
+
return ANSI_COLOR_CODES[colorName] || [];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Check if text contains a specific ANSI color code
|
|
294
|
+
function textHasColorCode(text, code) {
|
|
295
|
+
// Check for direct color code: \x1b[31m
|
|
296
|
+
if (text.includes(`\x1b[${code}m`)) return true;
|
|
297
|
+
// Check for color with modifiers: \x1b[1;31m, \x1b[0;31m, etc.
|
|
298
|
+
if (text.includes(`;${code}m`)) return true;
|
|
299
|
+
// Check for color at start of sequence: \x1b[31;1m
|
|
300
|
+
if (text.includes(`\x1b[${code};`)) return true;
|
|
301
|
+
return false;
|
|
107
302
|
}
|
|
108
303
|
|
|
109
304
|
// Check if a line contains a specific ANSI color
|
|
110
305
|
function lineHasColor(text, colorName) {
|
|
111
306
|
const codes = getAnsiColorCodes(colorName);
|
|
112
|
-
// Match ANSI escape sequences like \x1b[31m, \x1b[91m, \x1b[1;31m, etc.
|
|
113
307
|
for (const code of codes) {
|
|
114
|
-
|
|
115
|
-
if (text.includes(`\x1b[${code}m`)) return true;
|
|
116
|
-
// Check for color with modifiers: \x1b[1;31m, \x1b[0;31m, etc.
|
|
117
|
-
if (text.includes(`;${code}m`)) return true;
|
|
118
|
-
// Check for color at start of sequence: \x1b[31;1m
|
|
119
|
-
if (text.includes(`\x1b[${code};`)) return true;
|
|
308
|
+
if (textHasColorCode(text, code)) return true;
|
|
120
309
|
}
|
|
121
310
|
return false;
|
|
122
311
|
}
|
|
123
312
|
|
|
313
|
+
// Detect all colors in text and return bitmask
|
|
314
|
+
function detectColorBitmask(text) {
|
|
315
|
+
let bitmask = 0;
|
|
316
|
+
for (const [colorName, codes] of Object.entries(ANSI_COLOR_CODES)) {
|
|
317
|
+
for (const code of codes) {
|
|
318
|
+
if (textHasColorCode(text, code)) {
|
|
319
|
+
bitmask |= COLOR_BITS[colorName];
|
|
320
|
+
break; // Found this color, no need to check other codes for it
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return bitmask;
|
|
325
|
+
}
|
|
326
|
+
|
|
124
327
|
// Find parent of a node
|
|
125
328
|
function findParent(root, targetId, parent = null) {
|
|
126
329
|
if (!root) return null;
|
|
@@ -550,10 +753,10 @@ class ProcessManager {
|
|
|
550
753
|
this.selectedIndex = 0;
|
|
551
754
|
this.processes = new Map();
|
|
552
755
|
this.processRefs = new Map();
|
|
553
|
-
this.
|
|
756
|
+
this.maxOutputLines = 1000; // Lines kept in database
|
|
757
|
+
this.outputDb = new OutputDatabase(this.maxOutputLines);
|
|
554
758
|
this.totalLinesReceived = 0; // Track total lines ever received (never resets)
|
|
555
759
|
this.filter = '';
|
|
556
|
-
this.maxOutputLines = 1000; // Lines kept in memory
|
|
557
760
|
this.maxDomLines = 150; // Lines kept in DOM (buffer for varying heights)
|
|
558
761
|
this.lineRenderables = new Map(); // Reusable TextRenderables per pane
|
|
559
762
|
this.maxVisibleLines = null; // Calculated dynamically based on screen height
|
|
@@ -590,11 +793,12 @@ class ProcessManager {
|
|
|
590
793
|
this.outputBox = null; // Reference to the output container
|
|
591
794
|
this.destroyed = false; // Flag to prevent operations after cleanup
|
|
592
795
|
this.lastRenderedLineCount = 0; // Track how many lines we've rendered
|
|
796
|
+
this.hasNewLines = false; // Flag set when new lines are added, cleared after render
|
|
593
797
|
this.headerRenderable = null; // Reference to header text in running UI
|
|
594
798
|
this.processListRenderable = null; // Reference to process list text in running UI
|
|
595
799
|
this.renderScheduled = false; // Throttle renders for CPU efficiency
|
|
596
800
|
this.lastRenderTime = 0; // Timestamp of last render
|
|
597
|
-
this.minRenderInterval =
|
|
801
|
+
this.minRenderInterval = 1; // Minimum ms between renders (~60fps cap)
|
|
598
802
|
|
|
599
803
|
// Performance metrics
|
|
600
804
|
this.showPerformanceMetrics = this.config.showPerformanceMetrics || false;
|
|
@@ -609,16 +813,28 @@ class ProcessManager {
|
|
|
609
813
|
this.splitMode = false; // Whether waiting for split command after Ctrl+b
|
|
610
814
|
this.showSplitMenu = false; // Whether to show the command palette
|
|
611
815
|
this.splitMenuIndex = 0; // Selected item in split menu
|
|
612
|
-
this.paneScrollPositions = new Map(); // Store scroll positions per pane ID
|
|
613
|
-
this.paneScrollBoxes = new Map(); // Store ScrollBox references per pane ID
|
|
816
|
+
this.paneScrollPositions = new Map(); // Store scroll positions per pane ID (legacy)
|
|
817
|
+
this.paneScrollBoxes = new Map(); // Store ScrollBox references per pane ID (legacy)
|
|
614
818
|
this.paneFilterState = new Map(); // Track filter state per pane to detect changes
|
|
615
819
|
this.paneLineCount = new Map(); // Track how many lines we've rendered per pane
|
|
616
820
|
this.uiJustRebuilt = false; // Flag to skip redundant render after buildRunningUI
|
|
617
821
|
|
|
822
|
+
// Virtual scrolling state
|
|
823
|
+
this.paneScrollOffsets = new Map(); // Scroll offset from end per pane (0 = at bottom)
|
|
824
|
+
this.paneVisibleHeight = new Map(); // Visible height per pane for scroll calculations
|
|
825
|
+
this.paneOutputBoxes = new Map(); // Store output BoxRenderable references per pane
|
|
826
|
+
|
|
618
827
|
// Column view state (one pane per script, side by side)
|
|
619
828
|
this.isColumnView = false; // Whether column view is active
|
|
620
829
|
this.savedPaneRoot = null; // Saved pane tree to restore when toggling back
|
|
621
830
|
|
|
831
|
+
// Line format cache (keyed by line ID + settings hash)
|
|
832
|
+
this.lineFormatCache = new Map();
|
|
833
|
+
this.lineFormatCacheKey = ''; // Cache key based on display settings
|
|
834
|
+
|
|
835
|
+
// Pane content cache (keyed by pane ID -> {lastLineId, styledText})
|
|
836
|
+
this.paneContentCache = new Map();
|
|
837
|
+
|
|
622
838
|
// Copy mode state (select text to copy)
|
|
623
839
|
this.isCopyMode = false; // Whether in copy/select mode
|
|
624
840
|
this.copyModeCursor = 0; // Current cursor line index within visible lines
|
|
@@ -673,13 +889,27 @@ class ProcessManager {
|
|
|
673
889
|
return;
|
|
674
890
|
}
|
|
675
891
|
|
|
676
|
-
// Handle Ctrl+L - clear screen
|
|
892
|
+
// Handle Ctrl+L - clear screen and redraw (kills artifacts)
|
|
677
893
|
if (key.ctrl && key.name === 'l') {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
this.
|
|
681
|
-
this.buildRunningUI();
|
|
894
|
+
// Debounce - cancel any pending wipe and restart
|
|
895
|
+
if (this.wipeTimeout) {
|
|
896
|
+
clearTimeout(this.wipeTimeout);
|
|
682
897
|
}
|
|
898
|
+
|
|
899
|
+
// Save current phase and show wipe screen
|
|
900
|
+
const savedPhase = this.phase;
|
|
901
|
+
this.buildWipeScreen();
|
|
902
|
+
|
|
903
|
+
this.wipeTimeout = setTimeout(() => {
|
|
904
|
+
this.wipeTimeout = null;
|
|
905
|
+
if (savedPhase === 'running') {
|
|
906
|
+
this.buildRunningUI();
|
|
907
|
+
} else if (savedPhase === 'selection') {
|
|
908
|
+
this.buildSelectionUI();
|
|
909
|
+
} else if (savedPhase === 'settings') {
|
|
910
|
+
this.buildSettingsUI();
|
|
911
|
+
}
|
|
912
|
+
}, 50);
|
|
683
913
|
return;
|
|
684
914
|
}
|
|
685
915
|
|
|
@@ -874,13 +1104,13 @@ class ProcessManager {
|
|
|
874
1104
|
this.buildRunningUI();
|
|
875
1105
|
} else if (keyName === 'p') {
|
|
876
1106
|
// Toggle pause output scrolling globally
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1107
|
+
this.isPaused = !this.isPaused;
|
|
1108
|
+
// Reset scroll offset to bottom when unpausing
|
|
1109
|
+
if (!this.isPaused) {
|
|
1110
|
+
for (const paneId of getAllPaneIds(this.paneRoot)) {
|
|
1111
|
+
this.paneScrollOffsets.set(paneId, 0);
|
|
881
1112
|
}
|
|
882
1113
|
}
|
|
883
|
-
this.isPaused = !this.isPaused;
|
|
884
1114
|
this.updateStreamPauseState();
|
|
885
1115
|
this.buildRunningUI();
|
|
886
1116
|
} else if (keyName === 'f') {
|
|
@@ -1103,13 +1333,16 @@ class ProcessManager {
|
|
|
1103
1333
|
} else {
|
|
1104
1334
|
this.paneRoot = createPane([]); // Empty array means show all processes
|
|
1105
1335
|
}
|
|
1106
|
-
|
|
1336
|
+
// Focus the first actual pane (paneRoot might be a split node)
|
|
1337
|
+
const allPanes = getAllPaneIds(this.paneRoot);
|
|
1338
|
+
this.focusedPaneId = allPanes.length > 0 ? allPanes[0] : this.paneRoot.id;
|
|
1107
1339
|
|
|
1108
1340
|
selected.forEach(scriptName => {
|
|
1109
1341
|
this.startProcess(scriptName);
|
|
1110
1342
|
});
|
|
1111
1343
|
|
|
1112
|
-
|
|
1344
|
+
// Build the UI immediately (don't rely on render() since hasNewLines may be false)
|
|
1345
|
+
this.buildRunningUI();
|
|
1113
1346
|
}
|
|
1114
1347
|
|
|
1115
1348
|
startProcess(scriptName) {
|
|
@@ -1156,22 +1389,25 @@ class ProcessManager {
|
|
|
1156
1389
|
}
|
|
1157
1390
|
|
|
1158
1391
|
addOutputLine(processName, text) {
|
|
1159
|
-
//
|
|
1160
|
-
|
|
1161
|
-
this.outputLines.push({
|
|
1162
|
-
process: processName,
|
|
1163
|
-
processLower: processName.toLowerCase(),
|
|
1164
|
-
text,
|
|
1165
|
-
textLower: text.toLowerCase(),
|
|
1166
|
-
timestamp: Date.now(),
|
|
1167
|
-
lineNumber: ++this.totalLinesReceived, // Track absolute line number
|
|
1168
|
-
});
|
|
1392
|
+
// Don't write if database is closed
|
|
1393
|
+
if (this.destroyed) return;
|
|
1169
1394
|
|
|
1170
|
-
//
|
|
1171
|
-
|
|
1172
|
-
this.outputLines.shift();
|
|
1173
|
-
}
|
|
1395
|
+
// Detect colors in the text for efficient SQL filtering
|
|
1396
|
+
const colorBitmask = detectColorBitmask(text);
|
|
1174
1397
|
|
|
1398
|
+
// Always store the output line, even when paused
|
|
1399
|
+
// Insert into SQLite database
|
|
1400
|
+
this.outputDb.insert(
|
|
1401
|
+
++this.totalLinesReceived,
|
|
1402
|
+
processName,
|
|
1403
|
+
text,
|
|
1404
|
+
Date.now(),
|
|
1405
|
+
colorBitmask
|
|
1406
|
+
);
|
|
1407
|
+
|
|
1408
|
+
// Mark that new lines are available
|
|
1409
|
+
this.hasNewLines = true;
|
|
1410
|
+
|
|
1175
1411
|
// Only render if not paused - this prevents new output from appearing
|
|
1176
1412
|
// when the user is reviewing history
|
|
1177
1413
|
if (!this.isPaused) {
|
|
@@ -1820,19 +2056,38 @@ class ProcessManager {
|
|
|
1820
2056
|
// Toggle visibility of selected process in focused pane
|
|
1821
2057
|
toggleProcessVisibility() {
|
|
1822
2058
|
const scriptName = this.scripts[this.selectedIndex]?.name;
|
|
1823
|
-
if (!scriptName || !this.focusedPaneId)
|
|
2059
|
+
if (!scriptName || !this.focusedPaneId) {
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
1824
2062
|
|
|
1825
2063
|
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
1826
|
-
if (!pane)
|
|
2064
|
+
if (!pane) {
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
1827
2067
|
|
|
1828
|
-
// Initialize
|
|
2068
|
+
// Initialize arrays if needed
|
|
1829
2069
|
if (!pane.hidden) pane.hidden = [];
|
|
2070
|
+
if (!pane.processes) pane.processes = [];
|
|
1830
2071
|
|
|
1831
|
-
//
|
|
1832
|
-
|
|
1833
|
-
|
|
2072
|
+
// Check current visibility
|
|
2073
|
+
const isCurrentlyVisible = this.isProcessVisibleInPane(scriptName, pane);
|
|
2074
|
+
|
|
2075
|
+
if (isCurrentlyVisible) {
|
|
2076
|
+
// Hide it - add to hidden list
|
|
2077
|
+
if (!pane.hidden.includes(scriptName)) {
|
|
2078
|
+
pane.hidden.push(scriptName);
|
|
2079
|
+
}
|
|
2080
|
+
// Also remove from processes if it's there
|
|
2081
|
+
if (pane.processes.includes(scriptName)) {
|
|
2082
|
+
pane.processes = pane.processes.filter(p => p !== scriptName);
|
|
2083
|
+
}
|
|
1834
2084
|
} else {
|
|
1835
|
-
|
|
2085
|
+
// Show it - remove from hidden list
|
|
2086
|
+
pane.hidden = pane.hidden.filter(p => p !== scriptName);
|
|
2087
|
+
// If pane has a process filter, add this process to it
|
|
2088
|
+
if (pane.processes.length > 0 && !pane.processes.includes(scriptName)) {
|
|
2089
|
+
pane.processes.push(scriptName);
|
|
2090
|
+
}
|
|
1836
2091
|
}
|
|
1837
2092
|
|
|
1838
2093
|
this.savePaneLayout();
|
|
@@ -1846,38 +2101,46 @@ class ProcessManager {
|
|
|
1846
2101
|
debouncedSaveConfig(this.config);
|
|
1847
2102
|
}
|
|
1848
2103
|
|
|
1849
|
-
// Scroll the focused pane
|
|
2104
|
+
// Scroll the focused pane (virtual scrolling - adjusts offset into database)
|
|
1850
2105
|
scrollFocusedPane(direction) {
|
|
1851
2106
|
if (!this.focusedPaneId) return;
|
|
1852
2107
|
|
|
1853
|
-
const
|
|
1854
|
-
if (!
|
|
2108
|
+
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
2109
|
+
if (!pane) return;
|
|
1855
2110
|
|
|
1856
|
-
const
|
|
1857
|
-
const
|
|
1858
|
-
const
|
|
2111
|
+
const totalLines = this.outputDb.countForPane(pane);
|
|
2112
|
+
const visibleHeight = this.paneVisibleHeight.get(this.focusedPaneId) || 20;
|
|
2113
|
+
const currentOffset = this.paneScrollOffsets.get(this.focusedPaneId) || 0;
|
|
2114
|
+
const maxOffset = Math.max(0, totalLines - visibleHeight);
|
|
1859
2115
|
|
|
1860
|
-
let
|
|
2116
|
+
let newOffset = currentOffset;
|
|
1861
2117
|
|
|
1862
2118
|
if (direction === 'home') {
|
|
1863
|
-
|
|
2119
|
+
// Scroll to top (maximum offset from end)
|
|
2120
|
+
newOffset = maxOffset;
|
|
1864
2121
|
} else if (direction === 'end') {
|
|
1865
|
-
|
|
2122
|
+
// Scroll to bottom (offset 0 = most recent)
|
|
2123
|
+
newOffset = 0;
|
|
1866
2124
|
} else if (direction === 'pageup') {
|
|
1867
|
-
|
|
2125
|
+
newOffset = Math.min(maxOffset, currentOffset + visibleHeight);
|
|
1868
2126
|
} else if (direction === 'pagedown') {
|
|
1869
|
-
|
|
2127
|
+
newOffset = Math.max(0, currentOffset - visibleHeight);
|
|
2128
|
+
} else if (direction === 'up') {
|
|
2129
|
+
newOffset = Math.min(maxOffset, currentOffset + 1);
|
|
2130
|
+
} else if (direction === 'down') {
|
|
2131
|
+
newOffset = Math.max(0, currentOffset - 1);
|
|
1870
2132
|
}
|
|
1871
2133
|
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2134
|
+
// Only update if changed
|
|
2135
|
+
if (newOffset !== currentOffset) {
|
|
2136
|
+
this.paneScrollOffsets.set(this.focusedPaneId, newOffset);
|
|
2137
|
+
|
|
2138
|
+
// Auto-pause when manually scrolling (unless going to end)
|
|
2139
|
+
if (direction !== 'end' && !this.isPaused) {
|
|
2140
|
+
this.isPaused = true;
|
|
2141
|
+
this.updateStreamPauseState();
|
|
2142
|
+
}
|
|
2143
|
+
|
|
1881
2144
|
this.buildRunningUI();
|
|
1882
2145
|
}
|
|
1883
2146
|
}
|
|
@@ -2098,51 +2361,18 @@ class ProcessManager {
|
|
|
2098
2361
|
}
|
|
2099
2362
|
|
|
2100
2363
|
// Count horizontal splits (which reduce available height per pane)
|
|
2101
|
-
// Get output lines for a specific pane -
|
|
2364
|
+
// Get output lines for a specific pane - queries SQLite with filters
|
|
2102
2365
|
getOutputLinesForPane(pane) {
|
|
2103
|
-
|
|
2104
|
-
const hasProcessFilter = pane.processes.length > 0;
|
|
2105
|
-
const hasHiddenFilter = pane.hidden && pane.hidden.length > 0;
|
|
2106
|
-
const hasTextFilter = !!pane.filter;
|
|
2107
|
-
const hasColorFilter = !!pane.colorFilter;
|
|
2108
|
-
|
|
2109
|
-
if (!hasProcessFilter && !hasHiddenFilter && !hasTextFilter && !hasColorFilter) {
|
|
2110
|
-
return this.outputLines;
|
|
2111
|
-
}
|
|
2112
|
-
|
|
2113
|
-
// Build Sets for O(1) lookups
|
|
2114
|
-
const processSet = hasProcessFilter ? new Set(pane.processes) : null;
|
|
2115
|
-
const hiddenSet = hasHiddenFilter ? new Set(pane.hidden) : null;
|
|
2116
|
-
const filterLower = hasTextFilter ? pane.filter.toLowerCase() : null;
|
|
2117
|
-
|
|
2118
|
-
// Single pass through lines
|
|
2119
|
-
return this.outputLines.filter(line => {
|
|
2120
|
-
// Check process filter
|
|
2121
|
-
if (processSet && !processSet.has(line.process)) return false;
|
|
2122
|
-
|
|
2123
|
-
// Check hidden filter
|
|
2124
|
-
if (hiddenSet && hiddenSet.has(line.process)) return false;
|
|
2125
|
-
|
|
2126
|
-
// Check text filter (use cached lowercase from line if available)
|
|
2127
|
-
if (filterLower) {
|
|
2128
|
-
const processLower = line.processLower || line.process.toLowerCase();
|
|
2129
|
-
const textLower = line.textLower || line.text.toLowerCase();
|
|
2130
|
-
if (!processLower.includes(filterLower) && !textLower.includes(filterLower)) {
|
|
2131
|
-
return false;
|
|
2132
|
-
}
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
// Check color filter
|
|
2136
|
-
if (hasColorFilter && !lineHasColor(line.text, pane.colorFilter)) {
|
|
2137
|
-
return false;
|
|
2138
|
-
}
|
|
2139
|
-
|
|
2140
|
-
return true;
|
|
2141
|
-
});
|
|
2366
|
+
return this.outputDb.queryForPane(pane);
|
|
2142
2367
|
}
|
|
2143
2368
|
|
|
2144
2369
|
buildSettingsUI() {
|
|
2145
2370
|
// Remove old containers - use destroyRecursively to clean up all children
|
|
2371
|
+
if (this.wipeContainer) {
|
|
2372
|
+
this.renderer.root.remove(this.wipeContainer);
|
|
2373
|
+
this.wipeContainer.destroyRecursively();
|
|
2374
|
+
this.wipeContainer = null;
|
|
2375
|
+
}
|
|
2146
2376
|
if (this.selectionContainer) {
|
|
2147
2377
|
this.renderer.root.remove(this.selectionContainer);
|
|
2148
2378
|
this.selectionContainer.destroyRecursively();
|
|
@@ -2539,6 +2769,11 @@ class ProcessManager {
|
|
|
2539
2769
|
// Ignore
|
|
2540
2770
|
}
|
|
2541
2771
|
}
|
|
2772
|
+
|
|
2773
|
+
// Close the SQLite database
|
|
2774
|
+
if (this.outputDb) {
|
|
2775
|
+
this.outputDb.close();
|
|
2776
|
+
}
|
|
2542
2777
|
}
|
|
2543
2778
|
|
|
2544
2779
|
executeCommand(scriptName) {
|
|
@@ -2793,8 +3028,56 @@ class ProcessManager {
|
|
|
2793
3028
|
// 'committing' and 'pushing' phases ignore input (busy)
|
|
2794
3029
|
}
|
|
2795
3030
|
|
|
3031
|
+
// Full screen wipe to clear artifacts (Ctrl+L)
|
|
3032
|
+
buildWipeScreen() {
|
|
3033
|
+
// Remove old containers
|
|
3034
|
+
if (this.selectionContainer) {
|
|
3035
|
+
this.renderer.root.remove(this.selectionContainer);
|
|
3036
|
+
this.selectionContainer.destroyRecursively();
|
|
3037
|
+
this.selectionContainer = null;
|
|
3038
|
+
}
|
|
3039
|
+
if (this.settingsContainer) {
|
|
3040
|
+
this.renderer.root.remove(this.settingsContainer);
|
|
3041
|
+
this.settingsContainer.destroyRecursively();
|
|
3042
|
+
this.settingsContainer = null;
|
|
3043
|
+
}
|
|
3044
|
+
if (this.runningContainer) {
|
|
3045
|
+
this.renderer.root.remove(this.runningContainer);
|
|
3046
|
+
this.runningContainer.destroyRecursively();
|
|
3047
|
+
this.runningContainer = null;
|
|
3048
|
+
this.outputBox = null;
|
|
3049
|
+
this.paneScrollBoxes.clear();
|
|
3050
|
+
this.paneOutputBoxes.clear();
|
|
3051
|
+
this.lineRenderables.clear();
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
// Create full-screen wipe with different background color
|
|
3055
|
+
const wipeContainer = new BoxRenderable(this.renderer, {
|
|
3056
|
+
id: 'wipe-container',
|
|
3057
|
+
width: '100%',
|
|
3058
|
+
height: '100%',
|
|
3059
|
+
backgroundColor: COLORS.bg,
|
|
3060
|
+
justifyContent: 'center',
|
|
3061
|
+
alignItems: 'center',
|
|
3062
|
+
});
|
|
3063
|
+
|
|
3064
|
+
const wipeText = new TextRenderable(this.renderer, {
|
|
3065
|
+
id: 'wipe-text',
|
|
3066
|
+
content: t`${fg(COLORS.textDim)('Clearing...')}`,
|
|
3067
|
+
});
|
|
3068
|
+
wipeContainer.add(wipeText);
|
|
3069
|
+
|
|
3070
|
+
this.renderer.root.add(wipeContainer);
|
|
3071
|
+
this.wipeContainer = wipeContainer;
|
|
3072
|
+
}
|
|
3073
|
+
|
|
2796
3074
|
buildSelectionUI() {
|
|
2797
3075
|
// Remove old containers if they exist - use destroyRecursively to clean up all children
|
|
3076
|
+
if (this.wipeContainer) {
|
|
3077
|
+
this.renderer.root.remove(this.wipeContainer);
|
|
3078
|
+
this.wipeContainer.destroyRecursively();
|
|
3079
|
+
this.wipeContainer = null;
|
|
3080
|
+
}
|
|
2798
3081
|
if (this.selectionContainer) {
|
|
2799
3082
|
this.renderer.root.remove(this.selectionContainer);
|
|
2800
3083
|
this.selectionContainer.destroyRecursively();
|
|
@@ -3080,7 +3363,9 @@ class ProcessManager {
|
|
|
3080
3363
|
|
|
3081
3364
|
getPerformanceString() {
|
|
3082
3365
|
const metrics = this.getPerformanceMetrics();
|
|
3083
|
-
|
|
3366
|
+
const rendererStats = this.renderer.getStats ? this.renderer.getStats() : null;
|
|
3367
|
+
const rendererFps = rendererStats ? rendererStats.fps : '?';
|
|
3368
|
+
return `${metrics.fps}fps ${metrics.avgRenderTime}ms r:${rendererFps}fps db:${this.outputDb.count()}`;
|
|
3084
3369
|
}
|
|
3085
3370
|
|
|
3086
3371
|
getProcessListContent() {
|
|
@@ -3219,109 +3504,79 @@ class ProcessManager {
|
|
|
3219
3504
|
}
|
|
3220
3505
|
}
|
|
3221
3506
|
|
|
3222
|
-
//
|
|
3223
|
-
if (this.
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
for (const [paneId, scrollBox] of this.paneScrollBoxes.entries()) {
|
|
3507
|
+
// Skip update if paused or no new lines
|
|
3508
|
+
if (this.isPaused || !this.hasNewLines) return;
|
|
3509
|
+
|
|
3510
|
+
// Clear the flag
|
|
3511
|
+
this.hasNewLines = false;
|
|
3512
|
+
|
|
3513
|
+
// Virtual scrolling update - only update visible lines from database
|
|
3514
|
+
if (this.paneOutputBoxes.size > 0) {
|
|
3515
|
+
for (const [paneId, outputBox] of this.paneOutputBoxes.entries()) {
|
|
3232
3516
|
const pane = findPaneById(this.paneRoot, paneId);
|
|
3233
|
-
if (!pane || !
|
|
3234
|
-
// ScrollBox invalid, need full rebuild
|
|
3517
|
+
if (!pane || !outputBox) {
|
|
3235
3518
|
this.buildRunningUI();
|
|
3236
3519
|
return;
|
|
3237
3520
|
}
|
|
3238
3521
|
|
|
3239
|
-
// Only update
|
|
3240
|
-
const
|
|
3241
|
-
if (
|
|
3242
|
-
const lastUpdate = this.paneLastUpdate?.get(paneId) || 0;
|
|
3243
|
-
if (now - lastUpdate < 200) continue; // Update non-focused panes every 200ms
|
|
3244
|
-
if (!this.paneLastUpdate) this.paneLastUpdate = new Map();
|
|
3245
|
-
this.paneLastUpdate.set(paneId, now);
|
|
3246
|
-
}
|
|
3522
|
+
// Only update if at the bottom (offset=0)
|
|
3523
|
+
const scrollOffset = this.paneScrollOffsets.get(paneId) || 0;
|
|
3524
|
+
if (scrollOffset !== 0) continue;
|
|
3247
3525
|
|
|
3248
|
-
const
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
}
|
|
3277
|
-
const renderables = this.lineRenderables.get(paneId);
|
|
3278
|
-
|
|
3279
|
-
// Add new lines - reuse existing renderables or create new ones
|
|
3280
|
-
for (const line of newLines) {
|
|
3281
|
-
const processColor = this.processColors.get(line.process) || COLORS.text;
|
|
3282
|
-
|
|
3283
|
-
// Build content
|
|
3284
|
-
const lineNumber = this.showLineNumbers ? String(line.lineNumber).padStart(4, ' ') : '';
|
|
3285
|
-
const timestamp = this.showTimestamps ? (line.timeString || (line.timeString = new Date(line.timestamp).toLocaleTimeString('en-US', { hour12: false }))) : '';
|
|
3286
|
-
|
|
3287
|
-
let content;
|
|
3288
|
-
if (this.showLineNumbers && this.showTimestamps) {
|
|
3289
|
-
content = t`${fg(COLORS.textDim)(lineNumber)} ${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${line.text}`;
|
|
3290
|
-
} else if (this.showLineNumbers) {
|
|
3291
|
-
content = t`${fg(COLORS.textDim)(lineNumber)} ${fg(processColor)(`[${line.process}]`)} ${line.text}`;
|
|
3292
|
-
} else if (this.showTimestamps) {
|
|
3293
|
-
content = t`${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${line.text}`;
|
|
3294
|
-
} else {
|
|
3295
|
-
content = t`${fg(processColor)(`[${line.process}]`)} ${line.text}`;
|
|
3296
|
-
}
|
|
3297
|
-
|
|
3298
|
-
// Create new renderable and add to pool
|
|
3299
|
-
const outputLine = new TextRenderable(this.renderer, {
|
|
3300
|
-
id: `output-${pane.id}-${line.lineNumber}`,
|
|
3301
|
-
content: content,
|
|
3302
|
-
bg: '#000000',
|
|
3303
|
-
});
|
|
3304
|
-
|
|
3305
|
-
scrollBox.content.add(outputLine);
|
|
3306
|
-
renderables.push(outputLine);
|
|
3307
|
-
}
|
|
3308
|
-
|
|
3309
|
-
// Remove excess old lines - keep DOM small for performance
|
|
3310
|
-
while (renderables.length > maxDomLinesPerPane) {
|
|
3311
|
-
const oldLine = renderables.shift();
|
|
3312
|
-
scrollBox.content.remove(oldLine);
|
|
3313
|
-
oldLine.destroy();
|
|
3314
|
-
}
|
|
3315
|
-
|
|
3316
|
-
// Update to track the last absolute line number we rendered
|
|
3317
|
-
this.paneLineCount.set(paneId, newLines[newLines.length - 1].lineNumber);
|
|
3318
|
-
|
|
3319
|
-
// Auto-scroll to bottom if not paused
|
|
3320
|
-
if (!this.isPaused && scrollBox.scrollTo) {
|
|
3321
|
-
scrollBox.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER });
|
|
3526
|
+
const visibleHeight = this.paneVisibleHeight.get(paneId) || 20;
|
|
3527
|
+
const lines = this.outputDb.queryVisible(pane, visibleHeight, 0);
|
|
3528
|
+
|
|
3529
|
+
// Skip if no lines or last line hasn't changed
|
|
3530
|
+
if (lines.length === 0) continue;
|
|
3531
|
+
const lastLineNum = lines[lines.length - 1].lineNumber;
|
|
3532
|
+
const prevLastLine = this.paneLineCount.get(paneId) || 0;
|
|
3533
|
+
if (lastLineNum === prevLastLine) continue;
|
|
3534
|
+
|
|
3535
|
+
// Check pane content cache - if first line ID matches, we can append
|
|
3536
|
+
const firstLineId = lines[0].id;
|
|
3537
|
+
const cache = this.paneContentCache.get(paneId);
|
|
3538
|
+
|
|
3539
|
+
let styledText;
|
|
3540
|
+
if (cache && cache.firstLineId === firstLineId && cache.lineCount === lines.length - 1) {
|
|
3541
|
+
// Only 1 new line at the end - append to cached chunks
|
|
3542
|
+
const newLine = lines[lines.length - 1];
|
|
3543
|
+
const newLineContent = this.formatOutputLine(newLine, lines.length - 1, false, -1, -1);
|
|
3544
|
+
const chunks = cache.chunks.concat(newLineContent.chunks);
|
|
3545
|
+
styledText = new StyledText(chunks);
|
|
3546
|
+
this.paneContentCache.set(paneId, { firstLineId, lineCount: lines.length, chunks });
|
|
3547
|
+
} else {
|
|
3548
|
+
// Full rebuild
|
|
3549
|
+
const chunks = [];
|
|
3550
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3551
|
+
const lineContent = this.formatOutputLine(lines[i], i, false, -1, -1);
|
|
3552
|
+
for (let j = 0; j < lineContent.chunks.length; j++) {
|
|
3553
|
+
chunks.push(lineContent.chunks[j]);
|
|
3322
3554
|
}
|
|
3323
3555
|
}
|
|
3556
|
+
styledText = new StyledText(chunks);
|
|
3557
|
+
this.paneContentCache.set(paneId, { firstLineId, lineCount: lines.length, chunks });
|
|
3324
3558
|
}
|
|
3559
|
+
|
|
3560
|
+
// Destroy old renderable and create new one to avoid artifacts
|
|
3561
|
+
const renderables = this.lineRenderables.get(paneId);
|
|
3562
|
+
if (renderables && renderables.length > 0) {
|
|
3563
|
+
outputBox.remove(renderables[0]);
|
|
3564
|
+
renderables[0].destroy();
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
// Create new TextRenderable
|
|
3568
|
+
const outputText = new TextRenderable(this.renderer, {
|
|
3569
|
+
id: `output-${paneId}`,
|
|
3570
|
+
content: styledText,
|
|
3571
|
+
bg: '#000000',
|
|
3572
|
+
width: '100%',
|
|
3573
|
+
});
|
|
3574
|
+
outputBox.add(outputText);
|
|
3575
|
+
this.lineRenderables.set(paneId, [outputText]);
|
|
3576
|
+
|
|
3577
|
+
// Track last rendered line
|
|
3578
|
+
this.paneLineCount.set(paneId, lastLineNum);
|
|
3579
|
+
}
|
|
3325
3580
|
} else {
|
|
3326
3581
|
// First time or no panes exist - do full rebuild
|
|
3327
3582
|
this.buildRunningUI();
|
|
@@ -3336,9 +3591,8 @@ class ProcessManager {
|
|
|
3336
3591
|
}
|
|
3337
3592
|
|
|
3338
3593
|
// Remove old process bar items
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
this.processBarContainer.remove(child);
|
|
3594
|
+
for (const child of this.processBarContainer.getChildren()) {
|
|
3595
|
+
this.processBarContainer.remove(child.id);
|
|
3342
3596
|
child.destroy();
|
|
3343
3597
|
}
|
|
3344
3598
|
|
|
@@ -3355,7 +3609,7 @@ class ProcessManager {
|
|
|
3355
3609
|
const isVisible = this.isProcessVisibleInPane(script.name, focusedPane);
|
|
3356
3610
|
const nameColor = isSelected ? COLORS.accent : (isVisible ? processColor : COLORS.textDim);
|
|
3357
3611
|
const numberColor = isVisible ? processColor : COLORS.textDim;
|
|
3358
|
-
const indicator = isSelected ? '
|
|
3612
|
+
const indicator = isSelected ? '❯' : ' ';
|
|
3359
3613
|
const bracketColor = isVisible ? processColor : COLORS.textDim;
|
|
3360
3614
|
|
|
3361
3615
|
const numberLabel = index < 9 ? `${index + 1}` : ' ';
|
|
@@ -3375,7 +3629,83 @@ class ProcessManager {
|
|
|
3375
3629
|
});
|
|
3376
3630
|
}
|
|
3377
3631
|
|
|
3378
|
-
//
|
|
3632
|
+
// Get cache key for current display settings
|
|
3633
|
+
getFormatCacheKey() {
|
|
3634
|
+
return `${this.showLineNumbers}-${this.showTimestamps}-${this.renderer.width}`;
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
// Format a single line for display (returns styled text chunks)
|
|
3638
|
+
formatOutputLine(line, index, inCopyMode, copySelStart, copySelEnd) {
|
|
3639
|
+
// Copy mode can't use cache (dynamic highlighting)
|
|
3640
|
+
if (inCopyMode) {
|
|
3641
|
+
return this.formatOutputLineCopyMode(line, index, copySelStart, copySelEnd);
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
// Can't cache if we're padding to width (width can vary)
|
|
3645
|
+
// Check cache only if no padding needed
|
|
3646
|
+
const cacheKey = this.getFormatCacheKey();
|
|
3647
|
+
if (cacheKey !== this.lineFormatCacheKey) {
|
|
3648
|
+
// Settings changed, clear cache
|
|
3649
|
+
this.lineFormatCache.clear();
|
|
3650
|
+
this.lineFormatCacheKey = cacheKey;
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
// Format line
|
|
3654
|
+
const processColor = this.processColors.get(line.process) || COLORS.text;
|
|
3655
|
+
const lineNumber = this.showLineNumbers ? String(line.lineNumber).padStart(4, ' ') : '';
|
|
3656
|
+
const timestamp = this.showTimestamps ? (line.timeString || (line.timeString = new Date(line.timestamp).toLocaleTimeString('en-US', { hour12: false }))) : '';
|
|
3657
|
+
|
|
3658
|
+
let result;
|
|
3659
|
+
if (this.showLineNumbers && this.showTimestamps) {
|
|
3660
|
+
result = t`${fg(COLORS.textDim)(lineNumber)} ${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${line.text}\n`;
|
|
3661
|
+
} else if (this.showLineNumbers) {
|
|
3662
|
+
result = t`${fg(COLORS.textDim)(lineNumber)} ${fg(processColor)(`[${line.process}]`)} ${line.text}\n`;
|
|
3663
|
+
} else if (this.showTimestamps) {
|
|
3664
|
+
result = t`${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${line.text}\n`;
|
|
3665
|
+
} else {
|
|
3666
|
+
result = t`${fg(processColor)(`[${line.process}]`)} ${line.text}\n`;
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
return result;
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
// Format line in copy mode (no caching - dynamic highlighting)
|
|
3673
|
+
formatOutputLineCopyMode(line, index, copySelStart, copySelEnd) {
|
|
3674
|
+
const processColor = this.processColors.get(line.process) || COLORS.text;
|
|
3675
|
+
const lineNumber = this.showLineNumbers ? String(line.lineNumber).padStart(4, ' ') : '';
|
|
3676
|
+
const timestamp = this.showTimestamps ? (line.timeString || (line.timeString = new Date(line.timestamp).toLocaleTimeString('en-US', { hour12: false }))) : '';
|
|
3677
|
+
|
|
3678
|
+
const isCursorLine = index === this.copyModeCursor;
|
|
3679
|
+
const isSelectedLine = index >= copySelStart && index <= copySelEnd;
|
|
3680
|
+
|
|
3681
|
+
const marker = isCursorLine ? fg(COLORS.copyCursorText)('\u25b6 ') : ' ';
|
|
3682
|
+
let textColor, procColor, dimColor;
|
|
3683
|
+
if (isCursorLine) {
|
|
3684
|
+
textColor = COLORS.copyCursorText;
|
|
3685
|
+
procColor = COLORS.copyCursorText;
|
|
3686
|
+
dimColor = COLORS.copySelectText;
|
|
3687
|
+
} else if (isSelectedLine) {
|
|
3688
|
+
textColor = COLORS.copySelectText;
|
|
3689
|
+
procColor = processColor;
|
|
3690
|
+
dimColor = COLORS.textDim;
|
|
3691
|
+
} else {
|
|
3692
|
+
textColor = COLORS.textDim;
|
|
3693
|
+
procColor = COLORS.textDim;
|
|
3694
|
+
dimColor = COLORS.textDim;
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
if (this.showLineNumbers && this.showTimestamps) {
|
|
3698
|
+
return t`${marker}${fg(dimColor)(lineNumber)} ${fg(dimColor)(`[${timestamp}]`)} ${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}\n`;
|
|
3699
|
+
} else if (this.showLineNumbers) {
|
|
3700
|
+
return t`${marker}${fg(dimColor)(lineNumber)} ${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}\n`;
|
|
3701
|
+
} else if (this.showTimestamps) {
|
|
3702
|
+
return t`${marker}${fg(dimColor)(`[${timestamp}]`)} ${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}\n`;
|
|
3703
|
+
} else {
|
|
3704
|
+
return t`${marker}${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}\n`;
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
// Build a single pane's output area - uses single TextRenderable for efficiency
|
|
3379
3709
|
buildPaneOutput(pane, container, height) {
|
|
3380
3710
|
const isFocused = pane.id === this.focusedPaneId;
|
|
3381
3711
|
const lines = this.getOutputLinesForPane(pane);
|
|
@@ -3385,90 +3715,34 @@ class ProcessManager {
|
|
|
3385
3715
|
const maxLines = this.isPaused ? lines.length : Math.min(lines.length, height || 50);
|
|
3386
3716
|
const linesToShow = lines.slice(-maxLines);
|
|
3387
3717
|
|
|
3388
|
-
// Initialize renderable pool for this pane
|
|
3389
|
-
const renderables = [];
|
|
3390
|
-
this.lineRenderables.set(pane.id, renderables);
|
|
3391
|
-
|
|
3392
3718
|
// Determine copy mode selection range for this pane
|
|
3393
3719
|
const inCopyMode = this.isCopyMode && isFocused;
|
|
3394
3720
|
let copySelStart = -1;
|
|
3395
3721
|
let copySelEnd = -1;
|
|
3396
|
-
if (inCopyMode) {
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
copySelEnd = Math.max(this.copyModeAnchor, this.copyModeCursor);
|
|
3400
|
-
}
|
|
3722
|
+
if (inCopyMode && this.copyModeAnchor !== null) {
|
|
3723
|
+
copySelStart = Math.min(this.copyModeAnchor, this.copyModeCursor);
|
|
3724
|
+
copySelEnd = Math.max(this.copyModeAnchor, this.copyModeCursor);
|
|
3401
3725
|
}
|
|
3402
3726
|
|
|
3403
|
-
//
|
|
3727
|
+
// Build all lines as a single styled text (much more efficient than one renderable per line)
|
|
3728
|
+
const chunks = [];
|
|
3404
3729
|
for (let i = 0; i < linesToShow.length; i++) {
|
|
3405
|
-
const
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
// Build content with proper template literal
|
|
3409
|
-
const lineNumber = this.showLineNumbers ? String(line.lineNumber).padStart(4, ' ') : '';
|
|
3410
|
-
const timestamp = this.showTimestamps ? new Date(line.timestamp).toLocaleTimeString('en-US', { hour12: false }) : '';
|
|
3411
|
-
|
|
3412
|
-
// Determine copy mode highlighting for this line
|
|
3413
|
-
const isCursorLine = inCopyMode && i === this.copyModeCursor;
|
|
3414
|
-
const isSelectedLine = inCopyMode && i >= copySelStart && i <= copySelEnd;
|
|
3415
|
-
|
|
3416
|
-
let content;
|
|
3417
|
-
if (inCopyMode) {
|
|
3418
|
-
// In copy mode: cursor line gets bright marker + white text on blue bg
|
|
3419
|
-
// Selected lines get lighter text on darker blue bg
|
|
3420
|
-
// Unselected lines are dimmed to make selection stand out
|
|
3421
|
-
const marker = isCursorLine ? fg(COLORS.copyCursorText)('\u25b6 ') : ' ';
|
|
3422
|
-
|
|
3423
|
-
let textColor, procColor, dimColor;
|
|
3424
|
-
if (isCursorLine) {
|
|
3425
|
-
textColor = COLORS.copyCursorText;
|
|
3426
|
-
procColor = COLORS.copyCursorText;
|
|
3427
|
-
dimColor = COLORS.copySelectText;
|
|
3428
|
-
} else if (isSelectedLine) {
|
|
3429
|
-
textColor = COLORS.copySelectText;
|
|
3430
|
-
procColor = processColor;
|
|
3431
|
-
dimColor = COLORS.textDim;
|
|
3432
|
-
} else {
|
|
3433
|
-
textColor = COLORS.textDim;
|
|
3434
|
-
procColor = COLORS.textDim;
|
|
3435
|
-
dimColor = COLORS.textDim;
|
|
3436
|
-
}
|
|
3437
|
-
|
|
3438
|
-
if (this.showLineNumbers && this.showTimestamps) {
|
|
3439
|
-
content = t`${marker}${fg(dimColor)(lineNumber)} ${fg(dimColor)(`[${timestamp}]`)} ${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}`;
|
|
3440
|
-
} else if (this.showLineNumbers) {
|
|
3441
|
-
content = t`${marker}${fg(dimColor)(lineNumber)} ${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}`;
|
|
3442
|
-
} else if (this.showTimestamps) {
|
|
3443
|
-
content = t`${marker}${fg(dimColor)(`[${timestamp}]`)} ${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}`;
|
|
3444
|
-
} else {
|
|
3445
|
-
content = t`${marker}${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}`;
|
|
3446
|
-
}
|
|
3447
|
-
} else {
|
|
3448
|
-
if (this.showLineNumbers && this.showTimestamps) {
|
|
3449
|
-
content = t`${fg(COLORS.textDim)(lineNumber)} ${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${line.text}`;
|
|
3450
|
-
} else if (this.showLineNumbers) {
|
|
3451
|
-
content = t`${fg(COLORS.textDim)(lineNumber)} ${fg(processColor)(`[${line.process}]`)} ${line.text}`;
|
|
3452
|
-
} else if (this.showTimestamps) {
|
|
3453
|
-
content = t`${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${line.text}`;
|
|
3454
|
-
} else {
|
|
3455
|
-
content = t`${fg(processColor)(`[${line.process}]`)} ${line.text}`;
|
|
3456
|
-
}
|
|
3457
|
-
}
|
|
3458
|
-
|
|
3459
|
-
// High-contrast background: bright blue for cursor, medium blue for selected, black for rest
|
|
3460
|
-
const bgColor = isCursorLine ? COLORS.copyCursorBg : (isSelectedLine ? COLORS.copySelectBg : '#000000');
|
|
3461
|
-
|
|
3462
|
-
const outputLine = new TextRenderable(this.renderer, {
|
|
3463
|
-
id: `output-${pane.id}-${line.lineNumber}`,
|
|
3464
|
-
content: content,
|
|
3465
|
-
bg: bgColor,
|
|
3466
|
-
});
|
|
3467
|
-
|
|
3468
|
-
container.add(outputLine);
|
|
3469
|
-
renderables.push(outputLine);
|
|
3730
|
+
const lineContent = this.formatOutputLine(linesToShow[i], i, inCopyMode, copySelStart, copySelEnd);
|
|
3731
|
+
chunks.push(...lineContent.chunks);
|
|
3470
3732
|
}
|
|
3471
3733
|
|
|
3734
|
+
// Create single TextRenderable with all content
|
|
3735
|
+
const outputText = new TextRenderable(this.renderer, {
|
|
3736
|
+
id: `output-${pane.id}`,
|
|
3737
|
+
content: new StyledText(chunks),
|
|
3738
|
+
bg: '#000000',
|
|
3739
|
+
});
|
|
3740
|
+
|
|
3741
|
+
container.add(outputText);
|
|
3742
|
+
|
|
3743
|
+
// Store reference for incremental updates
|
|
3744
|
+
this.lineRenderables.set(pane.id, [outputText]);
|
|
3745
|
+
|
|
3472
3746
|
// Track last rendered line number
|
|
3473
3747
|
if (linesToShow.length > 0) {
|
|
3474
3748
|
this.paneLineCount.set(pane.id, linesToShow[linesToShow.length - 1].lineNumber);
|
|
@@ -3488,7 +3762,7 @@ class ProcessManager {
|
|
|
3488
3762
|
}
|
|
3489
3763
|
}
|
|
3490
3764
|
|
|
3491
|
-
// Build a pane panel with title bar
|
|
3765
|
+
// Build a pane panel with title bar - uses virtual scrolling (no ScrollBox)
|
|
3492
3766
|
buildPanePanel(pane, flexGrow = 1, availableHeight = null) {
|
|
3493
3767
|
const isFocused = pane.id === this.focusedPaneId;
|
|
3494
3768
|
const borderColor = isFocused ? COLORS.borderFocused : COLORS.border;
|
|
@@ -3504,14 +3778,20 @@ class ProcessManager {
|
|
|
3504
3778
|
const filterLabel = pane.filter ? ` /${pane.filter}` : '';
|
|
3505
3779
|
const namingInputLabel = (isFocused && this.isNamingMode) ? `Name: ${this.namingModeText}_` : '';
|
|
3506
3780
|
const filterInputLabel = (isFocused && this.isFilterMode) ? `/${pane.filter || ''}_` : '';
|
|
3507
|
-
|
|
3781
|
+
|
|
3782
|
+
// Show scroll position indicator when paused
|
|
3783
|
+
const scrollOffset = this.paneScrollOffsets.get(pane.id) || 0;
|
|
3784
|
+
const totalLines = this.outputDb.countForPane(pane);
|
|
3785
|
+
const scrollIndicator = (this.isPaused && scrollOffset > 0) ? ` [${scrollOffset}↑]` : '';
|
|
3786
|
+
|
|
3787
|
+
const title = ` ${focusLabel}${namingInputLabel || processLabel}${hiddenLabel}${filterInputLabel || filterLabel}${scrollIndicator} `;
|
|
3508
3788
|
|
|
3509
3789
|
const paneContainer = new BoxRenderable(this.renderer, {
|
|
3510
3790
|
id: `pane-${pane.id}`,
|
|
3511
3791
|
flexDirection: 'column',
|
|
3512
3792
|
flexGrow: flexGrow,
|
|
3513
|
-
flexShrink: 0,
|
|
3514
|
-
flexBasis: 0,
|
|
3793
|
+
flexShrink: 0,
|
|
3794
|
+
flexBasis: 0,
|
|
3515
3795
|
border: true,
|
|
3516
3796
|
borderStyle: 'rounded',
|
|
3517
3797
|
borderColor: borderColor,
|
|
@@ -3519,63 +3799,81 @@ class ProcessManager {
|
|
|
3519
3799
|
titleAlignment: 'left',
|
|
3520
3800
|
padding: 0,
|
|
3521
3801
|
overflow: 'hidden',
|
|
3522
|
-
backgroundColor: '#000000',
|
|
3802
|
+
backgroundColor: '#000000',
|
|
3523
3803
|
});
|
|
3524
3804
|
|
|
3525
|
-
//
|
|
3526
|
-
const
|
|
3805
|
+
// Calculate visible height (minus border)
|
|
3806
|
+
const visibleHeight = availableHeight ? Math.max(5, availableHeight - 2) : Math.max(5, this.renderer.height - 6);
|
|
3527
3807
|
|
|
3528
|
-
|
|
3808
|
+
// Store visible height for scroll calculations
|
|
3809
|
+
this.paneVisibleHeight.set(pane.id, visibleHeight);
|
|
3810
|
+
|
|
3811
|
+
// Create output container (no ScrollBox - we handle scrolling virtually)
|
|
3812
|
+
const outputBox = new BoxRenderable(this.renderer, {
|
|
3529
3813
|
id: `pane-output-${pane.id}`,
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
flexGrow: 1,
|
|
3537
|
-
flexShrink: 1,
|
|
3538
|
-
flexBasis: 0,
|
|
3539
|
-
paddingLeft: 1,
|
|
3540
|
-
backgroundColor: '#000000',
|
|
3541
|
-
},
|
|
3542
|
-
contentOptions: {
|
|
3543
|
-
backgroundColor: '#000000',
|
|
3544
|
-
width: '100%',
|
|
3545
|
-
},
|
|
3546
|
-
},
|
|
3814
|
+
flexDirection: 'column',
|
|
3815
|
+
flexGrow: 1,
|
|
3816
|
+
paddingLeft: 1,
|
|
3817
|
+
backgroundColor: '#000000',
|
|
3818
|
+
overflow: 'hidden',
|
|
3819
|
+
shouldFill: true,
|
|
3547
3820
|
});
|
|
3548
3821
|
|
|
3549
|
-
//
|
|
3550
|
-
|
|
3551
|
-
|
|
3822
|
+
// Query only the visible lines from database
|
|
3823
|
+
const lines = this.outputDb.queryVisible(pane, visibleHeight, scrollOffset);
|
|
3824
|
+
|
|
3825
|
+
// Build content for visible lines
|
|
3826
|
+
this.buildPaneOutputVirtual(pane, outputBox, lines, visibleHeight);
|
|
3827
|
+
|
|
3828
|
+
// Store reference to output box for updates
|
|
3829
|
+
this.paneOutputBoxes.set(pane.id, outputBox);
|
|
3830
|
+
|
|
3831
|
+
// Track the last line number for incremental updates
|
|
3832
|
+
if (lines.length > 0) {
|
|
3833
|
+
this.paneLineCount.set(pane.id, lines[lines.length - 1].lineNumber);
|
|
3552
3834
|
}
|
|
3553
3835
|
|
|
3554
|
-
|
|
3555
|
-
|
|
3836
|
+
paneContainer.add(outputBox);
|
|
3837
|
+
return paneContainer;
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3840
|
+
// Build pane output with virtual scrolling - only renders visible lines
|
|
3841
|
+
buildPaneOutputVirtual(pane, container, lines, visibleHeight) {
|
|
3842
|
+
const isFocused = pane.id === this.focusedPaneId;
|
|
3556
3843
|
|
|
3557
|
-
|
|
3844
|
+
// Determine copy mode selection range
|
|
3845
|
+
const inCopyMode = this.isCopyMode && isFocused;
|
|
3846
|
+
let copySelStart = -1;
|
|
3847
|
+
let copySelEnd = -1;
|
|
3848
|
+
if (inCopyMode && this.copyModeAnchor !== null) {
|
|
3849
|
+
copySelStart = Math.min(this.copyModeAnchor, this.copyModeCursor);
|
|
3850
|
+
copySelEnd = Math.max(this.copyModeAnchor, this.copyModeCursor);
|
|
3851
|
+
}
|
|
3558
3852
|
|
|
3559
|
-
//
|
|
3560
|
-
const
|
|
3561
|
-
|
|
3562
|
-
this.
|
|
3853
|
+
// Build all visible lines as single styled text
|
|
3854
|
+
const chunks = [];
|
|
3855
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3856
|
+
const lineContent = this.formatOutputLine(lines[i], i, inCopyMode, copySelStart, copySelEnd);
|
|
3857
|
+
chunks.push(...lineContent.chunks);
|
|
3563
3858
|
}
|
|
3564
3859
|
|
|
3565
|
-
//
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
// Restore saved scroll position when paused
|
|
3569
|
-
const savedPos = this.paneScrollPositions.get(pane.id);
|
|
3570
|
-
outputBox.scrollTo(savedPos);
|
|
3571
|
-
} else if (!this.isPaused) {
|
|
3572
|
-
// Auto-scroll to bottom when not paused
|
|
3573
|
-
outputBox.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER });
|
|
3574
|
-
}
|
|
3860
|
+
// Fill remaining visible area with blank lines to clear artifacts
|
|
3861
|
+
for (let i = lines.length; i < visibleHeight; i++) {
|
|
3862
|
+
chunks.push({ text: '\n' });
|
|
3575
3863
|
}
|
|
3576
3864
|
|
|
3577
|
-
|
|
3578
|
-
|
|
3865
|
+
// Create single TextRenderable with all visible content
|
|
3866
|
+
const outputText = new TextRenderable(this.renderer, {
|
|
3867
|
+
id: `output-${pane.id}`,
|
|
3868
|
+
content: new StyledText(chunks),
|
|
3869
|
+
bg: '#000000',
|
|
3870
|
+
width: '100%',
|
|
3871
|
+
});
|
|
3872
|
+
|
|
3873
|
+
container.add(outputText);
|
|
3874
|
+
|
|
3875
|
+
// Store reference for updates
|
|
3876
|
+
this.lineRenderables.set(pane.id, [outputText]);
|
|
3579
3877
|
}
|
|
3580
3878
|
|
|
3581
3879
|
// Recursively build the pane layout, passing available height down
|
|
@@ -4162,6 +4460,11 @@ class ProcessManager {
|
|
|
4162
4460
|
}
|
|
4163
4461
|
|
|
4164
4462
|
// Remove old containers if they exist - use destroyRecursively to clean up all children
|
|
4463
|
+
if (this.wipeContainer) {
|
|
4464
|
+
this.renderer.root.remove(this.wipeContainer);
|
|
4465
|
+
this.wipeContainer.destroyRecursively();
|
|
4466
|
+
this.wipeContainer = null;
|
|
4467
|
+
}
|
|
4165
4468
|
if (this.selectionContainer) {
|
|
4166
4469
|
this.renderer.root.remove(this.selectionContainer);
|
|
4167
4470
|
this.selectionContainer.destroyRecursively();
|
|
@@ -4221,7 +4524,7 @@ class ProcessManager {
|
|
|
4221
4524
|
const isVisible = this.isProcessVisibleInPane(script.name, focusedPane);
|
|
4222
4525
|
const nameColor = isSelected ? COLORS.accent : (isVisible ? processColor : COLORS.textDim);
|
|
4223
4526
|
const numberColor = isVisible ? processColor : COLORS.textDim;
|
|
4224
|
-
const indicator = isSelected ? '
|
|
4527
|
+
const indicator = isSelected ? '❯' : ' ';
|
|
4225
4528
|
const bracketColor = isVisible ? processColor : COLORS.textDim;
|
|
4226
4529
|
|
|
4227
4530
|
// Show number for first 9 processes
|
|
@@ -4532,7 +4835,9 @@ async function main() {
|
|
|
4532
4835
|
process.exit(1);
|
|
4533
4836
|
}
|
|
4534
4837
|
|
|
4535
|
-
const renderer = await createCliRenderer(
|
|
4838
|
+
const renderer = await createCliRenderer({
|
|
4839
|
+
backgroundColor: '#000000',
|
|
4840
|
+
});
|
|
4536
4841
|
renderer.start(); // Start the automatic render loop
|
|
4537
4842
|
const manager = new ProcessManager(renderer, scripts);
|
|
4538
4843
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "startall",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.25",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
11
11
|
"start": "bun index.js",
|
|
12
12
|
"demo:frontend": "node -e \"setInterval(() => console.log('Server running...'), 1000)\"",
|
|
13
|
-
"demo:backend": "node -e \"setInterval(() => console.log('API ready... API ready... API ready... API ready... API ready...'),
|
|
13
|
+
"demo:backend": "node -e \"setInterval(() => console.log('API ready... API ready... API ready... API ready... API ready...'), 100)\"",
|
|
14
14
|
"demo:worker": "node -e \"setInterval(() => console.log('Processing jobs...'), 500)\"",
|
|
15
15
|
"demo:worker2": "node -e \"setInterval(() => console.log('Processing jobs...'), 100)\"",
|
|
16
16
|
"demo:stress": "node -e \"let i=0; setInterval(() => { for(let j=0;j<10;j++) console.log('Line ' + (++i)); }, 16)\"",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
},
|
|
29
29
|
"type": "module",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@opentui/core": "^0.1.
|
|
32
|
-
"@opentui/react": "^0.1.
|
|
31
|
+
"@opentui/core": "^0.1.79",
|
|
32
|
+
"@opentui/react": "^0.1.79",
|
|
33
33
|
"chalk": "^5.6.2",
|
|
34
34
|
"react": "^19.2.3",
|
|
35
35
|
"strip-ansi": "^7.1.2",
|