startall 0.0.18 → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/index.js +389 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,7 @@ Traditional solutions fall short:
|
|
|
46
46
|
- **Persistent layouts**: Your pane configuration is saved between sessions
|
|
47
47
|
- **Process-specific views**: Show/hide specific processes in each pane
|
|
48
48
|
- **Colored output**: Each process gets unique color-coded output
|
|
49
|
+
- **Copy mode**: Select and copy output lines to clipboard (`y`)
|
|
49
50
|
- **Pause/resume**: Freeze output to review logs (`p`)
|
|
50
51
|
- **Scrollable history**: 1000-line buffer with mouse wheel support
|
|
51
52
|
- **Enhanced navigation**: Home/End/PageUp/PageDown keys
|
|
@@ -121,6 +122,15 @@ That's it! The TUI will:
|
|
|
121
122
|
- `Shift+Tab` - Previous pane
|
|
122
123
|
- `n` - Name current pane
|
|
123
124
|
|
|
125
|
+
*Copy Mode:*
|
|
126
|
+
- `y` - Enter copy mode (select text to copy to clipboard)
|
|
127
|
+
- `↑`/`↓` or `k`/`j` - Move cursor
|
|
128
|
+
- `Home`/`End` or `g`/`G` - Jump to first/last line
|
|
129
|
+
- `Page Up`/`Page Down` - Scroll by page
|
|
130
|
+
- `Space` - Start/clear selection from cursor
|
|
131
|
+
- `Enter` or `y` - Copy selected lines (or current line) to clipboard
|
|
132
|
+
- `Esc` or `q` - Exit copy mode
|
|
133
|
+
|
|
124
134
|
*Filtering & View:*
|
|
125
135
|
- `/` - Enter text filter mode
|
|
126
136
|
- `c` - Cycle color filter (red/yellow/green/blue/cyan/magenta/none)
|
package/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { createCliRenderer, TextRenderable, BoxRenderable, ScrollBoxRenderable, t, fg } from '@opentui/core';
|
|
4
|
-
import { spawn } from 'child_process';
|
|
5
|
-
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
4
|
+
import { spawn, execSync, spawnSync } from 'child_process';
|
|
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';
|
|
@@ -261,6 +261,76 @@ function deserializePaneTree(data) {
|
|
|
261
261
|
};
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
// Copy text to system clipboard
|
|
265
|
+
// Tries platform-specific commands first (most reliable), then OSC 52 as fallback
|
|
266
|
+
function copyToClipboard(text) {
|
|
267
|
+
let copied = false;
|
|
268
|
+
|
|
269
|
+
// Try platform-specific clipboard commands first (most reliable)
|
|
270
|
+
try {
|
|
271
|
+
const platform = process.platform;
|
|
272
|
+
if (platform === 'win32') {
|
|
273
|
+
// PowerShell Set-Clipboard handles unicode and doesn't add trailing newline like clip.exe
|
|
274
|
+
const result = spawnSync('powershell', ['-NoProfile', '-Command', 'Set-Clipboard -Value $input'], {
|
|
275
|
+
input: text,
|
|
276
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
277
|
+
timeout: 3000,
|
|
278
|
+
windowsHide: true,
|
|
279
|
+
});
|
|
280
|
+
if (result.status === 0) copied = true;
|
|
281
|
+
else {
|
|
282
|
+
// Fallback to clip.exe
|
|
283
|
+
const clipResult = spawnSync('clip', [], {
|
|
284
|
+
input: text,
|
|
285
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
286
|
+
timeout: 3000,
|
|
287
|
+
windowsHide: true,
|
|
288
|
+
});
|
|
289
|
+
if (clipResult.status === 0) copied = true;
|
|
290
|
+
}
|
|
291
|
+
} else if (platform === 'darwin') {
|
|
292
|
+
const result = spawnSync('pbcopy', [], {
|
|
293
|
+
input: text,
|
|
294
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
295
|
+
timeout: 3000,
|
|
296
|
+
});
|
|
297
|
+
if (result.status === 0) copied = true;
|
|
298
|
+
} else {
|
|
299
|
+
// Linux - try xclip, then xsel, then wl-copy (Wayland)
|
|
300
|
+
for (const cmd of [
|
|
301
|
+
['xclip', ['-selection', 'clipboard']],
|
|
302
|
+
['xsel', ['--clipboard', '--input']],
|
|
303
|
+
['wl-copy', []],
|
|
304
|
+
]) {
|
|
305
|
+
try {
|
|
306
|
+
const result = spawnSync(cmd[0], cmd[1], {
|
|
307
|
+
input: text,
|
|
308
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
309
|
+
timeout: 3000,
|
|
310
|
+
});
|
|
311
|
+
if (result.status === 0) { copied = true; break; }
|
|
312
|
+
} catch { /* try next */ }
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} catch {
|
|
316
|
+
// Platform commands unavailable
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Fallback: OSC 52 escape sequence (works in many modern terminals)
|
|
320
|
+
// Write directly to fd to bypass any buffering from the TUI renderer
|
|
321
|
+
if (!copied) {
|
|
322
|
+
try {
|
|
323
|
+
const encoded = Buffer.from(text).toString('base64');
|
|
324
|
+
const osc = `\x1b]52;c;${encoded}\x1b\\`;
|
|
325
|
+
writeSync(1, osc);
|
|
326
|
+
} catch {
|
|
327
|
+
// Nothing more we can do
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return copied;
|
|
332
|
+
}
|
|
333
|
+
|
|
264
334
|
// Color palette (inspired by Tokyo Night theme)
|
|
265
335
|
const COLORS = {
|
|
266
336
|
border: '#3b4261',
|
|
@@ -276,6 +346,11 @@ const COLORS = {
|
|
|
276
346
|
warning: '#e0af68',
|
|
277
347
|
cyan: '#7dcfff',
|
|
278
348
|
magenta: '#bb9af7',
|
|
349
|
+
// Copy mode colors (high contrast for visibility)
|
|
350
|
+
copyCursorBg: '#3d59a1', // Bright blue bg for cursor line
|
|
351
|
+
copySelectBg: '#2a3a6e', // Medium blue bg for selected range
|
|
352
|
+
copyCursorText: '#ffffff', // White text on cursor line
|
|
353
|
+
copySelectText: '#c0caf5', // Light text on selected lines
|
|
279
354
|
};
|
|
280
355
|
|
|
281
356
|
// Match string against pattern with wildcard support
|
|
@@ -428,6 +503,15 @@ class ProcessManager {
|
|
|
428
503
|
this.paneScrollBoxes = new Map(); // Store ScrollBox references per pane ID
|
|
429
504
|
this.paneFilterState = new Map(); // Track filter state per pane to detect changes
|
|
430
505
|
this.paneLineCount = new Map(); // Track how many lines we've rendered per pane
|
|
506
|
+
this.uiJustRebuilt = false; // Flag to skip redundant render after buildRunningUI
|
|
507
|
+
|
|
508
|
+
// Copy mode state (select text to copy)
|
|
509
|
+
this.isCopyMode = false; // Whether in copy/select mode
|
|
510
|
+
this.copyModeCursor = 0; // Current cursor line index within visible lines
|
|
511
|
+
this.copyModeAnchor = null; // Selection anchor (null = no selection started, number = anchor line index)
|
|
512
|
+
this.copyModeWasPaused = false; // Whether output was already paused before entering copy mode
|
|
513
|
+
this.copyFeedbackMessage = ''; // Temporary feedback message after copying
|
|
514
|
+
this.copyFeedbackTimer = null; // Timer to clear feedback message
|
|
431
515
|
|
|
432
516
|
// Assign colors to each script
|
|
433
517
|
this.processColors = new Map();
|
|
@@ -613,6 +697,10 @@ class ProcessManager {
|
|
|
613
697
|
if (pane) pane.filter = (pane.filter || '') + keyName;
|
|
614
698
|
this.buildRunningUI(); // Update UI to show filter change
|
|
615
699
|
}
|
|
700
|
+
}
|
|
701
|
+
// If in copy mode, handle copy mode input
|
|
702
|
+
else if (this.isCopyMode) {
|
|
703
|
+
this.handleCopyModeInput(keyName, keyEvent);
|
|
616
704
|
} else {
|
|
617
705
|
// Normal mode - handle commands
|
|
618
706
|
if (keyName === 'q') {
|
|
@@ -766,6 +854,9 @@ class ProcessManager {
|
|
|
766
854
|
this.showRunCommandModal = true;
|
|
767
855
|
this.runCommandModalIndex = 0;
|
|
768
856
|
this.buildRunningUI();
|
|
857
|
+
} else if (keyName === 'y') {
|
|
858
|
+
// Enter copy mode (select text to copy)
|
|
859
|
+
this.enterCopyMode();
|
|
769
860
|
} else if (keyName && keyName.length === 1 && !keyEvent.ctrl && !keyEvent.meta && !keyEvent.shift) {
|
|
770
861
|
// Check if this key is a custom shortcut
|
|
771
862
|
const shortcuts = this.config.shortcuts || {};
|
|
@@ -1578,6 +1669,204 @@ class ProcessManager {
|
|
|
1578
1669
|
}
|
|
1579
1670
|
}
|
|
1580
1671
|
|
|
1672
|
+
// Enter copy mode for the focused pane
|
|
1673
|
+
enterCopyMode() {
|
|
1674
|
+
if (!this.focusedPaneId) return;
|
|
1675
|
+
|
|
1676
|
+
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
1677
|
+
if (!pane) return;
|
|
1678
|
+
|
|
1679
|
+
const lines = this.getOutputLinesForPane(pane);
|
|
1680
|
+
if (lines.length === 0) return;
|
|
1681
|
+
|
|
1682
|
+
this.isCopyMode = true;
|
|
1683
|
+
this.copyModeAnchor = null;
|
|
1684
|
+
this.copyModeCursor = lines.length - 1; // Start at the last line
|
|
1685
|
+
|
|
1686
|
+
// Auto-pause output so lines don't shift while selecting
|
|
1687
|
+
this.copyModeWasPaused = this.isPaused;
|
|
1688
|
+
if (!this.isPaused) {
|
|
1689
|
+
this.isPaused = true;
|
|
1690
|
+
this.updateStreamPauseState();
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// Ensure cursor is visible — save scroll position to show the last line
|
|
1694
|
+
// Use MAX_SAFE_INTEGER so buildRunningUI scrolls to bottom where cursor starts
|
|
1695
|
+
this.paneScrollPositions.set(this.focusedPaneId, { x: 0, y: Number.MAX_SAFE_INTEGER });
|
|
1696
|
+
|
|
1697
|
+
this.buildRunningUI();
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// Exit copy mode
|
|
1701
|
+
exitCopyMode() {
|
|
1702
|
+
this.isCopyMode = false;
|
|
1703
|
+
this.copyModeCursor = 0;
|
|
1704
|
+
this.copyModeAnchor = null;
|
|
1705
|
+
|
|
1706
|
+
// Restore pause state
|
|
1707
|
+
if (!this.copyModeWasPaused) {
|
|
1708
|
+
this.isPaused = false;
|
|
1709
|
+
this.updateStreamPauseState();
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
this.buildRunningUI();
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// Handle keyboard input in copy mode
|
|
1716
|
+
handleCopyModeInput(keyName, keyEvent) {
|
|
1717
|
+
if (!this.focusedPaneId) {
|
|
1718
|
+
this.exitCopyMode();
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
1723
|
+
if (!pane) {
|
|
1724
|
+
this.exitCopyMode();
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
const lines = this.getOutputLinesForPane(pane);
|
|
1729
|
+
const lineCount = lines.length;
|
|
1730
|
+
if (lineCount === 0) {
|
|
1731
|
+
this.exitCopyMode();
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
if (keyName === 'escape' || keyName === 'q') {
|
|
1736
|
+
this.exitCopyMode();
|
|
1737
|
+
} else if (keyName === 'up' || keyName === 'k') {
|
|
1738
|
+
this.copyModeCursor = Math.max(0, this.copyModeCursor - 1);
|
|
1739
|
+
this.scrollCopyModeCursorIntoView();
|
|
1740
|
+
this.buildRunningUI();
|
|
1741
|
+
} else if (keyName === 'down' || keyName === 'j') {
|
|
1742
|
+
this.copyModeCursor = Math.min(lineCount - 1, this.copyModeCursor + 1);
|
|
1743
|
+
this.scrollCopyModeCursorIntoView();
|
|
1744
|
+
this.buildRunningUI();
|
|
1745
|
+
} else if (keyName === 'home' || (keyName === 'g' && !keyEvent.shift)) {
|
|
1746
|
+
this.copyModeCursor = 0;
|
|
1747
|
+
this.scrollCopyModeCursorIntoView();
|
|
1748
|
+
this.buildRunningUI();
|
|
1749
|
+
} else if (keyName === 'end' || keyName === 'G') {
|
|
1750
|
+
this.copyModeCursor = lineCount - 1;
|
|
1751
|
+
this.scrollCopyModeCursorIntoView();
|
|
1752
|
+
this.buildRunningUI();
|
|
1753
|
+
} else if (keyName === 'pageup') {
|
|
1754
|
+
const scrollBox = this.paneScrollBoxes.get(this.focusedPaneId);
|
|
1755
|
+
const pageSize = (scrollBox?.height || 20);
|
|
1756
|
+
this.copyModeCursor = Math.max(0, this.copyModeCursor - pageSize);
|
|
1757
|
+
this.scrollCopyModeCursorIntoView();
|
|
1758
|
+
this.buildRunningUI();
|
|
1759
|
+
} else if (keyName === 'pagedown') {
|
|
1760
|
+
const scrollBox = this.paneScrollBoxes.get(this.focusedPaneId);
|
|
1761
|
+
const pageSize = (scrollBox?.height || 20);
|
|
1762
|
+
this.copyModeCursor = Math.min(lineCount - 1, this.copyModeCursor + pageSize);
|
|
1763
|
+
this.scrollCopyModeCursorIntoView();
|
|
1764
|
+
this.buildRunningUI();
|
|
1765
|
+
} else if (keyName === 'space') {
|
|
1766
|
+
// Toggle selection anchor
|
|
1767
|
+
if (this.copyModeAnchor === null) {
|
|
1768
|
+
// Start selection from current cursor position
|
|
1769
|
+
this.copyModeAnchor = this.copyModeCursor;
|
|
1770
|
+
} else {
|
|
1771
|
+
// Clear selection
|
|
1772
|
+
this.copyModeAnchor = null;
|
|
1773
|
+
}
|
|
1774
|
+
this.buildRunningUI();
|
|
1775
|
+
} else if (keyName === 'enter' || keyName === 'return' || keyName === 'y') {
|
|
1776
|
+
// Copy selected lines (or current line if no selection)
|
|
1777
|
+
this.copySelectedLines();
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
// Compute scroll position to keep cursor visible and save to paneScrollPositions.
|
|
1782
|
+
// buildRunningUI() will restore this position after rebuilding the scrollbox.
|
|
1783
|
+
scrollCopyModeCursorIntoView() {
|
|
1784
|
+
const cursorY = this.copyModeCursor;
|
|
1785
|
+
|
|
1786
|
+
// Get current scroll position and viewport size from either live scrollBox or saved state
|
|
1787
|
+
const scrollBox = this.paneScrollBoxes.get(this.focusedPaneId);
|
|
1788
|
+
const savedPos = this.paneScrollPositions.get(this.focusedPaneId);
|
|
1789
|
+
const viewportHeight = scrollBox?.height || 20;
|
|
1790
|
+
const currentScrollY = scrollBox?.scrollTop ?? savedPos?.y ?? 0;
|
|
1791
|
+
|
|
1792
|
+
let newY = currentScrollY;
|
|
1793
|
+
|
|
1794
|
+
// If cursor is above the viewport, scroll up to show it
|
|
1795
|
+
if (cursorY < currentScrollY) {
|
|
1796
|
+
newY = cursorY;
|
|
1797
|
+
}
|
|
1798
|
+
// If cursor is below the viewport, scroll down to show it
|
|
1799
|
+
else if (cursorY >= currentScrollY + viewportHeight) {
|
|
1800
|
+
newY = cursorY - viewportHeight + 1;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
// Save computed position — buildRunningUI will restore it since we're paused
|
|
1804
|
+
this.paneScrollPositions.set(this.focusedPaneId, { x: 0, y: newY });
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// Copy the selected lines to clipboard
|
|
1808
|
+
copySelectedLines() {
|
|
1809
|
+
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
1810
|
+
if (!pane) {
|
|
1811
|
+
this.exitCopyMode();
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
const lines = this.getOutputLinesForPane(pane);
|
|
1816
|
+
if (lines.length === 0) {
|
|
1817
|
+
this.exitCopyMode();
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// Determine range to copy
|
|
1822
|
+
let startIdx, endIdx;
|
|
1823
|
+
if (this.copyModeAnchor !== null) {
|
|
1824
|
+
startIdx = Math.min(this.copyModeAnchor, this.copyModeCursor);
|
|
1825
|
+
endIdx = Math.max(this.copyModeAnchor, this.copyModeCursor);
|
|
1826
|
+
} else {
|
|
1827
|
+
// No selection - copy just the current line
|
|
1828
|
+
startIdx = this.copyModeCursor;
|
|
1829
|
+
endIdx = this.copyModeCursor;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
// Clamp to valid range
|
|
1833
|
+
startIdx = Math.max(0, startIdx);
|
|
1834
|
+
endIdx = Math.min(lines.length - 1, endIdx);
|
|
1835
|
+
|
|
1836
|
+
const lineCount = endIdx - startIdx + 1;
|
|
1837
|
+
|
|
1838
|
+
// Build text to copy (strip ANSI codes for clean clipboard text)
|
|
1839
|
+
const textToCopy = lines
|
|
1840
|
+
.slice(startIdx, endIdx + 1)
|
|
1841
|
+
.map(line => stripAnsi(line.text.trim()))
|
|
1842
|
+
.join('\n');
|
|
1843
|
+
|
|
1844
|
+
let success = false;
|
|
1845
|
+
if (textToCopy) {
|
|
1846
|
+
success = copyToClipboard(textToCopy);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
this.exitCopyMode();
|
|
1850
|
+
|
|
1851
|
+
// Show feedback message
|
|
1852
|
+
this.showCopyFeedback(success ? `Copied ${lineCount} line${lineCount > 1 ? 's' : ''}!` : `Copied ${lineCount} line${lineCount > 1 ? 's' : ''} (clipboard may need OSC 52)`);
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
// Show a temporary feedback message in the footer
|
|
1856
|
+
showCopyFeedback(message) {
|
|
1857
|
+
if (this.copyFeedbackTimer) {
|
|
1858
|
+
clearTimeout(this.copyFeedbackTimer);
|
|
1859
|
+
}
|
|
1860
|
+
this.copyFeedbackMessage = message;
|
|
1861
|
+
this.buildRunningUI();
|
|
1862
|
+
|
|
1863
|
+
this.copyFeedbackTimer = setTimeout(() => {
|
|
1864
|
+
this.copyFeedbackMessage = '';
|
|
1865
|
+
this.copyFeedbackTimer = null;
|
|
1866
|
+
this.buildRunningUI();
|
|
1867
|
+
}, 2000);
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1581
1870
|
// Check if a process is visible in the focused pane
|
|
1582
1871
|
isProcessVisibleInPane(scriptName, pane) {
|
|
1583
1872
|
if (!pane) return true;
|
|
@@ -2291,6 +2580,11 @@ class ProcessManager {
|
|
|
2291
2580
|
// Settings UI is rebuilt on each input
|
|
2292
2581
|
// No-op here as buildSettingsUI handles everything
|
|
2293
2582
|
} else if (this.phase === 'running') {
|
|
2583
|
+
// Skip redundant render if buildRunningUI() was already called this tick
|
|
2584
|
+
if (this.uiJustRebuilt) {
|
|
2585
|
+
this.uiJustRebuilt = false;
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2294
2588
|
// For running phase, only update output, don't rebuild entire UI
|
|
2295
2589
|
this.updateRunningUI();
|
|
2296
2590
|
}
|
|
@@ -2644,6 +2938,17 @@ class ProcessManager {
|
|
|
2644
2938
|
const renderables = [];
|
|
2645
2939
|
this.lineRenderables.set(pane.id, renderables);
|
|
2646
2940
|
|
|
2941
|
+
// Determine copy mode selection range for this pane
|
|
2942
|
+
const inCopyMode = this.isCopyMode && isFocused;
|
|
2943
|
+
let copySelStart = -1;
|
|
2944
|
+
let copySelEnd = -1;
|
|
2945
|
+
if (inCopyMode) {
|
|
2946
|
+
if (this.copyModeAnchor !== null) {
|
|
2947
|
+
copySelStart = Math.min(this.copyModeAnchor, this.copyModeCursor);
|
|
2948
|
+
copySelEnd = Math.max(this.copyModeAnchor, this.copyModeCursor);
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2647
2952
|
// Add lines (oldest first, so newest is at bottom)
|
|
2648
2953
|
for (let i = 0; i < linesToShow.length; i++) {
|
|
2649
2954
|
const line = linesToShow[i];
|
|
@@ -2653,21 +2958,60 @@ class ProcessManager {
|
|
|
2653
2958
|
const lineNumber = this.showLineNumbers ? String(line.lineNumber).padStart(4, ' ') : '';
|
|
2654
2959
|
const timestamp = this.showTimestamps ? new Date(line.timestamp).toLocaleTimeString('en-US', { hour12: false }) : '';
|
|
2655
2960
|
|
|
2961
|
+
// Determine copy mode highlighting for this line
|
|
2962
|
+
const isCursorLine = inCopyMode && i === this.copyModeCursor;
|
|
2963
|
+
const isSelectedLine = inCopyMode && i >= copySelStart && i <= copySelEnd;
|
|
2964
|
+
|
|
2656
2965
|
let content;
|
|
2657
|
-
if (
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2966
|
+
if (inCopyMode) {
|
|
2967
|
+
// In copy mode: cursor line gets bright marker + white text on blue bg
|
|
2968
|
+
// Selected lines get lighter text on darker blue bg
|
|
2969
|
+
// Unselected lines are dimmed to make selection stand out
|
|
2970
|
+
const marker = isCursorLine ? fg(COLORS.copyCursorText)('\u25b6 ') : ' ';
|
|
2971
|
+
|
|
2972
|
+
let textColor, procColor, dimColor;
|
|
2973
|
+
if (isCursorLine) {
|
|
2974
|
+
textColor = COLORS.copyCursorText;
|
|
2975
|
+
procColor = COLORS.copyCursorText;
|
|
2976
|
+
dimColor = COLORS.copySelectText;
|
|
2977
|
+
} else if (isSelectedLine) {
|
|
2978
|
+
textColor = COLORS.copySelectText;
|
|
2979
|
+
procColor = processColor;
|
|
2980
|
+
dimColor = COLORS.textDim;
|
|
2981
|
+
} else {
|
|
2982
|
+
textColor = COLORS.textDim;
|
|
2983
|
+
procColor = COLORS.textDim;
|
|
2984
|
+
dimColor = COLORS.textDim;
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
if (this.showLineNumbers && this.showTimestamps) {
|
|
2988
|
+
content = t`${marker}${fg(dimColor)(lineNumber)} ${fg(dimColor)(`[${timestamp}]`)} ${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}`;
|
|
2989
|
+
} else if (this.showLineNumbers) {
|
|
2990
|
+
content = t`${marker}${fg(dimColor)(lineNumber)} ${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}`;
|
|
2991
|
+
} else if (this.showTimestamps) {
|
|
2992
|
+
content = t`${marker}${fg(dimColor)(`[${timestamp}]`)} ${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}`;
|
|
2993
|
+
} else {
|
|
2994
|
+
content = t`${marker}${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}`;
|
|
2995
|
+
}
|
|
2663
2996
|
} else {
|
|
2664
|
-
|
|
2997
|
+
if (this.showLineNumbers && this.showTimestamps) {
|
|
2998
|
+
content = t`${fg(COLORS.textDim)(lineNumber)} ${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${line.text}`;
|
|
2999
|
+
} else if (this.showLineNumbers) {
|
|
3000
|
+
content = t`${fg(COLORS.textDim)(lineNumber)} ${fg(processColor)(`[${line.process}]`)} ${line.text}`;
|
|
3001
|
+
} else if (this.showTimestamps) {
|
|
3002
|
+
content = t`${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${line.text}`;
|
|
3003
|
+
} else {
|
|
3004
|
+
content = t`${fg(processColor)(`[${line.process}]`)} ${line.text}`;
|
|
3005
|
+
}
|
|
2665
3006
|
}
|
|
2666
3007
|
|
|
3008
|
+
// High-contrast background: bright blue for cursor, medium blue for selected, black for rest
|
|
3009
|
+
const bgColor = isCursorLine ? COLORS.copyCursorBg : (isSelectedLine ? COLORS.copySelectBg : '#000000');
|
|
3010
|
+
|
|
2667
3011
|
const outputLine = new TextRenderable(this.renderer, {
|
|
2668
3012
|
id: `output-${pane.id}-${line.lineNumber}`,
|
|
2669
3013
|
content: content,
|
|
2670
|
-
bg:
|
|
3014
|
+
bg: bgColor,
|
|
2671
3015
|
});
|
|
2672
3016
|
|
|
2673
3017
|
container.add(outputLine);
|
|
@@ -2761,6 +3105,12 @@ class ProcessManager {
|
|
|
2761
3105
|
|
|
2762
3106
|
this.buildPaneOutput(pane, outputBox.content, height);
|
|
2763
3107
|
|
|
3108
|
+
// Update paneLineCount so updateRunningUI() won't re-add these lines
|
|
3109
|
+
const paneLines = this.getOutputLinesForPane(pane);
|
|
3110
|
+
if (paneLines.length > 0) {
|
|
3111
|
+
this.paneLineCount.set(pane.id, paneLines[paneLines.length - 1].lineNumber);
|
|
3112
|
+
}
|
|
3113
|
+
|
|
2764
3114
|
// Restore or set scroll position immediately
|
|
2765
3115
|
if (outputBox && outputBox.scrollTo) {
|
|
2766
3116
|
if (this.isPaused && this.paneScrollPositions.has(pane.id)) {
|
|
@@ -3181,9 +3531,9 @@ class ProcessManager {
|
|
|
3181
3531
|
gap: 2,
|
|
3182
3532
|
});
|
|
3183
3533
|
|
|
3184
|
-
// Status (LIVE/PAUSED)
|
|
3185
|
-
const statusText = this.isPaused ? 'PAUSED' : 'LIVE';
|
|
3186
|
-
const statusColor = this.isPaused ? COLORS.warning : COLORS.success;
|
|
3534
|
+
// Status (LIVE/PAUSED/COPY)
|
|
3535
|
+
const statusText = this.isCopyMode ? 'COPY' : (this.isPaused ? 'PAUSED' : 'LIVE');
|
|
3536
|
+
const statusColor = this.isCopyMode ? COLORS.accent : (this.isPaused ? COLORS.warning : COLORS.success);
|
|
3187
3537
|
const statusIndicator = new TextRenderable(this.renderer, {
|
|
3188
3538
|
id: 'status-indicator',
|
|
3189
3539
|
content: t`${fg(statusColor)(statusText)}`,
|
|
@@ -3220,6 +3570,30 @@ class ProcessManager {
|
|
|
3220
3570
|
leftSide.add(inputIndicator);
|
|
3221
3571
|
}
|
|
3222
3572
|
|
|
3573
|
+
// Copy mode indicator if active
|
|
3574
|
+
if (this.isCopyMode) {
|
|
3575
|
+
const selCount = this.copyModeAnchor !== null
|
|
3576
|
+
? Math.abs(this.copyModeCursor - this.copyModeAnchor) + 1
|
|
3577
|
+
: 0;
|
|
3578
|
+
const copyText = selCount > 0
|
|
3579
|
+
? `COPY [${selCount} line${selCount > 1 ? 's' : ''}] Space:clear y/Enter:copy`
|
|
3580
|
+
: 'COPY Space:start selection y/Enter:copy line Esc:exit';
|
|
3581
|
+
const copyIndicator = new TextRenderable(this.renderer, {
|
|
3582
|
+
id: 'copy-mode-indicator',
|
|
3583
|
+
content: t`${fg(COLORS.accent)(copyText)}`,
|
|
3584
|
+
});
|
|
3585
|
+
leftSide.add(copyIndicator);
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
// Copy feedback message (shown briefly after copying)
|
|
3589
|
+
if (this.copyFeedbackMessage) {
|
|
3590
|
+
const feedbackIndicator = new TextRenderable(this.renderer, {
|
|
3591
|
+
id: 'copy-feedback',
|
|
3592
|
+
content: t`${fg(COLORS.success)(this.copyFeedbackMessage)}`,
|
|
3593
|
+
});
|
|
3594
|
+
leftSide.add(feedbackIndicator);
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3223
3597
|
// Color filter indicator if active on focused pane
|
|
3224
3598
|
if (focusedPane?.colorFilter) {
|
|
3225
3599
|
const colorMap = {
|
|
@@ -3264,6 +3638,7 @@ class ProcessManager {
|
|
|
3264
3638
|
{ key: '1-9', desc: 'toggle', color: COLORS.success },
|
|
3265
3639
|
{ key: 'i', desc: 'input', color: COLORS.success },
|
|
3266
3640
|
{ key: 'n', desc: 'name', color: COLORS.accent },
|
|
3641
|
+
{ key: 'y', desc: 'copy', color: COLORS.accent },
|
|
3267
3642
|
{ key: 'p', desc: 'pause', color: COLORS.warning },
|
|
3268
3643
|
{ key: '/', desc: 'filter', color: COLORS.cyan },
|
|
3269
3644
|
{ key: 'c', desc: 'color', color: COLORS.magenta },
|
|
@@ -3321,6 +3696,7 @@ class ProcessManager {
|
|
|
3321
3696
|
|
|
3322
3697
|
this.renderer.root.add(mainContainer);
|
|
3323
3698
|
this.runningContainer = mainContainer;
|
|
3699
|
+
this.uiJustRebuilt = true; // Prevent redundant render in the same tick
|
|
3324
3700
|
}
|
|
3325
3701
|
}
|
|
3326
3702
|
|