startall 0.0.20 → 0.0.22

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 +17 -0
  2. package/index.js +596 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -68,6 +68,11 @@ Traditional solutions fall short:
68
68
  - **Settings panel**: Configure ignore/include patterns, shortcuts, and more (`o`)
69
69
  - Wildcard support (`*`) for pattern matching
70
70
  - Per-script visibility toggles
71
+ - **Git integration**: Commit and push directly from the TUI (`g`)
72
+ - Auto-detects git repositories
73
+ - Shows branch name in the footer
74
+ - View staged, modified, and untracked files
75
+ - Stage all changes, write a commit message, and push - all without leaving the TUI
71
76
  - **Keyboard & mouse support**: Full keyboard navigation + mouse clicking/scrolling
72
77
  - **VSCode integration**: Optimized for VSCode integrated terminal
73
78
 
@@ -150,6 +155,7 @@ That's it! The TUI will:
150
155
  - `Mouse wheel` - Scroll output
151
156
 
152
157
  *Other:*
158
+ - `g` - Open git modal (commit & push, only in git repos)
153
159
  - `o` - Open settings
154
160
  - `q` - Quit (stops all processes)
155
161
  - `Ctrl+C` - Force quit
@@ -171,6 +177,17 @@ That's it! The TUI will:
171
177
  **Quick Commands Overlay:**
172
178
  - `Esc` - Close overlay and stop command (if running)
173
179
 
180
+ **Git Modal** (press `g` from Running Screen, only in git repos):
181
+ - `a` - Stage all changes
182
+ - `c` - Start writing commit message
183
+ - `p` - Push to remote
184
+ - `r` - Refresh git status
185
+ - `↑`/`↓` - Navigate file list
186
+ - `Esc` or `q` - Close git modal
187
+ - When writing commit message:
188
+ - `Enter` - Stage all and commit
189
+ - `Esc` - Cancel and go back to status
190
+
174
191
  ## Why Build This?
175
192
 
176
193
  Existing tools either:
package/index.js CHANGED
@@ -331,6 +331,107 @@ function copyToClipboard(text) {
331
331
  return copied;
332
332
  }
333
333
 
334
+ // Git utility functions
335
+ function isGitRepo() {
336
+ try {
337
+ execSync('git rev-parse --is-inside-work-tree', { stdio: 'pipe', windowsHide: true });
338
+ return true;
339
+ } catch {
340
+ return false;
341
+ }
342
+ }
343
+
344
+ function getGitBranch() {
345
+ try {
346
+ return execSync('git branch --show-current', { stdio: 'pipe', windowsHide: true }).toString().trim();
347
+ } catch {
348
+ return '';
349
+ }
350
+ }
351
+
352
+ function getGitStatus() {
353
+ try {
354
+ const output = execSync('git status --porcelain', { stdio: 'pipe', windowsHide: true }).toString().trim();
355
+ const staged = [];
356
+ const modified = [];
357
+ const untracked = [];
358
+
359
+ if (!output) return { staged, modified, untracked, clean: true };
360
+
361
+ output.split('\n').forEach(line => {
362
+ if (!line) return;
363
+ const indexStatus = line[0];
364
+ const workTreeStatus = line[1];
365
+ const filePath = line.substring(3);
366
+
367
+ // Staged changes (index has a non-space, non-? status)
368
+ if (indexStatus !== ' ' && indexStatus !== '?') {
369
+ staged.push({ status: indexStatus, file: filePath });
370
+ }
371
+ // Unstaged modifications
372
+ if (workTreeStatus === 'M' || workTreeStatus === 'D') {
373
+ modified.push({ status: workTreeStatus, file: filePath });
374
+ }
375
+ // Untracked files
376
+ if (indexStatus === '?' && workTreeStatus === '?') {
377
+ untracked.push({ file: filePath });
378
+ }
379
+ });
380
+
381
+ return { staged, modified, untracked, clean: false };
382
+ } catch {
383
+ return { staged: [], modified: [], untracked: [], clean: true, error: true };
384
+ }
385
+ }
386
+
387
+ function getGitRemoteStatus() {
388
+ try {
389
+ // Fetch to get latest remote state (silently)
390
+ const localRef = execSync('git rev-parse HEAD', { stdio: 'pipe', windowsHide: true }).toString().trim();
391
+ let remoteRef = '';
392
+ try {
393
+ remoteRef = execSync('git rev-parse @{u}', { stdio: 'pipe', windowsHide: true }).toString().trim();
394
+ } catch {
395
+ return { ahead: 0, behind: 0, hasRemote: false };
396
+ }
397
+ const ahead = parseInt(execSync(`git rev-list --count ${remoteRef}..${localRef}`, { stdio: 'pipe', windowsHide: true }).toString().trim()) || 0;
398
+ const behind = parseInt(execSync(`git rev-list --count ${localRef}..${remoteRef}`, { stdio: 'pipe', windowsHide: true }).toString().trim()) || 0;
399
+ return { ahead, behind, hasRemote: true };
400
+ } catch {
401
+ return { ahead: 0, behind: 0, hasRemote: false };
402
+ }
403
+ }
404
+
405
+ function gitStageAll() {
406
+ try {
407
+ execSync('git add -A', { stdio: 'pipe', windowsHide: true });
408
+ return { success: true };
409
+ } catch (err) {
410
+ return { success: false, error: err.stderr?.toString() || err.message };
411
+ }
412
+ }
413
+
414
+ function gitCommit(message) {
415
+ try {
416
+ const output = execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { stdio: 'pipe', windowsHide: true }).toString().trim();
417
+ return { success: true, output };
418
+ } catch (err) {
419
+ return { success: false, error: err.stderr?.toString() || err.stdout?.toString() || err.message };
420
+ }
421
+ }
422
+
423
+ function gitPush() {
424
+ try {
425
+ const output = execSync('git push', { stdio: 'pipe', windowsHide: true, timeout: 30000 }).toString().trim();
426
+ return { success: true, output: output || 'Pushed successfully' };
427
+ } catch (err) {
428
+ return { success: false, error: err.stderr?.toString() || err.message };
429
+ }
430
+ }
431
+
432
+ // Detect git repo once at startup
433
+ const IS_GIT_REPO = isGitRepo();
434
+
334
435
  // Color palette (inspired by Tokyo Night theme)
335
436
  const COLORS = {
336
437
  border: '#3b4261',
@@ -513,6 +614,17 @@ class ProcessManager {
513
614
  this.copyFeedbackMessage = ''; // Temporary feedback message after copying
514
615
  this.copyFeedbackTimer = null; // Timer to clear feedback message
515
616
 
617
+ // Git modal state
618
+ this.showGitModal = false; // Whether the git modal is visible
619
+ this.gitModalPhase = 'status'; // 'status' | 'commit' | 'committing' | 'pushing' | 'result'
620
+ this.gitBranch = ''; // Current git branch name
621
+ this.gitStatus = null; // Git status object { staged, modified, untracked, clean }
622
+ this.gitRemoteStatus = null; // Remote status { ahead, behind, hasRemote }
623
+ this.gitCommitMessage = ''; // Commit message being typed
624
+ this.gitModalOutput = []; // Output/status messages for the modal
625
+ this.gitModalSelectedIndex = 0; // Selected file index in the status list
626
+ this.gitStageSelection = 'all'; // 'all' for stage all (future: individual file staging)
627
+
516
628
  // Assign colors to each script
517
629
  this.processColors = new Map();
518
630
  const colors = ['#7aa2f7', '#bb9af7', '#9ece6a', '#f7768e', '#e0af68', '#73daca'];
@@ -548,6 +660,16 @@ class ProcessManager {
548
660
  return;
549
661
  }
550
662
 
663
+ // Handle Ctrl+L - clear screen buffer and redraw
664
+ if (key.ctrl && key.name === 'l') {
665
+ if (this.phase === 'running') {
666
+ this.outputLines = [];
667
+ this.totalLinesReceived = 0;
668
+ this.buildRunningUI();
669
+ }
670
+ return;
671
+ }
672
+
551
673
  this.handleInput(key.name, key);
552
674
  this.render();
553
675
  });
@@ -616,6 +738,12 @@ class ProcessManager {
616
738
  return;
617
739
  }
618
740
 
741
+ // Handle git modal
742
+ if (this.showGitModal) {
743
+ this.handleGitModalInput(keyName, keyEvent);
744
+ return;
745
+ }
746
+
619
747
  // Handle run command modal
620
748
  if (this.showRunCommandModal) {
621
749
  this.handleRunCommandModalInput(keyName, keyEvent);
@@ -857,6 +985,9 @@ class ProcessManager {
857
985
  } else if (keyName === 'y') {
858
986
  // Enter copy mode (select text to copy)
859
987
  this.enterCopyMode();
988
+ } else if (keyName === 'g' && IS_GIT_REPO) {
989
+ // Open git modal (commit & push)
990
+ this.openGitModal();
860
991
  } else if (keyName && keyName.length === 1 && !keyEvent.ctrl && !keyEvent.meta && !keyEvent.shift) {
861
992
  // Check if this key is a custom shortcut
862
993
  const shortcuts = this.config.shortcuts || {};
@@ -2396,6 +2527,157 @@ class ProcessManager {
2396
2527
  this.commandOverlayStatus = 'running';
2397
2528
  this.commandOverlayProcess = null;
2398
2529
  }
2530
+
2531
+ // Open the git modal and refresh status
2532
+ openGitModal() {
2533
+ this.showGitModal = true;
2534
+ this.gitModalPhase = 'status';
2535
+ this.gitCommitMessage = '';
2536
+ this.gitModalOutput = [];
2537
+ this.gitModalSelectedIndex = 0;
2538
+ this.refreshGitStatus();
2539
+ this.buildRunningUI();
2540
+ }
2541
+
2542
+ // Close the git modal
2543
+ closeGitModal() {
2544
+ this.showGitModal = false;
2545
+ this.gitModalPhase = 'status';
2546
+ this.gitCommitMessage = '';
2547
+ this.gitModalOutput = [];
2548
+ }
2549
+
2550
+ // Refresh git status data
2551
+ refreshGitStatus() {
2552
+ this.gitBranch = getGitBranch();
2553
+ this.gitStatus = getGitStatus();
2554
+ this.gitRemoteStatus = getGitRemoteStatus();
2555
+ }
2556
+
2557
+ // Handle keyboard input for the git modal
2558
+ handleGitModalInput(keyName, keyEvent) {
2559
+ if (this.gitModalPhase === 'status') {
2560
+ if (keyName === 'escape' || keyName === 'q') {
2561
+ this.closeGitModal();
2562
+ this.buildRunningUI();
2563
+ } else if (keyName === 'c') {
2564
+ // Start commit flow - switch to commit message input
2565
+ this.gitModalPhase = 'commit';
2566
+ this.gitCommitMessage = '';
2567
+ this.buildRunningUI();
2568
+ } else if (keyName === 'a') {
2569
+ // Stage all changes
2570
+ const result = gitStageAll();
2571
+ if (result.success) {
2572
+ this.gitModalOutput = ['All changes staged.'];
2573
+ } else {
2574
+ this.gitModalOutput = [`Error staging: ${result.error}`];
2575
+ }
2576
+ this.refreshGitStatus();
2577
+ this.buildRunningUI();
2578
+ } else if (keyName === 'p') {
2579
+ // Push
2580
+ this.gitModalPhase = 'pushing';
2581
+ this.gitModalOutput = ['Pushing...'];
2582
+ this.buildRunningUI();
2583
+ // Push asynchronously using setTimeout to allow UI update
2584
+ setTimeout(() => {
2585
+ const result = gitPush();
2586
+ if (result.success) {
2587
+ this.gitModalOutput = [result.output];
2588
+ } else {
2589
+ this.gitModalOutput = [`Push failed: ${result.error}`];
2590
+ }
2591
+ this.gitModalPhase = 'result';
2592
+ this.refreshGitStatus();
2593
+ this.buildRunningUI();
2594
+ }, 10);
2595
+ } else if (keyName === 'r') {
2596
+ // Refresh status
2597
+ this.refreshGitStatus();
2598
+ this.gitModalOutput = ['Status refreshed.'];
2599
+ this.buildRunningUI();
2600
+ } else if (keyName === 'up' || keyName === 'k') {
2601
+ this.gitModalSelectedIndex = Math.max(0, this.gitModalSelectedIndex - 1);
2602
+ this.buildRunningUI();
2603
+ } else if (keyName === 'down' || keyName === 'j') {
2604
+ const totalFiles = (this.gitStatus?.staged?.length || 0) + (this.gitStatus?.modified?.length || 0) + (this.gitStatus?.untracked?.length || 0);
2605
+ this.gitModalSelectedIndex = Math.min(Math.max(0, totalFiles - 1), this.gitModalSelectedIndex + 1);
2606
+ this.buildRunningUI();
2607
+ }
2608
+ } else if (this.gitModalPhase === 'commit') {
2609
+ if (keyName === 'escape') {
2610
+ this.gitModalPhase = 'status';
2611
+ this.gitCommitMessage = '';
2612
+ this.buildRunningUI();
2613
+ } else if (keyName === 'enter' || keyName === 'return') {
2614
+ if (this.gitCommitMessage.trim()) {
2615
+ // Stage all and commit
2616
+ this.gitModalPhase = 'committing';
2617
+ this.gitModalOutput = ['Staging and committing...'];
2618
+ this.buildRunningUI();
2619
+ setTimeout(() => {
2620
+ // Stage all first
2621
+ const stageResult = gitStageAll();
2622
+ if (!stageResult.success) {
2623
+ this.gitModalOutput = [`Error staging: ${stageResult.error}`];
2624
+ this.gitModalPhase = 'result';
2625
+ this.buildRunningUI();
2626
+ return;
2627
+ }
2628
+ // Then commit
2629
+ const commitResult = gitCommit(this.gitCommitMessage.trim());
2630
+ if (commitResult.success) {
2631
+ this.gitModalOutput = [commitResult.output, '', 'Commit successful! Press p to push, esc to close.'];
2632
+ this.gitModalPhase = 'status';
2633
+ } else {
2634
+ this.gitModalOutput = [`Commit failed: ${commitResult.error}`];
2635
+ this.gitModalPhase = 'result';
2636
+ }
2637
+ this.gitCommitMessage = '';
2638
+ this.refreshGitStatus();
2639
+ this.buildRunningUI();
2640
+ }, 10);
2641
+ }
2642
+ } else if (keyName === 'backspace') {
2643
+ this.gitCommitMessage = this.gitCommitMessage.slice(0, -1);
2644
+ this.buildRunningUI();
2645
+ } else if (keyName === 'space') {
2646
+ this.gitCommitMessage += ' ';
2647
+ this.buildRunningUI();
2648
+ } else if (keyName && keyName.length === 1 && !keyEvent.ctrl && !keyEvent.meta) {
2649
+ this.gitCommitMessage += keyName;
2650
+ this.buildRunningUI();
2651
+ }
2652
+ } else if (this.gitModalPhase === 'result') {
2653
+ // Any key returns to status or closes
2654
+ if (keyName === 'escape' || keyName === 'q') {
2655
+ this.closeGitModal();
2656
+ this.buildRunningUI();
2657
+ } else if (keyName === 'p') {
2658
+ // Allow pushing from result phase
2659
+ this.gitModalPhase = 'pushing';
2660
+ this.gitModalOutput = ['Pushing...'];
2661
+ this.buildRunningUI();
2662
+ setTimeout(() => {
2663
+ const result = gitPush();
2664
+ if (result.success) {
2665
+ this.gitModalOutput = [result.output];
2666
+ } else {
2667
+ this.gitModalOutput = [`Push failed: ${result.error}`];
2668
+ }
2669
+ this.gitModalPhase = 'result';
2670
+ this.refreshGitStatus();
2671
+ this.buildRunningUI();
2672
+ }, 10);
2673
+ } else {
2674
+ this.gitModalPhase = 'status';
2675
+ this.refreshGitStatus();
2676
+ this.buildRunningUI();
2677
+ }
2678
+ }
2679
+ // 'committing' and 'pushing' phases ignore input (busy)
2680
+ }
2399
2681
 
2400
2682
  buildSelectionUI() {
2401
2683
  // Remove old containers if they exist - use destroyRecursively to clean up all children
@@ -2545,6 +2827,18 @@ class ProcessManager {
2545
2827
  leftSide.add(titleText);
2546
2828
  this.headerText = titleText; // Save reference for countdown updates
2547
2829
 
2830
+ // Git branch indicator
2831
+ if (IS_GIT_REPO) {
2832
+ const branch = getGitBranch();
2833
+ if (branch) {
2834
+ const branchIndicator = new TextRenderable(this.renderer, {
2835
+ id: 'git-branch-indicator',
2836
+ content: t`${fg(COLORS.magenta)('\u2387')} ${fg(COLORS.magenta)(branch)}`,
2837
+ });
2838
+ leftSide.add(branchIndicator);
2839
+ }
2840
+ }
2841
+
2548
2842
  // VS Code hint
2549
2843
  if (IS_VSCODE) {
2550
2844
  const vscodeHint = new TextRenderable(this.renderer, {
@@ -3458,6 +3752,288 @@ class ProcessManager {
3458
3752
  parent.add(overlay);
3459
3753
  }
3460
3754
 
3755
+ // Build git commit & push modal overlay
3756
+ buildGitModal(parent) {
3757
+ const branch = this.gitBranch || 'unknown';
3758
+ const status = this.gitStatus || { staged: [], modified: [], untracked: [], clean: true };
3759
+ const remote = this.gitRemoteStatus || { ahead: 0, behind: 0, hasRemote: false };
3760
+
3761
+ // Title with branch name and status
3762
+ let titleIcon = '';
3763
+ if (this.gitModalPhase === 'committing') titleIcon = '...';
3764
+ else if (this.gitModalPhase === 'pushing') titleIcon = '...';
3765
+ else titleIcon = '';
3766
+ const title = ` Git: ${branch} ${titleIcon}`;
3767
+
3768
+ // Create centered overlay
3769
+ const overlay = new BoxRenderable(this.renderer, {
3770
+ id: 'git-modal',
3771
+ position: 'absolute',
3772
+ top: '10%',
3773
+ left: '15%',
3774
+ width: '70%',
3775
+ height: '80%',
3776
+ backgroundColor: COLORS.bg,
3777
+ border: true,
3778
+ borderStyle: 'rounded',
3779
+ borderColor: COLORS.accent,
3780
+ title: title,
3781
+ padding: 1,
3782
+ flexDirection: 'column',
3783
+ });
3784
+
3785
+ // Remote status line
3786
+ if (remote.hasRemote) {
3787
+ let remoteText = '';
3788
+ if (remote.ahead > 0 && remote.behind > 0) {
3789
+ remoteText = `${remote.ahead} ahead, ${remote.behind} behind remote`;
3790
+ } else if (remote.ahead > 0) {
3791
+ remoteText = `${remote.ahead} commit${remote.ahead > 1 ? 's' : ''} ahead of remote`;
3792
+ } else if (remote.behind > 0) {
3793
+ remoteText = `${remote.behind} commit${remote.behind > 1 ? 's' : ''} behind remote`;
3794
+ } else {
3795
+ remoteText = 'Up to date with remote';
3796
+ }
3797
+ const remoteColor = (remote.ahead > 0 || remote.behind > 0) ? COLORS.warning : COLORS.success;
3798
+ const remoteLine = new TextRenderable(this.renderer, {
3799
+ id: 'git-remote-status',
3800
+ content: t`${fg(remoteColor)(remoteText)}`,
3801
+ });
3802
+ overlay.add(remoteLine);
3803
+ } else {
3804
+ const noRemote = new TextRenderable(this.renderer, {
3805
+ id: 'git-no-remote',
3806
+ content: t`${fg(COLORS.textDim)('No remote tracking branch')}`,
3807
+ });
3808
+ overlay.add(noRemote);
3809
+ }
3810
+
3811
+ // Separator
3812
+ const sep1 = new BoxRenderable(this.renderer, {
3813
+ id: 'git-sep1',
3814
+ border: ['bottom'],
3815
+ borderStyle: 'single',
3816
+ borderColor: COLORS.border,
3817
+ marginTop: 1,
3818
+ marginBottom: 1,
3819
+ width: '100%',
3820
+ });
3821
+ overlay.add(sep1);
3822
+
3823
+ // Commit message input area (shown when in commit phase)
3824
+ if (this.gitModalPhase === 'commit') {
3825
+ const commitLabel = new TextRenderable(this.renderer, {
3826
+ id: 'git-commit-label',
3827
+ content: t`${fg(COLORS.accent)('Commit message:')}`,
3828
+ });
3829
+ overlay.add(commitLabel);
3830
+
3831
+ const commitInput = new BoxRenderable(this.renderer, {
3832
+ id: 'git-commit-input-box',
3833
+ border: true,
3834
+ borderStyle: 'single',
3835
+ borderColor: COLORS.accent,
3836
+ padding: 1,
3837
+ marginTop: 1,
3838
+ marginBottom: 1,
3839
+ width: '100%',
3840
+ });
3841
+
3842
+ const commitText = new TextRenderable(this.renderer, {
3843
+ id: 'git-commit-text',
3844
+ content: t`${fg(COLORS.text)(this.gitCommitMessage)}${fg(COLORS.accent)('_')}`,
3845
+ });
3846
+ commitInput.add(commitText);
3847
+ overlay.add(commitInput);
3848
+
3849
+ const commitHint = new TextRenderable(this.renderer, {
3850
+ id: 'git-commit-hint',
3851
+ content: t`${fg(COLORS.textDim)('All changes will be staged and committed.')}`,
3852
+ });
3853
+ overlay.add(commitHint);
3854
+ } else if (this.gitModalPhase === 'committing' || this.gitModalPhase === 'pushing') {
3855
+ // Show busy indicator
3856
+ const busyText = this.gitModalPhase === 'committing' ? 'Committing...' : 'Pushing...';
3857
+ const busyLine = new TextRenderable(this.renderer, {
3858
+ id: 'git-busy',
3859
+ content: t`${fg(COLORS.warning)(busyText)}`,
3860
+ });
3861
+ overlay.add(busyLine);
3862
+ } else {
3863
+ // Status view or result view - show file lists
3864
+
3865
+ // Show output messages if any
3866
+ if (this.gitModalOutput.length > 0) {
3867
+ this.gitModalOutput.forEach((line, idx) => {
3868
+ const outputLine = new TextRenderable(this.renderer, {
3869
+ id: `git-output-${idx}`,
3870
+ content: t`${fg(COLORS.success)(line)}`,
3871
+ });
3872
+ overlay.add(outputLine);
3873
+ });
3874
+
3875
+ const outputSep = new BoxRenderable(this.renderer, {
3876
+ id: 'git-output-sep',
3877
+ border: ['bottom'],
3878
+ borderStyle: 'single',
3879
+ borderColor: COLORS.border,
3880
+ marginTop: 1,
3881
+ marginBottom: 1,
3882
+ width: '100%',
3883
+ });
3884
+ overlay.add(outputSep);
3885
+ }
3886
+
3887
+ if (status.clean) {
3888
+ const cleanText = new TextRenderable(this.renderer, {
3889
+ id: 'git-clean',
3890
+ content: t`${fg(COLORS.success)('Working tree clean - nothing to commit.')}`,
3891
+ });
3892
+ overlay.add(cleanText);
3893
+ } else {
3894
+ // Scrollable file list
3895
+ const fileListHeight = Math.floor(this.renderer.height * 0.8) - 14;
3896
+ const fileList = new ScrollBoxRenderable(this.renderer, {
3897
+ id: 'git-file-list',
3898
+ height: Math.max(5, fileListHeight),
3899
+ scrollX: false,
3900
+ scrollY: true,
3901
+ focusable: true,
3902
+ style: {
3903
+ rootOptions: {
3904
+ flexGrow: 1,
3905
+ backgroundColor: COLORS.bg,
3906
+ },
3907
+ contentOptions: {
3908
+ backgroundColor: COLORS.bg,
3909
+ width: '100%',
3910
+ },
3911
+ },
3912
+ });
3913
+
3914
+ let fileIndex = 0;
3915
+
3916
+ // Staged files
3917
+ if (status.staged.length > 0) {
3918
+ const stagedHeader = new TextRenderable(this.renderer, {
3919
+ id: 'git-staged-header',
3920
+ content: t`${fg(COLORS.success)(bold('Staged Changes'))} ${fg(COLORS.textDim)(`(${status.staged.length})`)}`,
3921
+ });
3922
+ fileList.content.add(stagedHeader);
3923
+
3924
+ status.staged.forEach((file, idx) => {
3925
+ const isFocused = fileIndex === this.gitModalSelectedIndex;
3926
+ const indicator = isFocused ? '>' : ' ';
3927
+ const statusLabel = file.status === 'A' ? 'new' : file.status === 'M' ? 'mod' : file.status === 'D' ? 'del' : file.status === 'R' ? 'ren' : file.status;
3928
+
3929
+ const fileLine = new TextRenderable(this.renderer, {
3930
+ id: `git-staged-${idx}`,
3931
+ content: t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(COLORS.success)(statusLabel)} ${fg(COLORS.text)(file.file)}`,
3932
+ });
3933
+ fileList.content.add(fileLine);
3934
+ fileIndex++;
3935
+ });
3936
+ }
3937
+
3938
+ // Modified (unstaged) files
3939
+ if (status.modified.length > 0) {
3940
+ const modHeader = new TextRenderable(this.renderer, {
3941
+ id: 'git-modified-header',
3942
+ content: t`${fg(COLORS.warning)(bold('Unstaged Changes'))} ${fg(COLORS.textDim)(`(${status.modified.length})`)}`,
3943
+ });
3944
+ fileList.content.add(modHeader);
3945
+
3946
+ status.modified.forEach((file, idx) => {
3947
+ const isFocused = fileIndex === this.gitModalSelectedIndex;
3948
+ const indicator = isFocused ? '>' : ' ';
3949
+ const statusLabel = file.status === 'M' ? 'mod' : file.status === 'D' ? 'del' : file.status;
3950
+
3951
+ const fileLine = new TextRenderable(this.renderer, {
3952
+ id: `git-modified-${idx}`,
3953
+ content: t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(COLORS.warning)(statusLabel)} ${fg(COLORS.text)(file.file)}`,
3954
+ });
3955
+ fileList.content.add(fileLine);
3956
+ fileIndex++;
3957
+ });
3958
+ }
3959
+
3960
+ // Untracked files
3961
+ if (status.untracked.length > 0) {
3962
+ const untrackedHeader = new TextRenderable(this.renderer, {
3963
+ id: 'git-untracked-header',
3964
+ content: t`${fg(COLORS.error)(bold('Untracked Files'))} ${fg(COLORS.textDim)(`(${status.untracked.length})`)}`,
3965
+ });
3966
+ fileList.content.add(untrackedHeader);
3967
+
3968
+ status.untracked.forEach((file, idx) => {
3969
+ const isFocused = fileIndex === this.gitModalSelectedIndex;
3970
+ const indicator = isFocused ? '>' : ' ';
3971
+
3972
+ const fileLine = new TextRenderable(this.renderer, {
3973
+ id: `git-untracked-${idx}`,
3974
+ content: t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(COLORS.error)('new')} ${fg(COLORS.textDim)(file.file)}`,
3975
+ });
3976
+ fileList.content.add(fileLine);
3977
+ fileIndex++;
3978
+ });
3979
+ }
3980
+
3981
+ overlay.add(fileList);
3982
+ }
3983
+ }
3984
+
3985
+ // Footer hint bar
3986
+ const hintBar = new BoxRenderable(this.renderer, {
3987
+ id: 'git-hint-bar',
3988
+ border: ['top'],
3989
+ borderStyle: 'single',
3990
+ borderColor: COLORS.border,
3991
+ paddingTop: 1,
3992
+ paddingLeft: 1,
3993
+ marginTop: 1,
3994
+ flexDirection: 'row',
3995
+ gap: 2,
3996
+ });
3997
+
3998
+ if (this.gitModalPhase === 'commit') {
3999
+ const hints = [
4000
+ { key: 'enter', desc: 'commit', color: COLORS.success },
4001
+ { key: 'esc', desc: 'cancel', color: COLORS.error },
4002
+ ];
4003
+ hints.forEach(({ key, desc, color }) => {
4004
+ const hint = new TextRenderable(this.renderer, {
4005
+ id: `git-hint-${key}`,
4006
+ content: t`${fg(color)(key)} ${fg(COLORS.textDim)(desc)}`,
4007
+ });
4008
+ hintBar.add(hint);
4009
+ });
4010
+ } else if (this.gitModalPhase === 'committing' || this.gitModalPhase === 'pushing') {
4011
+ const hint = new TextRenderable(this.renderer, {
4012
+ id: 'git-hint-busy',
4013
+ content: t`${fg(COLORS.warning)('Please wait...')}`,
4014
+ });
4015
+ hintBar.add(hint);
4016
+ } else {
4017
+ const hints = [
4018
+ { key: 'c', desc: 'commit', color: COLORS.success },
4019
+ { key: 'a', desc: 'stage all', color: COLORS.warning },
4020
+ { key: 'p', desc: 'push', color: COLORS.cyan },
4021
+ { key: 'r', desc: 'refresh', color: COLORS.magenta },
4022
+ { key: 'esc', desc: 'close', color: COLORS.error },
4023
+ ];
4024
+ hints.forEach(({ key, desc, color }) => {
4025
+ const hint = new TextRenderable(this.renderer, {
4026
+ id: `git-hint-${key}`,
4027
+ content: t`${fg(color)(key)} ${fg(COLORS.textDim)(desc)}`,
4028
+ });
4029
+ hintBar.add(hint);
4030
+ });
4031
+ }
4032
+
4033
+ overlay.add(hintBar);
4034
+ parent.add(overlay);
4035
+ }
4036
+
3461
4037
  buildRunningUI() {
3462
4038
  // Save scroll positions before destroying
3463
4039
  for (const [paneId, scrollBox] of this.paneScrollBoxes.entries()) {
@@ -3600,6 +4176,19 @@ class ProcessManager {
3600
4176
  });
3601
4177
  leftSide.add(statusIndicator);
3602
4178
 
4179
+ // Git branch indicator
4180
+ if (IS_GIT_REPO) {
4181
+ const branch = this.gitBranch || getGitBranch();
4182
+ if (branch) {
4183
+ this.gitBranch = branch;
4184
+ const branchIndicator = new TextRenderable(this.renderer, {
4185
+ id: 'git-branch-indicator',
4186
+ content: t`${fg(COLORS.magenta)('\u2387')} ${fg(COLORS.magenta)(branch)}`,
4187
+ });
4188
+ leftSide.add(branchIndicator);
4189
+ }
4190
+ }
4191
+
3603
4192
  // VS Code hint
3604
4193
  if (IS_VSCODE) {
3605
4194
  const vscodeHint = new TextRenderable(this.renderer, {
@@ -3711,11 +4300,13 @@ class ProcessManager {
3711
4300
  { key: '/', desc: 'filter', color: COLORS.cyan },
3712
4301
  { key: 'c', desc: 'color', color: COLORS.magenta },
3713
4302
  { key: 'y', desc: 'copy', color: COLORS.accent },
4303
+ { key: '^L', desc: 'clear', color: COLORS.cyan },
3714
4304
  ],
3715
4305
  // Misc
3716
4306
  [
3717
4307
  { key: 'i', desc: 'input', color: COLORS.success },
3718
4308
  { key: 'n', desc: 'name', color: COLORS.accent },
4309
+ ...(IS_GIT_REPO ? [{ key: 'g', desc: 'git', color: COLORS.magenta }] : []),
3719
4310
  { key: 'o', desc: 'cfg', color: COLORS.magenta },
3720
4311
  { key: 'q', desc: 'quit', color: COLORS.error },
3721
4312
  ],
@@ -3787,6 +4378,11 @@ class ProcessManager {
3787
4378
  this.buildCommandOverlay(mainContainer);
3788
4379
  }
3789
4380
 
4381
+ // Add git modal if active
4382
+ if (this.showGitModal) {
4383
+ this.buildGitModal(mainContainer);
4384
+ }
4385
+
3790
4386
  this.renderer.root.add(mainContainer);
3791
4387
  this.runningContainer = mainContainer;
3792
4388
  this.uiJustRebuilt = true; // Prevent redundant render in the same tick
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "startall",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {