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.
- package/README.md +17 -0
- package/index.js +585 -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'];
|
|
@@ -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
|