diffstalker 0.2.3 → 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 +278 -758
- package/dist/KeyBindings.js +103 -91
- package/dist/ModalController.js +166 -0
- package/dist/MouseHandlers.js +37 -30
- 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 +7 -3
- package/dist/core/GitStateManager.js +28 -771
- package/dist/core/HistoryManager.js +72 -0
- package/dist/core/RemoteOperationManager.js +109 -0
- package/dist/core/WorkingTreeManager.js +412 -0
- package/dist/index.js +57 -57
- package/dist/state/FocusRing.js +40 -0
- package/dist/state/UIState.js +82 -48
- package/dist/ui/PaneRenderers.js +3 -6
- package/dist/ui/modals/BaseBranchPicker.js +4 -7
- package/dist/ui/modals/CommitActionConfirm.js +4 -4
- package/dist/ui/modals/DiscardConfirm.js +4 -7
- package/dist/ui/modals/FileFinder.js +3 -6
- package/dist/ui/modals/HotkeysModal.js +17 -21
- 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 +26 -94
- 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/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.4.json +236 -0
- package/package.json +1 -1
- package/dist/ui/modals/BranchPicker.js +0 -157
- package/dist/ui/modals/SoftResetConfirm.js +0 -68
- package/dist/ui/modals/StashListModal.js +0 -98
- 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())
|
|
41
46
|
return;
|
|
42
|
-
ctx.uiState.
|
|
47
|
+
ctx.uiState.advanceFocus();
|
|
48
|
+
});
|
|
49
|
+
screen.key(['S-tab'], () => {
|
|
50
|
+
if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
|
|
51
|
+
return;
|
|
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();
|
|
@@ -137,23 +160,12 @@ export function setupKeyBindings(screen, actions, ctx) {
|
|
|
137
160
|
actions.render();
|
|
138
161
|
}
|
|
139
162
|
});
|
|
140
|
-
//
|
|
141
|
-
screen.key(['S-p'], () => {
|
|
142
|
-
if (ctx.hasActiveModal() || ctx.isCommitInputFocused() || ctx.isRemoteInProgress())
|
|
143
|
-
return;
|
|
144
|
-
actions.push();
|
|
145
|
-
});
|
|
146
|
-
screen.key(['S-f'], () => {
|
|
147
|
-
if (ctx.hasActiveModal() || ctx.isCommitInputFocused() || ctx.isRemoteInProgress())
|
|
148
|
-
return;
|
|
149
|
-
actions.fetchRemote();
|
|
150
|
-
});
|
|
151
|
-
screen.key(['S-r'], () => {
|
|
152
|
-
if (ctx.hasActiveModal() || ctx.isCommitInputFocused() || ctx.isRemoteInProgress())
|
|
153
|
-
return;
|
|
154
|
-
actions.pullRebase();
|
|
155
|
-
});
|
|
163
|
+
// Escape: close modal first, then commit-tab escape logic
|
|
156
164
|
screen.key(['escape'], () => {
|
|
165
|
+
if (ctx.hasActiveModal()) {
|
|
166
|
+
actions.closeActiveModal();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
157
169
|
if (ctx.getBottomTab() === 'commit') {
|
|
158
170
|
if (ctx.isCommitInputFocused()) {
|
|
159
171
|
actions.unfocusCommitInput();
|
|
@@ -163,12 +175,32 @@ export function setupKeyBindings(screen, actions, ctx) {
|
|
|
163
175
|
}
|
|
164
176
|
}
|
|
165
177
|
});
|
|
166
|
-
//
|
|
167
|
-
screen.key(['r'], () =>
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
+
});
|
|
172
204
|
// Split ratio adjustments
|
|
173
205
|
screen.key(['-', '_', '['], () => {
|
|
174
206
|
ctx.uiState.adjustSplitRatio(-SPLIT_RATIO_STEP);
|
|
@@ -180,21 +212,42 @@ export function setupKeyBindings(screen, actions, ctx) {
|
|
|
180
212
|
ctx.layout.setSplitRatio(ctx.uiState.state.splitRatio);
|
|
181
213
|
actions.render();
|
|
182
214
|
});
|
|
183
|
-
// Theme picker
|
|
184
|
-
screen.key(['t'], () =>
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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)
|
|
190
242
|
screen.key(['b'], () => {
|
|
191
|
-
if (ctx.
|
|
243
|
+
if (ctx.getActiveModalType() === 'baseBranch') {
|
|
244
|
+
actions.closeActiveModal();
|
|
192
245
|
return;
|
|
193
|
-
if (ctx.getBottomTab() === 'compare') {
|
|
194
|
-
ctx.uiState.openModal('baseBranch');
|
|
195
246
|
}
|
|
196
|
-
|
|
197
|
-
|
|
247
|
+
if (ctx.hasActiveModal())
|
|
248
|
+
return;
|
|
249
|
+
if (ctx.getBottomTab() === 'compare') {
|
|
250
|
+
actions.openBaseBranchPicker();
|
|
198
251
|
}
|
|
199
252
|
});
|
|
200
253
|
// u: toggle uncommitted in compare view
|
|
@@ -204,7 +257,7 @@ export function setupKeyBindings(screen, actions, ctx) {
|
|
|
204
257
|
if (ctx.getBottomTab() === 'compare') {
|
|
205
258
|
ctx.uiState.toggleIncludeUncommitted();
|
|
206
259
|
const includeUncommitted = ctx.uiState.state.includeUncommitted;
|
|
207
|
-
ctx.getGitManager()?.refreshCompareDiff(includeUncommitted);
|
|
260
|
+
ctx.getGitManager()?.compare.refreshCompareDiff(includeUncommitted);
|
|
208
261
|
}
|
|
209
262
|
});
|
|
210
263
|
// Toggle flat file view (diff/commit tab only)
|
|
@@ -216,25 +269,14 @@ export function setupKeyBindings(screen, actions, ctx) {
|
|
|
216
269
|
ctx.uiState.toggleFlatViewMode();
|
|
217
270
|
}
|
|
218
271
|
});
|
|
219
|
-
// Discard changes (with confirmation)
|
|
272
|
+
// Discard changes (with confirmation, guarded)
|
|
220
273
|
screen.key(['d'], () => {
|
|
274
|
+
if (ctx.hasActiveModal())
|
|
275
|
+
return;
|
|
221
276
|
if (ctx.getBottomTab() === 'diff') {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const file = flatEntry.unstagedEntry;
|
|
226
|
-
if (file.status !== 'untracked') {
|
|
227
|
-
actions.showDiscardConfirm(file);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
const files = ctx.getStatusFiles();
|
|
233
|
-
const selectedFile = getFileAtIndex(files, ctx.getSelectedIndex());
|
|
234
|
-
// Only allow discard for unstaged modified files
|
|
235
|
-
if (selectedFile && !selectedFile.staged && selectedFile.status !== 'untracked') {
|
|
236
|
-
actions.showDiscardConfirm(selectedFile);
|
|
237
|
-
}
|
|
277
|
+
const file = ctx.resolveFileAtIndex(ctx.getSelectedIndex());
|
|
278
|
+
if (file && !file.staged && file.status !== 'untracked') {
|
|
279
|
+
actions.openDiscardConfirm(file);
|
|
238
280
|
}
|
|
239
281
|
}
|
|
240
282
|
});
|
|
@@ -253,42 +295,12 @@ export function setupKeyBindings(screen, actions, ctx) {
|
|
|
253
295
|
actions.navigatePrevHunk();
|
|
254
296
|
}
|
|
255
297
|
});
|
|
256
|
-
// Stash: save (global)
|
|
257
|
-
screen.key(['S-s'], () => {
|
|
258
|
-
if (ctx.hasActiveModal() || ctx.isCommitInputFocused() || ctx.isRemoteInProgress())
|
|
259
|
-
return;
|
|
260
|
-
actions.stash();
|
|
261
|
-
});
|
|
262
|
-
// Stash: pop (commit tab only)
|
|
263
|
-
screen.key(['o'], () => {
|
|
264
|
-
if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
|
|
265
|
-
return;
|
|
266
|
-
if (ctx.getBottomTab() === 'commit') {
|
|
267
|
-
actions.stashPop();
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
// Stash: list modal (commit tab only)
|
|
271
|
-
screen.key(['l'], () => {
|
|
272
|
-
if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
|
|
273
|
-
return;
|
|
274
|
-
if (ctx.getBottomTab() === 'commit') {
|
|
275
|
-
actions.openStashListModal();
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
// Soft reset HEAD~1 (commit tab only)
|
|
279
|
-
screen.key(['S-x'], () => {
|
|
280
|
-
if (ctx.hasActiveModal() || ctx.isCommitInputFocused() || ctx.isRemoteInProgress())
|
|
281
|
-
return;
|
|
282
|
-
if (ctx.getBottomTab() === 'commit') {
|
|
283
|
-
actions.showSoftResetConfirm();
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
298
|
// Cherry-pick selected commit (history tab only)
|
|
287
299
|
screen.key(['p'], () => {
|
|
288
300
|
if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
|
|
289
301
|
return;
|
|
290
302
|
if (ctx.getBottomTab() === 'history') {
|
|
291
|
-
actions.
|
|
303
|
+
actions.openCherryPickConfirm();
|
|
292
304
|
}
|
|
293
305
|
});
|
|
294
306
|
// Revert selected commit (history tab only)
|
|
@@ -296,7 +308,7 @@ export function setupKeyBindings(screen, actions, ctx) {
|
|
|
296
308
|
if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
|
|
297
309
|
return;
|
|
298
310
|
if (ctx.getBottomTab() === 'history') {
|
|
299
|
-
actions.
|
|
311
|
+
actions.openRevertConfirm();
|
|
300
312
|
}
|
|
301
313
|
});
|
|
302
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,7 +23,8 @@ 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) {
|
|
@@ -34,15 +36,13 @@ export function setupMouseHandlers(layout, actions, ctx) {
|
|
|
34
36
|
const clickedRow = layout.screenYToBottomPaneRow(mouse.y);
|
|
35
37
|
if (clickedRow >= 0) {
|
|
36
38
|
if (ctx.uiState.state.bottomTab === 'commit') {
|
|
37
|
-
|
|
38
|
-
if (clickedRow === 6) {
|
|
39
|
-
actions.toggleAmend();
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
actions.focusCommitInput();
|
|
43
|
-
}
|
|
39
|
+
handleCommitPaneClick(clickedRow, actions, ctx);
|
|
44
40
|
}
|
|
45
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
46
|
actions.selectHunkAtRow(clickedRow);
|
|
47
47
|
}
|
|
48
48
|
}
|
|
@@ -54,33 +54,40 @@ export function setupMouseHandlers(layout, actions, ctx) {
|
|
|
54
54
|
}
|
|
55
55
|
function handleFileListClick(row, x, actions, ctx) {
|
|
56
56
|
const state = ctx.uiState.state;
|
|
57
|
+
// Row-to-index mapping differs between flat and categorized mode
|
|
58
|
+
let fileIndex;
|
|
57
59
|
if (state.flatViewMode) {
|
|
58
60
|
// Flat mode: row 0 is header, files start at row 1
|
|
59
61
|
const absoluteRow = row + state.fileListScrollOffset;
|
|
60
|
-
const
|
|
62
|
+
const idx = absoluteRow - 1; // subtract header row
|
|
61
63
|
const flatFiles = ctx.getCachedFlatFiles();
|
|
62
|
-
|
|
63
|
-
return;
|
|
64
|
-
if (x !== undefined && x >= 2 && x <= 4) {
|
|
65
|
-
actions.toggleFileByIndex(fileIndex);
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
ctx.uiState.setSelectedIndex(fileIndex);
|
|
69
|
-
actions.selectFileByIndex(fileIndex);
|
|
70
|
-
}
|
|
64
|
+
fileIndex = idx >= 0 && idx < flatFiles.length ? idx : null;
|
|
71
65
|
}
|
|
72
66
|
else {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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');
|
|
84
91
|
}
|
|
85
92
|
}
|
|
86
93
|
function handleTopPaneClick(row, x, actions, ctx) {
|
|
@@ -151,7 +158,7 @@ function handleFooterClick(x, actions, ctx) {
|
|
|
151
158
|
ctx.getExplorerManager()?.toggleShowOnlyChanges();
|
|
152
159
|
}
|
|
153
160
|
else if (x === 0) {
|
|
154
|
-
|
|
161
|
+
actions.openHotkeysModal();
|
|
155
162
|
}
|
|
156
163
|
}
|
|
157
164
|
function handleTopPaneScroll(delta, layout, ctx) {
|