diffstalker 0.2.2 → 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/dist/App.js +118 -3
- package/dist/KeyBindings.js +75 -1
- package/dist/MouseHandlers.js +13 -2
- package/dist/core/ExplorerStateManager.js +21 -38
- package/dist/core/GitStateManager.js +142 -1
- package/dist/git/status.js +95 -0
- package/dist/index.js +55 -50
- package/dist/types/remote.js +5 -0
- package/dist/ui/PaneRenderers.js +14 -4
- package/dist/ui/modals/BranchPicker.js +157 -0
- package/dist/ui/modals/CommitActionConfirm.js +66 -0
- package/dist/ui/modals/FileFinder.js +30 -21
- package/dist/ui/modals/HotkeysModal.js +23 -0
- package/dist/ui/modals/SoftResetConfirm.js +68 -0
- package/dist/ui/modals/StashListModal.js +98 -0
- package/dist/ui/widgets/CommitPanel.js +113 -7
- package/dist/ui/widgets/Header.js +37 -3
- package/metrics/v0.2.3.json +243 -0
- package/package.json +5 -2
package/dist/ui/PaneRenderers.js
CHANGED
|
@@ -4,7 +4,7 @@ import { formatHistoryView } from './widgets/HistoryView.js';
|
|
|
4
4
|
import { formatCompareListView } from './widgets/CompareListView.js';
|
|
5
5
|
import { formatExplorerView } from './widgets/ExplorerView.js';
|
|
6
6
|
import { formatDiff, formatCombinedDiff, formatHistoryDiff } from './widgets/DiffView.js';
|
|
7
|
-
import { formatCommitPanel } from './widgets/CommitPanel.js';
|
|
7
|
+
import { formatCommitPanel, getCommitPanelTotalRows } from './widgets/CommitPanel.js';
|
|
8
8
|
import { formatExplorerContent } from './widgets/ExplorerContent.js';
|
|
9
9
|
/**
|
|
10
10
|
* Render the top pane content for the current tab.
|
|
@@ -31,10 +31,20 @@ export function renderTopPane(state, files, historyCommits, compareDiff, compare
|
|
|
31
31
|
/**
|
|
32
32
|
* Render the bottom pane content for the current tab.
|
|
33
33
|
*/
|
|
34
|
-
export function renderBottomPane(state, diff, historyState, compareSelectionState, explorerSelectedFile, commitFlowState, stagedCount, currentTheme, width, bottomPaneHeight, selectedHunkIndex, isFileStaged, combinedFileDiffs) {
|
|
34
|
+
export function renderBottomPane(state, diff, historyState, compareSelectionState, explorerSelectedFile, commitFlowState, stagedCount, currentTheme, width, bottomPaneHeight, selectedHunkIndex, isFileStaged, combinedFileDiffs, branch, remoteState, stashList, headCommit) {
|
|
35
35
|
if (state.bottomTab === 'commit') {
|
|
36
|
-
const
|
|
37
|
-
|
|
36
|
+
const panelOpts = {
|
|
37
|
+
state: commitFlowState,
|
|
38
|
+
stagedCount,
|
|
39
|
+
width,
|
|
40
|
+
branch,
|
|
41
|
+
remoteState,
|
|
42
|
+
stashList,
|
|
43
|
+
headCommit,
|
|
44
|
+
};
|
|
45
|
+
const totalRows = getCommitPanelTotalRows(panelOpts);
|
|
46
|
+
const content = formatCommitPanel(commitFlowState, stagedCount, width, branch, remoteState, stashList, headCommit, state.diffScrollOffset, bottomPaneHeight);
|
|
47
|
+
return { content, totalRows, hunkCount: 0, hunkBoundaries: [] };
|
|
38
48
|
}
|
|
39
49
|
if (state.bottomTab === 'history') {
|
|
40
50
|
const selectedCommit = historyState?.selectedCommit ?? null;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import blessed from 'neo-blessed';
|
|
2
|
+
/**
|
|
3
|
+
* BranchPicker modal for switching or creating branches.
|
|
4
|
+
* Text input at top for filtering; branch list below.
|
|
5
|
+
* If typed name matches no existing branch, shows "Create: <name>" as first option.
|
|
6
|
+
*/
|
|
7
|
+
export class BranchPicker {
|
|
8
|
+
box;
|
|
9
|
+
textbox;
|
|
10
|
+
screen;
|
|
11
|
+
branches;
|
|
12
|
+
filteredBranches = [];
|
|
13
|
+
selectedIndex = 0;
|
|
14
|
+
query = '';
|
|
15
|
+
showCreate = false;
|
|
16
|
+
onSwitch;
|
|
17
|
+
onCreate;
|
|
18
|
+
onCancel;
|
|
19
|
+
constructor(screen, branches, onSwitch, onCreate, onCancel) {
|
|
20
|
+
this.screen = screen;
|
|
21
|
+
this.branches = branches;
|
|
22
|
+
this.onSwitch = onSwitch;
|
|
23
|
+
this.onCreate = onCreate;
|
|
24
|
+
this.onCancel = onCancel;
|
|
25
|
+
this.filteredBranches = branches;
|
|
26
|
+
const width = Math.min(60, screen.width - 6);
|
|
27
|
+
const maxVisible = Math.min(branches.length + 1, 15);
|
|
28
|
+
const height = maxVisible + 7;
|
|
29
|
+
this.box = blessed.box({
|
|
30
|
+
parent: screen,
|
|
31
|
+
top: 'center',
|
|
32
|
+
left: 'center',
|
|
33
|
+
width,
|
|
34
|
+
height,
|
|
35
|
+
border: {
|
|
36
|
+
type: 'line',
|
|
37
|
+
},
|
|
38
|
+
style: {
|
|
39
|
+
border: {
|
|
40
|
+
fg: 'cyan',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
tags: true,
|
|
44
|
+
keys: false,
|
|
45
|
+
});
|
|
46
|
+
this.textbox = blessed.textarea({
|
|
47
|
+
parent: this.box,
|
|
48
|
+
top: 1,
|
|
49
|
+
left: 1,
|
|
50
|
+
width: width - 4,
|
|
51
|
+
height: 1,
|
|
52
|
+
inputOnFocus: true,
|
|
53
|
+
style: {
|
|
54
|
+
fg: 'white',
|
|
55
|
+
bg: 'default',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
this.setupKeyHandlers();
|
|
59
|
+
this.render();
|
|
60
|
+
}
|
|
61
|
+
setupKeyHandlers() {
|
|
62
|
+
this.textbox.key(['escape'], () => {
|
|
63
|
+
this.close();
|
|
64
|
+
this.onCancel();
|
|
65
|
+
});
|
|
66
|
+
this.textbox.key(['enter'], () => {
|
|
67
|
+
if (this.showCreate && this.selectedIndex === 0) {
|
|
68
|
+
this.close();
|
|
69
|
+
this.onCreate(this.query.trim());
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const adjustedIndex = this.showCreate ? this.selectedIndex - 1 : this.selectedIndex;
|
|
73
|
+
const branch = this.filteredBranches[adjustedIndex];
|
|
74
|
+
if (branch && !branch.current) {
|
|
75
|
+
this.close();
|
|
76
|
+
this.onSwitch(branch.name);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
this.textbox.key(['C-j', 'down'], () => {
|
|
81
|
+
const maxIndex = this.filteredBranches.length + (this.showCreate ? 1 : 0) - 1;
|
|
82
|
+
this.selectedIndex = Math.min(maxIndex, this.selectedIndex + 1);
|
|
83
|
+
this.render();
|
|
84
|
+
});
|
|
85
|
+
this.textbox.key(['C-k', 'up'], () => {
|
|
86
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
87
|
+
this.render();
|
|
88
|
+
});
|
|
89
|
+
this.textbox.on('keypress', () => {
|
|
90
|
+
setImmediate(() => {
|
|
91
|
+
const newQuery = this.textbox.getValue() || '';
|
|
92
|
+
if (newQuery !== this.query) {
|
|
93
|
+
this.query = newQuery;
|
|
94
|
+
this.selectedIndex = 0;
|
|
95
|
+
this.updateFilter();
|
|
96
|
+
this.render();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
updateFilter() {
|
|
102
|
+
const q = this.query.trim().toLowerCase();
|
|
103
|
+
if (!q) {
|
|
104
|
+
this.filteredBranches = this.branches;
|
|
105
|
+
this.showCreate = false;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
this.filteredBranches = this.branches.filter((b) => b.name.toLowerCase().includes(q));
|
|
109
|
+
// Show create option if no exact match
|
|
110
|
+
this.showCreate = !this.branches.some((b) => b.name === q);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
render() {
|
|
114
|
+
const lines = [];
|
|
115
|
+
lines.push('{bold}{cyan-fg}Switch / Create Branch{/cyan-fg}{/bold}');
|
|
116
|
+
lines.push(''); // Space for input
|
|
117
|
+
lines.push('');
|
|
118
|
+
if (this.showCreate) {
|
|
119
|
+
const isSelected = this.selectedIndex === 0;
|
|
120
|
+
if (isSelected) {
|
|
121
|
+
lines.push(`{green-fg}{bold}> Create: ${this.query.trim()}{/bold}{/green-fg}`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
lines.push(` {green-fg}Create: ${this.query.trim()}{/green-fg}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (let i = 0; i < this.filteredBranches.length; i++) {
|
|
128
|
+
const branch = this.filteredBranches[i];
|
|
129
|
+
const listIndex = this.showCreate ? i + 1 : i;
|
|
130
|
+
const isSelected = listIndex === this.selectedIndex;
|
|
131
|
+
let line = isSelected ? '{cyan-fg}{bold}> ' : ' ';
|
|
132
|
+
if (branch.current) {
|
|
133
|
+
line += '* ';
|
|
134
|
+
}
|
|
135
|
+
line += branch.name;
|
|
136
|
+
if (isSelected)
|
|
137
|
+
line += '{/bold}{/cyan-fg}';
|
|
138
|
+
if (branch.current)
|
|
139
|
+
line += ' {gray-fg}(current){/gray-fg}';
|
|
140
|
+
lines.push(line);
|
|
141
|
+
}
|
|
142
|
+
if (this.filteredBranches.length === 0 && !this.showCreate) {
|
|
143
|
+
lines.push('{gray-fg}No matching branches{/gray-fg}');
|
|
144
|
+
}
|
|
145
|
+
lines.push('');
|
|
146
|
+
lines.push('{gray-fg}Enter: select | Esc: cancel | Ctrl+j/k: navigate{/gray-fg}');
|
|
147
|
+
this.box.setContent(lines.join('\n'));
|
|
148
|
+
this.screen.render();
|
|
149
|
+
}
|
|
150
|
+
close() {
|
|
151
|
+
this.textbox.destroy();
|
|
152
|
+
this.box.destroy();
|
|
153
|
+
}
|
|
154
|
+
focus() {
|
|
155
|
+
this.textbox.focus();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import blessed from 'neo-blessed';
|
|
2
|
+
/**
|
|
3
|
+
* CommitActionConfirm modal for confirming cherry-pick or revert.
|
|
4
|
+
*/
|
|
5
|
+
export class CommitActionConfirm {
|
|
6
|
+
box;
|
|
7
|
+
screen;
|
|
8
|
+
onConfirm;
|
|
9
|
+
onCancel;
|
|
10
|
+
constructor(screen, verb, commit, 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 = 8;
|
|
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(verb, commit, 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(verb, commit, width) {
|
|
47
|
+
const lines = [];
|
|
48
|
+
const innerWidth = width - 6;
|
|
49
|
+
lines.push(`{bold}{yellow-fg} ${verb} commit?{/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}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}');
|
|
57
|
+
this.box.setContent(lines.join('\n'));
|
|
58
|
+
this.screen.render();
|
|
59
|
+
}
|
|
60
|
+
close() {
|
|
61
|
+
this.box.destroy();
|
|
62
|
+
}
|
|
63
|
+
focus() {
|
|
64
|
+
this.box.focus();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import blessed from 'neo-blessed';
|
|
2
2
|
import { Fzf } from 'fzf';
|
|
3
3
|
const MAX_RESULTS = 15;
|
|
4
|
+
const DEBOUNCE_MS = 15;
|
|
4
5
|
/**
|
|
5
|
-
* Highlight matched characters in a display path
|
|
6
|
+
* Highlight matched characters in a display path.
|
|
6
7
|
* The positions set refers to indices in the original full path,
|
|
7
8
|
* so we need an offset when the display path is truncated.
|
|
8
9
|
*/
|
|
@@ -25,19 +26,20 @@ export class FileFinder {
|
|
|
25
26
|
box;
|
|
26
27
|
textbox;
|
|
27
28
|
screen;
|
|
28
|
-
fzf;
|
|
29
29
|
allPaths;
|
|
30
30
|
results = [];
|
|
31
31
|
selectedIndex = 0;
|
|
32
32
|
query = '';
|
|
33
33
|
onSelect;
|
|
34
34
|
onCancel;
|
|
35
|
+
debounceTimer = null;
|
|
36
|
+
fzf;
|
|
35
37
|
constructor(screen, allPaths, onSelect, onCancel) {
|
|
36
38
|
this.screen = screen;
|
|
37
39
|
this.allPaths = allPaths;
|
|
38
|
-
this.fzf = new Fzf(allPaths, { limit: MAX_RESULTS, forward: false });
|
|
39
40
|
this.onSelect = onSelect;
|
|
40
41
|
this.onCancel = onCancel;
|
|
42
|
+
this.fzf = new Fzf(allPaths, { limit: MAX_RESULTS, casing: 'smart-case' });
|
|
41
43
|
// Create modal box
|
|
42
44
|
const width = Math.min(80, screen.width - 10);
|
|
43
45
|
const height = MAX_RESULTS + 6; // results + input + header + borders + padding
|
|
@@ -73,9 +75,9 @@ export class FileFinder {
|
|
|
73
75
|
});
|
|
74
76
|
// Setup key handlers
|
|
75
77
|
this.setupKeyHandlers();
|
|
76
|
-
// Initial render with
|
|
78
|
+
// Initial render with first N files
|
|
77
79
|
this.updateResults();
|
|
78
|
-
this.
|
|
80
|
+
this.renderContent();
|
|
79
81
|
}
|
|
80
82
|
setupKeyHandlers() {
|
|
81
83
|
// Handle escape to cancel
|
|
@@ -88,54 +90,59 @@ export class FileFinder {
|
|
|
88
90
|
if (this.results.length > 0) {
|
|
89
91
|
const selected = this.results[this.selectedIndex];
|
|
90
92
|
this.close();
|
|
91
|
-
this.onSelect(selected.
|
|
93
|
+
this.onSelect(selected.path);
|
|
92
94
|
}
|
|
93
95
|
});
|
|
94
96
|
// Handle up/down for navigation (Ctrl+j/k since j/k are for typing)
|
|
95
97
|
this.textbox.key(['C-j', 'down'], () => {
|
|
96
98
|
this.selectedIndex = Math.min(this.results.length - 1, this.selectedIndex + 1);
|
|
97
|
-
this.
|
|
99
|
+
this.renderContent();
|
|
98
100
|
});
|
|
99
101
|
this.textbox.key(['C-k', 'up'], () => {
|
|
100
102
|
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
101
|
-
this.
|
|
103
|
+
this.renderContent();
|
|
102
104
|
});
|
|
103
105
|
// Handle tab for next result
|
|
104
106
|
this.textbox.key(['tab'], () => {
|
|
105
107
|
this.selectedIndex = (this.selectedIndex + 1) % Math.max(1, this.results.length);
|
|
106
|
-
this.
|
|
108
|
+
this.renderContent();
|
|
107
109
|
});
|
|
108
110
|
// Handle shift-tab for previous result
|
|
109
111
|
this.textbox.key(['S-tab'], () => {
|
|
110
112
|
this.selectedIndex =
|
|
111
113
|
(this.selectedIndex - 1 + this.results.length) % Math.max(1, this.results.length);
|
|
112
|
-
this.
|
|
114
|
+
this.renderContent();
|
|
113
115
|
});
|
|
114
|
-
// Update results on keypress
|
|
116
|
+
// Update results on keypress with debounce
|
|
115
117
|
this.textbox.on('keypress', () => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
if (this.debounceTimer)
|
|
119
|
+
clearTimeout(this.debounceTimer);
|
|
120
|
+
this.debounceTimer = setTimeout(() => {
|
|
118
121
|
const newQuery = this.textbox.getValue() || '';
|
|
119
122
|
if (newQuery !== this.query) {
|
|
120
123
|
this.query = newQuery;
|
|
121
124
|
this.selectedIndex = 0;
|
|
122
125
|
this.updateResults();
|
|
123
|
-
this.
|
|
126
|
+
this.renderContent();
|
|
124
127
|
}
|
|
125
|
-
});
|
|
128
|
+
}, DEBOUNCE_MS);
|
|
126
129
|
});
|
|
127
130
|
}
|
|
128
131
|
updateResults() {
|
|
129
132
|
if (!this.query) {
|
|
130
|
-
// fzf returns nothing for empty query, show first N files
|
|
131
133
|
this.results = this.allPaths
|
|
132
134
|
.slice(0, MAX_RESULTS)
|
|
133
|
-
.map((
|
|
135
|
+
.map((p) => ({ path: p, positions: new Set(), score: 0 }));
|
|
134
136
|
return;
|
|
135
137
|
}
|
|
136
|
-
|
|
138
|
+
const entries = this.fzf.find(this.query);
|
|
139
|
+
this.results = entries.map((entry) => ({
|
|
140
|
+
path: entry.item,
|
|
141
|
+
score: entry.score,
|
|
142
|
+
positions: entry.positions,
|
|
143
|
+
}));
|
|
137
144
|
}
|
|
138
|
-
|
|
145
|
+
renderContent() {
|
|
139
146
|
const lines = [];
|
|
140
147
|
const width = this.box.width - 4;
|
|
141
148
|
// Header
|
|
@@ -151,7 +158,7 @@ export class FileFinder {
|
|
|
151
158
|
const result = this.results[i];
|
|
152
159
|
const isSelected = i === this.selectedIndex;
|
|
153
160
|
// Truncate path if needed
|
|
154
|
-
const fullPath = result.
|
|
161
|
+
const fullPath = result.path;
|
|
155
162
|
const maxLen = width - 4;
|
|
156
163
|
let displayPath = fullPath;
|
|
157
164
|
let offset = 0;
|
|
@@ -161,7 +168,7 @@ export class FileFinder {
|
|
|
161
168
|
// Account for the '…' prefix: display index 0 is '…', actual content starts at 1
|
|
162
169
|
offset = offset - 1;
|
|
163
170
|
}
|
|
164
|
-
// Highlight matched characters
|
|
171
|
+
// Highlight matched characters
|
|
165
172
|
const highlighted = highlightMatch(displayPath, result.positions, offset);
|
|
166
173
|
if (isSelected) {
|
|
167
174
|
lines.push(`{cyan-fg}{bold}> ${highlighted}{/bold}{/cyan-fg}`);
|
|
@@ -181,6 +188,8 @@ export class FileFinder {
|
|
|
181
188
|
this.screen.render();
|
|
182
189
|
}
|
|
183
190
|
close() {
|
|
191
|
+
if (this.debounceTimer)
|
|
192
|
+
clearTimeout(this.debounceTimer);
|
|
184
193
|
this.textbox.destroy();
|
|
185
194
|
this.box.destroy();
|
|
186
195
|
}
|
|
@@ -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
|
{
|
|
@@ -63,6 +67,25 @@ const hotkeyGroups = [
|
|
|
63
67
|
{ key: 'g', description: 'Show changes only' },
|
|
64
68
|
],
|
|
65
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' },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
66
89
|
{
|
|
67
90
|
title: 'Compare',
|
|
68
91
|
entries: [
|
|
@@ -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
|
+
}
|