startall 0.0.24 → 0.0.26
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 +672 -343
- package/package.json +4 -4
package/index.js
CHANGED
|
@@ -1,11 +1,184 @@
|
|
|
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
|
+
import { StringDecoder } from 'string_decoder';
|
|
11
|
+
|
|
12
|
+
// SQLite-backed output line storage
|
|
13
|
+
class OutputDatabase {
|
|
14
|
+
constructor(maxLines = 1000) {
|
|
15
|
+
this.db = new Database(':memory:');
|
|
16
|
+
this.maxLines = maxLines;
|
|
17
|
+
|
|
18
|
+
// Enable WAL mode for better concurrent read/write performance
|
|
19
|
+
this.db.exec('PRAGMA journal_mode = WAL');
|
|
20
|
+
|
|
21
|
+
// Create the output_lines table with colors bitmask for efficient filtering
|
|
22
|
+
this.db.exec(`
|
|
23
|
+
CREATE TABLE output_lines (
|
|
24
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
25
|
+
line_number INTEGER NOT NULL,
|
|
26
|
+
process TEXT NOT NULL,
|
|
27
|
+
process_lower TEXT NOT NULL,
|
|
28
|
+
text TEXT NOT NULL,
|
|
29
|
+
text_lower TEXT NOT NULL,
|
|
30
|
+
timestamp INTEGER NOT NULL,
|
|
31
|
+
colors INTEGER NOT NULL DEFAULT 0
|
|
32
|
+
)
|
|
33
|
+
`);
|
|
34
|
+
this.db.exec('CREATE INDEX idx_line_number ON output_lines(line_number)');
|
|
35
|
+
this.db.exec('CREATE INDEX idx_process ON output_lines(process)');
|
|
36
|
+
this.db.exec('CREATE INDEX idx_colors ON output_lines(colors)');
|
|
37
|
+
|
|
38
|
+
// Column list with aliases to match existing JS property names
|
|
39
|
+
this._columns = 'id, line_number AS lineNumber, process, process_lower AS processLower, text, text_lower AS textLower, timestamp, colors';
|
|
40
|
+
|
|
41
|
+
// Prepare reusable statements for performance
|
|
42
|
+
this._insertStmt = this.db.prepare(
|
|
43
|
+
'INSERT INTO output_lines (line_number, process, process_lower, text, text_lower, timestamp, colors) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
|
44
|
+
);
|
|
45
|
+
this._countStmt = this.db.prepare('SELECT COUNT(*) AS cnt FROM output_lines');
|
|
46
|
+
this._deleteOldStmt = this.db.prepare(
|
|
47
|
+
'DELETE FROM output_lines WHERE id NOT IN (SELECT id FROM output_lines ORDER BY id DESC LIMIT ?)'
|
|
48
|
+
);
|
|
49
|
+
this._selectAllStmt = this.db.prepare(`SELECT ${this._columns} FROM output_lines ORDER BY id ASC`);
|
|
50
|
+
this._clearStmt = this.db.prepare('DELETE FROM output_lines');
|
|
51
|
+
// Fast path for queryVisible with no filters
|
|
52
|
+
this._queryVisibleNoFilterStmt = this.db.prepare(`SELECT ${this._columns} FROM (
|
|
53
|
+
SELECT * FROM output_lines ORDER BY id DESC LIMIT ?
|
|
54
|
+
) ORDER BY id ASC`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
insert(lineNumber, process, text, timestamp, colorBitmask) {
|
|
58
|
+
this._insertStmt.run(lineNumber, process, process.toLowerCase(), text, text.toLowerCase(), timestamp, colorBitmask);
|
|
59
|
+
|
|
60
|
+
// Enforce max lines limit
|
|
61
|
+
const { cnt } = this._countStmt.get();
|
|
62
|
+
if (cnt > this.maxLines) {
|
|
63
|
+
this._deleteOldStmt.run(this.maxLines);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getAll() {
|
|
68
|
+
return this._selectAllStmt.all();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
count() {
|
|
72
|
+
return this._countStmt.get().cnt;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
clear() {
|
|
76
|
+
this._clearStmt.run();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Build SQL conditions and params for pane filters
|
|
80
|
+
_buildPaneFilters(pane) {
|
|
81
|
+
const conditions = [];
|
|
82
|
+
const params = [];
|
|
83
|
+
|
|
84
|
+
if (pane.processes && pane.processes.length > 0) {
|
|
85
|
+
const placeholders = pane.processes.map(() => '?').join(', ');
|
|
86
|
+
conditions.push(`process IN (${placeholders})`);
|
|
87
|
+
params.push(...pane.processes);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (pane.hidden && pane.hidden.length > 0) {
|
|
91
|
+
const placeholders = pane.hidden.map(() => '?').join(', ');
|
|
92
|
+
conditions.push(`process NOT IN (${placeholders})`);
|
|
93
|
+
params.push(...pane.hidden);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (pane.filter) {
|
|
97
|
+
const filterLower = pane.filter.toLowerCase();
|
|
98
|
+
conditions.push('(process_lower LIKE ? OR text_lower LIKE ?)');
|
|
99
|
+
params.push(`%${filterLower}%`, `%${filterLower}%`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (pane.colorFilter) {
|
|
103
|
+
// Use bitmask check: (colors & bit) != 0
|
|
104
|
+
const colorBit = this._getColorBit(pane.colorFilter);
|
|
105
|
+
if (colorBit > 0) {
|
|
106
|
+
conditions.push('(colors & ?) != 0');
|
|
107
|
+
params.push(colorBit);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { conditions, params };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Get color bitmask value for a color name
|
|
115
|
+
_getColorBit(colorName) {
|
|
116
|
+
const bits = { red: 1, yellow: 2, green: 4, blue: 8, cyan: 16, magenta: 32 };
|
|
117
|
+
return bits[colorName] || 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Query with pane filters applied via SQL
|
|
121
|
+
queryForPane(pane) {
|
|
122
|
+
const { conditions, params } = this._buildPaneFilters(pane);
|
|
123
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
124
|
+
const sql = `SELECT ${this._columns} FROM output_lines ${where} ORDER BY id ASC`;
|
|
125
|
+
return this.db.prepare(sql).all(...params);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Query lines newer than a given line_number, with pane filters
|
|
129
|
+
queryNewLines(afterLineNumber, pane, limit) {
|
|
130
|
+
const { conditions, params } = this._buildPaneFilters(pane);
|
|
131
|
+
conditions.unshift('line_number > ?');
|
|
132
|
+
params.unshift(afterLineNumber);
|
|
133
|
+
|
|
134
|
+
const where = `WHERE ${conditions.join(' AND ')}`;
|
|
135
|
+
// Get newest lines up to limit, but return them in ascending order
|
|
136
|
+
const sql = `SELECT ${this._columns} FROM (SELECT * FROM output_lines ${where} ORDER BY id DESC LIMIT ?) ORDER BY id ASC`;
|
|
137
|
+
params.push(limit);
|
|
138
|
+
|
|
139
|
+
return this.db.prepare(sql).all(...params);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Count lines matching pane filters (for calculating max scroll)
|
|
143
|
+
countForPane(pane) {
|
|
144
|
+
const { conditions, params } = this._buildPaneFilters(pane);
|
|
145
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
146
|
+
const sql = `SELECT COUNT(*) AS cnt FROM output_lines ${where}`;
|
|
147
|
+
return this.db.prepare(sql).get(...params).cnt;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Query visible lines for virtual scrolling: get `limit` lines starting from `offset` from the end
|
|
151
|
+
// offset=0 means the most recent lines, offset=10 means 10 lines back from the end
|
|
152
|
+
queryVisible(pane, limit, offsetFromEnd) {
|
|
153
|
+
const { conditions, params } = this._buildPaneFilters(pane);
|
|
154
|
+
const totalToFetch = limit + offsetFromEnd;
|
|
155
|
+
|
|
156
|
+
let rows;
|
|
157
|
+
if (conditions.length === 0) {
|
|
158
|
+
// Fast path: no filters, use prepared statement
|
|
159
|
+
rows = this._queryVisibleNoFilterStmt.all(totalToFetch);
|
|
160
|
+
} else {
|
|
161
|
+
// Slow path: build query with filters
|
|
162
|
+
const where = `WHERE ${conditions.join(' AND ')}`;
|
|
163
|
+
const sql = `SELECT ${this._columns} FROM (
|
|
164
|
+
SELECT * FROM output_lines ${where} ORDER BY id DESC LIMIT ?
|
|
165
|
+
) ORDER BY id ASC`;
|
|
166
|
+
params.push(totalToFetch);
|
|
167
|
+
rows = this.db.prepare(sql).all(...params);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// If offset > 0, trim the newest lines
|
|
171
|
+
if (offsetFromEnd > 0 && rows.length > limit) {
|
|
172
|
+
rows = rows.slice(0, limit);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return rows;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
close() {
|
|
179
|
+
this.db.close();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
9
182
|
|
|
10
183
|
// Configuration
|
|
11
184
|
const CONFIG_FILE = process.argv[2] || 'startall.json';
|
|
@@ -93,34 +266,65 @@ function getAllPaneIds(node, ids = []) {
|
|
|
93
266
|
return ids;
|
|
94
267
|
}
|
|
95
268
|
|
|
269
|
+
// Color bitmask values for efficient SQL filtering
|
|
270
|
+
const COLOR_BITS = {
|
|
271
|
+
red: 1, // 0b000001
|
|
272
|
+
yellow: 2, // 0b000010
|
|
273
|
+
green: 4, // 0b000100
|
|
274
|
+
blue: 8, // 0b001000
|
|
275
|
+
cyan: 16, // 0b010000
|
|
276
|
+
magenta: 32, // 0b100000
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// ANSI color codes (normal and bright variants)
|
|
280
|
+
const ANSI_COLOR_CODES = {
|
|
281
|
+
red: [31, 91],
|
|
282
|
+
yellow: [33, 93],
|
|
283
|
+
green: [32, 92],
|
|
284
|
+
blue: [34, 94],
|
|
285
|
+
cyan: [36, 96],
|
|
286
|
+
magenta: [35, 95],
|
|
287
|
+
};
|
|
288
|
+
|
|
96
289
|
// Get ANSI color codes for a color name (includes normal and bright variants)
|
|
97
290
|
function getAnsiColorCodes(colorName) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
291
|
+
return ANSI_COLOR_CODES[colorName] || [];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Check if text contains a specific ANSI color code
|
|
295
|
+
function textHasColorCode(text, code) {
|
|
296
|
+
// Check for direct color code: \x1b[31m
|
|
297
|
+
if (text.includes(`\x1b[${code}m`)) return true;
|
|
298
|
+
// Check for color with modifiers: \x1b[1;31m, \x1b[0;31m, etc.
|
|
299
|
+
if (text.includes(`;${code}m`)) return true;
|
|
300
|
+
// Check for color at start of sequence: \x1b[31;1m
|
|
301
|
+
if (text.includes(`\x1b[${code};`)) return true;
|
|
302
|
+
return false;
|
|
107
303
|
}
|
|
108
304
|
|
|
109
305
|
// Check if a line contains a specific ANSI color
|
|
110
306
|
function lineHasColor(text, colorName) {
|
|
111
307
|
const codes = getAnsiColorCodes(colorName);
|
|
112
|
-
// Match ANSI escape sequences like \x1b[31m, \x1b[91m, \x1b[1;31m, etc.
|
|
113
308
|
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;
|
|
309
|
+
if (textHasColorCode(text, code)) return true;
|
|
120
310
|
}
|
|
121
311
|
return false;
|
|
122
312
|
}
|
|
123
313
|
|
|
314
|
+
// Detect all colors in text and return bitmask
|
|
315
|
+
function detectColorBitmask(text) {
|
|
316
|
+
let bitmask = 0;
|
|
317
|
+
for (const [colorName, codes] of Object.entries(ANSI_COLOR_CODES)) {
|
|
318
|
+
for (const code of codes) {
|
|
319
|
+
if (textHasColorCode(text, code)) {
|
|
320
|
+
bitmask |= COLOR_BITS[colorName];
|
|
321
|
+
break; // Found this color, no need to check other codes for it
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return bitmask;
|
|
326
|
+
}
|
|
327
|
+
|
|
124
328
|
// Find parent of a node
|
|
125
329
|
function findParent(root, targetId, parent = null) {
|
|
126
330
|
if (!root) return null;
|
|
@@ -550,10 +754,10 @@ class ProcessManager {
|
|
|
550
754
|
this.selectedIndex = 0;
|
|
551
755
|
this.processes = new Map();
|
|
552
756
|
this.processRefs = new Map();
|
|
553
|
-
this.
|
|
757
|
+
this.maxOutputLines = 1000; // Lines kept in database
|
|
758
|
+
this.outputDb = new OutputDatabase(this.maxOutputLines);
|
|
554
759
|
this.totalLinesReceived = 0; // Track total lines ever received (never resets)
|
|
555
760
|
this.filter = '';
|
|
556
|
-
this.maxOutputLines = 1000; // Lines kept in memory
|
|
557
761
|
this.maxDomLines = 150; // Lines kept in DOM (buffer for varying heights)
|
|
558
762
|
this.lineRenderables = new Map(); // Reusable TextRenderables per pane
|
|
559
763
|
this.maxVisibleLines = null; // Calculated dynamically based on screen height
|
|
@@ -590,11 +794,12 @@ class ProcessManager {
|
|
|
590
794
|
this.outputBox = null; // Reference to the output container
|
|
591
795
|
this.destroyed = false; // Flag to prevent operations after cleanup
|
|
592
796
|
this.lastRenderedLineCount = 0; // Track how many lines we've rendered
|
|
797
|
+
this.hasNewLines = false; // Flag set when new lines are added, cleared after render
|
|
593
798
|
this.headerRenderable = null; // Reference to header text in running UI
|
|
594
799
|
this.processListRenderable = null; // Reference to process list text in running UI
|
|
595
800
|
this.renderScheduled = false; // Throttle renders for CPU efficiency
|
|
596
801
|
this.lastRenderTime = 0; // Timestamp of last render
|
|
597
|
-
this.minRenderInterval =
|
|
802
|
+
this.minRenderInterval = 1; // Minimum ms between renders (~60fps cap)
|
|
598
803
|
|
|
599
804
|
// Performance metrics
|
|
600
805
|
this.showPerformanceMetrics = this.config.showPerformanceMetrics || false;
|
|
@@ -609,16 +814,28 @@ class ProcessManager {
|
|
|
609
814
|
this.splitMode = false; // Whether waiting for split command after Ctrl+b
|
|
610
815
|
this.showSplitMenu = false; // Whether to show the command palette
|
|
611
816
|
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
|
|
817
|
+
this.paneScrollPositions = new Map(); // Store scroll positions per pane ID (legacy)
|
|
818
|
+
this.paneScrollBoxes = new Map(); // Store ScrollBox references per pane ID (legacy)
|
|
614
819
|
this.paneFilterState = new Map(); // Track filter state per pane to detect changes
|
|
615
820
|
this.paneLineCount = new Map(); // Track how many lines we've rendered per pane
|
|
616
821
|
this.uiJustRebuilt = false; // Flag to skip redundant render after buildRunningUI
|
|
617
822
|
|
|
823
|
+
// Virtual scrolling state
|
|
824
|
+
this.paneScrollOffsets = new Map(); // Scroll offset from end per pane (0 = at bottom)
|
|
825
|
+
this.paneVisibleHeight = new Map(); // Visible height per pane for scroll calculations
|
|
826
|
+
this.paneOutputBoxes = new Map(); // Store output BoxRenderable references per pane
|
|
827
|
+
|
|
618
828
|
// Column view state (one pane per script, side by side)
|
|
619
829
|
this.isColumnView = false; // Whether column view is active
|
|
620
830
|
this.savedPaneRoot = null; // Saved pane tree to restore when toggling back
|
|
621
831
|
|
|
832
|
+
// Line format cache (keyed by line ID + settings hash)
|
|
833
|
+
this.lineFormatCache = new Map();
|
|
834
|
+
this.lineFormatCacheKey = ''; // Cache key based on display settings
|
|
835
|
+
|
|
836
|
+
// Pane content cache (keyed by pane ID -> {lastLineId, styledText})
|
|
837
|
+
this.paneContentCache = new Map();
|
|
838
|
+
|
|
622
839
|
// Copy mode state (select text to copy)
|
|
623
840
|
this.isCopyMode = false; // Whether in copy/select mode
|
|
624
841
|
this.copyModeCursor = 0; // Current cursor line index within visible lines
|
|
@@ -673,13 +890,27 @@ class ProcessManager {
|
|
|
673
890
|
return;
|
|
674
891
|
}
|
|
675
892
|
|
|
676
|
-
// Handle Ctrl+L - clear screen
|
|
893
|
+
// Handle Ctrl+L - clear screen and redraw (kills artifacts)
|
|
677
894
|
if (key.ctrl && key.name === 'l') {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
this.
|
|
681
|
-
this.buildRunningUI();
|
|
895
|
+
// Debounce - cancel any pending wipe and restart
|
|
896
|
+
if (this.wipeTimeout) {
|
|
897
|
+
clearTimeout(this.wipeTimeout);
|
|
682
898
|
}
|
|
899
|
+
|
|
900
|
+
// Save current phase and show wipe screen
|
|
901
|
+
const savedPhase = this.phase;
|
|
902
|
+
this.buildWipeScreen();
|
|
903
|
+
|
|
904
|
+
this.wipeTimeout = setTimeout(() => {
|
|
905
|
+
this.wipeTimeout = null;
|
|
906
|
+
if (savedPhase === 'running') {
|
|
907
|
+
this.buildRunningUI();
|
|
908
|
+
} else if (savedPhase === 'selection') {
|
|
909
|
+
this.buildSelectionUI();
|
|
910
|
+
} else if (savedPhase === 'settings') {
|
|
911
|
+
this.buildSettingsUI();
|
|
912
|
+
}
|
|
913
|
+
}, 50);
|
|
683
914
|
return;
|
|
684
915
|
}
|
|
685
916
|
|
|
@@ -874,13 +1105,13 @@ class ProcessManager {
|
|
|
874
1105
|
this.buildRunningUI();
|
|
875
1106
|
} else if (keyName === 'p') {
|
|
876
1107
|
// Toggle pause output scrolling globally
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1108
|
+
this.isPaused = !this.isPaused;
|
|
1109
|
+
// Reset scroll offset to bottom when unpausing
|
|
1110
|
+
if (!this.isPaused) {
|
|
1111
|
+
for (const paneId of getAllPaneIds(this.paneRoot)) {
|
|
1112
|
+
this.paneScrollOffsets.set(paneId, 0);
|
|
881
1113
|
}
|
|
882
1114
|
}
|
|
883
|
-
this.isPaused = !this.isPaused;
|
|
884
1115
|
this.updateStreamPauseState();
|
|
885
1116
|
this.buildRunningUI();
|
|
886
1117
|
} else if (keyName === 'f') {
|
|
@@ -1103,13 +1334,16 @@ class ProcessManager {
|
|
|
1103
1334
|
} else {
|
|
1104
1335
|
this.paneRoot = createPane([]); // Empty array means show all processes
|
|
1105
1336
|
}
|
|
1106
|
-
|
|
1337
|
+
// Focus the first actual pane (paneRoot might be a split node)
|
|
1338
|
+
const allPanes = getAllPaneIds(this.paneRoot);
|
|
1339
|
+
this.focusedPaneId = allPanes.length > 0 ? allPanes[0] : this.paneRoot.id;
|
|
1107
1340
|
|
|
1108
1341
|
selected.forEach(scriptName => {
|
|
1109
1342
|
this.startProcess(scriptName);
|
|
1110
1343
|
});
|
|
1111
1344
|
|
|
1112
|
-
|
|
1345
|
+
// Build the UI immediately (don't rely on render() since hasNewLines may be false)
|
|
1346
|
+
this.buildRunningUI();
|
|
1113
1347
|
}
|
|
1114
1348
|
|
|
1115
1349
|
startProcess(scriptName) {
|
|
@@ -1125,9 +1359,19 @@ class ProcessManager {
|
|
|
1125
1359
|
shell: true,
|
|
1126
1360
|
});
|
|
1127
1361
|
|
|
1362
|
+
// Use StringDecoder to properly handle multi-byte UTF-8 characters
|
|
1363
|
+
// that may be split across data chunks
|
|
1364
|
+
const stdoutDecoder = new StringDecoder('utf8');
|
|
1365
|
+
const stderrDecoder = new StringDecoder('utf8');
|
|
1366
|
+
let stdoutBuffer = '';
|
|
1367
|
+
let stderrBuffer = '';
|
|
1368
|
+
|
|
1128
1369
|
proc.stdout.on('data', (data) => {
|
|
1129
|
-
|
|
1130
|
-
|
|
1370
|
+
// Decode properly handles partial multi-byte characters
|
|
1371
|
+
stdoutBuffer += stdoutDecoder.write(data);
|
|
1372
|
+
const lines = stdoutBuffer.split('\n');
|
|
1373
|
+
// Keep the last incomplete line in the buffer
|
|
1374
|
+
stdoutBuffer = lines.pop();
|
|
1131
1375
|
lines.forEach(line => {
|
|
1132
1376
|
if (line.trim()) {
|
|
1133
1377
|
this.addOutputLine(scriptName, line);
|
|
@@ -1136,8 +1380,11 @@ class ProcessManager {
|
|
|
1136
1380
|
});
|
|
1137
1381
|
|
|
1138
1382
|
proc.stderr.on('data', (data) => {
|
|
1139
|
-
|
|
1140
|
-
|
|
1383
|
+
// Decode properly handles partial multi-byte characters
|
|
1384
|
+
stderrBuffer += stderrDecoder.write(data);
|
|
1385
|
+
const lines = stderrBuffer.split('\n');
|
|
1386
|
+
// Keep the last incomplete line in the buffer
|
|
1387
|
+
stderrBuffer = lines.pop();
|
|
1141
1388
|
lines.forEach(line => {
|
|
1142
1389
|
if (line.trim()) {
|
|
1143
1390
|
this.addOutputLine(scriptName, line);
|
|
@@ -1146,6 +1393,16 @@ class ProcessManager {
|
|
|
1146
1393
|
});
|
|
1147
1394
|
|
|
1148
1395
|
proc.on('exit', (code) => {
|
|
1396
|
+
// Flush any remaining buffered content from the decoders
|
|
1397
|
+
const remainingStdout = stdoutBuffer + stdoutDecoder.end();
|
|
1398
|
+
const remainingStderr = stderrBuffer + stderrDecoder.end();
|
|
1399
|
+
if (remainingStdout.trim()) {
|
|
1400
|
+
this.addOutputLine(scriptName, remainingStdout);
|
|
1401
|
+
}
|
|
1402
|
+
if (remainingStderr.trim()) {
|
|
1403
|
+
this.addOutputLine(scriptName, remainingStderr);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1149
1406
|
const status = code === 0 ? 'exited' : 'crashed';
|
|
1150
1407
|
this.processes.set(scriptName, { status, exitCode: code });
|
|
1151
1408
|
this.addOutputLine(scriptName, `Process exited with code ${code}`);
|
|
@@ -1156,22 +1413,25 @@ class ProcessManager {
|
|
|
1156
1413
|
}
|
|
1157
1414
|
|
|
1158
1415
|
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
|
-
});
|
|
1416
|
+
// Don't write if database is closed
|
|
1417
|
+
if (this.destroyed) return;
|
|
1169
1418
|
|
|
1170
|
-
//
|
|
1171
|
-
|
|
1172
|
-
this.outputLines.shift();
|
|
1173
|
-
}
|
|
1419
|
+
// Detect colors in the text for efficient SQL filtering
|
|
1420
|
+
const colorBitmask = detectColorBitmask(text);
|
|
1174
1421
|
|
|
1422
|
+
// Always store the output line, even when paused
|
|
1423
|
+
// Insert into SQLite database
|
|
1424
|
+
this.outputDb.insert(
|
|
1425
|
+
++this.totalLinesReceived,
|
|
1426
|
+
processName,
|
|
1427
|
+
text,
|
|
1428
|
+
Date.now(),
|
|
1429
|
+
colorBitmask
|
|
1430
|
+
);
|
|
1431
|
+
|
|
1432
|
+
// Mark that new lines are available
|
|
1433
|
+
this.hasNewLines = true;
|
|
1434
|
+
|
|
1175
1435
|
// Only render if not paused - this prevents new output from appearing
|
|
1176
1436
|
// when the user is reviewing history
|
|
1177
1437
|
if (!this.isPaused) {
|
|
@@ -1820,19 +2080,38 @@ class ProcessManager {
|
|
|
1820
2080
|
// Toggle visibility of selected process in focused pane
|
|
1821
2081
|
toggleProcessVisibility() {
|
|
1822
2082
|
const scriptName = this.scripts[this.selectedIndex]?.name;
|
|
1823
|
-
if (!scriptName || !this.focusedPaneId)
|
|
2083
|
+
if (!scriptName || !this.focusedPaneId) {
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
1824
2086
|
|
|
1825
2087
|
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
1826
|
-
if (!pane)
|
|
2088
|
+
if (!pane) {
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
1827
2091
|
|
|
1828
|
-
// Initialize
|
|
2092
|
+
// Initialize arrays if needed
|
|
1829
2093
|
if (!pane.hidden) pane.hidden = [];
|
|
2094
|
+
if (!pane.processes) pane.processes = [];
|
|
1830
2095
|
|
|
1831
|
-
//
|
|
1832
|
-
|
|
1833
|
-
|
|
2096
|
+
// Check current visibility
|
|
2097
|
+
const isCurrentlyVisible = this.isProcessVisibleInPane(scriptName, pane);
|
|
2098
|
+
|
|
2099
|
+
if (isCurrentlyVisible) {
|
|
2100
|
+
// Hide it - add to hidden list
|
|
2101
|
+
if (!pane.hidden.includes(scriptName)) {
|
|
2102
|
+
pane.hidden.push(scriptName);
|
|
2103
|
+
}
|
|
2104
|
+
// Also remove from processes if it's there
|
|
2105
|
+
if (pane.processes.includes(scriptName)) {
|
|
2106
|
+
pane.processes = pane.processes.filter(p => p !== scriptName);
|
|
2107
|
+
}
|
|
1834
2108
|
} else {
|
|
1835
|
-
|
|
2109
|
+
// Show it - remove from hidden list
|
|
2110
|
+
pane.hidden = pane.hidden.filter(p => p !== scriptName);
|
|
2111
|
+
// If pane has a process filter, add this process to it
|
|
2112
|
+
if (pane.processes.length > 0 && !pane.processes.includes(scriptName)) {
|
|
2113
|
+
pane.processes.push(scriptName);
|
|
2114
|
+
}
|
|
1836
2115
|
}
|
|
1837
2116
|
|
|
1838
2117
|
this.savePaneLayout();
|
|
@@ -1846,38 +2125,46 @@ class ProcessManager {
|
|
|
1846
2125
|
debouncedSaveConfig(this.config);
|
|
1847
2126
|
}
|
|
1848
2127
|
|
|
1849
|
-
// Scroll the focused pane
|
|
2128
|
+
// Scroll the focused pane (virtual scrolling - adjusts offset into database)
|
|
1850
2129
|
scrollFocusedPane(direction) {
|
|
1851
2130
|
if (!this.focusedPaneId) return;
|
|
1852
2131
|
|
|
1853
|
-
const
|
|
1854
|
-
if (!
|
|
2132
|
+
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
2133
|
+
if (!pane) return;
|
|
1855
2134
|
|
|
1856
|
-
const
|
|
1857
|
-
const
|
|
1858
|
-
const
|
|
2135
|
+
const totalLines = this.outputDb.countForPane(pane);
|
|
2136
|
+
const visibleHeight = this.paneVisibleHeight.get(this.focusedPaneId) || 20;
|
|
2137
|
+
const currentOffset = this.paneScrollOffsets.get(this.focusedPaneId) || 0;
|
|
2138
|
+
const maxOffset = Math.max(0, totalLines - visibleHeight);
|
|
1859
2139
|
|
|
1860
|
-
let
|
|
2140
|
+
let newOffset = currentOffset;
|
|
1861
2141
|
|
|
1862
2142
|
if (direction === 'home') {
|
|
1863
|
-
|
|
2143
|
+
// Scroll to top (maximum offset from end)
|
|
2144
|
+
newOffset = maxOffset;
|
|
1864
2145
|
} else if (direction === 'end') {
|
|
1865
|
-
|
|
2146
|
+
// Scroll to bottom (offset 0 = most recent)
|
|
2147
|
+
newOffset = 0;
|
|
1866
2148
|
} else if (direction === 'pageup') {
|
|
1867
|
-
|
|
2149
|
+
newOffset = Math.min(maxOffset, currentOffset + visibleHeight);
|
|
1868
2150
|
} else if (direction === 'pagedown') {
|
|
1869
|
-
|
|
2151
|
+
newOffset = Math.max(0, currentOffset - visibleHeight);
|
|
2152
|
+
} else if (direction === 'up') {
|
|
2153
|
+
newOffset = Math.min(maxOffset, currentOffset + 1);
|
|
2154
|
+
} else if (direction === 'down') {
|
|
2155
|
+
newOffset = Math.max(0, currentOffset - 1);
|
|
1870
2156
|
}
|
|
1871
2157
|
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2158
|
+
// Only update if changed
|
|
2159
|
+
if (newOffset !== currentOffset) {
|
|
2160
|
+
this.paneScrollOffsets.set(this.focusedPaneId, newOffset);
|
|
2161
|
+
|
|
2162
|
+
// Auto-pause when manually scrolling (unless going to end)
|
|
2163
|
+
if (direction !== 'end' && !this.isPaused) {
|
|
2164
|
+
this.isPaused = true;
|
|
2165
|
+
this.updateStreamPauseState();
|
|
2166
|
+
}
|
|
2167
|
+
|
|
1881
2168
|
this.buildRunningUI();
|
|
1882
2169
|
}
|
|
1883
2170
|
}
|
|
@@ -2098,51 +2385,18 @@ class ProcessManager {
|
|
|
2098
2385
|
}
|
|
2099
2386
|
|
|
2100
2387
|
// Count horizontal splits (which reduce available height per pane)
|
|
2101
|
-
// Get output lines for a specific pane -
|
|
2388
|
+
// Get output lines for a specific pane - queries SQLite with filters
|
|
2102
2389
|
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
|
-
});
|
|
2390
|
+
return this.outputDb.queryForPane(pane);
|
|
2142
2391
|
}
|
|
2143
2392
|
|
|
2144
2393
|
buildSettingsUI() {
|
|
2145
2394
|
// Remove old containers - use destroyRecursively to clean up all children
|
|
2395
|
+
if (this.wipeContainer) {
|
|
2396
|
+
this.renderer.root.remove(this.wipeContainer);
|
|
2397
|
+
this.wipeContainer.destroyRecursively();
|
|
2398
|
+
this.wipeContainer = null;
|
|
2399
|
+
}
|
|
2146
2400
|
if (this.selectionContainer) {
|
|
2147
2401
|
this.renderer.root.remove(this.selectionContainer);
|
|
2148
2402
|
this.selectionContainer.destroyRecursively();
|
|
@@ -2539,6 +2793,11 @@ class ProcessManager {
|
|
|
2539
2793
|
// Ignore
|
|
2540
2794
|
}
|
|
2541
2795
|
}
|
|
2796
|
+
|
|
2797
|
+
// Close the SQLite database
|
|
2798
|
+
if (this.outputDb) {
|
|
2799
|
+
this.outputDb.close();
|
|
2800
|
+
}
|
|
2542
2801
|
}
|
|
2543
2802
|
|
|
2544
2803
|
executeCommand(scriptName) {
|
|
@@ -2793,8 +3052,56 @@ class ProcessManager {
|
|
|
2793
3052
|
// 'committing' and 'pushing' phases ignore input (busy)
|
|
2794
3053
|
}
|
|
2795
3054
|
|
|
3055
|
+
// Full screen wipe to clear artifacts (Ctrl+L)
|
|
3056
|
+
buildWipeScreen() {
|
|
3057
|
+
// Remove old containers
|
|
3058
|
+
if (this.selectionContainer) {
|
|
3059
|
+
this.renderer.root.remove(this.selectionContainer);
|
|
3060
|
+
this.selectionContainer.destroyRecursively();
|
|
3061
|
+
this.selectionContainer = null;
|
|
3062
|
+
}
|
|
3063
|
+
if (this.settingsContainer) {
|
|
3064
|
+
this.renderer.root.remove(this.settingsContainer);
|
|
3065
|
+
this.settingsContainer.destroyRecursively();
|
|
3066
|
+
this.settingsContainer = null;
|
|
3067
|
+
}
|
|
3068
|
+
if (this.runningContainer) {
|
|
3069
|
+
this.renderer.root.remove(this.runningContainer);
|
|
3070
|
+
this.runningContainer.destroyRecursively();
|
|
3071
|
+
this.runningContainer = null;
|
|
3072
|
+
this.outputBox = null;
|
|
3073
|
+
this.paneScrollBoxes.clear();
|
|
3074
|
+
this.paneOutputBoxes.clear();
|
|
3075
|
+
this.lineRenderables.clear();
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
// Create full-screen wipe with different background color
|
|
3079
|
+
const wipeContainer = new BoxRenderable(this.renderer, {
|
|
3080
|
+
id: 'wipe-container',
|
|
3081
|
+
width: '100%',
|
|
3082
|
+
height: '100%',
|
|
3083
|
+
backgroundColor: COLORS.bg,
|
|
3084
|
+
justifyContent: 'center',
|
|
3085
|
+
alignItems: 'center',
|
|
3086
|
+
});
|
|
3087
|
+
|
|
3088
|
+
const wipeText = new TextRenderable(this.renderer, {
|
|
3089
|
+
id: 'wipe-text',
|
|
3090
|
+
content: t`${fg(COLORS.textDim)('Clearing...')}`,
|
|
3091
|
+
});
|
|
3092
|
+
wipeContainer.add(wipeText);
|
|
3093
|
+
|
|
3094
|
+
this.renderer.root.add(wipeContainer);
|
|
3095
|
+
this.wipeContainer = wipeContainer;
|
|
3096
|
+
}
|
|
3097
|
+
|
|
2796
3098
|
buildSelectionUI() {
|
|
2797
3099
|
// Remove old containers if they exist - use destroyRecursively to clean up all children
|
|
3100
|
+
if (this.wipeContainer) {
|
|
3101
|
+
this.renderer.root.remove(this.wipeContainer);
|
|
3102
|
+
this.wipeContainer.destroyRecursively();
|
|
3103
|
+
this.wipeContainer = null;
|
|
3104
|
+
}
|
|
2798
3105
|
if (this.selectionContainer) {
|
|
2799
3106
|
this.renderer.root.remove(this.selectionContainer);
|
|
2800
3107
|
this.selectionContainer.destroyRecursively();
|
|
@@ -3080,7 +3387,9 @@ class ProcessManager {
|
|
|
3080
3387
|
|
|
3081
3388
|
getPerformanceString() {
|
|
3082
3389
|
const metrics = this.getPerformanceMetrics();
|
|
3083
|
-
|
|
3390
|
+
const rendererStats = this.renderer.getStats ? this.renderer.getStats() : null;
|
|
3391
|
+
const rendererFps = rendererStats ? rendererStats.fps : '?';
|
|
3392
|
+
return `${metrics.fps}fps ${metrics.avgRenderTime}ms r:${rendererFps}fps db:${this.outputDb.count()}`;
|
|
3084
3393
|
}
|
|
3085
3394
|
|
|
3086
3395
|
getProcessListContent() {
|
|
@@ -3219,109 +3528,79 @@ class ProcessManager {
|
|
|
3219
3528
|
}
|
|
3220
3529
|
}
|
|
3221
3530
|
|
|
3222
|
-
//
|
|
3223
|
-
if (this.
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
for (const [paneId, scrollBox] of this.paneScrollBoxes.entries()) {
|
|
3531
|
+
// Skip update if paused or no new lines
|
|
3532
|
+
if (this.isPaused || !this.hasNewLines) return;
|
|
3533
|
+
|
|
3534
|
+
// Clear the flag
|
|
3535
|
+
this.hasNewLines = false;
|
|
3536
|
+
|
|
3537
|
+
// Virtual scrolling update - only update visible lines from database
|
|
3538
|
+
if (this.paneOutputBoxes.size > 0) {
|
|
3539
|
+
for (const [paneId, outputBox] of this.paneOutputBoxes.entries()) {
|
|
3232
3540
|
const pane = findPaneById(this.paneRoot, paneId);
|
|
3233
|
-
if (!pane || !
|
|
3234
|
-
// ScrollBox invalid, need full rebuild
|
|
3541
|
+
if (!pane || !outputBox) {
|
|
3235
3542
|
this.buildRunningUI();
|
|
3236
3543
|
return;
|
|
3237
3544
|
}
|
|
3238
3545
|
|
|
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
|
-
}
|
|
3546
|
+
// Only update if at the bottom (offset=0)
|
|
3547
|
+
const scrollOffset = this.paneScrollOffsets.get(paneId) || 0;
|
|
3548
|
+
if (scrollOffset !== 0) continue;
|
|
3247
3549
|
|
|
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 });
|
|
3550
|
+
const visibleHeight = this.paneVisibleHeight.get(paneId) || 20;
|
|
3551
|
+
const lines = this.outputDb.queryVisible(pane, visibleHeight, 0);
|
|
3552
|
+
|
|
3553
|
+
// Skip if no lines or last line hasn't changed
|
|
3554
|
+
if (lines.length === 0) continue;
|
|
3555
|
+
const lastLineNum = lines[lines.length - 1].lineNumber;
|
|
3556
|
+
const prevLastLine = this.paneLineCount.get(paneId) || 0;
|
|
3557
|
+
if (lastLineNum === prevLastLine) continue;
|
|
3558
|
+
|
|
3559
|
+
// Check pane content cache - if first line ID matches, we can append
|
|
3560
|
+
const firstLineId = lines[0].id;
|
|
3561
|
+
const cache = this.paneContentCache.get(paneId);
|
|
3562
|
+
|
|
3563
|
+
let styledText;
|
|
3564
|
+
if (cache && cache.firstLineId === firstLineId && cache.lineCount === lines.length - 1) {
|
|
3565
|
+
// Only 1 new line at the end - append to cached chunks
|
|
3566
|
+
const newLine = lines[lines.length - 1];
|
|
3567
|
+
const newLineContent = this.formatOutputLine(newLine, lines.length - 1, false, -1, -1);
|
|
3568
|
+
const chunks = cache.chunks.concat(newLineContent.chunks);
|
|
3569
|
+
styledText = new StyledText(chunks);
|
|
3570
|
+
this.paneContentCache.set(paneId, { firstLineId, lineCount: lines.length, chunks });
|
|
3571
|
+
} else {
|
|
3572
|
+
// Full rebuild
|
|
3573
|
+
const chunks = [];
|
|
3574
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3575
|
+
const lineContent = this.formatOutputLine(lines[i], i, false, -1, -1);
|
|
3576
|
+
for (let j = 0; j < lineContent.chunks.length; j++) {
|
|
3577
|
+
chunks.push(lineContent.chunks[j]);
|
|
3322
3578
|
}
|
|
3323
3579
|
}
|
|
3580
|
+
styledText = new StyledText(chunks);
|
|
3581
|
+
this.paneContentCache.set(paneId, { firstLineId, lineCount: lines.length, chunks });
|
|
3324
3582
|
}
|
|
3583
|
+
|
|
3584
|
+
// Destroy old renderable and create new one to avoid artifacts
|
|
3585
|
+
const renderables = this.lineRenderables.get(paneId);
|
|
3586
|
+
if (renderables && renderables.length > 0) {
|
|
3587
|
+
outputBox.remove(renderables[0]);
|
|
3588
|
+
renderables[0].destroy();
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
// Create new TextRenderable
|
|
3592
|
+
const outputText = new TextRenderable(this.renderer, {
|
|
3593
|
+
id: `output-${paneId}`,
|
|
3594
|
+
content: styledText,
|
|
3595
|
+
bg: '#000000',
|
|
3596
|
+
width: '100%',
|
|
3597
|
+
});
|
|
3598
|
+
outputBox.add(outputText);
|
|
3599
|
+
this.lineRenderables.set(paneId, [outputText]);
|
|
3600
|
+
|
|
3601
|
+
// Track last rendered line
|
|
3602
|
+
this.paneLineCount.set(paneId, lastLineNum);
|
|
3603
|
+
}
|
|
3325
3604
|
} else {
|
|
3326
3605
|
// First time or no panes exist - do full rebuild
|
|
3327
3606
|
this.buildRunningUI();
|
|
@@ -3336,9 +3615,8 @@ class ProcessManager {
|
|
|
3336
3615
|
}
|
|
3337
3616
|
|
|
3338
3617
|
// Remove old process bar items
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
this.processBarContainer.remove(child);
|
|
3618
|
+
for (const child of this.processBarContainer.getChildren()) {
|
|
3619
|
+
this.processBarContainer.remove(child.id);
|
|
3342
3620
|
child.destroy();
|
|
3343
3621
|
}
|
|
3344
3622
|
|
|
@@ -3355,7 +3633,7 @@ class ProcessManager {
|
|
|
3355
3633
|
const isVisible = this.isProcessVisibleInPane(script.name, focusedPane);
|
|
3356
3634
|
const nameColor = isSelected ? COLORS.accent : (isVisible ? processColor : COLORS.textDim);
|
|
3357
3635
|
const numberColor = isVisible ? processColor : COLORS.textDim;
|
|
3358
|
-
const indicator = isSelected ? '
|
|
3636
|
+
const indicator = isSelected ? '❯' : ' ';
|
|
3359
3637
|
const bracketColor = isVisible ? processColor : COLORS.textDim;
|
|
3360
3638
|
|
|
3361
3639
|
const numberLabel = index < 9 ? `${index + 1}` : ' ';
|
|
@@ -3375,7 +3653,83 @@ class ProcessManager {
|
|
|
3375
3653
|
});
|
|
3376
3654
|
}
|
|
3377
3655
|
|
|
3378
|
-
//
|
|
3656
|
+
// Get cache key for current display settings
|
|
3657
|
+
getFormatCacheKey() {
|
|
3658
|
+
return `${this.showLineNumbers}-${this.showTimestamps}-${this.renderer.width}`;
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
// Format a single line for display (returns styled text chunks)
|
|
3662
|
+
formatOutputLine(line, index, inCopyMode, copySelStart, copySelEnd) {
|
|
3663
|
+
// Copy mode can't use cache (dynamic highlighting)
|
|
3664
|
+
if (inCopyMode) {
|
|
3665
|
+
return this.formatOutputLineCopyMode(line, index, copySelStart, copySelEnd);
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
// Can't cache if we're padding to width (width can vary)
|
|
3669
|
+
// Check cache only if no padding needed
|
|
3670
|
+
const cacheKey = this.getFormatCacheKey();
|
|
3671
|
+
if (cacheKey !== this.lineFormatCacheKey) {
|
|
3672
|
+
// Settings changed, clear cache
|
|
3673
|
+
this.lineFormatCache.clear();
|
|
3674
|
+
this.lineFormatCacheKey = cacheKey;
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
// Format line
|
|
3678
|
+
const processColor = this.processColors.get(line.process) || COLORS.text;
|
|
3679
|
+
const lineNumber = this.showLineNumbers ? String(line.lineNumber).padStart(4, ' ') : '';
|
|
3680
|
+
const timestamp = this.showTimestamps ? (line.timeString || (line.timeString = new Date(line.timestamp).toLocaleTimeString('en-US', { hour12: false }))) : '';
|
|
3681
|
+
|
|
3682
|
+
let result;
|
|
3683
|
+
if (this.showLineNumbers && this.showTimestamps) {
|
|
3684
|
+
result = t`${fg(COLORS.textDim)(lineNumber)} ${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${line.text}\n`;
|
|
3685
|
+
} else if (this.showLineNumbers) {
|
|
3686
|
+
result = t`${fg(COLORS.textDim)(lineNumber)} ${fg(processColor)(`[${line.process}]`)} ${line.text}\n`;
|
|
3687
|
+
} else if (this.showTimestamps) {
|
|
3688
|
+
result = t`${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${line.text}\n`;
|
|
3689
|
+
} else {
|
|
3690
|
+
result = t`${fg(processColor)(`[${line.process}]`)} ${line.text}\n`;
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
return result;
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
// Format line in copy mode (no caching - dynamic highlighting)
|
|
3697
|
+
formatOutputLineCopyMode(line, index, copySelStart, copySelEnd) {
|
|
3698
|
+
const processColor = this.processColors.get(line.process) || COLORS.text;
|
|
3699
|
+
const lineNumber = this.showLineNumbers ? String(line.lineNumber).padStart(4, ' ') : '';
|
|
3700
|
+
const timestamp = this.showTimestamps ? (line.timeString || (line.timeString = new Date(line.timestamp).toLocaleTimeString('en-US', { hour12: false }))) : '';
|
|
3701
|
+
|
|
3702
|
+
const isCursorLine = index === this.copyModeCursor;
|
|
3703
|
+
const isSelectedLine = index >= copySelStart && index <= copySelEnd;
|
|
3704
|
+
|
|
3705
|
+
const marker = isCursorLine ? fg(COLORS.copyCursorText)('\u25b6 ') : ' ';
|
|
3706
|
+
let textColor, procColor, dimColor;
|
|
3707
|
+
if (isCursorLine) {
|
|
3708
|
+
textColor = COLORS.copyCursorText;
|
|
3709
|
+
procColor = COLORS.copyCursorText;
|
|
3710
|
+
dimColor = COLORS.copySelectText;
|
|
3711
|
+
} else if (isSelectedLine) {
|
|
3712
|
+
textColor = COLORS.copySelectText;
|
|
3713
|
+
procColor = processColor;
|
|
3714
|
+
dimColor = COLORS.textDim;
|
|
3715
|
+
} else {
|
|
3716
|
+
textColor = COLORS.textDim;
|
|
3717
|
+
procColor = COLORS.textDim;
|
|
3718
|
+
dimColor = COLORS.textDim;
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3721
|
+
if (this.showLineNumbers && this.showTimestamps) {
|
|
3722
|
+
return t`${marker}${fg(dimColor)(lineNumber)} ${fg(dimColor)(`[${timestamp}]`)} ${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}\n`;
|
|
3723
|
+
} else if (this.showLineNumbers) {
|
|
3724
|
+
return t`${marker}${fg(dimColor)(lineNumber)} ${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}\n`;
|
|
3725
|
+
} else if (this.showTimestamps) {
|
|
3726
|
+
return t`${marker}${fg(dimColor)(`[${timestamp}]`)} ${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}\n`;
|
|
3727
|
+
} else {
|
|
3728
|
+
return t`${marker}${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}\n`;
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3732
|
+
// Build a single pane's output area - uses single TextRenderable for efficiency
|
|
3379
3733
|
buildPaneOutput(pane, container, height) {
|
|
3380
3734
|
const isFocused = pane.id === this.focusedPaneId;
|
|
3381
3735
|
const lines = this.getOutputLinesForPane(pane);
|
|
@@ -3385,90 +3739,34 @@ class ProcessManager {
|
|
|
3385
3739
|
const maxLines = this.isPaused ? lines.length : Math.min(lines.length, height || 50);
|
|
3386
3740
|
const linesToShow = lines.slice(-maxLines);
|
|
3387
3741
|
|
|
3388
|
-
// Initialize renderable pool for this pane
|
|
3389
|
-
const renderables = [];
|
|
3390
|
-
this.lineRenderables.set(pane.id, renderables);
|
|
3391
|
-
|
|
3392
3742
|
// Determine copy mode selection range for this pane
|
|
3393
3743
|
const inCopyMode = this.isCopyMode && isFocused;
|
|
3394
3744
|
let copySelStart = -1;
|
|
3395
3745
|
let copySelEnd = -1;
|
|
3396
|
-
if (inCopyMode) {
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
copySelEnd = Math.max(this.copyModeAnchor, this.copyModeCursor);
|
|
3400
|
-
}
|
|
3746
|
+
if (inCopyMode && this.copyModeAnchor !== null) {
|
|
3747
|
+
copySelStart = Math.min(this.copyModeAnchor, this.copyModeCursor);
|
|
3748
|
+
copySelEnd = Math.max(this.copyModeAnchor, this.copyModeCursor);
|
|
3401
3749
|
}
|
|
3402
3750
|
|
|
3403
|
-
//
|
|
3751
|
+
// Build all lines as a single styled text (much more efficient than one renderable per line)
|
|
3752
|
+
const chunks = [];
|
|
3404
3753
|
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);
|
|
3754
|
+
const lineContent = this.formatOutputLine(linesToShow[i], i, inCopyMode, copySelStart, copySelEnd);
|
|
3755
|
+
chunks.push(...lineContent.chunks);
|
|
3470
3756
|
}
|
|
3471
3757
|
|
|
3758
|
+
// Create single TextRenderable with all content
|
|
3759
|
+
const outputText = new TextRenderable(this.renderer, {
|
|
3760
|
+
id: `output-${pane.id}`,
|
|
3761
|
+
content: new StyledText(chunks),
|
|
3762
|
+
bg: '#000000',
|
|
3763
|
+
});
|
|
3764
|
+
|
|
3765
|
+
container.add(outputText);
|
|
3766
|
+
|
|
3767
|
+
// Store reference for incremental updates
|
|
3768
|
+
this.lineRenderables.set(pane.id, [outputText]);
|
|
3769
|
+
|
|
3472
3770
|
// Track last rendered line number
|
|
3473
3771
|
if (linesToShow.length > 0) {
|
|
3474
3772
|
this.paneLineCount.set(pane.id, linesToShow[linesToShow.length - 1].lineNumber);
|
|
@@ -3488,7 +3786,7 @@ class ProcessManager {
|
|
|
3488
3786
|
}
|
|
3489
3787
|
}
|
|
3490
3788
|
|
|
3491
|
-
// Build a pane panel with title bar
|
|
3789
|
+
// Build a pane panel with title bar - uses virtual scrolling (no ScrollBox)
|
|
3492
3790
|
buildPanePanel(pane, flexGrow = 1, availableHeight = null) {
|
|
3493
3791
|
const isFocused = pane.id === this.focusedPaneId;
|
|
3494
3792
|
const borderColor = isFocused ? COLORS.borderFocused : COLORS.border;
|
|
@@ -3504,14 +3802,20 @@ class ProcessManager {
|
|
|
3504
3802
|
const filterLabel = pane.filter ? ` /${pane.filter}` : '';
|
|
3505
3803
|
const namingInputLabel = (isFocused && this.isNamingMode) ? `Name: ${this.namingModeText}_` : '';
|
|
3506
3804
|
const filterInputLabel = (isFocused && this.isFilterMode) ? `/${pane.filter || ''}_` : '';
|
|
3507
|
-
|
|
3805
|
+
|
|
3806
|
+
// Show scroll position indicator when paused
|
|
3807
|
+
const scrollOffset = this.paneScrollOffsets.get(pane.id) || 0;
|
|
3808
|
+
const totalLines = this.outputDb.countForPane(pane);
|
|
3809
|
+
const scrollIndicator = (this.isPaused && scrollOffset > 0) ? ` [${scrollOffset}↑]` : '';
|
|
3810
|
+
|
|
3811
|
+
const title = ` ${focusLabel}${namingInputLabel || processLabel}${hiddenLabel}${filterInputLabel || filterLabel}${scrollIndicator} `;
|
|
3508
3812
|
|
|
3509
3813
|
const paneContainer = new BoxRenderable(this.renderer, {
|
|
3510
3814
|
id: `pane-${pane.id}`,
|
|
3511
3815
|
flexDirection: 'column',
|
|
3512
3816
|
flexGrow: flexGrow,
|
|
3513
|
-
flexShrink: 0,
|
|
3514
|
-
flexBasis: 0,
|
|
3817
|
+
flexShrink: 0,
|
|
3818
|
+
flexBasis: 0,
|
|
3515
3819
|
border: true,
|
|
3516
3820
|
borderStyle: 'rounded',
|
|
3517
3821
|
borderColor: borderColor,
|
|
@@ -3519,63 +3823,81 @@ class ProcessManager {
|
|
|
3519
3823
|
titleAlignment: 'left',
|
|
3520
3824
|
padding: 0,
|
|
3521
3825
|
overflow: 'hidden',
|
|
3522
|
-
backgroundColor: '#000000',
|
|
3826
|
+
backgroundColor: '#000000',
|
|
3523
3827
|
});
|
|
3524
3828
|
|
|
3525
|
-
//
|
|
3526
|
-
const
|
|
3829
|
+
// Calculate visible height (minus border)
|
|
3830
|
+
const visibleHeight = availableHeight ? Math.max(5, availableHeight - 2) : Math.max(5, this.renderer.height - 6);
|
|
3527
3831
|
|
|
3528
|
-
|
|
3832
|
+
// Store visible height for scroll calculations
|
|
3833
|
+
this.paneVisibleHeight.set(pane.id, visibleHeight);
|
|
3834
|
+
|
|
3835
|
+
// Create output container (no ScrollBox - we handle scrolling virtually)
|
|
3836
|
+
const outputBox = new BoxRenderable(this.renderer, {
|
|
3529
3837
|
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
|
-
},
|
|
3838
|
+
flexDirection: 'column',
|
|
3839
|
+
flexGrow: 1,
|
|
3840
|
+
paddingLeft: 1,
|
|
3841
|
+
backgroundColor: '#000000',
|
|
3842
|
+
overflow: 'hidden',
|
|
3843
|
+
shouldFill: true,
|
|
3547
3844
|
});
|
|
3548
3845
|
|
|
3549
|
-
//
|
|
3550
|
-
|
|
3551
|
-
|
|
3846
|
+
// Query only the visible lines from database
|
|
3847
|
+
const lines = this.outputDb.queryVisible(pane, visibleHeight, scrollOffset);
|
|
3848
|
+
|
|
3849
|
+
// Build content for visible lines
|
|
3850
|
+
this.buildPaneOutputVirtual(pane, outputBox, lines, visibleHeight);
|
|
3851
|
+
|
|
3852
|
+
// Store reference to output box for updates
|
|
3853
|
+
this.paneOutputBoxes.set(pane.id, outputBox);
|
|
3854
|
+
|
|
3855
|
+
// Track the last line number for incremental updates
|
|
3856
|
+
if (lines.length > 0) {
|
|
3857
|
+
this.paneLineCount.set(pane.id, lines[lines.length - 1].lineNumber);
|
|
3552
3858
|
}
|
|
3553
3859
|
|
|
3554
|
-
|
|
3555
|
-
|
|
3860
|
+
paneContainer.add(outputBox);
|
|
3861
|
+
return paneContainer;
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3864
|
+
// Build pane output with virtual scrolling - only renders visible lines
|
|
3865
|
+
buildPaneOutputVirtual(pane, container, lines, visibleHeight) {
|
|
3866
|
+
const isFocused = pane.id === this.focusedPaneId;
|
|
3556
3867
|
|
|
3557
|
-
|
|
3868
|
+
// Determine copy mode selection range
|
|
3869
|
+
const inCopyMode = this.isCopyMode && isFocused;
|
|
3870
|
+
let copySelStart = -1;
|
|
3871
|
+
let copySelEnd = -1;
|
|
3872
|
+
if (inCopyMode && this.copyModeAnchor !== null) {
|
|
3873
|
+
copySelStart = Math.min(this.copyModeAnchor, this.copyModeCursor);
|
|
3874
|
+
copySelEnd = Math.max(this.copyModeAnchor, this.copyModeCursor);
|
|
3875
|
+
}
|
|
3558
3876
|
|
|
3559
|
-
//
|
|
3560
|
-
const
|
|
3561
|
-
|
|
3562
|
-
this.
|
|
3877
|
+
// Build all visible lines as single styled text
|
|
3878
|
+
const chunks = [];
|
|
3879
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3880
|
+
const lineContent = this.formatOutputLine(lines[i], i, inCopyMode, copySelStart, copySelEnd);
|
|
3881
|
+
chunks.push(...lineContent.chunks);
|
|
3563
3882
|
}
|
|
3564
3883
|
|
|
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
|
-
}
|
|
3884
|
+
// Fill remaining visible area with blank lines to clear artifacts
|
|
3885
|
+
for (let i = lines.length; i < visibleHeight; i++) {
|
|
3886
|
+
chunks.push({ text: '\n' });
|
|
3575
3887
|
}
|
|
3576
3888
|
|
|
3577
|
-
|
|
3578
|
-
|
|
3889
|
+
// Create single TextRenderable with all visible content
|
|
3890
|
+
const outputText = new TextRenderable(this.renderer, {
|
|
3891
|
+
id: `output-${pane.id}`,
|
|
3892
|
+
content: new StyledText(chunks),
|
|
3893
|
+
bg: '#000000',
|
|
3894
|
+
width: '100%',
|
|
3895
|
+
});
|
|
3896
|
+
|
|
3897
|
+
container.add(outputText);
|
|
3898
|
+
|
|
3899
|
+
// Store reference for updates
|
|
3900
|
+
this.lineRenderables.set(pane.id, [outputText]);
|
|
3579
3901
|
}
|
|
3580
3902
|
|
|
3581
3903
|
// Recursively build the pane layout, passing available height down
|
|
@@ -4162,6 +4484,11 @@ class ProcessManager {
|
|
|
4162
4484
|
}
|
|
4163
4485
|
|
|
4164
4486
|
// Remove old containers if they exist - use destroyRecursively to clean up all children
|
|
4487
|
+
if (this.wipeContainer) {
|
|
4488
|
+
this.renderer.root.remove(this.wipeContainer);
|
|
4489
|
+
this.wipeContainer.destroyRecursively();
|
|
4490
|
+
this.wipeContainer = null;
|
|
4491
|
+
}
|
|
4165
4492
|
if (this.selectionContainer) {
|
|
4166
4493
|
this.renderer.root.remove(this.selectionContainer);
|
|
4167
4494
|
this.selectionContainer.destroyRecursively();
|
|
@@ -4221,7 +4548,7 @@ class ProcessManager {
|
|
|
4221
4548
|
const isVisible = this.isProcessVisibleInPane(script.name, focusedPane);
|
|
4222
4549
|
const nameColor = isSelected ? COLORS.accent : (isVisible ? processColor : COLORS.textDim);
|
|
4223
4550
|
const numberColor = isVisible ? processColor : COLORS.textDim;
|
|
4224
|
-
const indicator = isSelected ? '
|
|
4551
|
+
const indicator = isSelected ? '❯' : ' ';
|
|
4225
4552
|
const bracketColor = isVisible ? processColor : COLORS.textDim;
|
|
4226
4553
|
|
|
4227
4554
|
// Show number for first 9 processes
|
|
@@ -4532,7 +4859,9 @@ async function main() {
|
|
|
4532
4859
|
process.exit(1);
|
|
4533
4860
|
}
|
|
4534
4861
|
|
|
4535
|
-
const renderer = await createCliRenderer(
|
|
4862
|
+
const renderer = await createCliRenderer({
|
|
4863
|
+
backgroundColor: '#000000',
|
|
4864
|
+
});
|
|
4536
4865
|
renderer.start(); // Start the automatic render loop
|
|
4537
4866
|
const manager = new ProcessManager(renderer, scripts);
|
|
4538
4867
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "startall",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.26",
|
|
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",
|