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.
- package/README.md +17 -0
- package/index.js +596 -0
- 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
|