startall 0.0.20 → 0.0.21

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 +585 -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'];
@@ -616,6 +728,12 @@ class ProcessManager {
616
728
  return;
617
729
  }
618
730
 
731
+ // Handle git modal
732
+ if (this.showGitModal) {
733
+ this.handleGitModalInput(keyName, keyEvent);
734
+ return;
735
+ }
736
+
619
737
  // Handle run command modal
620
738
  if (this.showRunCommandModal) {
621
739
  this.handleRunCommandModalInput(keyName, keyEvent);
@@ -857,6 +975,9 @@ class ProcessManager {
857
975
  } else if (keyName === 'y') {
858
976
  // Enter copy mode (select text to copy)
859
977
  this.enterCopyMode();
978
+ } else if (keyName === 'g' && IS_GIT_REPO) {
979
+ // Open git modal (commit & push)
980
+ this.openGitModal();
860
981
  } else if (keyName && keyName.length === 1 && !keyEvent.ctrl && !keyEvent.meta && !keyEvent.shift) {
861
982
  // Check if this key is a custom shortcut
862
983
  const shortcuts = this.config.shortcuts || {};
@@ -2396,6 +2517,157 @@ class ProcessManager {
2396
2517
  this.commandOverlayStatus = 'running';
2397
2518
  this.commandOverlayProcess = null;
2398
2519
  }
2520
+
2521
+ // Open the git modal and refresh status
2522
+ openGitModal() {
2523
+ this.showGitModal = true;
2524
+ this.gitModalPhase = 'status';
2525
+ this.gitCommitMessage = '';
2526
+ this.gitModalOutput = [];
2527
+ this.gitModalSelectedIndex = 0;
2528
+ this.refreshGitStatus();
2529
+ this.buildRunningUI();
2530
+ }
2531
+
2532
+ // Close the git modal
2533
+ closeGitModal() {
2534
+ this.showGitModal = false;
2535
+ this.gitModalPhase = 'status';
2536
+ this.gitCommitMessage = '';
2537
+ this.gitModalOutput = [];
2538
+ }
2539
+
2540
+ // Refresh git status data
2541
+ refreshGitStatus() {
2542
+ this.gitBranch = getGitBranch();
2543
+ this.gitStatus = getGitStatus();
2544
+ this.gitRemoteStatus = getGitRemoteStatus();
2545
+ }
2546
+
2547
+ // Handle keyboard input for the git modal
2548
+ handleGitModalInput(keyName, keyEvent) {
2549
+ if (this.gitModalPhase === 'status') {
2550
+ if (keyName === 'escape' || keyName === 'q') {
2551
+ this.closeGitModal();
2552
+ this.buildRunningUI();
2553
+ } else if (keyName === 'c') {
2554
+ // Start commit flow - switch to commit message input
2555
+ this.gitModalPhase = 'commit';
2556
+ this.gitCommitMessage = '';
2557
+ this.buildRunningUI();
2558
+ } else if (keyName === 'a') {
2559
+ // Stage all changes
2560
+ const result = gitStageAll();
2561
+ if (result.success) {
2562
+ this.gitModalOutput = ['All changes staged.'];
2563
+ } else {
2564
+ this.gitModalOutput = [`Error staging: ${result.error}`];
2565
+ }
2566
+ this.refreshGitStatus();
2567
+ this.buildRunningUI();
2568
+ } else if (keyName === 'p') {
2569
+ // Push
2570
+ this.gitModalPhase = 'pushing';
2571
+ this.gitModalOutput = ['Pushing...'];
2572
+ this.buildRunningUI();
2573
+ // Push asynchronously using setTimeout to allow UI update
2574
+ setTimeout(() => {
2575
+ const result = gitPush();
2576
+ if (result.success) {
2577
+ this.gitModalOutput = [result.output];
2578
+ } else {
2579
+ this.gitModalOutput = [`Push failed: ${result.error}`];
2580
+ }
2581
+ this.gitModalPhase = 'result';
2582
+ this.refreshGitStatus();
2583
+ this.buildRunningUI();
2584
+ }, 10);
2585
+ } else if (keyName === 'r') {
2586
+ // Refresh status
2587
+ this.refreshGitStatus();
2588
+ this.gitModalOutput = ['Status refreshed.'];
2589
+ this.buildRunningUI();
2590
+ } else if (keyName === 'up' || keyName === 'k') {
2591
+ this.gitModalSelectedIndex = Math.max(0, this.gitModalSelectedIndex - 1);
2592
+ this.buildRunningUI();
2593
+ } else if (keyName === 'down' || keyName === 'j') {
2594
+ const totalFiles = (this.gitStatus?.staged?.length || 0) + (this.gitStatus?.modified?.length || 0) + (this.gitStatus?.untracked?.length || 0);
2595
+ this.gitModalSelectedIndex = Math.min(Math.max(0, totalFiles - 1), this.gitModalSelectedIndex + 1);
2596
+ this.buildRunningUI();
2597
+ }
2598
+ } else if (this.gitModalPhase === 'commit') {
2599
+ if (keyName === 'escape') {
2600
+ this.gitModalPhase = 'status';
2601
+ this.gitCommitMessage = '';
2602
+ this.buildRunningUI();
2603
+ } else if (keyName === 'enter' || keyName === 'return') {
2604
+ if (this.gitCommitMessage.trim()) {
2605
+ // Stage all and commit
2606
+ this.gitModalPhase = 'committing';
2607
+ this.gitModalOutput = ['Staging and committing...'];
2608
+ this.buildRunningUI();
2609
+ setTimeout(() => {
2610
+ // Stage all first
2611
+ const stageResult = gitStageAll();
2612
+ if (!stageResult.success) {
2613
+ this.gitModalOutput = [`Error staging: ${stageResult.error}`];
2614
+ this.gitModalPhase = 'result';
2615
+ this.buildRunningUI();
2616
+ return;
2617
+ }
2618
+ // Then commit
2619
+ const commitResult = gitCommit(this.gitCommitMessage.trim());
2620
+ if (commitResult.success) {
2621
+ this.gitModalOutput = [commitResult.output, '', 'Commit successful! Press p to push, esc to close.'];
2622
+ this.gitModalPhase = 'status';
2623
+ } else {
2624
+ this.gitModalOutput = [`Commit failed: ${commitResult.error}`];
2625
+ this.gitModalPhase = 'result';
2626
+ }
2627
+ this.gitCommitMessage = '';
2628
+ this.refreshGitStatus();
2629
+ this.buildRunningUI();
2630
+ }, 10);
2631
+ }
2632
+ } else if (keyName === 'backspace') {
2633
+ this.gitCommitMessage = this.gitCommitMessage.slice(0, -1);
2634
+ this.buildRunningUI();
2635
+ } else if (keyName === 'space') {
2636
+ this.gitCommitMessage += ' ';
2637
+ this.buildRunningUI();
2638
+ } else if (keyName && keyName.length === 1 && !keyEvent.ctrl && !keyEvent.meta) {
2639
+ this.gitCommitMessage += keyName;
2640
+ this.buildRunningUI();
2641
+ }
2642
+ } else if (this.gitModalPhase === 'result') {
2643
+ // Any key returns to status or closes
2644
+ if (keyName === 'escape' || keyName === 'q') {
2645
+ this.closeGitModal();
2646
+ this.buildRunningUI();
2647
+ } else if (keyName === 'p') {
2648
+ // Allow pushing from result phase
2649
+ this.gitModalPhase = 'pushing';
2650
+ this.gitModalOutput = ['Pushing...'];
2651
+ this.buildRunningUI();
2652
+ setTimeout(() => {
2653
+ const result = gitPush();
2654
+ if (result.success) {
2655
+ this.gitModalOutput = [result.output];
2656
+ } else {
2657
+ this.gitModalOutput = [`Push failed: ${result.error}`];
2658
+ }
2659
+ this.gitModalPhase = 'result';
2660
+ this.refreshGitStatus();
2661
+ this.buildRunningUI();
2662
+ }, 10);
2663
+ } else {
2664
+ this.gitModalPhase = 'status';
2665
+ this.refreshGitStatus();
2666
+ this.buildRunningUI();
2667
+ }
2668
+ }
2669
+ // 'committing' and 'pushing' phases ignore input (busy)
2670
+ }
2399
2671
 
2400
2672
  buildSelectionUI() {
2401
2673
  // Remove old containers if they exist - use destroyRecursively to clean up all children
@@ -2545,6 +2817,18 @@ class ProcessManager {
2545
2817
  leftSide.add(titleText);
2546
2818
  this.headerText = titleText; // Save reference for countdown updates
2547
2819
 
2820
+ // Git branch indicator
2821
+ if (IS_GIT_REPO) {
2822
+ const branch = getGitBranch();
2823
+ if (branch) {
2824
+ const branchIndicator = new TextRenderable(this.renderer, {
2825
+ id: 'git-branch-indicator',
2826
+ content: t`${fg(COLORS.magenta)('\u2387')} ${fg(COLORS.magenta)(branch)}`,
2827
+ });
2828
+ leftSide.add(branchIndicator);
2829
+ }
2830
+ }
2831
+
2548
2832
  // VS Code hint
2549
2833
  if (IS_VSCODE) {
2550
2834
  const vscodeHint = new TextRenderable(this.renderer, {
@@ -3458,6 +3742,288 @@ class ProcessManager {
3458
3742
  parent.add(overlay);
3459
3743
  }
3460
3744
 
3745
+ // Build git commit & push modal overlay
3746
+ buildGitModal(parent) {
3747
+ const branch = this.gitBranch || 'unknown';
3748
+ const status = this.gitStatus || { staged: [], modified: [], untracked: [], clean: true };
3749
+ const remote = this.gitRemoteStatus || { ahead: 0, behind: 0, hasRemote: false };
3750
+
3751
+ // Title with branch name and status
3752
+ let titleIcon = '';
3753
+ if (this.gitModalPhase === 'committing') titleIcon = '...';
3754
+ else if (this.gitModalPhase === 'pushing') titleIcon = '...';
3755
+ else titleIcon = '';
3756
+ const title = ` Git: ${branch} ${titleIcon}`;
3757
+
3758
+ // Create centered overlay
3759
+ const overlay = new BoxRenderable(this.renderer, {
3760
+ id: 'git-modal',
3761
+ position: 'absolute',
3762
+ top: '10%',
3763
+ left: '15%',
3764
+ width: '70%',
3765
+ height: '80%',
3766
+ backgroundColor: COLORS.bg,
3767
+ border: true,
3768
+ borderStyle: 'rounded',
3769
+ borderColor: COLORS.accent,
3770
+ title: title,
3771
+ padding: 1,
3772
+ flexDirection: 'column',
3773
+ });
3774
+
3775
+ // Remote status line
3776
+ if (remote.hasRemote) {
3777
+ let remoteText = '';
3778
+ if (remote.ahead > 0 && remote.behind > 0) {
3779
+ remoteText = `${remote.ahead} ahead, ${remote.behind} behind remote`;
3780
+ } else if (remote.ahead > 0) {
3781
+ remoteText = `${remote.ahead} commit${remote.ahead > 1 ? 's' : ''} ahead of remote`;
3782
+ } else if (remote.behind > 0) {
3783
+ remoteText = `${remote.behind} commit${remote.behind > 1 ? 's' : ''} behind remote`;
3784
+ } else {
3785
+ remoteText = 'Up to date with remote';
3786
+ }
3787
+ const remoteColor = (remote.ahead > 0 || remote.behind > 0) ? COLORS.warning : COLORS.success;
3788
+ const remoteLine = new TextRenderable(this.renderer, {
3789
+ id: 'git-remote-status',
3790
+ content: t`${fg(remoteColor)(remoteText)}`,
3791
+ });
3792
+ overlay.add(remoteLine);
3793
+ } else {
3794
+ const noRemote = new TextRenderable(this.renderer, {
3795
+ id: 'git-no-remote',
3796
+ content: t`${fg(COLORS.textDim)('No remote tracking branch')}`,
3797
+ });
3798
+ overlay.add(noRemote);
3799
+ }
3800
+
3801
+ // Separator
3802
+ const sep1 = new BoxRenderable(this.renderer, {
3803
+ id: 'git-sep1',
3804
+ border: ['bottom'],
3805
+ borderStyle: 'single',
3806
+ borderColor: COLORS.border,
3807
+ marginTop: 1,
3808
+ marginBottom: 1,
3809
+ width: '100%',
3810
+ });
3811
+ overlay.add(sep1);
3812
+
3813
+ // Commit message input area (shown when in commit phase)
3814
+ if (this.gitModalPhase === 'commit') {
3815
+ const commitLabel = new TextRenderable(this.renderer, {
3816
+ id: 'git-commit-label',
3817
+ content: t`${fg(COLORS.accent)('Commit message:')}`,
3818
+ });
3819
+ overlay.add(commitLabel);
3820
+
3821
+ const commitInput = new BoxRenderable(this.renderer, {
3822
+ id: 'git-commit-input-box',
3823
+ border: true,
3824
+ borderStyle: 'single',
3825
+ borderColor: COLORS.accent,
3826
+ padding: 1,
3827
+ marginTop: 1,
3828
+ marginBottom: 1,
3829
+ width: '100%',
3830
+ });
3831
+
3832
+ const commitText = new TextRenderable(this.renderer, {
3833
+ id: 'git-commit-text',
3834
+ content: t`${fg(COLORS.text)(this.gitCommitMessage)}${fg(COLORS.accent)('_')}`,
3835
+ });
3836
+ commitInput.add(commitText);
3837
+ overlay.add(commitInput);
3838
+
3839
+ const commitHint = new TextRenderable(this.renderer, {
3840
+ id: 'git-commit-hint',
3841
+ content: t`${fg(COLORS.textDim)('All changes will be staged and committed.')}`,
3842
+ });
3843
+ overlay.add(commitHint);
3844
+ } else if (this.gitModalPhase === 'committing' || this.gitModalPhase === 'pushing') {
3845
+ // Show busy indicator
3846
+ const busyText = this.gitModalPhase === 'committing' ? 'Committing...' : 'Pushing...';
3847
+ const busyLine = new TextRenderable(this.renderer, {
3848
+ id: 'git-busy',
3849
+ content: t`${fg(COLORS.warning)(busyText)}`,
3850
+ });
3851
+ overlay.add(busyLine);
3852
+ } else {
3853
+ // Status view or result view - show file lists
3854
+
3855
+ // Show output messages if any
3856
+ if (this.gitModalOutput.length > 0) {
3857
+ this.gitModalOutput.forEach((line, idx) => {
3858
+ const outputLine = new TextRenderable(this.renderer, {
3859
+ id: `git-output-${idx}`,
3860
+ content: t`${fg(COLORS.success)(line)}`,
3861
+ });
3862
+ overlay.add(outputLine);
3863
+ });
3864
+
3865
+ const outputSep = new BoxRenderable(this.renderer, {
3866
+ id: 'git-output-sep',
3867
+ border: ['bottom'],
3868
+ borderStyle: 'single',
3869
+ borderColor: COLORS.border,
3870
+ marginTop: 1,
3871
+ marginBottom: 1,
3872
+ width: '100%',
3873
+ });
3874
+ overlay.add(outputSep);
3875
+ }
3876
+
3877
+ if (status.clean) {
3878
+ const cleanText = new TextRenderable(this.renderer, {
3879
+ id: 'git-clean',
3880
+ content: t`${fg(COLORS.success)('Working tree clean - nothing to commit.')}`,
3881
+ });
3882
+ overlay.add(cleanText);
3883
+ } else {
3884
+ // Scrollable file list
3885
+ const fileListHeight = Math.floor(this.renderer.height * 0.8) - 14;
3886
+ const fileList = new ScrollBoxRenderable(this.renderer, {
3887
+ id: 'git-file-list',
3888
+ height: Math.max(5, fileListHeight),
3889
+ scrollX: false,
3890
+ scrollY: true,
3891
+ focusable: true,
3892
+ style: {
3893
+ rootOptions: {
3894
+ flexGrow: 1,
3895
+ backgroundColor: COLORS.bg,
3896
+ },
3897
+ contentOptions: {
3898
+ backgroundColor: COLORS.bg,
3899
+ width: '100%',
3900
+ },
3901
+ },
3902
+ });
3903
+
3904
+ let fileIndex = 0;
3905
+
3906
+ // Staged files
3907
+ if (status.staged.length > 0) {
3908
+ const stagedHeader = new TextRenderable(this.renderer, {
3909
+ id: 'git-staged-header',
3910
+ content: t`${fg(COLORS.success)(bold('Staged Changes'))} ${fg(COLORS.textDim)(`(${status.staged.length})`)}`,
3911
+ });
3912
+ fileList.content.add(stagedHeader);
3913
+
3914
+ status.staged.forEach((file, idx) => {
3915
+ const isFocused = fileIndex === this.gitModalSelectedIndex;
3916
+ const indicator = isFocused ? '>' : ' ';
3917
+ const statusLabel = file.status === 'A' ? 'new' : file.status === 'M' ? 'mod' : file.status === 'D' ? 'del' : file.status === 'R' ? 'ren' : file.status;
3918
+
3919
+ const fileLine = new TextRenderable(this.renderer, {
3920
+ id: `git-staged-${idx}`,
3921
+ content: t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(COLORS.success)(statusLabel)} ${fg(COLORS.text)(file.file)}`,
3922
+ });
3923
+ fileList.content.add(fileLine);
3924
+ fileIndex++;
3925
+ });
3926
+ }
3927
+
3928
+ // Modified (unstaged) files
3929
+ if (status.modified.length > 0) {
3930
+ const modHeader = new TextRenderable(this.renderer, {
3931
+ id: 'git-modified-header',
3932
+ content: t`${fg(COLORS.warning)(bold('Unstaged Changes'))} ${fg(COLORS.textDim)(`(${status.modified.length})`)}`,
3933
+ });
3934
+ fileList.content.add(modHeader);
3935
+
3936
+ status.modified.forEach((file, idx) => {
3937
+ const isFocused = fileIndex === this.gitModalSelectedIndex;
3938
+ const indicator = isFocused ? '>' : ' ';
3939
+ const statusLabel = file.status === 'M' ? 'mod' : file.status === 'D' ? 'del' : file.status;
3940
+
3941
+ const fileLine = new TextRenderable(this.renderer, {
3942
+ id: `git-modified-${idx}`,
3943
+ content: t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(COLORS.warning)(statusLabel)} ${fg(COLORS.text)(file.file)}`,
3944
+ });
3945
+ fileList.content.add(fileLine);
3946
+ fileIndex++;
3947
+ });
3948
+ }
3949
+
3950
+ // Untracked files
3951
+ if (status.untracked.length > 0) {
3952
+ const untrackedHeader = new TextRenderable(this.renderer, {
3953
+ id: 'git-untracked-header',
3954
+ content: t`${fg(COLORS.error)(bold('Untracked Files'))} ${fg(COLORS.textDim)(`(${status.untracked.length})`)}`,
3955
+ });
3956
+ fileList.content.add(untrackedHeader);
3957
+
3958
+ status.untracked.forEach((file, idx) => {
3959
+ const isFocused = fileIndex === this.gitModalSelectedIndex;
3960
+ const indicator = isFocused ? '>' : ' ';
3961
+
3962
+ const fileLine = new TextRenderable(this.renderer, {
3963
+ id: `git-untracked-${idx}`,
3964
+ content: t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(COLORS.error)('new')} ${fg(COLORS.textDim)(file.file)}`,
3965
+ });
3966
+ fileList.content.add(fileLine);
3967
+ fileIndex++;
3968
+ });
3969
+ }
3970
+
3971
+ overlay.add(fileList);
3972
+ }
3973
+ }
3974
+
3975
+ // Footer hint bar
3976
+ const hintBar = new BoxRenderable(this.renderer, {
3977
+ id: 'git-hint-bar',
3978
+ border: ['top'],
3979
+ borderStyle: 'single',
3980
+ borderColor: COLORS.border,
3981
+ paddingTop: 1,
3982
+ paddingLeft: 1,
3983
+ marginTop: 1,
3984
+ flexDirection: 'row',
3985
+ gap: 2,
3986
+ });
3987
+
3988
+ if (this.gitModalPhase === 'commit') {
3989
+ const hints = [
3990
+ { key: 'enter', desc: 'commit', color: COLORS.success },
3991
+ { key: 'esc', desc: 'cancel', color: COLORS.error },
3992
+ ];
3993
+ hints.forEach(({ key, desc, color }) => {
3994
+ const hint = new TextRenderable(this.renderer, {
3995
+ id: `git-hint-${key}`,
3996
+ content: t`${fg(color)(key)} ${fg(COLORS.textDim)(desc)}`,
3997
+ });
3998
+ hintBar.add(hint);
3999
+ });
4000
+ } else if (this.gitModalPhase === 'committing' || this.gitModalPhase === 'pushing') {
4001
+ const hint = new TextRenderable(this.renderer, {
4002
+ id: 'git-hint-busy',
4003
+ content: t`${fg(COLORS.warning)('Please wait...')}`,
4004
+ });
4005
+ hintBar.add(hint);
4006
+ } else {
4007
+ const hints = [
4008
+ { key: 'c', desc: 'commit', color: COLORS.success },
4009
+ { key: 'a', desc: 'stage all', color: COLORS.warning },
4010
+ { key: 'p', desc: 'push', color: COLORS.cyan },
4011
+ { key: 'r', desc: 'refresh', color: COLORS.magenta },
4012
+ { key: 'esc', desc: 'close', color: COLORS.error },
4013
+ ];
4014
+ hints.forEach(({ key, desc, color }) => {
4015
+ const hint = new TextRenderable(this.renderer, {
4016
+ id: `git-hint-${key}`,
4017
+ content: t`${fg(color)(key)} ${fg(COLORS.textDim)(desc)}`,
4018
+ });
4019
+ hintBar.add(hint);
4020
+ });
4021
+ }
4022
+
4023
+ overlay.add(hintBar);
4024
+ parent.add(overlay);
4025
+ }
4026
+
3461
4027
  buildRunningUI() {
3462
4028
  // Save scroll positions before destroying
3463
4029
  for (const [paneId, scrollBox] of this.paneScrollBoxes.entries()) {
@@ -3600,6 +4166,19 @@ class ProcessManager {
3600
4166
  });
3601
4167
  leftSide.add(statusIndicator);
3602
4168
 
4169
+ // Git branch indicator
4170
+ if (IS_GIT_REPO) {
4171
+ const branch = this.gitBranch || getGitBranch();
4172
+ if (branch) {
4173
+ this.gitBranch = branch;
4174
+ const branchIndicator = new TextRenderable(this.renderer, {
4175
+ id: 'git-branch-indicator',
4176
+ content: t`${fg(COLORS.magenta)('\u2387')} ${fg(COLORS.magenta)(branch)}`,
4177
+ });
4178
+ leftSide.add(branchIndicator);
4179
+ }
4180
+ }
4181
+
3603
4182
  // VS Code hint
3604
4183
  if (IS_VSCODE) {
3605
4184
  const vscodeHint = new TextRenderable(this.renderer, {
@@ -3716,6 +4295,7 @@ class ProcessManager {
3716
4295
  [
3717
4296
  { key: 'i', desc: 'input', color: COLORS.success },
3718
4297
  { key: 'n', desc: 'name', color: COLORS.accent },
4298
+ ...(IS_GIT_REPO ? [{ key: 'g', desc: 'git', color: COLORS.magenta }] : []),
3719
4299
  { key: 'o', desc: 'cfg', color: COLORS.magenta },
3720
4300
  { key: 'q', desc: 'quit', color: COLORS.error },
3721
4301
  ],
@@ -3787,6 +4367,11 @@ class ProcessManager {
3787
4367
  this.buildCommandOverlay(mainContainer);
3788
4368
  }
3789
4369
 
4370
+ // Add git modal if active
4371
+ if (this.showGitModal) {
4372
+ this.buildGitModal(mainContainer);
4373
+ }
4374
+
3790
4375
  this.renderer.root.add(mainContainer);
3791
4376
  this.runningContainer = mainContainer;
3792
4377
  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.21",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {