diffstalker 0.2.0 → 0.2.2
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/.github/workflows/release.yml +8 -0
- package/README.md +43 -35
- package/bun.lock +82 -3
- package/dist/App.js +555 -552
- package/dist/FollowMode.js +85 -0
- package/dist/KeyBindings.js +228 -0
- package/dist/MouseHandlers.js +192 -0
- package/dist/core/ExplorerStateManager.js +423 -78
- package/dist/core/GitStateManager.js +260 -119
- package/dist/git/diff.js +102 -17
- package/dist/git/status.js +16 -54
- package/dist/git/test-helpers.js +67 -0
- package/dist/index.js +60 -53
- package/dist/ipc/CommandClient.js +6 -7
- package/dist/state/UIState.js +39 -4
- package/dist/ui/PaneRenderers.js +76 -0
- package/dist/ui/modals/FileFinder.js +193 -0
- package/dist/ui/modals/HotkeysModal.js +12 -3
- package/dist/ui/modals/ThemePicker.js +1 -2
- package/dist/ui/widgets/CommitPanel.js +1 -1
- package/dist/ui/widgets/CompareListView.js +123 -80
- package/dist/ui/widgets/DiffView.js +228 -180
- package/dist/ui/widgets/ExplorerContent.js +15 -28
- package/dist/ui/widgets/ExplorerView.js +148 -43
- package/dist/ui/widgets/FileList.js +62 -95
- package/dist/ui/widgets/FlatFileList.js +65 -0
- package/dist/ui/widgets/Footer.js +25 -11
- package/dist/ui/widgets/Header.js +17 -52
- 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/fileCategories.js +37 -0
- package/dist/utils/fileTree.js +148 -0
- package/dist/utils/flatFileList.js +67 -0
- package/dist/utils/layoutCalculations.js +5 -3
- package/eslint.metrics.js +15 -0
- package/metrics/.gitkeep +0 -0
- package/metrics/v0.2.1.json +268 -0
- package/metrics/v0.2.2.json +229 -0
- package/package.json +9 -2
- package/dist/utils/ansiToBlessed.js +0 -125
- package/dist/utils/mouseCoordinates.js +0 -165
- package/dist/utils/rowCalculations.js +0 -246
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { FilePathWatcher } from './core/FilePathWatcher.js';
|
|
2
|
+
/**
|
|
3
|
+
* Manages the file-watching follow mode.
|
|
4
|
+
* Watches a target file for repository path changes and file navigation.
|
|
5
|
+
*/
|
|
6
|
+
export class FollowMode {
|
|
7
|
+
targetFile;
|
|
8
|
+
getCurrentRepoPath;
|
|
9
|
+
callbacks;
|
|
10
|
+
watcher = null;
|
|
11
|
+
_watcherState = { enabled: false };
|
|
12
|
+
constructor(targetFile, getCurrentRepoPath, callbacks) {
|
|
13
|
+
this.targetFile = targetFile;
|
|
14
|
+
this.getCurrentRepoPath = getCurrentRepoPath;
|
|
15
|
+
this.callbacks = callbacks;
|
|
16
|
+
}
|
|
17
|
+
get watcherState() {
|
|
18
|
+
return this._watcherState;
|
|
19
|
+
}
|
|
20
|
+
get isEnabled() {
|
|
21
|
+
return this.watcher !== null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Start watching the target file.
|
|
25
|
+
*/
|
|
26
|
+
start() {
|
|
27
|
+
this.watcher = new FilePathWatcher(this.targetFile);
|
|
28
|
+
this.watcher.on('path-change', (state) => {
|
|
29
|
+
if (state.path && state.path !== this.getCurrentRepoPath()) {
|
|
30
|
+
this._watcherState = {
|
|
31
|
+
enabled: true,
|
|
32
|
+
sourceFile: state.sourceFile ?? this.targetFile,
|
|
33
|
+
rawContent: state.rawContent ?? undefined,
|
|
34
|
+
lastUpdate: state.lastUpdate ?? undefined,
|
|
35
|
+
};
|
|
36
|
+
this.callbacks.onRepoChange(state.path, this._watcherState);
|
|
37
|
+
}
|
|
38
|
+
// Navigate to the followed file if it's within the repo
|
|
39
|
+
if (state.rawContent) {
|
|
40
|
+
this.callbacks.onFileNavigate(state.rawContent);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
this._watcherState = {
|
|
44
|
+
enabled: true,
|
|
45
|
+
sourceFile: this.targetFile,
|
|
46
|
+
};
|
|
47
|
+
this.watcher.start();
|
|
48
|
+
// Switch to the repo described in the target file
|
|
49
|
+
const initialState = this.watcher.state;
|
|
50
|
+
if (initialState.path && initialState.path !== this.getCurrentRepoPath()) {
|
|
51
|
+
this._watcherState = {
|
|
52
|
+
enabled: true,
|
|
53
|
+
sourceFile: initialState.sourceFile ?? this.targetFile,
|
|
54
|
+
rawContent: initialState.rawContent ?? undefined,
|
|
55
|
+
lastUpdate: initialState.lastUpdate ?? undefined,
|
|
56
|
+
};
|
|
57
|
+
this.callbacks.onRepoChange(initialState.path, this._watcherState);
|
|
58
|
+
}
|
|
59
|
+
else if (initialState.rawContent) {
|
|
60
|
+
this._watcherState.rawContent = initialState.rawContent;
|
|
61
|
+
this.callbacks.onFileNavigate(initialState.rawContent);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Toggle follow mode on/off.
|
|
66
|
+
*/
|
|
67
|
+
toggle() {
|
|
68
|
+
if (this.watcher) {
|
|
69
|
+
this.stop();
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
this.start();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Stop watching.
|
|
77
|
+
*/
|
|
78
|
+
stop() {
|
|
79
|
+
if (this.watcher) {
|
|
80
|
+
this.watcher.stop();
|
|
81
|
+
this.watcher = null;
|
|
82
|
+
this._watcherState = { enabled: false };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
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
|
+
/**
|
|
5
|
+
* Register all keyboard bindings on the blessed screen.
|
|
6
|
+
*/
|
|
7
|
+
export function setupKeyBindings(screen, actions, ctx) {
|
|
8
|
+
// Quit
|
|
9
|
+
screen.key(['q', 'C-c'], () => {
|
|
10
|
+
actions.exit();
|
|
11
|
+
});
|
|
12
|
+
// Navigation (skip if modal is open)
|
|
13
|
+
screen.key(['j', 'down'], () => {
|
|
14
|
+
if (ctx.hasActiveModal())
|
|
15
|
+
return;
|
|
16
|
+
actions.navigateDown();
|
|
17
|
+
});
|
|
18
|
+
screen.key(['k', 'up'], () => {
|
|
19
|
+
if (ctx.hasActiveModal())
|
|
20
|
+
return;
|
|
21
|
+
actions.navigateUp();
|
|
22
|
+
});
|
|
23
|
+
// Tab switching (skip if modal is open)
|
|
24
|
+
const tabs = [
|
|
25
|
+
['1', 'diff'],
|
|
26
|
+
['2', 'commit'],
|
|
27
|
+
['3', 'history'],
|
|
28
|
+
['4', 'compare'],
|
|
29
|
+
['5', 'explorer'],
|
|
30
|
+
];
|
|
31
|
+
for (const [key, tab] of tabs) {
|
|
32
|
+
screen.key([key], () => {
|
|
33
|
+
if (ctx.hasActiveModal())
|
|
34
|
+
return;
|
|
35
|
+
ctx.uiState.setTab(tab);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// Pane toggle (skip if modal is open)
|
|
39
|
+
screen.key(['tab'], () => {
|
|
40
|
+
if (ctx.hasActiveModal())
|
|
41
|
+
return;
|
|
42
|
+
ctx.uiState.togglePane();
|
|
43
|
+
});
|
|
44
|
+
// Staging operations (skip if modal is open)
|
|
45
|
+
// Context-aware: hunk staging when diff pane is focused on diff tab
|
|
46
|
+
screen.key(['s'], () => {
|
|
47
|
+
if (ctx.hasActiveModal())
|
|
48
|
+
return;
|
|
49
|
+
if (ctx.getBottomTab() === 'diff' && ctx.getCurrentPane() === 'diff') {
|
|
50
|
+
actions.toggleCurrentHunk();
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
actions.stageSelected();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
screen.key(['S-u'], () => {
|
|
57
|
+
if (ctx.hasActiveModal())
|
|
58
|
+
return;
|
|
59
|
+
actions.unstageSelected();
|
|
60
|
+
});
|
|
61
|
+
screen.key(['S-a'], () => {
|
|
62
|
+
if (ctx.hasActiveModal())
|
|
63
|
+
return;
|
|
64
|
+
actions.stageAll();
|
|
65
|
+
});
|
|
66
|
+
screen.key(['S-z'], () => {
|
|
67
|
+
if (ctx.hasActiveModal())
|
|
68
|
+
return;
|
|
69
|
+
actions.unstageAll();
|
|
70
|
+
});
|
|
71
|
+
// Select/toggle (skip if modal is open)
|
|
72
|
+
screen.key(['enter', 'space'], () => {
|
|
73
|
+
if (ctx.hasActiveModal())
|
|
74
|
+
return;
|
|
75
|
+
if (ctx.getBottomTab() === 'explorer' && ctx.getCurrentPane() === 'explorer') {
|
|
76
|
+
actions.enterExplorerDirectory();
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
actions.toggleSelected();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
// Explorer: go up directory (skip if modal is open)
|
|
83
|
+
screen.key(['backspace'], () => {
|
|
84
|
+
if (ctx.hasActiveModal())
|
|
85
|
+
return;
|
|
86
|
+
if (ctx.getBottomTab() === 'explorer' && ctx.getCurrentPane() === 'explorer') {
|
|
87
|
+
actions.goExplorerUp();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
// Explorer: toggle show only changes filter
|
|
91
|
+
screen.key(['g'], () => {
|
|
92
|
+
if (ctx.hasActiveModal())
|
|
93
|
+
return;
|
|
94
|
+
if (ctx.getBottomTab() === 'explorer') {
|
|
95
|
+
ctx.getExplorerManager()?.toggleShowOnlyChanges();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
// Explorer: open file finder
|
|
99
|
+
screen.key(['/'], () => {
|
|
100
|
+
if (ctx.hasActiveModal())
|
|
101
|
+
return;
|
|
102
|
+
if (ctx.getBottomTab() === 'explorer') {
|
|
103
|
+
actions.openFileFinder();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
// Ctrl+P: open file finder from any tab
|
|
107
|
+
screen.key(['C-p'], () => {
|
|
108
|
+
if (ctx.hasActiveModal())
|
|
109
|
+
return;
|
|
110
|
+
actions.openFileFinder();
|
|
111
|
+
});
|
|
112
|
+
// Commit (skip if modal is open)
|
|
113
|
+
screen.key(['c'], () => {
|
|
114
|
+
if (ctx.hasActiveModal())
|
|
115
|
+
return;
|
|
116
|
+
ctx.uiState.setTab('commit');
|
|
117
|
+
});
|
|
118
|
+
// Commit panel specific keys (only when on commit tab)
|
|
119
|
+
screen.key(['i'], () => {
|
|
120
|
+
if (ctx.getBottomTab() === 'commit' && !ctx.isCommitInputFocused()) {
|
|
121
|
+
actions.focusCommitInput();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
screen.key(['a'], () => {
|
|
125
|
+
if (ctx.getBottomTab() === 'commit' && !ctx.isCommitInputFocused()) {
|
|
126
|
+
ctx.commitFlowState.toggleAmend();
|
|
127
|
+
actions.render();
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
ctx.uiState.toggleAutoTab();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
screen.key(['escape'], () => {
|
|
134
|
+
if (ctx.getBottomTab() === 'commit') {
|
|
135
|
+
if (ctx.isCommitInputFocused()) {
|
|
136
|
+
actions.unfocusCommitInput();
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
ctx.uiState.setTab('diff');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
// Refresh
|
|
144
|
+
screen.key(['r'], () => actions.refresh());
|
|
145
|
+
// Display toggles
|
|
146
|
+
screen.key(['w'], () => ctx.uiState.toggleWrapMode());
|
|
147
|
+
screen.key(['m'], () => actions.toggleMouseMode());
|
|
148
|
+
screen.key(['S-t'], () => ctx.uiState.toggleAutoTab());
|
|
149
|
+
// Split ratio adjustments
|
|
150
|
+
screen.key(['-', '_', '['], () => {
|
|
151
|
+
ctx.uiState.adjustSplitRatio(-SPLIT_RATIO_STEP);
|
|
152
|
+
ctx.layout.setSplitRatio(ctx.uiState.state.splitRatio);
|
|
153
|
+
actions.render();
|
|
154
|
+
});
|
|
155
|
+
screen.key(['=', '+', ']'], () => {
|
|
156
|
+
ctx.uiState.adjustSplitRatio(SPLIT_RATIO_STEP);
|
|
157
|
+
ctx.layout.setSplitRatio(ctx.uiState.state.splitRatio);
|
|
158
|
+
actions.render();
|
|
159
|
+
});
|
|
160
|
+
// Theme picker
|
|
161
|
+
screen.key(['t'], () => ctx.uiState.openModal('theme'));
|
|
162
|
+
// Hotkeys modal
|
|
163
|
+
screen.key(['?'], () => ctx.uiState.toggleModal('hotkeys'));
|
|
164
|
+
// Follow toggle
|
|
165
|
+
screen.key(['f'], () => actions.toggleFollow());
|
|
166
|
+
// Compare view: base branch picker
|
|
167
|
+
screen.key(['b'], () => {
|
|
168
|
+
if (ctx.getBottomTab() === 'compare') {
|
|
169
|
+
ctx.uiState.openModal('baseBranch');
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
// u: toggle uncommitted in compare view
|
|
173
|
+
screen.key(['u'], () => {
|
|
174
|
+
if (ctx.hasActiveModal())
|
|
175
|
+
return;
|
|
176
|
+
if (ctx.getBottomTab() === 'compare') {
|
|
177
|
+
ctx.uiState.toggleIncludeUncommitted();
|
|
178
|
+
const includeUncommitted = ctx.uiState.state.includeUncommitted;
|
|
179
|
+
ctx.getGitManager()?.refreshCompareDiff(includeUncommitted);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
// Toggle flat file view (diff/commit tab only)
|
|
183
|
+
screen.key(['h'], () => {
|
|
184
|
+
if (ctx.hasActiveModal())
|
|
185
|
+
return;
|
|
186
|
+
const tab = ctx.getBottomTab();
|
|
187
|
+
if (tab === 'diff' || tab === 'commit') {
|
|
188
|
+
ctx.uiState.toggleFlatViewMode();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
// Discard changes (with confirmation)
|
|
192
|
+
screen.key(['d'], () => {
|
|
193
|
+
if (ctx.getBottomTab() === 'diff') {
|
|
194
|
+
if (ctx.uiState.state.flatViewMode) {
|
|
195
|
+
const flatEntry = getFlatFileAtIndex(ctx.getCachedFlatFiles(), ctx.getSelectedIndex());
|
|
196
|
+
if (flatEntry?.unstagedEntry) {
|
|
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
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
// Hunk navigation (only when diff pane focused on diff tab)
|
|
214
|
+
screen.key(['n'], () => {
|
|
215
|
+
if (ctx.hasActiveModal())
|
|
216
|
+
return;
|
|
217
|
+
if (ctx.getBottomTab() === 'diff' && ctx.getCurrentPane() === 'diff') {
|
|
218
|
+
actions.navigateNextHunk();
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
screen.key(['S-n'], () => {
|
|
222
|
+
if (ctx.hasActiveModal())
|
|
223
|
+
return;
|
|
224
|
+
if (ctx.getBottomTab() === 'diff' && ctx.getCurrentPane() === 'diff') {
|
|
225
|
+
actions.navigatePrevHunk();
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { getFileListTotalRows, getFileIndexFromRow } from './ui/widgets/FileList.js';
|
|
2
|
+
import { getFlatFileListTotalRows } from './ui/widgets/FlatFileList.js';
|
|
3
|
+
import { getCompareListTotalRows, getCompareSelectionFromRow, } from './ui/widgets/CompareListView.js';
|
|
4
|
+
import { getExplorerTotalRows } from './ui/widgets/ExplorerView.js';
|
|
5
|
+
import { getExplorerContentTotalRows } from './ui/widgets/ExplorerContent.js';
|
|
6
|
+
const SCROLL_AMOUNT = 3;
|
|
7
|
+
/**
|
|
8
|
+
* Register all mouse event handlers on the layout.
|
|
9
|
+
*/
|
|
10
|
+
export function setupMouseHandlers(layout, actions, ctx) {
|
|
11
|
+
// Mouse wheel on top pane
|
|
12
|
+
layout.topPane.on('wheeldown', () => {
|
|
13
|
+
handleTopPaneScroll(SCROLL_AMOUNT, layout, ctx);
|
|
14
|
+
});
|
|
15
|
+
layout.topPane.on('wheelup', () => {
|
|
16
|
+
handleTopPaneScroll(-SCROLL_AMOUNT, layout, ctx);
|
|
17
|
+
});
|
|
18
|
+
// Mouse wheel on bottom pane
|
|
19
|
+
layout.bottomPane.on('wheeldown', () => {
|
|
20
|
+
handleBottomPaneScroll(SCROLL_AMOUNT, layout, ctx);
|
|
21
|
+
});
|
|
22
|
+
layout.bottomPane.on('wheelup', () => {
|
|
23
|
+
handleBottomPaneScroll(-SCROLL_AMOUNT, layout, ctx);
|
|
24
|
+
});
|
|
25
|
+
// Click on top pane to select item
|
|
26
|
+
layout.topPane.on('click', (mouse) => {
|
|
27
|
+
const clickedRow = layout.screenYToTopPaneRow(mouse.y);
|
|
28
|
+
if (clickedRow >= 0) {
|
|
29
|
+
handleTopPaneClick(clickedRow, mouse.x, actions, ctx);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
// Click on bottom pane to select hunk (diff tab)
|
|
33
|
+
layout.bottomPane.on('click', (mouse) => {
|
|
34
|
+
const clickedRow = layout.screenYToBottomPaneRow(mouse.y);
|
|
35
|
+
if (clickedRow >= 0) {
|
|
36
|
+
actions.selectHunkAtRow(clickedRow);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
// Click on footer for tabs and toggles
|
|
40
|
+
layout.footerBox.on('click', (mouse) => {
|
|
41
|
+
handleFooterClick(mouse.x, actions, ctx);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function handleFileListClick(row, x, actions, ctx) {
|
|
45
|
+
const state = ctx.uiState.state;
|
|
46
|
+
if (state.flatViewMode) {
|
|
47
|
+
// Flat mode: row 0 is header, files start at row 1
|
|
48
|
+
const absoluteRow = row + state.fileListScrollOffset;
|
|
49
|
+
const fileIndex = absoluteRow - 1; // subtract header row
|
|
50
|
+
const flatFiles = ctx.getCachedFlatFiles();
|
|
51
|
+
if (fileIndex < 0 || fileIndex >= flatFiles.length)
|
|
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
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const files = ctx.getStatusFiles();
|
|
63
|
+
const fileIndex = getFileIndexFromRow(row + state.fileListScrollOffset, files);
|
|
64
|
+
if (fileIndex === null || fileIndex < 0)
|
|
65
|
+
return;
|
|
66
|
+
if (x !== undefined && x >= 2 && x <= 4) {
|
|
67
|
+
actions.toggleFileByIndex(fileIndex);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
ctx.uiState.setSelectedIndex(fileIndex);
|
|
71
|
+
actions.selectFileByIndex(fileIndex);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function handleTopPaneClick(row, x, actions, ctx) {
|
|
76
|
+
const state = ctx.uiState.state;
|
|
77
|
+
if (state.bottomTab === 'history') {
|
|
78
|
+
const index = state.historyScrollOffset + row;
|
|
79
|
+
ctx.uiState.setHistorySelectedIndex(index);
|
|
80
|
+
actions.selectHistoryCommitByIndex(index);
|
|
81
|
+
}
|
|
82
|
+
else if (state.bottomTab === 'compare') {
|
|
83
|
+
const commits = ctx.getCompareCommits();
|
|
84
|
+
const files = ctx.getCompareFiles();
|
|
85
|
+
const selection = getCompareSelectionFromRow(state.compareScrollOffset + row, commits, files);
|
|
86
|
+
if (selection) {
|
|
87
|
+
actions.selectCompareItem(selection);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else if (state.bottomTab === 'explorer') {
|
|
91
|
+
const index = state.explorerScrollOffset + row;
|
|
92
|
+
const explorerManager = ctx.getExplorerManager();
|
|
93
|
+
const isAlreadySelected = explorerManager?.state.selectedIndex === index;
|
|
94
|
+
const displayRow = explorerManager?.state.displayRows[index];
|
|
95
|
+
if (isAlreadySelected && displayRow?.node.isDirectory) {
|
|
96
|
+
actions.enterExplorerDirectory();
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
explorerManager?.selectIndex(index);
|
|
100
|
+
ctx.uiState.setExplorerSelectedIndex(index);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
handleFileListClick(row, x, actions, ctx);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function handleFooterClick(x, actions, ctx) {
|
|
108
|
+
const width = ctx.getScreenWidth();
|
|
109
|
+
// Tabs are right-aligned
|
|
110
|
+
const tabPositions = [
|
|
111
|
+
{ tab: 'explorer', width: 11 },
|
|
112
|
+
{ tab: 'compare', width: 10 },
|
|
113
|
+
{ tab: 'history', width: 10 },
|
|
114
|
+
{ tab: 'commit', width: 9 },
|
|
115
|
+
{ tab: 'diff', width: 7 },
|
|
116
|
+
];
|
|
117
|
+
let rightEdge = width;
|
|
118
|
+
for (const { tab, width: tabWidth } of tabPositions) {
|
|
119
|
+
const leftEdge = rightEdge - tabWidth - 1;
|
|
120
|
+
if (x >= leftEdge && x < rightEdge) {
|
|
121
|
+
ctx.uiState.setTab(tab);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
rightEdge = leftEdge;
|
|
125
|
+
}
|
|
126
|
+
// Left side toggles (approximate positions)
|
|
127
|
+
if (x >= 2 && x <= 9) {
|
|
128
|
+
actions.toggleMouseMode();
|
|
129
|
+
}
|
|
130
|
+
else if (x >= 11 && x <= 16) {
|
|
131
|
+
ctx.uiState.toggleAutoTab();
|
|
132
|
+
}
|
|
133
|
+
else if (x >= 18 && x <= 23) {
|
|
134
|
+
ctx.uiState.toggleWrapMode();
|
|
135
|
+
}
|
|
136
|
+
else if (x >= 25 && x <= 32) {
|
|
137
|
+
actions.toggleFollow();
|
|
138
|
+
}
|
|
139
|
+
else if (x >= 34 && x <= 43 && ctx.uiState.state.bottomTab === 'explorer') {
|
|
140
|
+
ctx.getExplorerManager()?.toggleShowOnlyChanges();
|
|
141
|
+
}
|
|
142
|
+
else if (x === 0) {
|
|
143
|
+
ctx.uiState.openModal('hotkeys');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function handleTopPaneScroll(delta, layout, ctx) {
|
|
147
|
+
const state = ctx.uiState.state;
|
|
148
|
+
const visibleHeight = layout.dimensions.topPaneHeight;
|
|
149
|
+
if (state.bottomTab === 'history') {
|
|
150
|
+
const totalRows = ctx.getHistoryCommitCount();
|
|
151
|
+
const maxOffset = Math.max(0, totalRows - visibleHeight);
|
|
152
|
+
const newOffset = Math.min(maxOffset, Math.max(0, state.historyScrollOffset + delta));
|
|
153
|
+
ctx.uiState.setHistoryScrollOffset(newOffset);
|
|
154
|
+
}
|
|
155
|
+
else if (state.bottomTab === 'compare') {
|
|
156
|
+
const totalRows = getCompareListTotalRows(ctx.getCompareCommits(), ctx.getCompareFiles());
|
|
157
|
+
const maxOffset = Math.max(0, totalRows - visibleHeight);
|
|
158
|
+
const newOffset = Math.min(maxOffset, Math.max(0, state.compareScrollOffset + delta));
|
|
159
|
+
ctx.uiState.setCompareScrollOffset(newOffset);
|
|
160
|
+
}
|
|
161
|
+
else if (state.bottomTab === 'explorer') {
|
|
162
|
+
const totalRows = getExplorerTotalRows(ctx.getExplorerManager()?.state.displayRows ?? []);
|
|
163
|
+
const maxOffset = Math.max(0, totalRows - visibleHeight);
|
|
164
|
+
const newOffset = Math.min(maxOffset, Math.max(0, state.explorerScrollOffset + delta));
|
|
165
|
+
ctx.uiState.setExplorerScrollOffset(newOffset);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const totalRows = state.flatViewMode
|
|
169
|
+
? getFlatFileListTotalRows(ctx.getCachedFlatFiles())
|
|
170
|
+
: getFileListTotalRows(ctx.getStatusFiles());
|
|
171
|
+
const maxOffset = Math.max(0, totalRows - visibleHeight);
|
|
172
|
+
const newOffset = Math.min(maxOffset, Math.max(0, state.fileListScrollOffset + delta));
|
|
173
|
+
ctx.uiState.setFileListScrollOffset(newOffset);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function handleBottomPaneScroll(delta, layout, ctx) {
|
|
177
|
+
const state = ctx.uiState.state;
|
|
178
|
+
const visibleHeight = layout.dimensions.bottomPaneHeight;
|
|
179
|
+
const width = ctx.getScreenWidth();
|
|
180
|
+
if (state.bottomTab === 'explorer') {
|
|
181
|
+
const selectedFile = ctx.getExplorerManager()?.state.selectedFile;
|
|
182
|
+
const totalRows = getExplorerContentTotalRows(selectedFile?.content ?? null, selectedFile?.path ?? null, selectedFile?.truncated ?? false, width, state.wrapMode);
|
|
183
|
+
const maxOffset = Math.max(0, totalRows - visibleHeight);
|
|
184
|
+
const newOffset = Math.min(maxOffset, Math.max(0, state.explorerFileScrollOffset + delta));
|
|
185
|
+
ctx.uiState.setExplorerFileScrollOffset(newOffset);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
const maxOffset = Math.max(0, ctx.getBottomPaneTotalRows() - visibleHeight);
|
|
189
|
+
const newOffset = Math.min(maxOffset, Math.max(0, state.diffScrollOffset + delta));
|
|
190
|
+
ctx.uiState.setDiffScrollOffset(newOffset);
|
|
191
|
+
}
|
|
192
|
+
}
|