startall 0.0.13 → 0.0.15

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 +31 -7
  2. package/index.js +522 -28
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -56,9 +56,15 @@ 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**: Run any script on-demand without adding it to the persistent processes
60
+ - Press `e` to open command picker and select any script
61
+ - Or assign keyboard shortcuts for instant access (press assigned key to run)
62
+ - Perfect for build scripts, tests, or any short-running command
63
+ - Output shown in popup overlay with Esc to close
64
+ - Configure shortcuts in settings (`o` → Quick Commands section)
59
65
  - **Interactive input mode**: Send commands to running processes via stdin (`i`)
60
66
  - Perfect for dev servers that accept commands (Vite, Rust watch, etc.)
61
- - **Settings panel**: Configure ignore/include patterns (`o`)
67
+ - **Settings panel**: Configure ignore/include patterns, shortcuts, and more (`o`)
62
68
  - Wildcard support (`*`) for pattern matching
63
69
  - Per-script visibility toggles
64
70
  - **Keyboard & mouse support**: Full keyboard navigation + mouse clicking/scrolling
@@ -103,6 +109,8 @@ That's it! The TUI will:
103
109
  - `s` - Stop/start selected process
104
110
  - `r` - Restart selected process
105
111
  - `i` - Send input to selected process (interactive mode)
112
+ - `e` - Execute any script (opens command picker)
113
+ - `a-z` - Run assigned quick command (if configured)
106
114
 
107
115
  *Pane Management:*
108
116
  - `\` - Open command palette
@@ -137,13 +145,22 @@ That's it! The TUI will:
137
145
  - `Ctrl+C` - Force quit
138
146
 
139
147
  **Settings Screen:**
140
- - `Tab`/`←`/`→` - Switch sections (Ignore/Include/Scripts)
148
+ - `Tab`/`←`/`→` - Switch sections (Display/Ignore/Include/Quick Commands/Script List)
141
149
  - `↑`/`↓` - Navigate items
142
- - `a` - Add new pattern (Ignore/Include sections)
143
- - `d` or `Backspace` - Delete pattern
144
- - `Space` or `Enter` - Toggle script (Scripts section)
150
+ - `i` - Add new ignore pattern
151
+ - `n` - Add new include pattern
152
+ - `Space` or `Enter` - Toggle option (Display) / Assign shortcut (Quick Commands) / Toggle ignore (Script List)
153
+ - `d` or `Backspace` - Delete pattern or shortcut
145
154
  - `Esc` or `q` - Return to previous screen
146
155
 
156
+ **Run Command Picker:**
157
+ - `↑`/`↓` or `k`/`j` - Navigate scripts
158
+ - `Enter` - Run selected script
159
+ - `Esc` or `q` - Close picker
160
+
161
+ **Quick Commands Overlay:**
162
+ - `Esc` - Close overlay and stop command (if running)
163
+
147
164
  ## Why Build This?
148
165
 
149
166
  Existing tools either:
@@ -164,12 +181,19 @@ Existing tools either:
164
181
  {
165
182
  "defaultSelection": ["frontend", "backend"],
166
183
  "include": ["dev:*"],
167
- "ignore": ["*:test"]
184
+ "ignore": ["*:test"],
185
+ "shortcuts": {
186
+ "b": "build",
187
+ "t": "test",
188
+ "l": "lint"
189
+ }
168
190
  }
169
191
  ```
192
+ - `defaultSelection`: scripts to auto-select on startup
170
193
  - `include` (optional): if defined, only scripts matching these patterns are shown
171
194
  - `ignore`: scripts matching these patterns are hidden
172
- - Both support wildcards (`*`)
195
+ - `shortcuts`: keyboard shortcuts for running commands on-demand
196
+ - All patterns support wildcards (`*`)
173
197
 
174
198
  ## Roadmap
175
199
 
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,25 @@ 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
382
+
383
+ // Run command modal state
384
+ this.showRunCommandModal = false; // Whether the run command picker is visible
385
+ this.runCommandModalIndex = 0; // Selected index in the modal
362
386
  this.outputBox = null; // Reference to the output container
363
387
  this.destroyed = false; // Flag to prevent operations after cleanup
364
388
  this.lastRenderedLineCount = 0; // Track how many lines we've rendered
@@ -465,6 +489,21 @@ class ProcessManager {
465
489
  this.handleSettingsInput(keyName, keyEvent);
466
490
  return;
467
491
  } else if (this.phase === 'running') {
492
+ // Handle command overlay
493
+ if (this.showCommandOverlay) {
494
+ if (keyName === 'escape') {
495
+ this.closeCommandOverlay();
496
+ this.buildRunningUI();
497
+ }
498
+ return;
499
+ }
500
+
501
+ // Handle run command modal
502
+ if (this.showRunCommandModal) {
503
+ this.handleRunCommandModalInput(keyName, keyEvent);
504
+ return;
505
+ }
506
+
468
507
  // Handle split menu
469
508
  if (this.showSplitMenu) {
470
509
  this.handleSplitMenuInput(keyName, keyEvent);
@@ -679,6 +718,22 @@ class ProcessManager {
679
718
  this.inputModeText = '';
680
719
  this.buildRunningUI();
681
720
  }
721
+ } else if (keyName === 'e') {
722
+ // Open run command modal
723
+ this.showRunCommandModal = true;
724
+ this.runCommandModalIndex = 0;
725
+ this.buildRunningUI();
726
+ } else if (keyName && keyName.length === 1 && !keyEvent.ctrl && !keyEvent.meta && !keyEvent.shift) {
727
+ // Check if this key is a custom shortcut
728
+ const shortcuts = this.config.shortcuts || {};
729
+ const scriptName = shortcuts[keyName];
730
+ if (scriptName) {
731
+ // Find the script in allScripts (since it might be ignored/filtered out)
732
+ const script = this.allScripts.find(s => s.name === scriptName);
733
+ if (script) {
734
+ this.executeCommand(scriptName);
735
+ }
736
+ }
682
737
  }
683
738
  }
684
739
  }
@@ -890,6 +945,40 @@ class ProcessManager {
890
945
  }
891
946
 
892
947
  handleSettingsInput(keyName, keyEvent) {
948
+ // Handle shortcut assignment mode
949
+ if (this.isAssigningShortcut) {
950
+ if (keyName === 'escape') {
951
+ this.isAssigningShortcut = false;
952
+ this.shortcutScriptName = '';
953
+ this.buildSettingsUI();
954
+ return;
955
+ } else if (keyName && keyName.length === 1 && !keyEvent.ctrl && !keyEvent.meta && !keyEvent.shift) {
956
+ // Assign this key to the script
957
+ if (!this.config.shortcuts) this.config.shortcuts = {};
958
+
959
+ // Remove any existing shortcut for this script (one key per script)
960
+ for (const [key, scriptName] of Object.entries(this.config.shortcuts)) {
961
+ if (scriptName === this.shortcutScriptName) {
962
+ delete this.config.shortcuts[key];
963
+ }
964
+ }
965
+
966
+ // Also remove this key if it's assigned to another script (one script per key)
967
+ if (this.config.shortcuts[keyName]) {
968
+ delete this.config.shortcuts[keyName];
969
+ }
970
+
971
+ // Assign the new shortcut
972
+ this.config.shortcuts[keyName] = this.shortcutScriptName;
973
+ saveConfig(this.config);
974
+ this.isAssigningShortcut = false;
975
+ this.shortcutScriptName = '';
976
+ this.buildSettingsUI();
977
+ return;
978
+ }
979
+ return;
980
+ }
981
+
893
982
  // Handle text input mode for adding patterns
894
983
  if (this.isAddingPattern) {
895
984
  if (keyName === 'escape') {
@@ -944,21 +1033,21 @@ class ProcessManager {
944
1033
  }
945
1034
  } else if (keyName === 'tab' || keyName === 'right') {
946
1035
  // Switch section
947
- const sections = ['display', 'ignore', 'include', 'scripts'];
1036
+ const sections = ['display', 'ignore', 'include', 'shortcuts', 'scripts'];
948
1037
  const idx = sections.indexOf(this.settingsSection);
949
1038
  this.settingsSection = sections[(idx + 1) % sections.length];
950
1039
  this.settingsIndex = 0;
951
1040
  this.buildSettingsUI();
952
1041
  } else if (keyEvent.shift && keyName === 'tab') {
953
1042
  // Switch section backwards
954
- const sections = ['display', 'ignore', 'include', 'scripts'];
1043
+ const sections = ['display', 'ignore', 'include', 'shortcuts', 'scripts'];
955
1044
  const idx = sections.indexOf(this.settingsSection);
956
1045
  this.settingsSection = sections[(idx - 1 + sections.length) % sections.length];
957
1046
  this.settingsIndex = 0;
958
1047
  this.buildSettingsUI();
959
1048
  } else if (keyName === 'left') {
960
1049
  // Switch section backwards
961
- const sections = ['display', 'ignore', 'include', 'scripts'];
1050
+ const sections = ['display', 'ignore', 'include', 'shortcuts', 'scripts'];
962
1051
  const idx = sections.indexOf(this.settingsSection);
963
1052
  this.settingsSection = sections[(idx - 1 + sections.length) % sections.length];
964
1053
  this.settingsIndex = 0;
@@ -969,7 +1058,7 @@ class ProcessManager {
969
1058
  this.buildSettingsUI();
970
1059
  } else {
971
1060
  // Move to previous section
972
- const sections = ['display', 'ignore', 'include', 'scripts'];
1061
+ const sections = ['display', 'ignore', 'include', 'shortcuts', 'scripts'];
973
1062
  const idx = sections.indexOf(this.settingsSection);
974
1063
  if (idx > 0) {
975
1064
  this.settingsSection = sections[idx - 1];
@@ -984,7 +1073,7 @@ class ProcessManager {
984
1073
  this.buildSettingsUI();
985
1074
  } else {
986
1075
  // Move to next section
987
- const sections = ['display', 'ignore', 'include', 'scripts'];
1076
+ const sections = ['display', 'ignore', 'include', 'shortcuts', 'scripts'];
988
1077
  const idx = sections.indexOf(this.settingsSection);
989
1078
  if (idx < sections.length - 1) {
990
1079
  this.settingsSection = sections[idx + 1];
@@ -1005,14 +1094,16 @@ class ProcessManager {
1005
1094
  this.newPatternText = '';
1006
1095
  this.buildSettingsUI();
1007
1096
  } else if (keyName === 'd' || keyName === 'backspace') {
1008
- // Delete selected pattern
1097
+ // Delete selected pattern or shortcut
1009
1098
  this.deleteSelectedItem();
1010
1099
  this.buildSettingsUI();
1011
1100
  } else if (keyName === 'space' || keyName === 'enter' || keyName === 'return') {
1012
- // Toggle display options, script visibility
1101
+ // Toggle display options, script visibility, or assign shortcut
1013
1102
  if (this.settingsSection === 'display') {
1014
1103
  this.toggleDisplayOption();
1015
1104
  this.buildSettingsUI();
1105
+ } else if (this.settingsSection === 'shortcuts') {
1106
+ this.assignShortcut();
1016
1107
  } else if (this.settingsSection === 'scripts') {
1017
1108
  this.toggleScriptIgnore();
1018
1109
  this.buildSettingsUI();
@@ -1029,6 +1120,8 @@ class ProcessManager {
1029
1120
  } else if (this.settingsSection === 'include') {
1030
1121
  const count = this.config.include?.length || 0;
1031
1122
  return count > 0 ? count - 1 : 0;
1123
+ } else if (this.settingsSection === 'shortcuts') {
1124
+ return Math.max(0, this.allScripts.length - 1);
1032
1125
  } else if (this.settingsSection === 'scripts') {
1033
1126
  return Math.max(0, this.allScripts.length - 1);
1034
1127
  }
@@ -1059,6 +1152,27 @@ class ProcessManager {
1059
1152
  saveConfig(this.config);
1060
1153
  this.applyFilters();
1061
1154
  this.settingsIndex = Math.max(0, Math.min(this.settingsIndex, (this.config.include?.length || 1) - 1));
1155
+ } else if (this.settingsSection === 'shortcuts') {
1156
+ const script = this.allScripts[this.settingsIndex];
1157
+ if (script && this.config.shortcuts) {
1158
+ // Find and delete any shortcut assigned to this script
1159
+ for (const [key, scriptName] of Object.entries(this.config.shortcuts)) {
1160
+ if (scriptName === script.name) {
1161
+ delete this.config.shortcuts[key];
1162
+ saveConfig(this.config);
1163
+ break;
1164
+ }
1165
+ }
1166
+ }
1167
+ }
1168
+ }
1169
+
1170
+ assignShortcut() {
1171
+ const script = this.allScripts[this.settingsIndex];
1172
+ if (script) {
1173
+ this.isAssigningShortcut = true;
1174
+ this.shortcutScriptName = script.name;
1175
+ this.buildSettingsUI();
1062
1176
  }
1063
1177
  }
1064
1178
 
@@ -1167,6 +1281,28 @@ class ProcessManager {
1167
1281
  }
1168
1282
  }
1169
1283
 
1284
+ handleRunCommandModalInput(keyName, keyEvent) {
1285
+ if (keyName === 'escape' || keyName === 'q') {
1286
+ this.showRunCommandModal = false;
1287
+ this.buildRunningUI();
1288
+ return;
1289
+ }
1290
+
1291
+ if (keyName === 'up' || keyName === 'k') {
1292
+ this.runCommandModalIndex = Math.max(0, this.runCommandModalIndex - 1);
1293
+ this.buildRunningUI();
1294
+ } else if (keyName === 'down' || keyName === 'j') {
1295
+ this.runCommandModalIndex = Math.min(this.allScripts.length - 1, this.runCommandModalIndex + 1);
1296
+ this.buildRunningUI();
1297
+ } else if (keyName === 'enter' || keyName === 'return') {
1298
+ const selectedScript = this.allScripts[this.runCommandModalIndex];
1299
+ if (selectedScript) {
1300
+ this.showRunCommandModal = false;
1301
+ this.executeCommand(selectedScript.name);
1302
+ }
1303
+ }
1304
+ }
1305
+
1170
1306
  getSplitMenuItems() {
1171
1307
  const allPanes = getAllPaneIds(this.paneRoot);
1172
1308
  const items = [
@@ -1494,6 +1630,24 @@ class ProcessManager {
1494
1630
  this.settingsContainer.add(inputBar);
1495
1631
  }
1496
1632
 
1633
+ // Input prompt if assigning shortcut
1634
+ if (this.isAssigningShortcut) {
1635
+ const inputBar = new BoxRenderable(this.renderer, {
1636
+ id: 'input-bar',
1637
+ border: ['left'],
1638
+ borderStyle: 'single',
1639
+ borderColor: COLORS.accent,
1640
+ paddingLeft: 1,
1641
+ marginBottom: 1,
1642
+ });
1643
+ const inputText = new TextRenderable(this.renderer, {
1644
+ id: 'input-text',
1645
+ content: t`${fg(COLORS.textDim)('Press a key to assign as shortcut for')} ${fg(COLORS.accent)(this.shortcutScriptName)} ${fg(COLORS.textDim)('(esc to cancel)')}`,
1646
+ });
1647
+ inputBar.add(inputText);
1648
+ this.settingsContainer.add(inputBar);
1649
+ }
1650
+
1497
1651
  // Combined content panel with all sections
1498
1652
  const contentPanel = new BoxRenderable(this.renderer, {
1499
1653
  id: 'content-panel',
@@ -1556,16 +1710,31 @@ class ProcessManager {
1556
1710
 
1557
1711
  contentPanel.add(leftColumn);
1558
1712
 
1559
- // Right column - Scripts list
1713
+ // Middle column - Quick Commands
1714
+ const shortcutsBox = new BoxRenderable(this.renderer, {
1715
+ id: 'shortcuts-box',
1716
+ flexDirection: 'column',
1717
+ border: true,
1718
+ borderStyle: 'rounded',
1719
+ borderColor: this.settingsSection === 'shortcuts' ? COLORS.borderFocused : COLORS.border,
1720
+ title: ' Quick Commands ',
1721
+ titleAlignment: 'left',
1722
+ flexGrow: 1,
1723
+ padding: 1,
1724
+ });
1725
+ this.buildShortcutsSectionContent(shortcutsBox);
1726
+ contentPanel.add(shortcutsBox);
1727
+
1728
+ // Right column - Script List
1560
1729
  const scriptsBox = new BoxRenderable(this.renderer, {
1561
1730
  id: 'scripts-box',
1562
1731
  flexDirection: 'column',
1563
1732
  border: true,
1564
1733
  borderStyle: 'rounded',
1565
1734
  borderColor: this.settingsSection === 'scripts' ? COLORS.borderFocused : COLORS.border,
1566
- title: ' Scripts ',
1735
+ title: ' Script List ',
1567
1736
  titleAlignment: 'left',
1568
- flexGrow: 2,
1737
+ flexGrow: 1,
1569
1738
  padding: 1,
1570
1739
  });
1571
1740
  this.buildScriptsSectionContent(scriptsBox);
@@ -1586,19 +1755,27 @@ class ProcessManager {
1586
1755
  gap: 2,
1587
1756
  });
1588
1757
 
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
- ];
1758
+ let shortcuts;
1759
+ if (this.isAddingPattern) {
1760
+ shortcuts = [
1761
+ { key: 'enter', desc: 'save' },
1762
+ { key: 'esc', desc: 'cancel' },
1763
+ ];
1764
+ } else if (this.isAssigningShortcut) {
1765
+ shortcuts = [
1766
+ { key: 'any key', desc: 'assign' },
1767
+ { key: 'esc', desc: 'cancel' },
1768
+ ];
1769
+ } else {
1770
+ shortcuts = [
1771
+ { key: 'tab', desc: 'section' },
1772
+ { key: 'space', desc: this.settingsSection === 'shortcuts' ? 'assign' : 'toggle' },
1773
+ { key: 'i', desc: 'add ignore' },
1774
+ { key: 'n', desc: 'add include' },
1775
+ { key: 'd', desc: 'delete' },
1776
+ { key: 'esc', desc: 'back' },
1777
+ ];
1778
+ }
1602
1779
 
1603
1780
  shortcuts.forEach(({ key, desc }) => {
1604
1781
  const shortcut = new TextRenderable(this.renderer, {
@@ -1679,6 +1856,38 @@ class ProcessManager {
1679
1856
  }
1680
1857
  }
1681
1858
 
1859
+ buildShortcutsSectionContent(container) {
1860
+ const shortcuts = this.config.shortcuts || {};
1861
+
1862
+ // Helper to find shortcut key for a script
1863
+ const getShortcutKey = (scriptName) => {
1864
+ for (const [key, name] of Object.entries(shortcuts)) {
1865
+ if (name === scriptName) return key;
1866
+ }
1867
+ return null;
1868
+ };
1869
+
1870
+ this.allScripts.forEach((script, idx) => {
1871
+ const isFocused = this.settingsSection === 'shortcuts' && idx === this.settingsIndex;
1872
+ const indicator = isFocused ? '>' : ' ';
1873
+ const shortcutKey = getShortcutKey(script.name);
1874
+ const processColor = this.processColors.get(script.name) || COLORS.text;
1875
+
1876
+ let content;
1877
+ if (shortcutKey) {
1878
+ content = t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(COLORS.warning)(`[${shortcutKey}]`)} ${fg(processColor)(script.displayName)}`;
1879
+ } else {
1880
+ content = t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(COLORS.textDim)('[ ]')} ${fg(processColor)(script.displayName)}`;
1881
+ }
1882
+
1883
+ const line = new TextRenderable(this.renderer, {
1884
+ id: `shortcut-item-${idx}`,
1885
+ content: content,
1886
+ });
1887
+ container.add(line);
1888
+ });
1889
+ }
1890
+
1682
1891
  buildScriptsSectionContent(container) {
1683
1892
  const ignorePatterns = this.config.ignore || [];
1684
1893
 
@@ -1691,9 +1900,16 @@ class ProcessManager {
1691
1900
  const processColor = this.processColors.get(script.name) || COLORS.text;
1692
1901
  const nameColor = isIgnored ? COLORS.textDim : processColor;
1693
1902
 
1903
+ let content;
1904
+ if (isIgnored) {
1905
+ content = t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(checkColor)(checkbox)} ${fg(nameColor)(script.displayName)} ${fg(COLORS.textDim)('(ignored)')}`;
1906
+ } else {
1907
+ content = t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(checkColor)(checkbox)} ${fg(nameColor)(script.displayName)}`;
1908
+ }
1909
+
1694
1910
  const line = new TextRenderable(this.renderer, {
1695
1911
  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)')}` : ''}`,
1912
+ content: content,
1697
1913
  });
1698
1914
  container.add(line);
1699
1915
  });
@@ -1723,6 +1939,15 @@ class ProcessManager {
1723
1939
  this.countdownInterval = null;
1724
1940
  }
1725
1941
 
1942
+ // Clean up command overlay process if running
1943
+ if (this.commandOverlayProcess && this.commandOverlayProcess.pid) {
1944
+ try {
1945
+ kill(this.commandOverlayProcess.pid, 'SIGKILL');
1946
+ } catch (err) {
1947
+ // Ignore
1948
+ }
1949
+ }
1950
+
1726
1951
  for (const [scriptName, proc] of this.processRefs.entries()) {
1727
1952
  try {
1728
1953
  if (proc.pid) {
@@ -1733,6 +1958,75 @@ class ProcessManager {
1733
1958
  }
1734
1959
  }
1735
1960
  }
1961
+
1962
+ executeCommand(scriptName) {
1963
+ // Initialize overlay state
1964
+ this.showCommandOverlay = true;
1965
+ this.commandOverlayOutput = [];
1966
+ this.commandOverlayScript = scriptName;
1967
+ this.commandOverlayStatus = 'running';
1968
+
1969
+ // Spawn the process
1970
+ const proc = spawn('npm', ['run', scriptName], {
1971
+ env: {
1972
+ ...process.env,
1973
+ FORCE_COLOR: '1',
1974
+ COLORTERM: 'truecolor',
1975
+ },
1976
+ shell: true,
1977
+ });
1978
+
1979
+ this.commandOverlayProcess = proc;
1980
+
1981
+ proc.stdout.on('data', (data) => {
1982
+ const text = data.toString();
1983
+ const lines = text.split('\n');
1984
+ lines.forEach(line => {
1985
+ if (line.trim()) {
1986
+ this.commandOverlayOutput.push(line);
1987
+ this.buildRunningUI();
1988
+ }
1989
+ });
1990
+ });
1991
+
1992
+ proc.stderr.on('data', (data) => {
1993
+ const text = data.toString();
1994
+ const lines = text.split('\n');
1995
+ lines.forEach(line => {
1996
+ if (line.trim()) {
1997
+ this.commandOverlayOutput.push(line);
1998
+ this.buildRunningUI();
1999
+ }
2000
+ });
2001
+ });
2002
+
2003
+ proc.on('exit', (code) => {
2004
+ this.commandOverlayStatus = code === 0 ? 'exited' : 'crashed';
2005
+ this.commandOverlayOutput.push('');
2006
+ this.commandOverlayOutput.push(`Process exited with code ${code}`);
2007
+ this.commandOverlayProcess = null;
2008
+ this.buildRunningUI();
2009
+ });
2010
+
2011
+ this.buildRunningUI();
2012
+ }
2013
+
2014
+ closeCommandOverlay() {
2015
+ // Kill the process if still running
2016
+ if (this.commandOverlayProcess && this.commandOverlayProcess.pid) {
2017
+ try {
2018
+ kill(this.commandOverlayProcess.pid, 'SIGKILL');
2019
+ } catch (err) {
2020
+ // Ignore
2021
+ }
2022
+ }
2023
+
2024
+ this.showCommandOverlay = false;
2025
+ this.commandOverlayOutput = [];
2026
+ this.commandOverlayScript = '';
2027
+ this.commandOverlayStatus = 'running';
2028
+ this.commandOverlayProcess = null;
2029
+ }
1736
2030
 
1737
2031
  buildSelectionUI() {
1738
2032
  // Remove old containers if they exist - use destroyRecursively to clean up all children
@@ -2335,6 +2629,196 @@ class ProcessManager {
2335
2629
  parent.add(overlay);
2336
2630
  }
2337
2631
 
2632
+ // Build command output overlay
2633
+ buildCommandOverlay(parent) {
2634
+ const statusIcon = this.commandOverlayStatus === 'running' ? '●' :
2635
+ this.commandOverlayStatus === 'exited' ? '✓' : '✖';
2636
+ const statusColor = this.commandOverlayStatus === 'running' ? COLORS.warning :
2637
+ this.commandOverlayStatus === 'exited' ? COLORS.success : COLORS.error;
2638
+ const title = ` ${statusIcon} ${this.commandOverlayScript} `;
2639
+
2640
+ // Create centered overlay with scrollable content
2641
+ const overlay = new BoxRenderable(this.renderer, {
2642
+ id: 'command-overlay',
2643
+ position: 'absolute',
2644
+ top: '10%',
2645
+ left: '10%',
2646
+ width: '80%',
2647
+ height: '80%',
2648
+ backgroundColor: COLORS.bg,
2649
+ border: true,
2650
+ borderStyle: 'rounded',
2651
+ borderColor: statusColor,
2652
+ title: title,
2653
+ padding: 0,
2654
+ flexDirection: 'column',
2655
+ });
2656
+
2657
+ // Scrollable output content
2658
+ const outputBox = new ScrollBoxRenderable(this.renderer, {
2659
+ id: 'command-output',
2660
+ height: Math.floor(this.renderer.height * 0.8) - 4,
2661
+ scrollX: false,
2662
+ scrollY: true,
2663
+ focusable: true,
2664
+ style: {
2665
+ rootOptions: {
2666
+ flexGrow: 1,
2667
+ paddingLeft: 1,
2668
+ paddingRight: 1,
2669
+ backgroundColor: COLORS.bg,
2670
+ },
2671
+ contentOptions: {
2672
+ backgroundColor: COLORS.bg,
2673
+ width: '100%',
2674
+ },
2675
+ },
2676
+ });
2677
+
2678
+ // Add output lines
2679
+ this.commandOverlayOutput.forEach((line, idx) => {
2680
+ const outputLine = new TextRenderable(this.renderer, {
2681
+ id: `cmd-output-${idx}`,
2682
+ content: line,
2683
+ });
2684
+ outputBox.content.add(outputLine);
2685
+ });
2686
+
2687
+ // Auto-scroll to bottom
2688
+ if (outputBox.scrollTo) {
2689
+ outputBox.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER });
2690
+ }
2691
+
2692
+ overlay.add(outputBox);
2693
+
2694
+ // Footer hint
2695
+ const hintBar = new BoxRenderable(this.renderer, {
2696
+ id: 'command-hint-bar',
2697
+ border: ['top'],
2698
+ borderStyle: 'single',
2699
+ borderColor: COLORS.border,
2700
+ paddingTop: 1,
2701
+ paddingLeft: 1,
2702
+ });
2703
+
2704
+ const hint = new TextRenderable(this.renderer, {
2705
+ id: 'command-hint',
2706
+ content: t`${fg(COLORS.textDim)('Press')} ${fg(COLORS.accent)('Esc')} ${fg(COLORS.textDim)('to close')}`,
2707
+ });
2708
+ hintBar.add(hint);
2709
+ overlay.add(hintBar);
2710
+
2711
+ parent.add(overlay);
2712
+ }
2713
+
2714
+ // Build run command picker modal
2715
+ buildRunCommandModal(parent) {
2716
+ // Create centered overlay
2717
+ const overlay = new BoxRenderable(this.renderer, {
2718
+ id: 'run-command-modal',
2719
+ position: 'absolute',
2720
+ top: '20%',
2721
+ left: '25%',
2722
+ width: '50%',
2723
+ height: '60%',
2724
+ backgroundColor: COLORS.bgLight,
2725
+ border: true,
2726
+ borderStyle: 'rounded',
2727
+ borderColor: COLORS.accent,
2728
+ title: ' Run Command ',
2729
+ padding: 1,
2730
+ flexDirection: 'column',
2731
+ });
2732
+
2733
+ // Scrollable list of scripts
2734
+ const listBox = new ScrollBoxRenderable(this.renderer, {
2735
+ id: 'run-command-list',
2736
+ height: Math.floor(this.renderer.height * 0.6) - 4,
2737
+ scrollX: false,
2738
+ scrollY: true,
2739
+ focusable: true,
2740
+ style: {
2741
+ rootOptions: {
2742
+ flexGrow: 1,
2743
+ backgroundColor: COLORS.bgLight,
2744
+ },
2745
+ contentOptions: {
2746
+ backgroundColor: COLORS.bgLight,
2747
+ width: '100%',
2748
+ },
2749
+ },
2750
+ });
2751
+
2752
+ this.allScripts.forEach((script, idx) => {
2753
+ const isFocused = idx === this.runCommandModalIndex;
2754
+ const indicator = isFocused ? '>' : ' ';
2755
+ const bgColor = isFocused ? COLORS.bgHighlight : null;
2756
+ const processColor = this.processColors.get(script.name) || COLORS.text;
2757
+
2758
+ // Check if this script has a shortcut
2759
+ const shortcuts = this.config.shortcuts || {};
2760
+ let shortcutKey = null;
2761
+ for (const [key, scriptName] of Object.entries(shortcuts)) {
2762
+ if (scriptName === script.name) {
2763
+ shortcutKey = key;
2764
+ break;
2765
+ }
2766
+ }
2767
+
2768
+ const itemContainer = new BoxRenderable(this.renderer, {
2769
+ id: `run-cmd-item-${idx}`,
2770
+ backgroundColor: bgColor,
2771
+ paddingLeft: 1,
2772
+ });
2773
+
2774
+ let content;
2775
+ if (shortcutKey) {
2776
+ content = t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(processColor)(script.displayName)} ${fg(COLORS.textDim)(`(${shortcutKey})`)}`;
2777
+ } else {
2778
+ content = t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(processColor)(script.displayName)}`;
2779
+ }
2780
+
2781
+ const itemText = new TextRenderable(this.renderer, {
2782
+ id: `run-cmd-text-${idx}`,
2783
+ content: content,
2784
+ });
2785
+
2786
+ itemContainer.add(itemText);
2787
+ listBox.content.add(itemContainer);
2788
+ });
2789
+
2790
+ // Auto-scroll to focused item
2791
+ if (listBox.scrollTo) {
2792
+ const lineHeight = 1;
2793
+ const viewportHeight = Math.floor(this.renderer.height * 0.6) - 4;
2794
+ const focusedY = this.runCommandModalIndex * lineHeight;
2795
+ if (focusedY < listBox.scrollTop || focusedY >= listBox.scrollTop + viewportHeight) {
2796
+ listBox.scrollTo({ x: 0, y: Math.max(0, focusedY - Math.floor(viewportHeight / 2)) });
2797
+ }
2798
+ }
2799
+
2800
+ overlay.add(listBox);
2801
+
2802
+ // Footer hint
2803
+ const hintBar = new BoxRenderable(this.renderer, {
2804
+ id: 'run-cmd-hint-bar',
2805
+ border: ['top'],
2806
+ borderStyle: 'single',
2807
+ borderColor: COLORS.border,
2808
+ paddingTop: 1,
2809
+ paddingLeft: 1,
2810
+ });
2811
+
2812
+ const hint = new TextRenderable(this.renderer, {
2813
+ id: 'run-cmd-hint',
2814
+ content: t`${fg(COLORS.textDim)('↑/↓ navigate')} ${fg(COLORS.accent)('Enter')} ${fg(COLORS.textDim)('run')} ${fg(COLORS.accent)('Esc')} ${fg(COLORS.textDim)('close')}`,
2815
+ });
2816
+ hintBar.add(hint);
2817
+ overlay.add(hintBar);
2818
+
2819
+ parent.add(overlay);
2820
+ }
2821
+
2338
2822
  buildRunningUI() {
2339
2823
  // Save scroll positions before destroying
2340
2824
  for (const [paneId, scrollBox] of this.paneScrollBoxes.entries()) {
@@ -2562,6 +3046,16 @@ class ProcessManager {
2562
3046
  this.buildSplitMenuOverlay(mainContainer);
2563
3047
  }
2564
3048
 
3049
+ // Add run command modal if active
3050
+ if (this.showRunCommandModal) {
3051
+ this.buildRunCommandModal(mainContainer);
3052
+ }
3053
+
3054
+ // Add command output overlay if active
3055
+ if (this.showCommandOverlay) {
3056
+ this.buildCommandOverlay(mainContainer);
3057
+ }
3058
+
2565
3059
  this.renderer.root.add(mainContainer);
2566
3060
  this.runningContainer = mainContainer;
2567
3061
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "startall",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {