startall 0.0.13 → 0.0.14

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.
Files changed (3) hide show
  1. package/README.md +23 -7
  2. package/index.js +372 -28
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -56,9 +56,13 @@ Traditional solutions fall short:
56
56
  - **Quick process toggle**: Use number keys `1-9` for instant visibility control
57
57
 
58
58
  ### 🔧 Advanced Controls
59
+ - **Quick Commands**: Assign keyboard shortcuts to run scripts on-demand
60
+ - Press assigned key to run command in a popup overlay
61
+ - Perfect for build scripts, tests, or any short-running command
62
+ - Configure in settings (`o` → Quick Commands section)
59
63
  - **Interactive input mode**: Send commands to running processes via stdin (`i`)
60
64
  - Perfect for dev servers that accept commands (Vite, Rust watch, etc.)
61
- - **Settings panel**: Configure ignore/include patterns (`o`)
65
+ - **Settings panel**: Configure ignore/include patterns, shortcuts, and more (`o`)
62
66
  - Wildcard support (`*`) for pattern matching
63
67
  - Per-script visibility toggles
64
68
  - **Keyboard & mouse support**: Full keyboard navigation + mouse clicking/scrolling
@@ -103,6 +107,7 @@ That's it! The TUI will:
103
107
  - `s` - Stop/start selected process
104
108
  - `r` - Restart selected process
105
109
  - `i` - Send input to selected process (interactive mode)
110
+ - `a-z` - Run assigned quick command (if configured)
106
111
 
107
112
  *Pane Management:*
108
113
  - `\` - Open command palette
@@ -137,13 +142,17 @@ That's it! The TUI will:
137
142
  - `Ctrl+C` - Force quit
138
143
 
139
144
  **Settings Screen:**
140
- - `Tab`/`←`/`→` - Switch sections (Ignore/Include/Scripts)
145
+ - `Tab`/`←`/`→` - Switch sections (Display/Ignore/Include/Quick Commands/Script List)
141
146
  - `↑`/`↓` - Navigate items
142
- - `a` - Add new pattern (Ignore/Include sections)
143
- - `d` or `Backspace` - Delete pattern
144
- - `Space` or `Enter` - Toggle script (Scripts section)
147
+ - `i` - Add new ignore pattern
148
+ - `n` - Add new include pattern
149
+ - `Space` or `Enter` - Toggle option (Display) / Assign shortcut (Quick Commands) / Toggle ignore (Script List)
150
+ - `d` or `Backspace` - Delete pattern or shortcut
145
151
  - `Esc` or `q` - Return to previous screen
146
152
 
153
+ **Quick Commands Overlay:**
154
+ - `Esc` - Close overlay and stop command (if running)
155
+
147
156
  ## Why Build This?
148
157
 
149
158
  Existing tools either:
@@ -164,12 +173,19 @@ Existing tools either:
164
173
  {
165
174
  "defaultSelection": ["frontend", "backend"],
166
175
  "include": ["dev:*"],
167
- "ignore": ["*:test"]
176
+ "ignore": ["*:test"],
177
+ "shortcuts": {
178
+ "b": "build",
179
+ "t": "test",
180
+ "l": "lint"
181
+ }
168
182
  }
169
183
  ```
184
+ - `defaultSelection`: scripts to auto-select on startup
170
185
  - `include` (optional): if defined, only scripts matching these patterns are shown
171
186
  - `ignore`: scripts matching these patterns are hidden
172
- - Both support wildcards (`*`)
187
+ - `shortcuts`: keyboard shortcuts for running commands on-demand
188
+ - All patterns support wildcards (`*`)
173
189
 
174
190
  ## Roadmap
175
191
 
package/index.js CHANGED
@@ -10,7 +10,18 @@ import stripAnsi from 'strip-ansi';
10
10
  // Configuration
11
11
  const CONFIG_FILE = process.argv[2] || 'startall.json';
12
12
  const COUNTDOWN_SECONDS = 10;
13
- const APP_VERSION = 'v0.0.4';
13
+
14
+ // Read version from package.json
15
+ function getAppVersion() {
16
+ try {
17
+ const packagePath = new URL('./package.json', import.meta.url);
18
+ const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
19
+ return `v${pkg.version}`;
20
+ } catch (error) {
21
+ return 'v0.0.0'; // Fallback if package.json can't be read
22
+ }
23
+ }
24
+ const APP_VERSION = getAppVersion();
14
25
 
15
26
  // Detect if running inside VS Code's integrated terminal
16
27
  const IS_VSCODE = process.env.TERM_PROGRAM === 'vscode';
@@ -307,10 +318,10 @@ function loadConfig() {
307
318
  try {
308
319
  return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
309
320
  } catch {
310
- return { defaultSelection: [], ignore: [] };
321
+ return { defaultSelection: [], ignore: [], shortcuts: {} };
311
322
  }
312
323
  }
313
- return { defaultSelection: [], ignore: [] };
324
+ return { defaultSelection: [], ignore: [], shortcuts: {} };
314
325
  }
315
326
 
316
327
  // Save config
@@ -353,12 +364,21 @@ class ProcessManager {
353
364
  this.inputModeText = ''; // Text being typed for stdin
354
365
 
355
366
  // Settings menu state
356
- this.settingsSection = 'display'; // 'display' | 'ignore' | 'include' | 'scripts'
367
+ this.settingsSection = 'display'; // 'display' | 'ignore' | 'include' | 'scripts' | 'shortcuts'
357
368
  this.settingsIndex = 0; // Current selection index within section
358
369
  this.isAddingPattern = false; // Whether typing a new pattern
359
370
  this.newPatternText = ''; // Text being typed for new pattern
371
+ this.isAssigningShortcut = false; // Whether waiting for a key to assign as shortcut
372
+ this.shortcutScriptName = ''; // Script name being assigned a shortcut
360
373
  this.settingsContainer = null; // UI reference
361
374
  this.previousPhase = 'selection'; // Track where we came from
375
+
376
+ // Command execution overlay state
377
+ this.showCommandOverlay = false; // Whether command output overlay is visible
378
+ this.commandOverlayOutput = []; // Output lines from command
379
+ this.commandOverlayScript = ''; // Script name being executed
380
+ this.commandOverlayStatus = 'running'; // 'running' | 'exited' | 'crashed'
381
+ this.commandOverlayProcess = null; // Process reference
362
382
  this.outputBox = null; // Reference to the output container
363
383
  this.destroyed = false; // Flag to prevent operations after cleanup
364
384
  this.lastRenderedLineCount = 0; // Track how many lines we've rendered
@@ -465,6 +485,15 @@ class ProcessManager {
465
485
  this.handleSettingsInput(keyName, keyEvent);
466
486
  return;
467
487
  } else if (this.phase === 'running') {
488
+ // Handle command overlay
489
+ if (this.showCommandOverlay) {
490
+ if (keyName === 'escape') {
491
+ this.closeCommandOverlay();
492
+ this.buildRunningUI();
493
+ }
494
+ return;
495
+ }
496
+
468
497
  // Handle split menu
469
498
  if (this.showSplitMenu) {
470
499
  this.handleSplitMenuInput(keyName, keyEvent);
@@ -679,6 +708,17 @@ class ProcessManager {
679
708
  this.inputModeText = '';
680
709
  this.buildRunningUI();
681
710
  }
711
+ } else if (keyName && keyName.length === 1 && !keyEvent.ctrl && !keyEvent.meta && !keyEvent.shift) {
712
+ // Check if this key is a custom shortcut
713
+ const shortcuts = this.config.shortcuts || {};
714
+ const scriptName = shortcuts[keyName];
715
+ if (scriptName) {
716
+ // Find the script in allScripts (since it might be ignored/filtered out)
717
+ const script = this.allScripts.find(s => s.name === scriptName);
718
+ if (script) {
719
+ this.executeCommand(scriptName);
720
+ }
721
+ }
682
722
  }
683
723
  }
684
724
  }
@@ -890,6 +930,40 @@ class ProcessManager {
890
930
  }
891
931
 
892
932
  handleSettingsInput(keyName, keyEvent) {
933
+ // Handle shortcut assignment mode
934
+ if (this.isAssigningShortcut) {
935
+ if (keyName === 'escape') {
936
+ this.isAssigningShortcut = false;
937
+ this.shortcutScriptName = '';
938
+ this.buildSettingsUI();
939
+ return;
940
+ } else if (keyName && keyName.length === 1 && !keyEvent.ctrl && !keyEvent.meta && !keyEvent.shift) {
941
+ // Assign this key to the script
942
+ if (!this.config.shortcuts) this.config.shortcuts = {};
943
+
944
+ // Remove any existing shortcut for this script (one key per script)
945
+ for (const [key, scriptName] of Object.entries(this.config.shortcuts)) {
946
+ if (scriptName === this.shortcutScriptName) {
947
+ delete this.config.shortcuts[key];
948
+ }
949
+ }
950
+
951
+ // Also remove this key if it's assigned to another script (one script per key)
952
+ if (this.config.shortcuts[keyName]) {
953
+ delete this.config.shortcuts[keyName];
954
+ }
955
+
956
+ // Assign the new shortcut
957
+ this.config.shortcuts[keyName] = this.shortcutScriptName;
958
+ saveConfig(this.config);
959
+ this.isAssigningShortcut = false;
960
+ this.shortcutScriptName = '';
961
+ this.buildSettingsUI();
962
+ return;
963
+ }
964
+ return;
965
+ }
966
+
893
967
  // Handle text input mode for adding patterns
894
968
  if (this.isAddingPattern) {
895
969
  if (keyName === 'escape') {
@@ -944,21 +1018,21 @@ class ProcessManager {
944
1018
  }
945
1019
  } else if (keyName === 'tab' || keyName === 'right') {
946
1020
  // Switch section
947
- const sections = ['display', 'ignore', 'include', 'scripts'];
1021
+ const sections = ['display', 'ignore', 'include', 'shortcuts', 'scripts'];
948
1022
  const idx = sections.indexOf(this.settingsSection);
949
1023
  this.settingsSection = sections[(idx + 1) % sections.length];
950
1024
  this.settingsIndex = 0;
951
1025
  this.buildSettingsUI();
952
1026
  } else if (keyEvent.shift && keyName === 'tab') {
953
1027
  // Switch section backwards
954
- const sections = ['display', 'ignore', 'include', 'scripts'];
1028
+ const sections = ['display', 'ignore', 'include', 'shortcuts', 'scripts'];
955
1029
  const idx = sections.indexOf(this.settingsSection);
956
1030
  this.settingsSection = sections[(idx - 1 + sections.length) % sections.length];
957
1031
  this.settingsIndex = 0;
958
1032
  this.buildSettingsUI();
959
1033
  } else if (keyName === 'left') {
960
1034
  // Switch section backwards
961
- const sections = ['display', 'ignore', 'include', 'scripts'];
1035
+ const sections = ['display', 'ignore', 'include', 'shortcuts', 'scripts'];
962
1036
  const idx = sections.indexOf(this.settingsSection);
963
1037
  this.settingsSection = sections[(idx - 1 + sections.length) % sections.length];
964
1038
  this.settingsIndex = 0;
@@ -969,7 +1043,7 @@ class ProcessManager {
969
1043
  this.buildSettingsUI();
970
1044
  } else {
971
1045
  // Move to previous section
972
- const sections = ['display', 'ignore', 'include', 'scripts'];
1046
+ const sections = ['display', 'ignore', 'include', 'shortcuts', 'scripts'];
973
1047
  const idx = sections.indexOf(this.settingsSection);
974
1048
  if (idx > 0) {
975
1049
  this.settingsSection = sections[idx - 1];
@@ -984,7 +1058,7 @@ class ProcessManager {
984
1058
  this.buildSettingsUI();
985
1059
  } else {
986
1060
  // Move to next section
987
- const sections = ['display', 'ignore', 'include', 'scripts'];
1061
+ const sections = ['display', 'ignore', 'include', 'shortcuts', 'scripts'];
988
1062
  const idx = sections.indexOf(this.settingsSection);
989
1063
  if (idx < sections.length - 1) {
990
1064
  this.settingsSection = sections[idx + 1];
@@ -1005,14 +1079,16 @@ class ProcessManager {
1005
1079
  this.newPatternText = '';
1006
1080
  this.buildSettingsUI();
1007
1081
  } else if (keyName === 'd' || keyName === 'backspace') {
1008
- // Delete selected pattern
1082
+ // Delete selected pattern or shortcut
1009
1083
  this.deleteSelectedItem();
1010
1084
  this.buildSettingsUI();
1011
1085
  } else if (keyName === 'space' || keyName === 'enter' || keyName === 'return') {
1012
- // Toggle display options, script visibility
1086
+ // Toggle display options, script visibility, or assign shortcut
1013
1087
  if (this.settingsSection === 'display') {
1014
1088
  this.toggleDisplayOption();
1015
1089
  this.buildSettingsUI();
1090
+ } else if (this.settingsSection === 'shortcuts') {
1091
+ this.assignShortcut();
1016
1092
  } else if (this.settingsSection === 'scripts') {
1017
1093
  this.toggleScriptIgnore();
1018
1094
  this.buildSettingsUI();
@@ -1029,6 +1105,8 @@ class ProcessManager {
1029
1105
  } else if (this.settingsSection === 'include') {
1030
1106
  const count = this.config.include?.length || 0;
1031
1107
  return count > 0 ? count - 1 : 0;
1108
+ } else if (this.settingsSection === 'shortcuts') {
1109
+ return Math.max(0, this.allScripts.length - 1);
1032
1110
  } else if (this.settingsSection === 'scripts') {
1033
1111
  return Math.max(0, this.allScripts.length - 1);
1034
1112
  }
@@ -1059,6 +1137,27 @@ class ProcessManager {
1059
1137
  saveConfig(this.config);
1060
1138
  this.applyFilters();
1061
1139
  this.settingsIndex = Math.max(0, Math.min(this.settingsIndex, (this.config.include?.length || 1) - 1));
1140
+ } else if (this.settingsSection === 'shortcuts') {
1141
+ const script = this.allScripts[this.settingsIndex];
1142
+ if (script && this.config.shortcuts) {
1143
+ // Find and delete any shortcut assigned to this script
1144
+ for (const [key, scriptName] of Object.entries(this.config.shortcuts)) {
1145
+ if (scriptName === script.name) {
1146
+ delete this.config.shortcuts[key];
1147
+ saveConfig(this.config);
1148
+ break;
1149
+ }
1150
+ }
1151
+ }
1152
+ }
1153
+ }
1154
+
1155
+ assignShortcut() {
1156
+ const script = this.allScripts[this.settingsIndex];
1157
+ if (script) {
1158
+ this.isAssigningShortcut = true;
1159
+ this.shortcutScriptName = script.name;
1160
+ this.buildSettingsUI();
1062
1161
  }
1063
1162
  }
1064
1163
 
@@ -1494,6 +1593,24 @@ class ProcessManager {
1494
1593
  this.settingsContainer.add(inputBar);
1495
1594
  }
1496
1595
 
1596
+ // Input prompt if assigning shortcut
1597
+ if (this.isAssigningShortcut) {
1598
+ const inputBar = new BoxRenderable(this.renderer, {
1599
+ id: 'input-bar',
1600
+ border: ['left'],
1601
+ borderStyle: 'single',
1602
+ borderColor: COLORS.accent,
1603
+ paddingLeft: 1,
1604
+ marginBottom: 1,
1605
+ });
1606
+ const inputText = new TextRenderable(this.renderer, {
1607
+ id: 'input-text',
1608
+ content: t`${fg(COLORS.textDim)('Press a key to assign as shortcut for')} ${fg(COLORS.accent)(this.shortcutScriptName)} ${fg(COLORS.textDim)('(esc to cancel)')}`,
1609
+ });
1610
+ inputBar.add(inputText);
1611
+ this.settingsContainer.add(inputBar);
1612
+ }
1613
+
1497
1614
  // Combined content panel with all sections
1498
1615
  const contentPanel = new BoxRenderable(this.renderer, {
1499
1616
  id: 'content-panel',
@@ -1556,16 +1673,31 @@ class ProcessManager {
1556
1673
 
1557
1674
  contentPanel.add(leftColumn);
1558
1675
 
1559
- // Right column - Scripts list
1676
+ // Middle column - Quick Commands
1677
+ const shortcutsBox = new BoxRenderable(this.renderer, {
1678
+ id: 'shortcuts-box',
1679
+ flexDirection: 'column',
1680
+ border: true,
1681
+ borderStyle: 'rounded',
1682
+ borderColor: this.settingsSection === 'shortcuts' ? COLORS.borderFocused : COLORS.border,
1683
+ title: ' Quick Commands ',
1684
+ titleAlignment: 'left',
1685
+ flexGrow: 1,
1686
+ padding: 1,
1687
+ });
1688
+ this.buildShortcutsSectionContent(shortcutsBox);
1689
+ contentPanel.add(shortcutsBox);
1690
+
1691
+ // Right column - Script List
1560
1692
  const scriptsBox = new BoxRenderable(this.renderer, {
1561
1693
  id: 'scripts-box',
1562
1694
  flexDirection: 'column',
1563
1695
  border: true,
1564
1696
  borderStyle: 'rounded',
1565
1697
  borderColor: this.settingsSection === 'scripts' ? COLORS.borderFocused : COLORS.border,
1566
- title: ' Scripts ',
1698
+ title: ' Script List ',
1567
1699
  titleAlignment: 'left',
1568
- flexGrow: 2,
1700
+ flexGrow: 1,
1569
1701
  padding: 1,
1570
1702
  });
1571
1703
  this.buildScriptsSectionContent(scriptsBox);
@@ -1586,19 +1718,27 @@ class ProcessManager {
1586
1718
  gap: 2,
1587
1719
  });
1588
1720
 
1589
- const shortcuts = this.isAddingPattern
1590
- ? [
1591
- { key: 'enter', desc: 'save' },
1592
- { key: 'esc', desc: 'cancel' },
1593
- ]
1594
- : [
1595
- { key: 'tab', desc: 'section' },
1596
- { key: 'space', desc: 'toggle' },
1597
- { key: 'i', desc: 'add ignore' },
1598
- { key: 'n', desc: 'add include' },
1599
- { key: 'd', desc: 'delete' },
1600
- { key: 'esc', desc: 'back' },
1601
- ];
1721
+ let shortcuts;
1722
+ if (this.isAddingPattern) {
1723
+ shortcuts = [
1724
+ { key: 'enter', desc: 'save' },
1725
+ { key: 'esc', desc: 'cancel' },
1726
+ ];
1727
+ } else if (this.isAssigningShortcut) {
1728
+ shortcuts = [
1729
+ { key: 'any key', desc: 'assign' },
1730
+ { key: 'esc', desc: 'cancel' },
1731
+ ];
1732
+ } else {
1733
+ shortcuts = [
1734
+ { key: 'tab', desc: 'section' },
1735
+ { key: 'space', desc: this.settingsSection === 'shortcuts' ? 'assign' : 'toggle' },
1736
+ { key: 'i', desc: 'add ignore' },
1737
+ { key: 'n', desc: 'add include' },
1738
+ { key: 'd', desc: 'delete' },
1739
+ { key: 'esc', desc: 'back' },
1740
+ ];
1741
+ }
1602
1742
 
1603
1743
  shortcuts.forEach(({ key, desc }) => {
1604
1744
  const shortcut = new TextRenderable(this.renderer, {
@@ -1679,6 +1819,38 @@ class ProcessManager {
1679
1819
  }
1680
1820
  }
1681
1821
 
1822
+ buildShortcutsSectionContent(container) {
1823
+ const shortcuts = this.config.shortcuts || {};
1824
+
1825
+ // Helper to find shortcut key for a script
1826
+ const getShortcutKey = (scriptName) => {
1827
+ for (const [key, name] of Object.entries(shortcuts)) {
1828
+ if (name === scriptName) return key;
1829
+ }
1830
+ return null;
1831
+ };
1832
+
1833
+ this.allScripts.forEach((script, idx) => {
1834
+ const isFocused = this.settingsSection === 'shortcuts' && idx === this.settingsIndex;
1835
+ const indicator = isFocused ? '>' : ' ';
1836
+ const shortcutKey = getShortcutKey(script.name);
1837
+ const processColor = this.processColors.get(script.name) || COLORS.text;
1838
+
1839
+ let content;
1840
+ if (shortcutKey) {
1841
+ content = t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(COLORS.warning)(`[${shortcutKey}]`)} ${fg(processColor)(script.displayName)}`;
1842
+ } else {
1843
+ content = t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(COLORS.textDim)('[ ]')} ${fg(processColor)(script.displayName)}`;
1844
+ }
1845
+
1846
+ const line = new TextRenderable(this.renderer, {
1847
+ id: `shortcut-item-${idx}`,
1848
+ content: content,
1849
+ });
1850
+ container.add(line);
1851
+ });
1852
+ }
1853
+
1682
1854
  buildScriptsSectionContent(container) {
1683
1855
  const ignorePatterns = this.config.ignore || [];
1684
1856
 
@@ -1691,9 +1863,16 @@ class ProcessManager {
1691
1863
  const processColor = this.processColors.get(script.name) || COLORS.text;
1692
1864
  const nameColor = isIgnored ? COLORS.textDim : processColor;
1693
1865
 
1866
+ let content;
1867
+ if (isIgnored) {
1868
+ content = t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(checkColor)(checkbox)} ${fg(nameColor)(script.displayName)} ${fg(COLORS.textDim)('(ignored)')}`;
1869
+ } else {
1870
+ content = t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(checkColor)(checkbox)} ${fg(nameColor)(script.displayName)}`;
1871
+ }
1872
+
1694
1873
  const line = new TextRenderable(this.renderer, {
1695
1874
  id: `script-toggle-${idx}`,
1696
- content: t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(checkColor)(checkbox)} ${fg(nameColor)(script.displayName)}${isIgnored ? t` ${fg(COLORS.textDim)('(ignored)')}` : ''}`,
1875
+ content: content,
1697
1876
  });
1698
1877
  container.add(line);
1699
1878
  });
@@ -1723,6 +1902,15 @@ class ProcessManager {
1723
1902
  this.countdownInterval = null;
1724
1903
  }
1725
1904
 
1905
+ // Clean up command overlay process if running
1906
+ if (this.commandOverlayProcess && this.commandOverlayProcess.pid) {
1907
+ try {
1908
+ kill(this.commandOverlayProcess.pid, 'SIGKILL');
1909
+ } catch (err) {
1910
+ // Ignore
1911
+ }
1912
+ }
1913
+
1726
1914
  for (const [scriptName, proc] of this.processRefs.entries()) {
1727
1915
  try {
1728
1916
  if (proc.pid) {
@@ -1733,6 +1921,75 @@ class ProcessManager {
1733
1921
  }
1734
1922
  }
1735
1923
  }
1924
+
1925
+ executeCommand(scriptName) {
1926
+ // Initialize overlay state
1927
+ this.showCommandOverlay = true;
1928
+ this.commandOverlayOutput = [];
1929
+ this.commandOverlayScript = scriptName;
1930
+ this.commandOverlayStatus = 'running';
1931
+
1932
+ // Spawn the process
1933
+ const proc = spawn('npm', ['run', scriptName], {
1934
+ env: {
1935
+ ...process.env,
1936
+ FORCE_COLOR: '1',
1937
+ COLORTERM: 'truecolor',
1938
+ },
1939
+ shell: true,
1940
+ });
1941
+
1942
+ this.commandOverlayProcess = proc;
1943
+
1944
+ proc.stdout.on('data', (data) => {
1945
+ const text = data.toString();
1946
+ const lines = text.split('\n');
1947
+ lines.forEach(line => {
1948
+ if (line.trim()) {
1949
+ this.commandOverlayOutput.push(line);
1950
+ this.buildRunningUI();
1951
+ }
1952
+ });
1953
+ });
1954
+
1955
+ proc.stderr.on('data', (data) => {
1956
+ const text = data.toString();
1957
+ const lines = text.split('\n');
1958
+ lines.forEach(line => {
1959
+ if (line.trim()) {
1960
+ this.commandOverlayOutput.push(line);
1961
+ this.buildRunningUI();
1962
+ }
1963
+ });
1964
+ });
1965
+
1966
+ proc.on('exit', (code) => {
1967
+ this.commandOverlayStatus = code === 0 ? 'exited' : 'crashed';
1968
+ this.commandOverlayOutput.push('');
1969
+ this.commandOverlayOutput.push(`Process exited with code ${code}`);
1970
+ this.commandOverlayProcess = null;
1971
+ this.buildRunningUI();
1972
+ });
1973
+
1974
+ this.buildRunningUI();
1975
+ }
1976
+
1977
+ closeCommandOverlay() {
1978
+ // Kill the process if still running
1979
+ if (this.commandOverlayProcess && this.commandOverlayProcess.pid) {
1980
+ try {
1981
+ kill(this.commandOverlayProcess.pid, 'SIGKILL');
1982
+ } catch (err) {
1983
+ // Ignore
1984
+ }
1985
+ }
1986
+
1987
+ this.showCommandOverlay = false;
1988
+ this.commandOverlayOutput = [];
1989
+ this.commandOverlayScript = '';
1990
+ this.commandOverlayStatus = 'running';
1991
+ this.commandOverlayProcess = null;
1992
+ }
1736
1993
 
1737
1994
  buildSelectionUI() {
1738
1995
  // Remove old containers if they exist - use destroyRecursively to clean up all children
@@ -2335,6 +2592,88 @@ class ProcessManager {
2335
2592
  parent.add(overlay);
2336
2593
  }
2337
2594
 
2595
+ // Build command output overlay
2596
+ buildCommandOverlay(parent) {
2597
+ const statusIcon = this.commandOverlayStatus === 'running' ? '●' :
2598
+ this.commandOverlayStatus === 'exited' ? '✓' : '✖';
2599
+ const statusColor = this.commandOverlayStatus === 'running' ? COLORS.warning :
2600
+ this.commandOverlayStatus === 'exited' ? COLORS.success : COLORS.error;
2601
+ const title = ` ${statusIcon} ${this.commandOverlayScript} `;
2602
+
2603
+ // Create centered overlay with scrollable content
2604
+ const overlay = new BoxRenderable(this.renderer, {
2605
+ id: 'command-overlay',
2606
+ position: 'absolute',
2607
+ top: '10%',
2608
+ left: '10%',
2609
+ width: '80%',
2610
+ height: '80%',
2611
+ backgroundColor: COLORS.bg,
2612
+ border: true,
2613
+ borderStyle: 'rounded',
2614
+ borderColor: statusColor,
2615
+ title: title,
2616
+ padding: 0,
2617
+ flexDirection: 'column',
2618
+ });
2619
+
2620
+ // Scrollable output content
2621
+ const outputBox = new ScrollBoxRenderable(this.renderer, {
2622
+ id: 'command-output',
2623
+ height: Math.floor(this.renderer.height * 0.8) - 4,
2624
+ scrollX: false,
2625
+ scrollY: true,
2626
+ focusable: true,
2627
+ style: {
2628
+ rootOptions: {
2629
+ flexGrow: 1,
2630
+ paddingLeft: 1,
2631
+ paddingRight: 1,
2632
+ backgroundColor: COLORS.bg,
2633
+ },
2634
+ contentOptions: {
2635
+ backgroundColor: COLORS.bg,
2636
+ width: '100%',
2637
+ },
2638
+ },
2639
+ });
2640
+
2641
+ // Add output lines
2642
+ this.commandOverlayOutput.forEach((line, idx) => {
2643
+ const outputLine = new TextRenderable(this.renderer, {
2644
+ id: `cmd-output-${idx}`,
2645
+ content: line,
2646
+ });
2647
+ outputBox.content.add(outputLine);
2648
+ });
2649
+
2650
+ // Auto-scroll to bottom
2651
+ if (outputBox.scrollTo) {
2652
+ outputBox.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER });
2653
+ }
2654
+
2655
+ overlay.add(outputBox);
2656
+
2657
+ // Footer hint
2658
+ const hintBar = new BoxRenderable(this.renderer, {
2659
+ id: 'command-hint-bar',
2660
+ border: ['top'],
2661
+ borderStyle: 'single',
2662
+ borderColor: COLORS.border,
2663
+ paddingTop: 1,
2664
+ paddingLeft: 1,
2665
+ });
2666
+
2667
+ const hint = new TextRenderable(this.renderer, {
2668
+ id: 'command-hint',
2669
+ content: t`${fg(COLORS.textDim)('Press')} ${fg(COLORS.accent)('Esc')} ${fg(COLORS.textDim)('to close')}`,
2670
+ });
2671
+ hintBar.add(hint);
2672
+ overlay.add(hintBar);
2673
+
2674
+ parent.add(overlay);
2675
+ }
2676
+
2338
2677
  buildRunningUI() {
2339
2678
  // Save scroll positions before destroying
2340
2679
  for (const [paneId, scrollBox] of this.paneScrollBoxes.entries()) {
@@ -2562,6 +2901,11 @@ class ProcessManager {
2562
2901
  this.buildSplitMenuOverlay(mainContainer);
2563
2902
  }
2564
2903
 
2904
+ // Add command output overlay if active
2905
+ if (this.showCommandOverlay) {
2906
+ this.buildCommandOverlay(mainContainer);
2907
+ }
2908
+
2565
2909
  this.renderer.root.add(mainContainer);
2566
2910
  this.runningContainer = mainContainer;
2567
2911
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "startall",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {