diffstalker 0.2.2 → 0.2.4
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 +2 -2
- package/dist/App.js +299 -664
- package/dist/KeyBindings.js +125 -39
- package/dist/ModalController.js +166 -0
- package/dist/MouseHandlers.js +43 -25
- package/dist/NavigationController.js +290 -0
- package/dist/StagingOperations.js +199 -0
- package/dist/config.js +39 -0
- package/dist/core/CompareManager.js +134 -0
- package/dist/core/ExplorerStateManager.js +27 -40
- package/dist/core/GitStateManager.js +28 -630
- package/dist/core/HistoryManager.js +72 -0
- package/dist/core/RemoteOperationManager.js +109 -0
- package/dist/core/WorkingTreeManager.js +412 -0
- package/dist/git/status.js +95 -0
- package/dist/index.js +59 -54
- package/dist/state/FocusRing.js +40 -0
- package/dist/state/UIState.js +82 -48
- package/dist/types/remote.js +5 -0
- package/dist/ui/PaneRenderers.js +11 -4
- package/dist/ui/modals/BaseBranchPicker.js +4 -7
- package/dist/ui/modals/CommitActionConfirm.js +66 -0
- package/dist/ui/modals/DiscardConfirm.js +4 -7
- package/dist/ui/modals/FileFinder.js +33 -27
- package/dist/ui/modals/HotkeysModal.js +32 -13
- package/dist/ui/modals/Modal.js +1 -0
- package/dist/ui/modals/RepoPicker.js +109 -0
- package/dist/ui/modals/ThemePicker.js +4 -7
- package/dist/ui/widgets/CommitPanel.js +52 -14
- package/dist/ui/widgets/CompareListView.js +1 -11
- package/dist/ui/widgets/DiffView.js +2 -27
- package/dist/ui/widgets/ExplorerContent.js +1 -4
- package/dist/ui/widgets/ExplorerView.js +1 -11
- package/dist/ui/widgets/FileList.js +2 -8
- package/dist/ui/widgets/Footer.js +1 -0
- package/dist/ui/widgets/Header.js +37 -3
- package/dist/utils/ansi.js +38 -0
- package/dist/utils/ansiTruncate.js +1 -5
- package/dist/utils/displayRows.js +72 -59
- package/dist/utils/fileCategories.js +7 -0
- package/dist/utils/fileResolution.js +23 -0
- package/dist/utils/languageDetection.js +3 -2
- package/dist/utils/logger.js +32 -0
- package/metrics/v0.2.3.json +243 -0
- package/metrics/v0.2.4.json +236 -0
- package/package.json +5 -2
- package/dist/utils/layoutCalculations.js +0 -100
package/dist/KeyBindings.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { SPLIT_RATIO_STEP } from './ui/Layout.js';
|
|
2
|
-
import { getFileAtIndex } from './ui/widgets/FileList.js';
|
|
3
|
-
import { getFlatFileAtIndex } from './utils/flatFileList.js';
|
|
4
2
|
/**
|
|
5
3
|
* Register all keyboard bindings on the blessed screen.
|
|
6
4
|
*/
|
|
7
5
|
export function setupKeyBindings(screen, actions, ctx) {
|
|
8
|
-
// Quit
|
|
9
|
-
screen.key(['q'
|
|
6
|
+
// Quit: q closes modal if open, Ctrl+C always exits
|
|
7
|
+
screen.key(['q'], () => {
|
|
8
|
+
if (ctx.hasActiveModal()) {
|
|
9
|
+
actions.closeActiveModal();
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
actions.exit();
|
|
13
|
+
});
|
|
14
|
+
screen.key(['C-c'], () => {
|
|
10
15
|
actions.exit();
|
|
11
16
|
});
|
|
12
17
|
// Navigation (skip if modal is open)
|
|
@@ -35,11 +40,16 @@ export function setupKeyBindings(screen, actions, ctx) {
|
|
|
35
40
|
ctx.uiState.setTab(tab);
|
|
36
41
|
});
|
|
37
42
|
}
|
|
38
|
-
//
|
|
43
|
+
// Focus zone cycling (skip if modal or commit input is active)
|
|
39
44
|
screen.key(['tab'], () => {
|
|
40
|
-
if (ctx.hasActiveModal())
|
|
45
|
+
if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
|
|
46
|
+
return;
|
|
47
|
+
ctx.uiState.advanceFocus();
|
|
48
|
+
});
|
|
49
|
+
screen.key(['S-tab'], () => {
|
|
50
|
+
if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
|
|
41
51
|
return;
|
|
42
|
-
ctx.uiState.
|
|
52
|
+
ctx.uiState.retreatFocus();
|
|
43
53
|
});
|
|
44
54
|
// Staging operations (skip if modal is open)
|
|
45
55
|
// Context-aware: hunk staging when diff pane is focused on diff tab
|
|
@@ -72,6 +82,17 @@ export function setupKeyBindings(screen, actions, ctx) {
|
|
|
72
82
|
screen.key(['enter', 'space'], () => {
|
|
73
83
|
if (ctx.hasActiveModal())
|
|
74
84
|
return;
|
|
85
|
+
const zone = ctx.getFocusedZone();
|
|
86
|
+
// Zone-aware dispatch for commit panel elements
|
|
87
|
+
if (zone === 'commitMessage' && !ctx.isCommitInputFocused()) {
|
|
88
|
+
actions.focusCommitInput();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (zone === 'commitAmend') {
|
|
92
|
+
ctx.commitFlowState.toggleAmend();
|
|
93
|
+
actions.render();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
75
96
|
if (ctx.getBottomTab() === 'explorer' && ctx.getCurrentPane() === 'explorer') {
|
|
76
97
|
actions.enterExplorerDirectory();
|
|
77
98
|
}
|
|
@@ -122,6 +143,8 @@ export function setupKeyBindings(screen, actions, ctx) {
|
|
|
122
143
|
}
|
|
123
144
|
});
|
|
124
145
|
screen.key(['a'], () => {
|
|
146
|
+
if (ctx.hasActiveModal())
|
|
147
|
+
return;
|
|
125
148
|
if (ctx.getBottomTab() === 'commit' && !ctx.isCommitInputFocused()) {
|
|
126
149
|
ctx.commitFlowState.toggleAmend();
|
|
127
150
|
actions.render();
|
|
@@ -130,7 +153,19 @@ export function setupKeyBindings(screen, actions, ctx) {
|
|
|
130
153
|
ctx.uiState.toggleAutoTab();
|
|
131
154
|
}
|
|
132
155
|
});
|
|
156
|
+
// Ctrl+a: toggle amend on commit tab (works even when input is focused)
|
|
157
|
+
screen.key(['C-a'], () => {
|
|
158
|
+
if (ctx.getBottomTab() === 'commit') {
|
|
159
|
+
ctx.commitFlowState.toggleAmend();
|
|
160
|
+
actions.render();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
// Escape: close modal first, then commit-tab escape logic
|
|
133
164
|
screen.key(['escape'], () => {
|
|
165
|
+
if (ctx.hasActiveModal()) {
|
|
166
|
+
actions.closeActiveModal();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
134
169
|
if (ctx.getBottomTab() === 'commit') {
|
|
135
170
|
if (ctx.isCommitInputFocused()) {
|
|
136
171
|
actions.unfocusCommitInput();
|
|
@@ -140,12 +175,32 @@ export function setupKeyBindings(screen, actions, ctx) {
|
|
|
140
175
|
}
|
|
141
176
|
}
|
|
142
177
|
});
|
|
143
|
-
//
|
|
144
|
-
screen.key(['r'], () =>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
178
|
+
// Repo picker (toggle)
|
|
179
|
+
screen.key(['r'], () => {
|
|
180
|
+
if (ctx.getActiveModalType() === 'repoPicker') {
|
|
181
|
+
actions.closeActiveModal();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (ctx.hasActiveModal())
|
|
185
|
+
return;
|
|
186
|
+
actions.openRepoPicker();
|
|
187
|
+
});
|
|
188
|
+
// Display toggles (guarded)
|
|
189
|
+
screen.key(['w'], () => {
|
|
190
|
+
if (ctx.hasActiveModal())
|
|
191
|
+
return;
|
|
192
|
+
ctx.uiState.toggleWrapMode();
|
|
193
|
+
});
|
|
194
|
+
screen.key(['m'], () => {
|
|
195
|
+
if (ctx.hasActiveModal())
|
|
196
|
+
return;
|
|
197
|
+
actions.toggleMouseMode();
|
|
198
|
+
});
|
|
199
|
+
screen.key(['S-t'], () => {
|
|
200
|
+
if (ctx.hasActiveModal())
|
|
201
|
+
return;
|
|
202
|
+
ctx.uiState.toggleAutoTab();
|
|
203
|
+
});
|
|
149
204
|
// Split ratio adjustments
|
|
150
205
|
screen.key(['-', '_', '['], () => {
|
|
151
206
|
ctx.uiState.adjustSplitRatio(-SPLIT_RATIO_STEP);
|
|
@@ -157,16 +212,42 @@ export function setupKeyBindings(screen, actions, ctx) {
|
|
|
157
212
|
ctx.layout.setSplitRatio(ctx.uiState.state.splitRatio);
|
|
158
213
|
actions.render();
|
|
159
214
|
});
|
|
160
|
-
// Theme picker
|
|
161
|
-
screen.key(['t'], () =>
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
215
|
+
// Theme picker (toggle)
|
|
216
|
+
screen.key(['t'], () => {
|
|
217
|
+
if (ctx.getActiveModalType() === 'theme') {
|
|
218
|
+
actions.closeActiveModal();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (ctx.hasActiveModal())
|
|
222
|
+
return;
|
|
223
|
+
actions.openThemePicker();
|
|
224
|
+
});
|
|
225
|
+
// Hotkeys modal (toggle)
|
|
226
|
+
screen.key(['?'], () => {
|
|
227
|
+
if (ctx.getActiveModalType() === 'hotkeys') {
|
|
228
|
+
actions.closeActiveModal();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (ctx.hasActiveModal())
|
|
232
|
+
return;
|
|
233
|
+
actions.openHotkeysModal();
|
|
234
|
+
});
|
|
235
|
+
// Follow toggle (guarded)
|
|
236
|
+
screen.key(['f'], () => {
|
|
237
|
+
if (ctx.hasActiveModal())
|
|
238
|
+
return;
|
|
239
|
+
actions.toggleFollow();
|
|
240
|
+
});
|
|
241
|
+
// Compare view: base branch picker (toggle)
|
|
167
242
|
screen.key(['b'], () => {
|
|
243
|
+
if (ctx.getActiveModalType() === 'baseBranch') {
|
|
244
|
+
actions.closeActiveModal();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (ctx.hasActiveModal())
|
|
248
|
+
return;
|
|
168
249
|
if (ctx.getBottomTab() === 'compare') {
|
|
169
|
-
|
|
250
|
+
actions.openBaseBranchPicker();
|
|
170
251
|
}
|
|
171
252
|
});
|
|
172
253
|
// u: toggle uncommitted in compare view
|
|
@@ -176,7 +257,7 @@ export function setupKeyBindings(screen, actions, ctx) {
|
|
|
176
257
|
if (ctx.getBottomTab() === 'compare') {
|
|
177
258
|
ctx.uiState.toggleIncludeUncommitted();
|
|
178
259
|
const includeUncommitted = ctx.uiState.state.includeUncommitted;
|
|
179
|
-
ctx.getGitManager()?.refreshCompareDiff(includeUncommitted);
|
|
260
|
+
ctx.getGitManager()?.compare.refreshCompareDiff(includeUncommitted);
|
|
180
261
|
}
|
|
181
262
|
});
|
|
182
263
|
// Toggle flat file view (diff/commit tab only)
|
|
@@ -188,25 +269,14 @@ export function setupKeyBindings(screen, actions, ctx) {
|
|
|
188
269
|
ctx.uiState.toggleFlatViewMode();
|
|
189
270
|
}
|
|
190
271
|
});
|
|
191
|
-
// Discard changes (with confirmation)
|
|
272
|
+
// Discard changes (with confirmation, guarded)
|
|
192
273
|
screen.key(['d'], () => {
|
|
274
|
+
if (ctx.hasActiveModal())
|
|
275
|
+
return;
|
|
193
276
|
if (ctx.getBottomTab() === 'diff') {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const file = flatEntry.unstagedEntry;
|
|
198
|
-
if (file.status !== 'untracked') {
|
|
199
|
-
actions.showDiscardConfirm(file);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
else {
|
|
204
|
-
const files = ctx.getStatusFiles();
|
|
205
|
-
const selectedFile = getFileAtIndex(files, ctx.getSelectedIndex());
|
|
206
|
-
// Only allow discard for unstaged modified files
|
|
207
|
-
if (selectedFile && !selectedFile.staged && selectedFile.status !== 'untracked') {
|
|
208
|
-
actions.showDiscardConfirm(selectedFile);
|
|
209
|
-
}
|
|
277
|
+
const file = ctx.resolveFileAtIndex(ctx.getSelectedIndex());
|
|
278
|
+
if (file && !file.staged && file.status !== 'untracked') {
|
|
279
|
+
actions.openDiscardConfirm(file);
|
|
210
280
|
}
|
|
211
281
|
}
|
|
212
282
|
});
|
|
@@ -225,4 +295,20 @@ export function setupKeyBindings(screen, actions, ctx) {
|
|
|
225
295
|
actions.navigatePrevHunk();
|
|
226
296
|
}
|
|
227
297
|
});
|
|
298
|
+
// Cherry-pick selected commit (history tab only)
|
|
299
|
+
screen.key(['p'], () => {
|
|
300
|
+
if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
|
|
301
|
+
return;
|
|
302
|
+
if (ctx.getBottomTab() === 'history') {
|
|
303
|
+
actions.openCherryPickConfirm();
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
// Revert selected commit (history tab only)
|
|
307
|
+
screen.key(['v'], () => {
|
|
308
|
+
if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
|
|
309
|
+
return;
|
|
310
|
+
if (ctx.getBottomTab() === 'history') {
|
|
311
|
+
actions.openRevertConfirm();
|
|
312
|
+
}
|
|
313
|
+
});
|
|
228
314
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { ThemePicker } from './ui/modals/ThemePicker.js';
|
|
2
|
+
import { HotkeysModal } from './ui/modals/HotkeysModal.js';
|
|
3
|
+
import { BaseBranchPicker } from './ui/modals/BaseBranchPicker.js';
|
|
4
|
+
import { DiscardConfirm } from './ui/modals/DiscardConfirm.js';
|
|
5
|
+
import { FileFinder } from './ui/modals/FileFinder.js';
|
|
6
|
+
import { CommitActionConfirm } from './ui/modals/CommitActionConfirm.js';
|
|
7
|
+
import { RepoPicker } from './ui/modals/RepoPicker.js';
|
|
8
|
+
import { saveConfig } from './config.js';
|
|
9
|
+
import * as logger from './utils/logger.js';
|
|
10
|
+
/**
|
|
11
|
+
* Manages all modal dialogs: creation, focus, and dismissal.
|
|
12
|
+
* Single source of truth for modal state.
|
|
13
|
+
*/
|
|
14
|
+
export class ModalController {
|
|
15
|
+
ctx;
|
|
16
|
+
activeModal = null;
|
|
17
|
+
activeModalType = null;
|
|
18
|
+
constructor(ctx) {
|
|
19
|
+
this.ctx = ctx;
|
|
20
|
+
}
|
|
21
|
+
hasActiveModal() {
|
|
22
|
+
return this.activeModal !== null;
|
|
23
|
+
}
|
|
24
|
+
getActiveModalType() {
|
|
25
|
+
return this.activeModalType;
|
|
26
|
+
}
|
|
27
|
+
closeActiveModal() {
|
|
28
|
+
if (this.activeModal) {
|
|
29
|
+
this.activeModal.destroy();
|
|
30
|
+
this.activeModal = null;
|
|
31
|
+
this.activeModalType = null;
|
|
32
|
+
this.ctx.render();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
clearModal() {
|
|
36
|
+
this.activeModal = null;
|
|
37
|
+
this.activeModalType = null;
|
|
38
|
+
}
|
|
39
|
+
openThemePicker() {
|
|
40
|
+
this.activeModalType = 'theme';
|
|
41
|
+
this.activeModal = new ThemePicker(this.ctx.screen, this.ctx.getCurrentTheme(), (theme) => {
|
|
42
|
+
this.ctx.setCurrentTheme(theme);
|
|
43
|
+
saveConfig({ theme });
|
|
44
|
+
this.clearModal();
|
|
45
|
+
this.ctx.render();
|
|
46
|
+
}, () => {
|
|
47
|
+
this.clearModal();
|
|
48
|
+
});
|
|
49
|
+
this.activeModal.focus();
|
|
50
|
+
}
|
|
51
|
+
openHotkeysModal() {
|
|
52
|
+
this.activeModalType = 'hotkeys';
|
|
53
|
+
this.activeModal = new HotkeysModal(this.ctx.screen, () => {
|
|
54
|
+
this.clearModal();
|
|
55
|
+
});
|
|
56
|
+
this.activeModal.focus();
|
|
57
|
+
}
|
|
58
|
+
openBaseBranchPicker() {
|
|
59
|
+
const gm = this.ctx.getGitManager();
|
|
60
|
+
if (!gm)
|
|
61
|
+
return;
|
|
62
|
+
this.activeModalType = 'baseBranch';
|
|
63
|
+
gm.compare
|
|
64
|
+
.getCandidateBaseBranches()
|
|
65
|
+
.then((branches) => {
|
|
66
|
+
const currentBranch = gm.compare.compareState.compareBaseBranch ?? null;
|
|
67
|
+
const modal = new BaseBranchPicker(this.ctx.screen, branches, currentBranch, (branch) => {
|
|
68
|
+
this.clearModal();
|
|
69
|
+
const includeUncommitted = this.ctx.uiState.state.includeUncommitted;
|
|
70
|
+
gm.compare.setCompareBaseBranch(branch, includeUncommitted);
|
|
71
|
+
}, () => {
|
|
72
|
+
this.clearModal();
|
|
73
|
+
});
|
|
74
|
+
this.activeModal = modal;
|
|
75
|
+
modal.focus();
|
|
76
|
+
})
|
|
77
|
+
.catch((err) => {
|
|
78
|
+
this.clearModal();
|
|
79
|
+
logger.error('Failed to load base branches', err);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
openDiscardConfirm(file) {
|
|
83
|
+
this.activeModalType = 'discard';
|
|
84
|
+
this.activeModal = new DiscardConfirm(this.ctx.screen, file.path, async () => {
|
|
85
|
+
this.clearModal();
|
|
86
|
+
await this.ctx.getGitManager()?.workingTree.discard(file);
|
|
87
|
+
}, () => {
|
|
88
|
+
this.clearModal();
|
|
89
|
+
});
|
|
90
|
+
this.activeModal.focus();
|
|
91
|
+
}
|
|
92
|
+
async openFileFinder() {
|
|
93
|
+
const explorer = this.ctx.getExplorerManager();
|
|
94
|
+
let allPaths = explorer?.getCachedFilePaths() ?? [];
|
|
95
|
+
if (allPaths.length === 0) {
|
|
96
|
+
await explorer?.loadFilePaths();
|
|
97
|
+
allPaths = explorer?.getCachedFilePaths() ?? [];
|
|
98
|
+
}
|
|
99
|
+
if (allPaths.length === 0)
|
|
100
|
+
return;
|
|
101
|
+
this.activeModalType = 'fileFinder';
|
|
102
|
+
this.activeModal = new FileFinder(this.ctx.screen, allPaths, async (selectedPath) => {
|
|
103
|
+
this.clearModal();
|
|
104
|
+
if (this.ctx.uiState.state.bottomTab !== 'explorer') {
|
|
105
|
+
this.ctx.uiState.setTab('explorer');
|
|
106
|
+
}
|
|
107
|
+
const success = await explorer?.navigateToPath(selectedPath);
|
|
108
|
+
if (success) {
|
|
109
|
+
const selectedIndex = explorer?.state.selectedIndex ?? 0;
|
|
110
|
+
this.ctx.uiState.setExplorerSelectedIndex(selectedIndex);
|
|
111
|
+
this.ctx.uiState.setExplorerFileScrollOffset(0);
|
|
112
|
+
const visibleHeight = this.ctx.getTopPaneHeight();
|
|
113
|
+
if (selectedIndex >= visibleHeight) {
|
|
114
|
+
this.ctx.uiState.setExplorerScrollOffset(selectedIndex - Math.floor(visibleHeight / 2));
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
this.ctx.uiState.setExplorerScrollOffset(0);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
this.ctx.render();
|
|
121
|
+
}, () => {
|
|
122
|
+
this.clearModal();
|
|
123
|
+
this.ctx.render();
|
|
124
|
+
});
|
|
125
|
+
this.activeModal.focus();
|
|
126
|
+
}
|
|
127
|
+
openCherryPickConfirm() {
|
|
128
|
+
const commit = this.ctx.getGitManager()?.history.historyState.selectedCommit;
|
|
129
|
+
if (!commit)
|
|
130
|
+
return;
|
|
131
|
+
this.activeModalType = 'commitAction';
|
|
132
|
+
this.activeModal = new CommitActionConfirm(this.ctx.screen, 'Cherry-pick', commit, () => {
|
|
133
|
+
this.clearModal();
|
|
134
|
+
this.ctx.getGitManager()?.remote.cherryPick(commit.hash);
|
|
135
|
+
}, () => {
|
|
136
|
+
this.clearModal();
|
|
137
|
+
});
|
|
138
|
+
this.activeModal.focus();
|
|
139
|
+
}
|
|
140
|
+
openRevertConfirm() {
|
|
141
|
+
const commit = this.ctx.getGitManager()?.history.historyState.selectedCommit;
|
|
142
|
+
if (!commit)
|
|
143
|
+
return;
|
|
144
|
+
this.activeModalType = 'commitAction';
|
|
145
|
+
this.activeModal = new CommitActionConfirm(this.ctx.screen, 'Revert', commit, () => {
|
|
146
|
+
this.clearModal();
|
|
147
|
+
this.ctx.getGitManager()?.remote.revertCommit(commit.hash);
|
|
148
|
+
}, () => {
|
|
149
|
+
this.clearModal();
|
|
150
|
+
});
|
|
151
|
+
this.activeModal.focus();
|
|
152
|
+
}
|
|
153
|
+
openRepoPicker() {
|
|
154
|
+
const repos = this.ctx.getRecentRepos();
|
|
155
|
+
const currentRepo = this.ctx.getRepoPath();
|
|
156
|
+
this.activeModalType = 'repoPicker';
|
|
157
|
+
this.activeModal = new RepoPicker(this.ctx.screen, repos, currentRepo, (selected) => {
|
|
158
|
+
this.clearModal();
|
|
159
|
+
this.ctx.onRepoSwitch(selected);
|
|
160
|
+
}, () => {
|
|
161
|
+
this.clearModal();
|
|
162
|
+
this.ctx.render();
|
|
163
|
+
});
|
|
164
|
+
this.activeModal.focus();
|
|
165
|
+
}
|
|
166
|
+
}
|
package/dist/MouseHandlers.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TAB_ZONES } from './state/UIState.js';
|
|
1
2
|
import { getFileListTotalRows, getFileIndexFromRow } from './ui/widgets/FileList.js';
|
|
2
3
|
import { getFlatFileListTotalRows } from './ui/widgets/FlatFileList.js';
|
|
3
4
|
import { getCompareListTotalRows, getCompareSelectionFromRow, } from './ui/widgets/CompareListView.js';
|
|
@@ -22,18 +23,28 @@ export function setupMouseHandlers(layout, actions, ctx) {
|
|
|
22
23
|
layout.bottomPane.on('wheelup', () => {
|
|
23
24
|
handleBottomPaneScroll(-SCROLL_AMOUNT, layout, ctx);
|
|
24
25
|
});
|
|
25
|
-
// Click on top pane to select item
|
|
26
|
+
// Click on top pane to select item (does NOT change focus zone —
|
|
27
|
+
// preserves diff pane focus so hunk staging with 's' keeps working)
|
|
26
28
|
layout.topPane.on('click', (mouse) => {
|
|
27
29
|
const clickedRow = layout.screenYToTopPaneRow(mouse.y);
|
|
28
30
|
if (clickedRow >= 0) {
|
|
29
31
|
handleTopPaneClick(clickedRow, mouse.x, actions, ctx);
|
|
30
32
|
}
|
|
31
33
|
});
|
|
32
|
-
// Click on bottom pane
|
|
34
|
+
// Click on bottom pane
|
|
33
35
|
layout.bottomPane.on('click', (mouse) => {
|
|
34
36
|
const clickedRow = layout.screenYToBottomPaneRow(mouse.y);
|
|
35
37
|
if (clickedRow >= 0) {
|
|
36
|
-
|
|
38
|
+
if (ctx.uiState.state.bottomTab === 'commit') {
|
|
39
|
+
handleCommitPaneClick(clickedRow, actions, ctx);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
// Set focus to the bottom-pane zone for this tab
|
|
43
|
+
const zones = TAB_ZONES[ctx.uiState.state.bottomTab];
|
|
44
|
+
const bottomZone = zones[zones.length - 1];
|
|
45
|
+
ctx.uiState.setFocusedZone(bottomZone);
|
|
46
|
+
actions.selectHunkAtRow(clickedRow);
|
|
47
|
+
}
|
|
37
48
|
}
|
|
38
49
|
});
|
|
39
50
|
// Click on footer for tabs and toggles
|
|
@@ -43,33 +54,40 @@ export function setupMouseHandlers(layout, actions, ctx) {
|
|
|
43
54
|
}
|
|
44
55
|
function handleFileListClick(row, x, actions, ctx) {
|
|
45
56
|
const state = ctx.uiState.state;
|
|
57
|
+
// Row-to-index mapping differs between flat and categorized mode
|
|
58
|
+
let fileIndex;
|
|
46
59
|
if (state.flatViewMode) {
|
|
47
60
|
// Flat mode: row 0 is header, files start at row 1
|
|
48
61
|
const absoluteRow = row + state.fileListScrollOffset;
|
|
49
|
-
const
|
|
62
|
+
const idx = absoluteRow - 1; // subtract header row
|
|
50
63
|
const flatFiles = ctx.getCachedFlatFiles();
|
|
51
|
-
|
|
52
|
-
return;
|
|
53
|
-
if (x !== undefined && x >= 2 && x <= 4) {
|
|
54
|
-
actions.toggleFileByIndex(fileIndex);
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
ctx.uiState.setSelectedIndex(fileIndex);
|
|
58
|
-
actions.selectFileByIndex(fileIndex);
|
|
59
|
-
}
|
|
64
|
+
fileIndex = idx >= 0 && idx < flatFiles.length ? idx : null;
|
|
60
65
|
}
|
|
61
66
|
else {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
fileIndex = getFileIndexFromRow(row + state.fileListScrollOffset, ctx.getStatusFiles());
|
|
68
|
+
}
|
|
69
|
+
if (fileIndex === null || fileIndex < 0)
|
|
70
|
+
return;
|
|
71
|
+
if (x !== undefined && x >= 2 && x <= 4) {
|
|
72
|
+
actions.toggleFileByIndex(fileIndex);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
ctx.uiState.setSelectedIndex(fileIndex);
|
|
76
|
+
actions.selectFileByIndex(fileIndex);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function handleCommitPaneClick(row, actions, ctx) {
|
|
80
|
+
// Commit panel layout: rows 0-4 = title + message box, row 5 = blank, row 6 = amend
|
|
81
|
+
const absoluteRow = row + ctx.uiState.state.diffScrollOffset;
|
|
82
|
+
if (absoluteRow === 6) {
|
|
83
|
+
ctx.uiState.setFocusedZone('commitAmend');
|
|
84
|
+
}
|
|
85
|
+
else if (absoluteRow >= 2 && absoluteRow <= 4) {
|
|
86
|
+
ctx.uiState.setFocusedZone('commitMessage');
|
|
87
|
+
actions.focusCommitInput();
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
ctx.uiState.setFocusedZone('commitMessage');
|
|
73
91
|
}
|
|
74
92
|
}
|
|
75
93
|
function handleTopPaneClick(row, x, actions, ctx) {
|
|
@@ -140,7 +158,7 @@ function handleFooterClick(x, actions, ctx) {
|
|
|
140
158
|
ctx.getExplorerManager()?.toggleShowOnlyChanges();
|
|
141
159
|
}
|
|
142
160
|
else if (x === 0) {
|
|
143
|
-
|
|
161
|
+
actions.openHotkeysModal();
|
|
144
162
|
}
|
|
145
163
|
}
|
|
146
164
|
function handleTopPaneScroll(delta, layout, ctx) {
|