diffstalker 0.2.1 → 0.2.3
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/.dependency-cruiser.cjs +67 -0
- package/.githooks/pre-commit +2 -0
- package/.githooks/pre-push +15 -0
- package/README.md +43 -35
- package/bun.lock +60 -4
- package/dist/App.js +495 -131
- package/dist/KeyBindings.js +134 -10
- package/dist/MouseHandlers.js +67 -20
- package/dist/core/ExplorerStateManager.js +37 -75
- package/dist/core/GitStateManager.js +252 -46
- package/dist/git/diff.js +99 -18
- package/dist/git/status.js +111 -54
- package/dist/git/test-helpers.js +67 -0
- package/dist/index.js +54 -43
- package/dist/ipc/CommandClient.js +6 -7
- package/dist/state/UIState.js +22 -0
- package/dist/types/remote.js +5 -0
- package/dist/ui/PaneRenderers.js +45 -15
- package/dist/ui/modals/BranchPicker.js +157 -0
- package/dist/ui/modals/CommitActionConfirm.js +66 -0
- package/dist/ui/modals/FileFinder.js +45 -75
- package/dist/ui/modals/HotkeysModal.js +35 -3
- package/dist/ui/modals/SoftResetConfirm.js +68 -0
- package/dist/ui/modals/StashListModal.js +98 -0
- package/dist/ui/modals/ThemePicker.js +1 -2
- package/dist/ui/widgets/CommitPanel.js +113 -7
- package/dist/ui/widgets/CompareListView.js +44 -23
- package/dist/ui/widgets/DiffView.js +216 -170
- package/dist/ui/widgets/ExplorerView.js +50 -54
- package/dist/ui/widgets/FileList.js +62 -95
- package/dist/ui/widgets/FlatFileList.js +65 -0
- package/dist/ui/widgets/Footer.js +25 -15
- package/dist/ui/widgets/Header.js +51 -9
- package/dist/ui/widgets/fileRowFormatters.js +73 -0
- package/dist/utils/ansiTruncate.js +0 -1
- package/dist/utils/displayRows.js +101 -21
- package/dist/utils/flatFileList.js +67 -0
- package/dist/utils/layoutCalculations.js +5 -3
- package/eslint.metrics.js +0 -1
- package/metrics/v0.2.2.json +229 -0
- package/metrics/v0.2.3.json +243 -0
- package/package.json +10 -3
|
@@ -23,6 +23,10 @@ const hotkeyGroups = [
|
|
|
23
23
|
{ key: 'c', description: 'Commit panel' },
|
|
24
24
|
{ key: 'r', description: 'Refresh' },
|
|
25
25
|
{ key: 'q', description: 'Quit' },
|
|
26
|
+
{ key: 'P', description: 'Push to remote' },
|
|
27
|
+
{ key: 'F', description: 'Fetch from remote' },
|
|
28
|
+
{ key: 'R', description: 'Pull --rebase' },
|
|
29
|
+
{ key: 'S', description: 'Stash save (global)' },
|
|
26
30
|
],
|
|
27
31
|
},
|
|
28
32
|
{
|
|
@@ -45,6 +49,7 @@ const hotkeyGroups = [
|
|
|
45
49
|
{
|
|
46
50
|
title: 'Toggles',
|
|
47
51
|
entries: [
|
|
52
|
+
{ key: 'h', description: 'Flat file view' },
|
|
48
53
|
{ key: 'm', description: 'Mouse mode' },
|
|
49
54
|
{ key: 'w', description: 'Wrap mode' },
|
|
50
55
|
{ key: 'f', description: 'Follow mode' },
|
|
@@ -57,6 +62,28 @@ const hotkeyGroups = [
|
|
|
57
62
|
entries: [
|
|
58
63
|
{ key: 'Enter', description: 'Enter directory' },
|
|
59
64
|
{ key: 'Backspace', description: 'Go up' },
|
|
65
|
+
{ key: '/', description: 'Find file' },
|
|
66
|
+
{ key: 'Ctrl+P', description: 'Find file (any tab)' },
|
|
67
|
+
{ key: 'g', description: 'Show changes only' },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
title: 'Commit Panel',
|
|
72
|
+
entries: [
|
|
73
|
+
{ key: 'i/Enter', description: 'Edit message' },
|
|
74
|
+
{ key: 'a', description: 'Toggle amend' },
|
|
75
|
+
{ key: 'Ctrl+a', description: 'Toggle amend (typing)' },
|
|
76
|
+
{ key: 'o', description: 'Pop stash' },
|
|
77
|
+
{ key: 'l', description: 'Stash list modal' },
|
|
78
|
+
{ key: 'b', description: 'Branch picker' },
|
|
79
|
+
{ key: 'X', description: 'Soft reset HEAD~1' },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
title: 'History',
|
|
84
|
+
entries: [
|
|
85
|
+
{ key: 'p', description: 'Cherry-pick commit' },
|
|
86
|
+
{ key: 'v', description: 'Revert commit' },
|
|
60
87
|
],
|
|
61
88
|
},
|
|
62
89
|
{
|
|
@@ -67,8 +94,13 @@ const hotkeyGroups = [
|
|
|
67
94
|
],
|
|
68
95
|
},
|
|
69
96
|
{
|
|
70
|
-
title: 'Diff',
|
|
71
|
-
entries: [
|
|
97
|
+
title: 'Diff (pane focus)',
|
|
98
|
+
entries: [
|
|
99
|
+
{ key: 'n', description: 'Next hunk' },
|
|
100
|
+
{ key: 'N', description: 'Previous hunk' },
|
|
101
|
+
{ key: 's', description: 'Toggle hunk staged/unstaged' },
|
|
102
|
+
{ key: 'd', description: 'Discard changes' },
|
|
103
|
+
],
|
|
72
104
|
},
|
|
73
105
|
];
|
|
74
106
|
/**
|
|
@@ -186,7 +218,7 @@ export class HotkeysModal {
|
|
|
186
218
|
this.box.setContent(lines.join('\n'));
|
|
187
219
|
this.screen.render();
|
|
188
220
|
}
|
|
189
|
-
renderGroups(groups,
|
|
221
|
+
renderGroups(groups, _colWidth) {
|
|
190
222
|
const lines = [];
|
|
191
223
|
for (const group of groups) {
|
|
192
224
|
lines.push(`{bold}{gray-fg}${group.title}{/gray-fg}{/bold}`);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import blessed from 'neo-blessed';
|
|
2
|
+
/**
|
|
3
|
+
* SoftResetConfirm modal for confirming soft reset HEAD~1.
|
|
4
|
+
*/
|
|
5
|
+
export class SoftResetConfirm {
|
|
6
|
+
box;
|
|
7
|
+
screen;
|
|
8
|
+
onConfirm;
|
|
9
|
+
onCancel;
|
|
10
|
+
constructor(screen, headCommit, onConfirm, onCancel) {
|
|
11
|
+
this.screen = screen;
|
|
12
|
+
this.onConfirm = onConfirm;
|
|
13
|
+
this.onCancel = onCancel;
|
|
14
|
+
const width = Math.min(60, screen.width - 6);
|
|
15
|
+
const height = 9;
|
|
16
|
+
this.box = blessed.box({
|
|
17
|
+
parent: screen,
|
|
18
|
+
top: 'center',
|
|
19
|
+
left: 'center',
|
|
20
|
+
width,
|
|
21
|
+
height,
|
|
22
|
+
border: {
|
|
23
|
+
type: 'line',
|
|
24
|
+
},
|
|
25
|
+
style: {
|
|
26
|
+
border: {
|
|
27
|
+
fg: 'yellow',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
tags: true,
|
|
31
|
+
keys: true,
|
|
32
|
+
});
|
|
33
|
+
this.setupKeyHandlers();
|
|
34
|
+
this.renderContent(headCommit, width);
|
|
35
|
+
}
|
|
36
|
+
setupKeyHandlers() {
|
|
37
|
+
this.box.key(['y', 'Y'], () => {
|
|
38
|
+
this.close();
|
|
39
|
+
this.onConfirm();
|
|
40
|
+
});
|
|
41
|
+
this.box.key(['n', 'N', 'escape', 'q'], () => {
|
|
42
|
+
this.close();
|
|
43
|
+
this.onCancel();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
renderContent(commit, width) {
|
|
47
|
+
const lines = [];
|
|
48
|
+
const innerWidth = width - 6;
|
|
49
|
+
lines.push('{bold}{yellow-fg} Soft Reset HEAD~1?{/yellow-fg}{/bold}');
|
|
50
|
+
lines.push('');
|
|
51
|
+
const msg = commit.message.length > innerWidth
|
|
52
|
+
? commit.message.slice(0, innerWidth - 3) + '\u2026'
|
|
53
|
+
: commit.message;
|
|
54
|
+
lines.push(`{yellow-fg}${commit.shortHash}{/yellow-fg} ${msg}`);
|
|
55
|
+
lines.push('');
|
|
56
|
+
lines.push('{gray-fg}Changes will return to staged state{/gray-fg}');
|
|
57
|
+
lines.push('');
|
|
58
|
+
lines.push('{gray-fg}Press {/gray-fg}{green-fg}y{/green-fg}{gray-fg} to confirm, {/gray-fg}{red-fg}n{/red-fg}{gray-fg} or Esc to cancel{/gray-fg}');
|
|
59
|
+
this.box.setContent(lines.join('\n'));
|
|
60
|
+
this.screen.render();
|
|
61
|
+
}
|
|
62
|
+
close() {
|
|
63
|
+
this.box.destroy();
|
|
64
|
+
}
|
|
65
|
+
focus() {
|
|
66
|
+
this.box.focus();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import blessed from 'neo-blessed';
|
|
2
|
+
/**
|
|
3
|
+
* StashListModal shows stash entries and allows popping one.
|
|
4
|
+
*/
|
|
5
|
+
export class StashListModal {
|
|
6
|
+
box;
|
|
7
|
+
screen;
|
|
8
|
+
entries;
|
|
9
|
+
selectedIndex = 0;
|
|
10
|
+
onPop;
|
|
11
|
+
onCancel;
|
|
12
|
+
constructor(screen, entries, onPop, onCancel) {
|
|
13
|
+
this.screen = screen;
|
|
14
|
+
this.entries = entries;
|
|
15
|
+
this.onPop = onPop;
|
|
16
|
+
this.onCancel = onCancel;
|
|
17
|
+
// Create modal box
|
|
18
|
+
const width = Math.min(70, screen.width - 6);
|
|
19
|
+
const maxVisible = Math.min(entries.length, 15);
|
|
20
|
+
const height = maxVisible + 6;
|
|
21
|
+
this.box = blessed.box({
|
|
22
|
+
parent: screen,
|
|
23
|
+
top: 'center',
|
|
24
|
+
left: 'center',
|
|
25
|
+
width,
|
|
26
|
+
height,
|
|
27
|
+
border: {
|
|
28
|
+
type: 'line',
|
|
29
|
+
},
|
|
30
|
+
style: {
|
|
31
|
+
border: {
|
|
32
|
+
fg: 'cyan',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
tags: true,
|
|
36
|
+
keys: true,
|
|
37
|
+
scrollable: true,
|
|
38
|
+
alwaysScroll: true,
|
|
39
|
+
});
|
|
40
|
+
this.setupKeyHandlers();
|
|
41
|
+
this.render();
|
|
42
|
+
}
|
|
43
|
+
setupKeyHandlers() {
|
|
44
|
+
this.box.key(['escape', 'q'], () => {
|
|
45
|
+
this.close();
|
|
46
|
+
this.onCancel();
|
|
47
|
+
});
|
|
48
|
+
this.box.key(['enter'], () => {
|
|
49
|
+
if (this.entries.length > 0) {
|
|
50
|
+
const index = this.entries[this.selectedIndex].index;
|
|
51
|
+
this.close();
|
|
52
|
+
this.onPop(index);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
this.box.key(['up', 'k'], () => {
|
|
56
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
57
|
+
this.render();
|
|
58
|
+
});
|
|
59
|
+
this.box.key(['down', 'j'], () => {
|
|
60
|
+
this.selectedIndex = Math.min(this.entries.length - 1, this.selectedIndex + 1);
|
|
61
|
+
this.render();
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
render() {
|
|
65
|
+
const lines = [];
|
|
66
|
+
const width = this.box.width - 4;
|
|
67
|
+
lines.push('{bold}{cyan-fg} Stash List{/cyan-fg}{/bold}');
|
|
68
|
+
lines.push('');
|
|
69
|
+
if (this.entries.length === 0) {
|
|
70
|
+
lines.push('{gray-fg}No stash entries{/gray-fg}');
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
for (let i = 0; i < this.entries.length; i++) {
|
|
74
|
+
const entry = this.entries[i];
|
|
75
|
+
const isSelected = i === this.selectedIndex;
|
|
76
|
+
const msg = entry.message.length > width - 10
|
|
77
|
+
? entry.message.slice(0, width - 13) + '\u2026'
|
|
78
|
+
: entry.message;
|
|
79
|
+
if (isSelected) {
|
|
80
|
+
lines.push(`{cyan-fg}{bold}> {${i}} ${msg}{/bold}{/cyan-fg}`);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
lines.push(` {gray-fg}{${i}}{/gray-fg} ${msg}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
lines.push('');
|
|
88
|
+
lines.push('{gray-fg}j/k: navigate | Enter: pop | Esc: cancel{/gray-fg}');
|
|
89
|
+
this.box.setContent(lines.join('\n'));
|
|
90
|
+
this.screen.render();
|
|
91
|
+
}
|
|
92
|
+
close() {
|
|
93
|
+
this.box.destroy();
|
|
94
|
+
}
|
|
95
|
+
focus() {
|
|
96
|
+
this.box.focus();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import blessed from 'neo-blessed';
|
|
2
|
-
import { themes, themeOrder
|
|
2
|
+
import { themes, themeOrder } from '../../themes.js';
|
|
3
3
|
/**
|
|
4
4
|
* ThemePicker modal for selecting diff themes.
|
|
5
5
|
*/
|
|
@@ -85,7 +85,6 @@ export class ThemePicker {
|
|
|
85
85
|
// Preview section
|
|
86
86
|
lines.push('');
|
|
87
87
|
lines.push('{gray-fg}Preview:{/gray-fg}');
|
|
88
|
-
const previewTheme = getTheme(themeOrder[this.selectedIndex]);
|
|
89
88
|
// Simple preview - just show add/del colors
|
|
90
89
|
lines.push(` {green-fg}+ added line{/green-fg}`);
|
|
91
90
|
lines.push(` {red-fg}- deleted line{/red-fg}`);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Build all lines for the commit panel (used for both rendering and totalRows).
|
|
3
3
|
*/
|
|
4
|
-
export function
|
|
4
|
+
export function buildCommitPanelLines(opts) {
|
|
5
|
+
const { state, stagedCount, width, branch, remoteState, stashList, headCommit } = opts;
|
|
5
6
|
const lines = [];
|
|
6
7
|
// Title
|
|
7
8
|
let title = '{bold}Commit Message{/bold}';
|
|
@@ -11,7 +12,6 @@ export function formatCommitPanel(state, stagedCount, width) {
|
|
|
11
12
|
lines.push(title);
|
|
12
13
|
lines.push('');
|
|
13
14
|
// Message input area
|
|
14
|
-
const borderChar = state.inputFocused ? '\u2502' : '\u2502';
|
|
15
15
|
const borderColor = state.inputFocused ? 'cyan' : 'gray';
|
|
16
16
|
// Top border
|
|
17
17
|
const innerWidth = Math.max(20, width - 6);
|
|
@@ -24,7 +24,7 @@ export function formatCommitPanel(state, stagedCount, width) {
|
|
|
24
24
|
const truncatedMessage = displayMessage.length > innerWidth
|
|
25
25
|
? displayMessage.slice(0, innerWidth - 1) + '\u2026'
|
|
26
26
|
: displayMessage.padEnd(innerWidth);
|
|
27
|
-
lines.push(`{${borderColor}-fg}
|
|
27
|
+
lines.push(`{${borderColor}-fg}\u2502{/${borderColor}-fg} ${messageColor}${truncatedMessage}${messageEnd} {${borderColor}-fg}\u2502{/${borderColor}-fg}`);
|
|
28
28
|
// Bottom border
|
|
29
29
|
lines.push(`{${borderColor}-fg}\u2514${'─'.repeat(innerWidth + 2)}\u2518{/${borderColor}-fg}`);
|
|
30
30
|
lines.push('');
|
|
@@ -45,10 +45,116 @@ export function formatCommitPanel(state, stagedCount, width) {
|
|
|
45
45
|
lines.push('');
|
|
46
46
|
// Help text
|
|
47
47
|
const helpText = state.inputFocused
|
|
48
|
-
? 'Enter: commit | Esc: unfocus'
|
|
49
|
-
: 'i/Enter: edit |
|
|
48
|
+
? 'Enter: commit | Ctrl+a: amend | Esc: unfocus'
|
|
49
|
+
: 'i/Enter: edit | a: amend | Esc: back';
|
|
50
50
|
lines.push(`{gray-fg}Staged: ${stagedCount} file(s) | ${helpText}{/gray-fg}`);
|
|
51
|
-
|
|
51
|
+
// Stash section
|
|
52
|
+
const stashEntries = stashList ?? [];
|
|
53
|
+
lines.push('');
|
|
54
|
+
lines.push(`{gray-fg}${'─'.repeat(3)} Stash (${stashEntries.length}) ${'─'.repeat(3)}{/gray-fg}`);
|
|
55
|
+
if (stashEntries.length > 0) {
|
|
56
|
+
const maxShow = 5;
|
|
57
|
+
for (let i = 0; i < Math.min(stashEntries.length, maxShow); i++) {
|
|
58
|
+
const entry = stashEntries[i];
|
|
59
|
+
const msg = entry.message.length > width - 10
|
|
60
|
+
? entry.message.slice(0, width - 13) + '\u2026'
|
|
61
|
+
: entry.message;
|
|
62
|
+
lines.push(`{gray-fg}{${i}}{/gray-fg}: ${msg}`);
|
|
63
|
+
}
|
|
64
|
+
if (stashEntries.length > maxShow) {
|
|
65
|
+
lines.push(`{gray-fg}... ${stashEntries.length - maxShow} more{/gray-fg}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
lines.push('{gray-fg}(empty){/gray-fg}');
|
|
70
|
+
}
|
|
71
|
+
lines.push('{gray-fg}S: save | o: pop | l: list{/gray-fg}');
|
|
72
|
+
// Branch section
|
|
73
|
+
if (branch) {
|
|
74
|
+
lines.push('');
|
|
75
|
+
lines.push(`{gray-fg}${'─'.repeat(3)} Branch ${'─'.repeat(3)}{/gray-fg}`);
|
|
76
|
+
let branchLine = `{bold}* ${branch.current}{/bold}`;
|
|
77
|
+
if (branch.tracking) {
|
|
78
|
+
branchLine += ` {gray-fg}\u2192{/gray-fg} ${branch.tracking}`;
|
|
79
|
+
}
|
|
80
|
+
lines.push(branchLine);
|
|
81
|
+
lines.push('{gray-fg}b: switch/create{/gray-fg}');
|
|
82
|
+
}
|
|
83
|
+
// Undo section
|
|
84
|
+
lines.push('');
|
|
85
|
+
lines.push(`{gray-fg}${'─'.repeat(3)} Undo ${'─'.repeat(3)}{/gray-fg}`);
|
|
86
|
+
if (headCommit) {
|
|
87
|
+
lines.push(`{gray-fg}HEAD: {yellow-fg}${headCommit.shortHash}{/yellow-fg} ${headCommit.message}{/gray-fg}`);
|
|
88
|
+
}
|
|
89
|
+
lines.push('{gray-fg}X: soft reset HEAD~1{/gray-fg}');
|
|
90
|
+
// Remote section
|
|
91
|
+
if (branch) {
|
|
92
|
+
lines.push('');
|
|
93
|
+
lines.push(`{gray-fg}${'─'.repeat(3)} Remote ${'─'.repeat(3)}{/gray-fg}`);
|
|
94
|
+
// Tracking info
|
|
95
|
+
if (branch.tracking) {
|
|
96
|
+
let tracking = `${branch.current} {gray-fg}\u2192{/gray-fg} ${branch.tracking}`;
|
|
97
|
+
if (branch.ahead > 0)
|
|
98
|
+
tracking += ` {green-fg}\u2191${branch.ahead}{/green-fg}`;
|
|
99
|
+
if (branch.behind > 0)
|
|
100
|
+
tracking += ` {red-fg}\u2193${branch.behind}{/red-fg}`;
|
|
101
|
+
lines.push(tracking);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
lines.push(`{gray-fg}${branch.current} (no remote tracking){/gray-fg}`);
|
|
105
|
+
}
|
|
106
|
+
// Remote status
|
|
107
|
+
if (remoteState?.inProgress && remoteState.operation) {
|
|
108
|
+
const labels = {
|
|
109
|
+
push: 'Pushing...',
|
|
110
|
+
fetch: 'Fetching...',
|
|
111
|
+
pull: 'Rebasing...',
|
|
112
|
+
stash: 'Stashing...',
|
|
113
|
+
stashPop: 'Popping stash...',
|
|
114
|
+
branchSwitch: 'Switching branch...',
|
|
115
|
+
branchCreate: 'Creating branch...',
|
|
116
|
+
softReset: 'Resetting...',
|
|
117
|
+
cherryPick: 'Cherry-picking...',
|
|
118
|
+
revert: 'Reverting...',
|
|
119
|
+
};
|
|
120
|
+
lines.push(`{yellow-fg}${labels[remoteState.operation] ?? ''}{/yellow-fg}`);
|
|
121
|
+
}
|
|
122
|
+
else if (remoteState?.error) {
|
|
123
|
+
const brief = remoteState.error.length > 50
|
|
124
|
+
? remoteState.error.slice(0, 50) + '\u2026'
|
|
125
|
+
: remoteState.error;
|
|
126
|
+
lines.push(`{red-fg}${brief}{/red-fg}`);
|
|
127
|
+
}
|
|
128
|
+
else if (remoteState?.lastResult) {
|
|
129
|
+
lines.push(`{green-fg}${remoteState.lastResult}{/green-fg}`);
|
|
130
|
+
}
|
|
131
|
+
lines.push('{gray-fg}P: push | F: fetch | R: pull --rebase{/gray-fg}');
|
|
132
|
+
}
|
|
133
|
+
return lines;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Get total row count for the commit panel (for scroll calculations).
|
|
137
|
+
*/
|
|
138
|
+
export function getCommitPanelTotalRows(opts) {
|
|
139
|
+
return buildCommitPanelLines(opts).length;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Format the commit panel as blessed-compatible tagged string.
|
|
143
|
+
*/
|
|
144
|
+
export function formatCommitPanel(state, stagedCount, width, branch, remoteState, stashList, headCommit, scrollOffset = 0, visibleHeight) {
|
|
145
|
+
const allLines = buildCommitPanelLines({
|
|
146
|
+
state,
|
|
147
|
+
stagedCount,
|
|
148
|
+
width,
|
|
149
|
+
branch,
|
|
150
|
+
remoteState,
|
|
151
|
+
stashList,
|
|
152
|
+
headCommit,
|
|
153
|
+
});
|
|
154
|
+
if (visibleHeight && allLines.length > visibleHeight) {
|
|
155
|
+
return allLines.slice(scrollOffset, scrollOffset + visibleHeight).join('\n');
|
|
156
|
+
}
|
|
157
|
+
return allLines.join('\n');
|
|
52
158
|
}
|
|
53
159
|
/**
|
|
54
160
|
* Format inactive commit panel.
|
|
@@ -135,6 +135,47 @@ function formatFileRow(file, treeRow, isSelected, isFocused, width) {
|
|
|
135
135
|
}
|
|
136
136
|
return `{escape}${line}{/escape}`;
|
|
137
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Check if a row is currently selected.
|
|
140
|
+
*/
|
|
141
|
+
function isRowSelected(row, selectedItem) {
|
|
142
|
+
if (!selectedItem)
|
|
143
|
+
return false;
|
|
144
|
+
if (row.type === 'commit' && row.commitIndex !== undefined) {
|
|
145
|
+
return selectedItem.type === 'commit' && selectedItem.index === row.commitIndex;
|
|
146
|
+
}
|
|
147
|
+
if (row.type === 'file' && row.fileIndex !== undefined) {
|
|
148
|
+
return selectedItem.type === 'file' && selectedItem.index === row.fileIndex;
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Format a section header line (e.g. "▼ Commits (5)").
|
|
154
|
+
*/
|
|
155
|
+
function formatSectionHeader(label, count) {
|
|
156
|
+
return `{escape}${ANSI_CYAN}${ANSI_BOLD}▼ ${label}${ANSI_RESET} ${ANSI_GRAY}(${count})${ANSI_RESET}{/escape}`;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Format a single compare list row, returning null for unrenderable rows.
|
|
160
|
+
*/
|
|
161
|
+
function formatCompareRow(row, selectedItem, isFocused, commits, files, width) {
|
|
162
|
+
if (row.type === 'section-header') {
|
|
163
|
+
const isCommits = row.sectionType === 'commits';
|
|
164
|
+
return formatSectionHeader(isCommits ? 'Commits' : 'Files', isCommits ? commits.length : files.length);
|
|
165
|
+
}
|
|
166
|
+
if (row.type === 'spacer')
|
|
167
|
+
return '';
|
|
168
|
+
if (row.type === 'directory' && row.treeRow)
|
|
169
|
+
return formatDirectoryRow(row.treeRow, width);
|
|
170
|
+
const selected = isRowSelected(row, selectedItem);
|
|
171
|
+
if (row.type === 'commit' && row.commit && row.commitIndex !== undefined) {
|
|
172
|
+
return formatCommitRow(row.commit, selected, isFocused, width);
|
|
173
|
+
}
|
|
174
|
+
if (row.type === 'file' && row.file && row.fileIndex !== undefined && row.treeRow) {
|
|
175
|
+
return formatFileRow(row.file, row.treeRow, selected, isFocused, width);
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
138
179
|
/**
|
|
139
180
|
* Format the compare list view as blessed-compatible tagged string.
|
|
140
181
|
*/
|
|
@@ -147,29 +188,9 @@ export function formatCompareListView(commits, files, selectedItem, isFocused, w
|
|
|
147
188
|
const visibleRows = maxHeight
|
|
148
189
|
? rows.slice(scrollOffset, scrollOffset + maxHeight)
|
|
149
190
|
: rows.slice(scrollOffset);
|
|
150
|
-
const lines =
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const isCommits = row.sectionType === 'commits';
|
|
154
|
-
const count = isCommits ? commits.length : files.length;
|
|
155
|
-
const label = isCommits ? 'Commits' : 'Files';
|
|
156
|
-
lines.push(`{escape}${ANSI_CYAN}${ANSI_BOLD}▼ ${label}${ANSI_RESET} ${ANSI_GRAY}(${count})${ANSI_RESET}{/escape}`);
|
|
157
|
-
}
|
|
158
|
-
else if (row.type === 'spacer') {
|
|
159
|
-
lines.push('');
|
|
160
|
-
}
|
|
161
|
-
else if (row.type === 'commit' && row.commit && row.commitIndex !== undefined) {
|
|
162
|
-
const isSelected = selectedItem?.type === 'commit' && selectedItem.index === row.commitIndex;
|
|
163
|
-
lines.push(formatCommitRow(row.commit, isSelected, isFocused, width));
|
|
164
|
-
}
|
|
165
|
-
else if (row.type === 'directory' && row.treeRow) {
|
|
166
|
-
lines.push(formatDirectoryRow(row.treeRow, width));
|
|
167
|
-
}
|
|
168
|
-
else if (row.type === 'file' && row.file && row.fileIndex !== undefined && row.treeRow) {
|
|
169
|
-
const isSelected = selectedItem?.type === 'file' && selectedItem.index === row.fileIndex;
|
|
170
|
-
lines.push(formatFileRow(row.file, row.treeRow, isSelected, isFocused, width));
|
|
171
|
-
}
|
|
172
|
-
}
|
|
191
|
+
const lines = visibleRows
|
|
192
|
+
.map((row) => formatCompareRow(row, selectedItem, isFocused, commits, files, width))
|
|
193
|
+
.filter((line) => line !== null);
|
|
173
194
|
return lines.join('\n');
|
|
174
195
|
}
|
|
175
196
|
/**
|