diffstalker 0.2.0 → 0.2.1
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/.github/workflows/release.yml +8 -0
- package/bun.lock +23 -0
- package/dist/App.js +225 -471
- package/dist/FollowMode.js +85 -0
- package/dist/KeyBindings.js +178 -0
- package/dist/MouseHandlers.js +156 -0
- package/dist/core/ExplorerStateManager.js +444 -78
- package/dist/core/GitStateManager.js +169 -93
- package/dist/git/diff.js +4 -0
- package/dist/index.js +54 -53
- package/dist/state/UIState.js +17 -4
- package/dist/ui/PaneRenderers.js +56 -0
- package/dist/ui/modals/FileFinder.js +232 -0
- package/dist/ui/widgets/CompareListView.js +86 -64
- package/dist/ui/widgets/DiffView.js +19 -17
- package/dist/ui/widgets/ExplorerContent.js +15 -28
- package/dist/ui/widgets/ExplorerView.js +140 -31
- package/dist/ui/widgets/Footer.js +6 -2
- package/dist/ui/widgets/Header.js +3 -46
- package/dist/utils/fileCategories.js +37 -0
- package/dist/utils/fileTree.js +148 -0
- package/eslint.metrics.js +16 -0
- package/metrics/.gitkeep +0 -0
- package/metrics/v0.2.1.json +268 -0
- package/package.json +4 -1
- package/dist/utils/ansiToBlessed.js +0 -125
- package/dist/utils/mouseCoordinates.js +0 -165
- package/dist/utils/rowCalculations.js +0 -246
package/dist/App.js
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import blessed from 'neo-blessed';
|
|
2
|
-
import { LayoutManager
|
|
2
|
+
import { LayoutManager } from './ui/Layout.js';
|
|
3
|
+
import { setupKeyBindings } from './KeyBindings.js';
|
|
4
|
+
import { renderTopPane, renderBottomPane } from './ui/PaneRenderers.js';
|
|
5
|
+
import { setupMouseHandlers } from './MouseHandlers.js';
|
|
6
|
+
import { FollowMode } from './FollowMode.js';
|
|
3
7
|
import { formatHeader } from './ui/widgets/Header.js';
|
|
4
8
|
import { formatFooter } from './ui/widgets/Footer.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { formatHistoryView, getCommitAtIndex, } from './ui/widgets/HistoryView.js';
|
|
9
|
-
import { formatCompareListView, getCompareListTotalRows, getNextCompareSelection, getRowFromCompareSelection, getCompareSelectionFromRow, } from './ui/widgets/CompareListView.js';
|
|
10
|
-
import { formatExplorerView, getExplorerTotalRows, } from './ui/widgets/ExplorerView.js';
|
|
11
|
-
import { formatExplorerContent, getExplorerContentTotalRows, } from './ui/widgets/ExplorerContent.js';
|
|
9
|
+
import { getFileAtIndex, getRowFromFileIndex } from './ui/widgets/FileList.js';
|
|
10
|
+
import { getCommitAtIndex } from './ui/widgets/HistoryView.js';
|
|
11
|
+
import { getNextCompareSelection, getRowFromCompareSelection, } from './ui/widgets/CompareListView.js';
|
|
12
12
|
import { ExplorerStateManager, } from './core/ExplorerStateManager.js';
|
|
13
13
|
import { ThemePicker } from './ui/modals/ThemePicker.js';
|
|
14
14
|
import { HotkeysModal } from './ui/modals/HotkeysModal.js';
|
|
15
15
|
import { BaseBranchPicker } from './ui/modals/BaseBranchPicker.js';
|
|
16
16
|
import { DiscardConfirm } from './ui/modals/DiscardConfirm.js';
|
|
17
|
+
import { FileFinder } from './ui/modals/FileFinder.js';
|
|
17
18
|
import { CommitFlowState } from './state/CommitFlowState.js';
|
|
18
19
|
import { UIState } from './state/UIState.js';
|
|
19
20
|
import { getManagerForRepo, removeManagerForRepo, } from './core/GitStateManager.js';
|
|
20
|
-
import { FilePathWatcher } from './core/FilePathWatcher.js';
|
|
21
21
|
import { saveConfig } from './config.js';
|
|
22
|
+
import { getCategoryForIndex, getIndexForCategoryPosition, } from './utils/fileCategories.js';
|
|
22
23
|
/**
|
|
23
24
|
* Main application controller.
|
|
24
25
|
* Coordinates between GitStateManager, UIState, and blessed widgets.
|
|
@@ -28,13 +29,12 @@ export class App {
|
|
|
28
29
|
layout;
|
|
29
30
|
uiState;
|
|
30
31
|
gitManager = null;
|
|
31
|
-
|
|
32
|
+
followMode = null;
|
|
32
33
|
explorerManager = null;
|
|
33
34
|
config;
|
|
34
35
|
commandServer;
|
|
35
36
|
// Current state
|
|
36
37
|
repoPath;
|
|
37
|
-
watcherState = { enabled: false };
|
|
38
38
|
currentTheme;
|
|
39
39
|
// Commit flow state
|
|
40
40
|
commitFlowState;
|
|
@@ -43,6 +43,8 @@ export class App {
|
|
|
43
43
|
activeModal = null;
|
|
44
44
|
// Cached total rows for scroll bounds (single source of truth from render)
|
|
45
45
|
bottomPaneTotalRows = 0;
|
|
46
|
+
// Selection anchor: remembers category + position before stage/unstage
|
|
47
|
+
pendingSelectionAnchor = null;
|
|
46
48
|
constructor(options) {
|
|
47
49
|
this.config = options.config;
|
|
48
50
|
this.commandServer = options.commandServer ?? null;
|
|
@@ -116,12 +118,16 @@ export class App {
|
|
|
116
118
|
// Setup keyboard handlers
|
|
117
119
|
this.setupKeyboardHandlers();
|
|
118
120
|
// Setup mouse handlers
|
|
119
|
-
this.
|
|
121
|
+
this.setupMouseEventHandlers();
|
|
120
122
|
// Setup state change listeners
|
|
121
123
|
this.setupStateListeners();
|
|
122
|
-
// Setup
|
|
124
|
+
// Setup follow mode if enabled
|
|
123
125
|
if (this.config.watcherEnabled) {
|
|
124
|
-
this.
|
|
126
|
+
this.followMode = new FollowMode(this.config.targetFile, () => this.repoPath, {
|
|
127
|
+
onRepoChange: (newPath, state) => this.handleFollowRepoChange(newPath, state),
|
|
128
|
+
onFileNavigate: (rawContent) => this.handleFollowFileNavigate(rawContent),
|
|
129
|
+
});
|
|
130
|
+
this.followMode.start();
|
|
125
131
|
}
|
|
126
132
|
// Setup IPC command handler if command server provided
|
|
127
133
|
if (this.commandServer) {
|
|
@@ -133,328 +139,72 @@ export class App {
|
|
|
133
139
|
this.render();
|
|
134
140
|
}
|
|
135
141
|
setupKeyboardHandlers() {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
this.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
this.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
this.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
this.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
this.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
this.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
this.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return;
|
|
197
|
-
this.stageAll();
|
|
198
|
-
});
|
|
199
|
-
this.screen.key(['S-z'], () => {
|
|
200
|
-
if (this.activeModal)
|
|
201
|
-
return;
|
|
202
|
-
this.unstageAll();
|
|
203
|
-
});
|
|
204
|
-
// Select/toggle (skip if modal is open)
|
|
205
|
-
this.screen.key(['enter', 'space'], () => {
|
|
206
|
-
if (this.activeModal)
|
|
207
|
-
return;
|
|
208
|
-
const state = this.uiState.state;
|
|
209
|
-
if (state.bottomTab === 'explorer' && state.currentPane === 'explorer') {
|
|
210
|
-
this.enterExplorerDirectory();
|
|
142
|
+
setupKeyBindings(this.screen, {
|
|
143
|
+
exit: () => this.exit(),
|
|
144
|
+
navigateDown: () => this.navigateDown(),
|
|
145
|
+
navigateUp: () => this.navigateUp(),
|
|
146
|
+
stageSelected: () => this.stageSelected(),
|
|
147
|
+
unstageSelected: () => this.unstageSelected(),
|
|
148
|
+
stageAll: () => this.stageAll(),
|
|
149
|
+
unstageAll: () => this.unstageAll(),
|
|
150
|
+
toggleSelected: () => this.toggleSelected(),
|
|
151
|
+
enterExplorerDirectory: () => this.enterExplorerDirectory(),
|
|
152
|
+
goExplorerUp: () => this.goExplorerUp(),
|
|
153
|
+
openFileFinder: () => this.openFileFinder(),
|
|
154
|
+
focusCommitInput: () => this.focusCommitInput(),
|
|
155
|
+
unfocusCommitInput: () => this.unfocusCommitInput(),
|
|
156
|
+
refresh: () => this.refresh(),
|
|
157
|
+
toggleMouseMode: () => this.toggleMouseMode(),
|
|
158
|
+
toggleFollow: () => this.toggleFollow(),
|
|
159
|
+
showDiscardConfirm: (file) => this.showDiscardConfirm(file),
|
|
160
|
+
render: () => this.render(),
|
|
161
|
+
}, {
|
|
162
|
+
hasActiveModal: () => this.activeModal !== null,
|
|
163
|
+
getBottomTab: () => this.uiState.state.bottomTab,
|
|
164
|
+
getCurrentPane: () => this.uiState.state.currentPane,
|
|
165
|
+
isCommitInputFocused: () => this.commitFlowState.state.inputFocused,
|
|
166
|
+
getStatusFiles: () => this.gitManager?.state.status?.files ?? [],
|
|
167
|
+
getSelectedIndex: () => this.uiState.state.selectedIndex,
|
|
168
|
+
uiState: this.uiState,
|
|
169
|
+
explorerManager: this.explorerManager,
|
|
170
|
+
commitFlowState: this.commitFlowState,
|
|
171
|
+
gitManager: this.gitManager,
|
|
172
|
+
layout: this.layout,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
setupMouseEventHandlers() {
|
|
176
|
+
setupMouseHandlers(this.layout, {
|
|
177
|
+
selectHistoryCommitByIndex: (index) => this.selectHistoryCommitByIndex(index),
|
|
178
|
+
selectCompareItem: (selection) => this.selectCompareItem(selection),
|
|
179
|
+
selectFileByIndex: (index) => this.selectFileByIndex(index),
|
|
180
|
+
toggleFileByIndex: (index) => this.toggleFileByIndex(index),
|
|
181
|
+
toggleMouseMode: () => this.toggleMouseMode(),
|
|
182
|
+
toggleFollow: () => this.toggleFollow(),
|
|
183
|
+
render: () => this.render(),
|
|
184
|
+
}, {
|
|
185
|
+
uiState: this.uiState,
|
|
186
|
+
explorerManager: this.explorerManager,
|
|
187
|
+
getStatusFiles: () => this.gitManager?.state.status?.files ?? [],
|
|
188
|
+
getHistoryCommitCount: () => this.gitManager?.historyState.commits.length ?? 0,
|
|
189
|
+
getCompareCommits: () => this.gitManager?.compareState?.compareDiff?.commits ?? [],
|
|
190
|
+
getCompareFiles: () => this.gitManager?.compareState?.compareDiff?.files ?? [],
|
|
191
|
+
getBottomPaneTotalRows: () => this.bottomPaneTotalRows,
|
|
192
|
+
getScreenWidth: () => this.screen.width || 80,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
async toggleFileByIndex(index) {
|
|
196
|
+
const files = this.gitManager?.state.status?.files ?? [];
|
|
197
|
+
const file = getFileAtIndex(files, index);
|
|
198
|
+
if (file) {
|
|
199
|
+
this.pendingSelectionAnchor = getCategoryForIndex(files, this.uiState.state.selectedIndex);
|
|
200
|
+
if (file.staged) {
|
|
201
|
+
await this.gitManager?.unstage(file);
|
|
211
202
|
}
|
|
212
203
|
else {
|
|
213
|
-
this.
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
// Explorer: go up directory (skip if modal is open)
|
|
217
|
-
this.screen.key(['backspace'], () => {
|
|
218
|
-
if (this.activeModal)
|
|
219
|
-
return;
|
|
220
|
-
const state = this.uiState.state;
|
|
221
|
-
if (state.bottomTab === 'explorer' && state.currentPane === 'explorer') {
|
|
222
|
-
this.goExplorerUp();
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
// Commit (skip if modal is open)
|
|
226
|
-
this.screen.key(['c'], () => {
|
|
227
|
-
if (this.activeModal)
|
|
228
|
-
return;
|
|
229
|
-
this.uiState.setTab('commit');
|
|
230
|
-
});
|
|
231
|
-
// Commit panel specific keys (only when on commit tab)
|
|
232
|
-
this.screen.key(['i'], () => {
|
|
233
|
-
if (this.uiState.state.bottomTab === 'commit' && !this.commitFlowState.state.inputFocused) {
|
|
234
|
-
this.focusCommitInput();
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
this.screen.key(['a'], () => {
|
|
238
|
-
if (this.uiState.state.bottomTab === 'commit' && !this.commitFlowState.state.inputFocused) {
|
|
239
|
-
this.commitFlowState.toggleAmend();
|
|
240
|
-
this.render();
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
this.screen.key(['escape'], () => {
|
|
244
|
-
if (this.uiState.state.bottomTab === 'commit') {
|
|
245
|
-
if (this.commitFlowState.state.inputFocused) {
|
|
246
|
-
this.unfocusCommitInput();
|
|
247
|
-
}
|
|
248
|
-
else {
|
|
249
|
-
this.uiState.setTab('diff');
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
// Refresh
|
|
254
|
-
this.screen.key(['r'], () => this.refresh());
|
|
255
|
-
// Display toggles
|
|
256
|
-
this.screen.key(['w'], () => this.uiState.toggleWrapMode());
|
|
257
|
-
this.screen.key(['m'], () => this.toggleMouseMode());
|
|
258
|
-
this.screen.key(['S-t'], () => this.uiState.toggleAutoTab());
|
|
259
|
-
// Split ratio adjustments
|
|
260
|
-
this.screen.key(['-', '_'], () => {
|
|
261
|
-
this.uiState.adjustSplitRatio(-SPLIT_RATIO_STEP);
|
|
262
|
-
this.layout.setSplitRatio(this.uiState.state.splitRatio);
|
|
263
|
-
this.render();
|
|
264
|
-
});
|
|
265
|
-
this.screen.key(['=', '+'], () => {
|
|
266
|
-
this.uiState.adjustSplitRatio(SPLIT_RATIO_STEP);
|
|
267
|
-
this.layout.setSplitRatio(this.uiState.state.splitRatio);
|
|
268
|
-
this.render();
|
|
269
|
-
});
|
|
270
|
-
// Theme picker
|
|
271
|
-
this.screen.key(['t'], () => this.uiState.openModal('theme'));
|
|
272
|
-
// Hotkeys modal
|
|
273
|
-
this.screen.key(['?'], () => this.uiState.toggleModal('hotkeys'));
|
|
274
|
-
// Follow toggle
|
|
275
|
-
this.screen.key(['f'], () => this.toggleFollow());
|
|
276
|
-
// Compare view: base branch picker
|
|
277
|
-
this.screen.key(['b'], () => {
|
|
278
|
-
if (this.uiState.state.bottomTab === 'compare') {
|
|
279
|
-
this.uiState.openModal('baseBranch');
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
// Compare view: toggle uncommitted
|
|
283
|
-
this.screen.key(['u'], () => {
|
|
284
|
-
if (this.uiState.state.bottomTab === 'compare') {
|
|
285
|
-
this.uiState.toggleIncludeUncommitted();
|
|
286
|
-
const includeUncommitted = this.uiState.state.includeUncommitted;
|
|
287
|
-
this.gitManager?.refreshCompareDiff(includeUncommitted);
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
// Discard changes (with confirmation)
|
|
291
|
-
this.screen.key(['d'], () => {
|
|
292
|
-
if (this.uiState.state.bottomTab === 'diff') {
|
|
293
|
-
const files = this.gitManager?.state.status?.files ?? [];
|
|
294
|
-
const selectedIndex = this.uiState.state.selectedIndex;
|
|
295
|
-
const selectedFile = files[selectedIndex];
|
|
296
|
-
// Only allow discard for unstaged modified files
|
|
297
|
-
if (selectedFile && !selectedFile.staged && selectedFile.status !== 'untracked') {
|
|
298
|
-
this.showDiscardConfirm(selectedFile);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
setupMouseHandlers() {
|
|
304
|
-
const SCROLL_AMOUNT = 3;
|
|
305
|
-
// Mouse wheel on top pane
|
|
306
|
-
this.layout.topPane.on('wheeldown', () => {
|
|
307
|
-
this.handleTopPaneScroll(SCROLL_AMOUNT);
|
|
308
|
-
});
|
|
309
|
-
this.layout.topPane.on('wheelup', () => {
|
|
310
|
-
this.handleTopPaneScroll(-SCROLL_AMOUNT);
|
|
311
|
-
});
|
|
312
|
-
// Mouse wheel on bottom pane
|
|
313
|
-
this.layout.bottomPane.on('wheeldown', () => {
|
|
314
|
-
this.handleBottomPaneScroll(SCROLL_AMOUNT);
|
|
315
|
-
});
|
|
316
|
-
this.layout.bottomPane.on('wheelup', () => {
|
|
317
|
-
this.handleBottomPaneScroll(-SCROLL_AMOUNT);
|
|
318
|
-
});
|
|
319
|
-
// Click on top pane to select item
|
|
320
|
-
this.layout.topPane.on('click', (mouse) => {
|
|
321
|
-
// Convert screen Y to pane-relative row (blessed click coords are screen-relative)
|
|
322
|
-
const clickedRow = this.layout.screenYToTopPaneRow(mouse.y);
|
|
323
|
-
if (clickedRow >= 0) {
|
|
324
|
-
this.handleTopPaneClick(clickedRow);
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
// Click on footer for tabs and toggles
|
|
328
|
-
this.layout.footerBox.on('click', (mouse) => {
|
|
329
|
-
this.handleFooterClick(mouse.x);
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
handleTopPaneClick(row) {
|
|
333
|
-
const state = this.uiState.state;
|
|
334
|
-
if (state.bottomTab === 'history') {
|
|
335
|
-
const index = state.historyScrollOffset + row;
|
|
336
|
-
this.uiState.setHistorySelectedIndex(index);
|
|
337
|
-
this.selectHistoryCommitByIndex(index);
|
|
338
|
-
}
|
|
339
|
-
else if (state.bottomTab === 'compare') {
|
|
340
|
-
// For compare view, need to map row to selection
|
|
341
|
-
const compareState = this.gitManager?.compareState;
|
|
342
|
-
const commits = compareState?.compareDiff?.commits ?? [];
|
|
343
|
-
const files = compareState?.compareDiff?.files ?? [];
|
|
344
|
-
const selection = getCompareSelectionFromRow(state.compareScrollOffset + row, commits, files);
|
|
345
|
-
if (selection) {
|
|
346
|
-
this.selectCompareItem(selection);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
else if (state.bottomTab === 'explorer') {
|
|
350
|
-
const index = state.explorerScrollOffset + row;
|
|
351
|
-
this.explorerManager?.selectIndex(index);
|
|
352
|
-
this.uiState.setExplorerSelectedIndex(index);
|
|
353
|
-
}
|
|
354
|
-
else {
|
|
355
|
-
// Diff tab - select file
|
|
356
|
-
const files = this.gitManager?.state.status?.files ?? [];
|
|
357
|
-
// Account for section headers in file list
|
|
358
|
-
const fileIndex = getFileIndexFromRow(row + state.fileListScrollOffset, files);
|
|
359
|
-
if (fileIndex !== null && fileIndex >= 0) {
|
|
360
|
-
this.uiState.setSelectedIndex(fileIndex);
|
|
361
|
-
this.selectFileByIndex(fileIndex);
|
|
204
|
+
await this.gitManager?.stage(file);
|
|
362
205
|
}
|
|
363
206
|
}
|
|
364
207
|
}
|
|
365
|
-
handleFooterClick(x) {
|
|
366
|
-
const width = this.screen.width || 80;
|
|
367
|
-
// Footer layout: left side has toggles, right side has tabs
|
|
368
|
-
// Tabs are right-aligned, so we calculate from the right
|
|
369
|
-
// Tab format: [1]Diff [2]Commit [3]History [4]Compare [5]Explorer
|
|
370
|
-
// Approximate positions from right edge
|
|
371
|
-
const tabPositions = [
|
|
372
|
-
{ tab: 'explorer', label: '[5]Explorer', width: 11 },
|
|
373
|
-
{ tab: 'compare', label: '[4]Compare', width: 10 },
|
|
374
|
-
{ tab: 'history', label: '[3]History', width: 10 },
|
|
375
|
-
{ tab: 'commit', label: '[2]Commit', width: 9 },
|
|
376
|
-
{ tab: 'diff', label: '[1]Diff', width: 7 },
|
|
377
|
-
];
|
|
378
|
-
let rightEdge = width;
|
|
379
|
-
for (const { tab, width: tabWidth } of tabPositions) {
|
|
380
|
-
const leftEdge = rightEdge - tabWidth - 1; // -1 for space
|
|
381
|
-
if (x >= leftEdge && x < rightEdge) {
|
|
382
|
-
this.uiState.setTab(tab);
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
rightEdge = leftEdge;
|
|
386
|
-
}
|
|
387
|
-
// Left side toggles (approximate positions)
|
|
388
|
-
// Format: ? [scroll] [auto] [wrap] [dots]
|
|
389
|
-
if (x >= 2 && x <= 9) {
|
|
390
|
-
// [scroll] or m:[select]
|
|
391
|
-
this.toggleMouseMode();
|
|
392
|
-
}
|
|
393
|
-
else if (x >= 11 && x <= 16) {
|
|
394
|
-
// [auto]
|
|
395
|
-
this.uiState.toggleAutoTab();
|
|
396
|
-
}
|
|
397
|
-
else if (x >= 18 && x <= 23) {
|
|
398
|
-
// [wrap]
|
|
399
|
-
this.uiState.toggleWrapMode();
|
|
400
|
-
}
|
|
401
|
-
else if (x >= 25 && x <= 30 && this.uiState.state.bottomTab === 'explorer') {
|
|
402
|
-
// [dots] - only visible in explorer
|
|
403
|
-
this.uiState.toggleMiddleDots();
|
|
404
|
-
}
|
|
405
|
-
else if (x === 0) {
|
|
406
|
-
// ? - open hotkeys
|
|
407
|
-
this.uiState.openModal('hotkeys');
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
handleTopPaneScroll(delta) {
|
|
411
|
-
const state = this.uiState.state;
|
|
412
|
-
const visibleHeight = this.layout.dimensions.topPaneHeight;
|
|
413
|
-
if (state.bottomTab === 'history') {
|
|
414
|
-
const totalRows = this.gitManager?.historyState.commits.length ?? 0;
|
|
415
|
-
const maxOffset = Math.max(0, totalRows - visibleHeight);
|
|
416
|
-
const newOffset = Math.min(maxOffset, Math.max(0, state.historyScrollOffset + delta));
|
|
417
|
-
this.uiState.setHistoryScrollOffset(newOffset);
|
|
418
|
-
}
|
|
419
|
-
else if (state.bottomTab === 'compare') {
|
|
420
|
-
const compareState = this.gitManager?.compareState;
|
|
421
|
-
const totalRows = getCompareListTotalRows(compareState?.compareDiff?.commits ?? [], compareState?.compareDiff?.files ?? []);
|
|
422
|
-
const maxOffset = Math.max(0, totalRows - visibleHeight);
|
|
423
|
-
const newOffset = Math.min(maxOffset, Math.max(0, state.compareScrollOffset + delta));
|
|
424
|
-
this.uiState.setCompareScrollOffset(newOffset);
|
|
425
|
-
}
|
|
426
|
-
else if (state.bottomTab === 'explorer') {
|
|
427
|
-
const totalRows = getExplorerTotalRows(this.explorerManager?.state.items ?? []);
|
|
428
|
-
const maxOffset = Math.max(0, totalRows - visibleHeight);
|
|
429
|
-
const newOffset = Math.min(maxOffset, Math.max(0, state.explorerScrollOffset + delta));
|
|
430
|
-
this.uiState.setExplorerScrollOffset(newOffset);
|
|
431
|
-
}
|
|
432
|
-
else {
|
|
433
|
-
const files = this.gitManager?.state.status?.files ?? [];
|
|
434
|
-
const totalRows = getFileListTotalRows(files);
|
|
435
|
-
const maxOffset = Math.max(0, totalRows - visibleHeight);
|
|
436
|
-
const newOffset = Math.min(maxOffset, Math.max(0, state.fileListScrollOffset + delta));
|
|
437
|
-
this.uiState.setFileListScrollOffset(newOffset);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
handleBottomPaneScroll(delta) {
|
|
441
|
-
const state = this.uiState.state;
|
|
442
|
-
const visibleHeight = this.layout.dimensions.bottomPaneHeight;
|
|
443
|
-
const width = this.screen.width || 80;
|
|
444
|
-
if (state.bottomTab === 'explorer') {
|
|
445
|
-
const selectedFile = this.explorerManager?.state.selectedFile;
|
|
446
|
-
const totalRows = getExplorerContentTotalRows(selectedFile?.content ?? null, selectedFile?.path ?? null, selectedFile?.truncated ?? false, width, state.wrapMode);
|
|
447
|
-
const maxOffset = Math.max(0, totalRows - visibleHeight);
|
|
448
|
-
const newOffset = Math.min(maxOffset, Math.max(0, state.explorerFileScrollOffset + delta));
|
|
449
|
-
this.uiState.setExplorerFileScrollOffset(newOffset);
|
|
450
|
-
}
|
|
451
|
-
else {
|
|
452
|
-
// Use cached totalRows from last render (single source of truth)
|
|
453
|
-
const maxOffset = Math.max(0, this.bottomPaneTotalRows - visibleHeight);
|
|
454
|
-
const newOffset = Math.min(maxOffset, Math.max(0, state.diffScrollOffset + delta));
|
|
455
|
-
this.uiState.setDiffScrollOffset(newOffset);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
208
|
setupStateListeners() {
|
|
459
209
|
// Update footer when UI state changes
|
|
460
210
|
this.uiState.on('change', () => {
|
|
@@ -470,7 +220,7 @@ export class App {
|
|
|
470
220
|
}
|
|
471
221
|
else if (tab === 'explorer') {
|
|
472
222
|
// Explorer is already loaded on init, but refresh if needed
|
|
473
|
-
if (!this.explorerManager?.state.
|
|
223
|
+
if (!this.explorerManager?.state.displayRows.length) {
|
|
474
224
|
this.explorerManager?.loadDirectory('');
|
|
475
225
|
}
|
|
476
226
|
}
|
|
@@ -532,48 +282,47 @@ export class App {
|
|
|
532
282
|
}, 500);
|
|
533
283
|
});
|
|
534
284
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
this.
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
this.initGitManager();
|
|
547
|
-
this.render();
|
|
548
|
-
}
|
|
549
|
-
// Navigate to the followed file if it's within the repo
|
|
550
|
-
if (state.rawContent) {
|
|
551
|
-
this.navigateToFile(state.rawContent);
|
|
552
|
-
this.render();
|
|
553
|
-
}
|
|
554
|
-
});
|
|
555
|
-
this.watcherState = {
|
|
556
|
-
enabled: true,
|
|
557
|
-
sourceFile: this.config.targetFile,
|
|
558
|
-
};
|
|
559
|
-
this.fileWatcher.start();
|
|
560
|
-
// Navigate to the initially followed file
|
|
561
|
-
const initialState = this.fileWatcher.state;
|
|
562
|
-
if (initialState.rawContent) {
|
|
563
|
-
this.watcherState.rawContent = initialState.rawContent;
|
|
564
|
-
this.navigateToFile(initialState.rawContent);
|
|
565
|
-
}
|
|
285
|
+
handleFollowRepoChange(newPath, _state) {
|
|
286
|
+
const oldRepoPath = this.repoPath;
|
|
287
|
+
this.repoPath = newPath;
|
|
288
|
+
this.initGitManager(oldRepoPath);
|
|
289
|
+
this.resetRepoSpecificState();
|
|
290
|
+
this.loadCurrentTabData();
|
|
291
|
+
this.render();
|
|
292
|
+
}
|
|
293
|
+
handleFollowFileNavigate(rawContent) {
|
|
294
|
+
this.navigateToFile(rawContent);
|
|
295
|
+
this.render();
|
|
566
296
|
}
|
|
567
|
-
initGitManager() {
|
|
297
|
+
initGitManager(oldRepoPath) {
|
|
568
298
|
// Clean up existing manager
|
|
569
299
|
if (this.gitManager) {
|
|
570
300
|
this.gitManager.removeAllListeners();
|
|
571
|
-
|
|
301
|
+
// Use oldRepoPath if provided (when switching repos), otherwise use current path
|
|
302
|
+
removeManagerForRepo(oldRepoPath ?? this.repoPath);
|
|
572
303
|
}
|
|
573
304
|
// Get or create manager for this repo
|
|
574
305
|
this.gitManager = getManagerForRepo(this.repoPath);
|
|
575
306
|
// Listen to state changes
|
|
576
307
|
this.gitManager.on('state-change', () => {
|
|
308
|
+
const files = this.gitManager?.state.status?.files ?? [];
|
|
309
|
+
if (this.pendingSelectionAnchor) {
|
|
310
|
+
// Restore selection to same category + position after stage/unstage
|
|
311
|
+
const anchor = this.pendingSelectionAnchor;
|
|
312
|
+
this.pendingSelectionAnchor = null;
|
|
313
|
+
const newIndex = getIndexForCategoryPosition(files, anchor.category, anchor.categoryIndex);
|
|
314
|
+
this.uiState.setSelectedIndex(newIndex);
|
|
315
|
+
this.selectFileByIndex(newIndex);
|
|
316
|
+
}
|
|
317
|
+
else if (files.length > 0) {
|
|
318
|
+
// Default: clamp selected index to valid range
|
|
319
|
+
const maxIndex = files.length - 1;
|
|
320
|
+
if (this.uiState.state.selectedIndex > maxIndex) {
|
|
321
|
+
this.uiState.setSelectedIndex(maxIndex);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// Update explorer git status when git state changes
|
|
325
|
+
this.updateExplorerGitStatus();
|
|
577
326
|
this.render();
|
|
578
327
|
});
|
|
579
328
|
this.gitManager.on('history-state-change', (historyState) => {
|
|
@@ -607,6 +356,7 @@ export class App {
|
|
|
607
356
|
const options = {
|
|
608
357
|
hideHidden: true,
|
|
609
358
|
hideGitignored: true,
|
|
359
|
+
showOnlyChanges: false,
|
|
610
360
|
};
|
|
611
361
|
this.explorerManager = new ExplorerStateManager(this.repoPath, options);
|
|
612
362
|
// Listen to state changes
|
|
@@ -615,6 +365,58 @@ export class App {
|
|
|
615
365
|
});
|
|
616
366
|
// Load root directory
|
|
617
367
|
this.explorerManager.loadDirectory('');
|
|
368
|
+
// Update git status after tree is loaded
|
|
369
|
+
this.updateExplorerGitStatus();
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Build git status map and update explorer.
|
|
373
|
+
*/
|
|
374
|
+
updateExplorerGitStatus() {
|
|
375
|
+
if (!this.explorerManager || !this.gitManager)
|
|
376
|
+
return;
|
|
377
|
+
const files = this.gitManager.state.status?.files ?? [];
|
|
378
|
+
const statusMap = {
|
|
379
|
+
files: new Map(),
|
|
380
|
+
directories: new Set(),
|
|
381
|
+
};
|
|
382
|
+
for (const file of files) {
|
|
383
|
+
statusMap.files.set(file.path, { status: file.status, staged: file.staged });
|
|
384
|
+
// Mark all parent directories as having changed children
|
|
385
|
+
const parts = file.path.split('/');
|
|
386
|
+
let dirPath = '';
|
|
387
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
388
|
+
dirPath = dirPath ? `${dirPath}/${parts[i]}` : parts[i];
|
|
389
|
+
statusMap.directories.add(dirPath);
|
|
390
|
+
}
|
|
391
|
+
// Also mark root as having changes
|
|
392
|
+
statusMap.directories.add('');
|
|
393
|
+
}
|
|
394
|
+
this.explorerManager.setGitStatus(statusMap);
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Reset UI state that's specific to a repository.
|
|
398
|
+
* Called when switching to a new repo via file watcher.
|
|
399
|
+
*/
|
|
400
|
+
resetRepoSpecificState() {
|
|
401
|
+
// Reset compare selection (App-level state)
|
|
402
|
+
this.compareSelection = null;
|
|
403
|
+
// Reset UI state scroll offsets and selections
|
|
404
|
+
this.uiState.resetForNewRepo();
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Load data for the current tab.
|
|
408
|
+
* Called after switching repos to refresh tab-specific data.
|
|
409
|
+
*/
|
|
410
|
+
loadCurrentTabData() {
|
|
411
|
+
const tab = this.uiState.state.bottomTab;
|
|
412
|
+
if (tab === 'history') {
|
|
413
|
+
this.gitManager?.loadHistory();
|
|
414
|
+
}
|
|
415
|
+
else if (tab === 'compare') {
|
|
416
|
+
this.gitManager?.refreshCompareDiff(this.uiState.state.includeUncommitted);
|
|
417
|
+
}
|
|
418
|
+
// Diff tab data is loaded by gitManager.refresh() in initGitManager
|
|
419
|
+
// Explorer data is loaded by initExplorerManager()
|
|
618
420
|
}
|
|
619
421
|
setupCommandHandler() {
|
|
620
422
|
if (!this.commandServer)
|
|
@@ -853,8 +655,8 @@ export class App {
|
|
|
853
655
|
// Explorer navigation
|
|
854
656
|
navigateExplorerUp() {
|
|
855
657
|
const state = this.uiState.state;
|
|
856
|
-
const
|
|
857
|
-
if (
|
|
658
|
+
const rows = this.explorerManager?.state.displayRows ?? [];
|
|
659
|
+
if (rows.length === 0)
|
|
858
660
|
return;
|
|
859
661
|
const newScrollOffset = this.explorerManager?.navigateUp(state.explorerScrollOffset);
|
|
860
662
|
if (newScrollOffset !== null && newScrollOffset !== undefined) {
|
|
@@ -864,8 +666,8 @@ export class App {
|
|
|
864
666
|
}
|
|
865
667
|
navigateExplorerDown() {
|
|
866
668
|
const state = this.uiState.state;
|
|
867
|
-
const
|
|
868
|
-
if (
|
|
669
|
+
const rows = this.explorerManager?.state.displayRows ?? [];
|
|
670
|
+
if (rows.length === 0)
|
|
869
671
|
return;
|
|
870
672
|
const visibleHeight = this.layout.dimensions.topPaneHeight;
|
|
871
673
|
const newScrollOffset = this.explorerManager?.navigateDown(state.explorerScrollOffset, visibleHeight);
|
|
@@ -876,15 +678,17 @@ export class App {
|
|
|
876
678
|
}
|
|
877
679
|
async enterExplorerDirectory() {
|
|
878
680
|
await this.explorerManager?.enterDirectory();
|
|
879
|
-
|
|
681
|
+
// Reset file content scroll when expanding/collapsing
|
|
880
682
|
this.uiState.setExplorerFileScrollOffset(0);
|
|
881
|
-
|
|
683
|
+
// Sync selected index from explorer manager (it maintains selection by path)
|
|
684
|
+
this.uiState.setExplorerSelectedIndex(this.explorerManager?.state.selectedIndex ?? 0);
|
|
882
685
|
}
|
|
883
686
|
async goExplorerUp() {
|
|
884
687
|
await this.explorerManager?.goUp();
|
|
885
|
-
|
|
688
|
+
// Reset file content scroll when collapsing
|
|
886
689
|
this.uiState.setExplorerFileScrollOffset(0);
|
|
887
|
-
|
|
690
|
+
// Sync selected index from explorer manager
|
|
691
|
+
this.uiState.setExplorerSelectedIndex(this.explorerManager?.state.selectedIndex ?? 0);
|
|
888
692
|
}
|
|
889
693
|
selectFileByIndex(index) {
|
|
890
694
|
const files = this.gitManager?.state.status?.files ?? [];
|
|
@@ -921,22 +725,28 @@ export class App {
|
|
|
921
725
|
// Git operations
|
|
922
726
|
async stageSelected() {
|
|
923
727
|
const files = this.gitManager?.state.status?.files ?? [];
|
|
924
|
-
const
|
|
728
|
+
const index = this.uiState.state.selectedIndex;
|
|
729
|
+
const selectedFile = getFileAtIndex(files, index);
|
|
925
730
|
if (selectedFile && !selectedFile.staged) {
|
|
731
|
+
this.pendingSelectionAnchor = getCategoryForIndex(files, index);
|
|
926
732
|
await this.gitManager?.stage(selectedFile);
|
|
927
733
|
}
|
|
928
734
|
}
|
|
929
735
|
async unstageSelected() {
|
|
930
736
|
const files = this.gitManager?.state.status?.files ?? [];
|
|
931
|
-
const
|
|
737
|
+
const index = this.uiState.state.selectedIndex;
|
|
738
|
+
const selectedFile = getFileAtIndex(files, index);
|
|
932
739
|
if (selectedFile?.staged) {
|
|
740
|
+
this.pendingSelectionAnchor = getCategoryForIndex(files, index);
|
|
933
741
|
await this.gitManager?.unstage(selectedFile);
|
|
934
742
|
}
|
|
935
743
|
}
|
|
936
744
|
async toggleSelected() {
|
|
937
745
|
const files = this.gitManager?.state.status?.files ?? [];
|
|
938
|
-
const
|
|
746
|
+
const index = this.uiState.state.selectedIndex;
|
|
747
|
+
const selectedFile = getFileAtIndex(files, index);
|
|
939
748
|
if (selectedFile) {
|
|
749
|
+
this.pendingSelectionAnchor = getCategoryForIndex(files, index);
|
|
940
750
|
if (selectedFile.staged) {
|
|
941
751
|
await this.gitManager?.unstage(selectedFile);
|
|
942
752
|
}
|
|
@@ -960,6 +770,26 @@ export class App {
|
|
|
960
770
|
});
|
|
961
771
|
this.activeModal.focus();
|
|
962
772
|
}
|
|
773
|
+
async openFileFinder() {
|
|
774
|
+
const allPaths = (await this.explorerManager?.getAllFilePaths()) ?? [];
|
|
775
|
+
if (allPaths.length === 0)
|
|
776
|
+
return;
|
|
777
|
+
this.activeModal = new FileFinder(this.screen, allPaths, async (selectedPath) => {
|
|
778
|
+
this.activeModal = null;
|
|
779
|
+
// Navigate to the selected file in explorer
|
|
780
|
+
const success = await this.explorerManager?.navigateToPath(selectedPath);
|
|
781
|
+
if (success) {
|
|
782
|
+
// Reset scroll to show selected file
|
|
783
|
+
this.uiState.setExplorerScrollOffset(0);
|
|
784
|
+
this.uiState.setExplorerFileScrollOffset(0);
|
|
785
|
+
}
|
|
786
|
+
this.render();
|
|
787
|
+
}, () => {
|
|
788
|
+
this.activeModal = null;
|
|
789
|
+
this.render();
|
|
790
|
+
});
|
|
791
|
+
this.activeModal.focus();
|
|
792
|
+
}
|
|
963
793
|
async commit(message) {
|
|
964
794
|
await this.gitManager?.commit(message);
|
|
965
795
|
}
|
|
@@ -979,14 +809,13 @@ export class App {
|
|
|
979
809
|
}
|
|
980
810
|
}
|
|
981
811
|
toggleFollow() {
|
|
982
|
-
if (this.
|
|
983
|
-
this.
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
else {
|
|
988
|
-
this.setupFileWatcher();
|
|
812
|
+
if (!this.followMode) {
|
|
813
|
+
this.followMode = new FollowMode(this.config.targetFile, () => this.repoPath, {
|
|
814
|
+
onRepoChange: (newPath, state) => this.handleFollowRepoChange(newPath, state),
|
|
815
|
+
onFileNavigate: (rawContent) => this.handleFollowFileNavigate(rawContent),
|
|
816
|
+
});
|
|
989
817
|
}
|
|
818
|
+
this.followMode.toggle();
|
|
990
819
|
this.render();
|
|
991
820
|
}
|
|
992
821
|
focusCommitInput() {
|
|
@@ -1019,114 +848,39 @@ export class App {
|
|
|
1019
848
|
updateHeader() {
|
|
1020
849
|
const gitState = this.gitManager?.state;
|
|
1021
850
|
const width = this.screen.width || 80;
|
|
1022
|
-
const content = formatHeader(this.repoPath, gitState?.status?.branch ?? null, gitState?.isLoading ?? false, gitState?.error ?? null,
|
|
851
|
+
const content = formatHeader(this.repoPath, gitState?.status?.branch ?? null, gitState?.isLoading ?? false, gitState?.error ?? null, width);
|
|
1023
852
|
this.layout.headerBox.setContent(content);
|
|
1024
853
|
}
|
|
1025
854
|
updateTopPane() {
|
|
1026
|
-
const gitState = this.gitManager?.state;
|
|
1027
|
-
const historyState = this.gitManager?.historyState;
|
|
1028
|
-
const compareState = this.gitManager?.compareState;
|
|
1029
|
-
const files = gitState?.status?.files ?? [];
|
|
1030
855
|
const state = this.uiState.state;
|
|
1031
856
|
const width = this.screen.width || 80;
|
|
1032
|
-
|
|
1033
|
-
if (state.bottomTab === 'history') {
|
|
1034
|
-
const commits = historyState?.commits ?? [];
|
|
1035
|
-
content = formatHistoryView(commits, state.historySelectedIndex, state.currentPane === 'history', width, state.historyScrollOffset, this.layout.dimensions.topPaneHeight);
|
|
1036
|
-
}
|
|
1037
|
-
else if (state.bottomTab === 'compare') {
|
|
1038
|
-
const compareDiff = compareState?.compareDiff;
|
|
1039
|
-
const commits = compareDiff?.commits ?? [];
|
|
1040
|
-
const compareFiles = compareDiff?.files ?? [];
|
|
1041
|
-
content = formatCompareListView(commits, compareFiles, this.compareSelection, state.currentPane === 'compare', width, state.compareScrollOffset, this.layout.dimensions.topPaneHeight);
|
|
1042
|
-
}
|
|
1043
|
-
else if (state.bottomTab === 'explorer') {
|
|
1044
|
-
const explorerState = this.explorerManager?.state;
|
|
1045
|
-
const items = explorerState?.items ?? [];
|
|
1046
|
-
content = formatExplorerView(items, state.explorerSelectedIndex, state.currentPane === 'explorer', width, state.explorerScrollOffset, this.layout.dimensions.topPaneHeight, explorerState?.isLoading ?? false, explorerState?.error ?? null);
|
|
1047
|
-
}
|
|
1048
|
-
else {
|
|
1049
|
-
content = formatFileList(files, state.selectedIndex, state.currentPane === 'files', width, state.fileListScrollOffset, this.layout.dimensions.topPaneHeight);
|
|
1050
|
-
}
|
|
857
|
+
const content = renderTopPane(state, this.gitManager?.state.status?.files ?? [], this.gitManager?.historyState?.commits ?? [], this.gitManager?.compareState?.compareDiff ?? null, this.compareSelection, this.explorerManager?.state, width, this.layout.dimensions.topPaneHeight);
|
|
1051
858
|
this.layout.topPane.setContent(content);
|
|
1052
859
|
}
|
|
1053
860
|
updateBottomPane() {
|
|
1054
|
-
const gitState = this.gitManager?.state;
|
|
1055
|
-
const historyState = this.gitManager?.historyState;
|
|
1056
|
-
const diff = gitState?.diff ?? null;
|
|
1057
861
|
const state = this.uiState.state;
|
|
1058
862
|
const width = this.screen.width || 80;
|
|
1059
|
-
const files =
|
|
863
|
+
const files = this.gitManager?.state.status?.files ?? [];
|
|
1060
864
|
const stagedCount = files.filter((f) => f.staged).length;
|
|
1061
865
|
// Update staged count for commit validation
|
|
1062
866
|
this.commitFlowState.setStagedCount(stagedCount);
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
if (this.
|
|
1069
|
-
|
|
1070
|
-
this.commitTextarea.show();
|
|
1071
|
-
}
|
|
1072
|
-
else {
|
|
1073
|
-
this.commitTextarea.hide();
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
else if (state.bottomTab === 'history') {
|
|
1078
|
-
// Hide commit textarea when not on commit tab
|
|
1079
|
-
if (this.commitTextarea) {
|
|
1080
|
-
this.commitTextarea.hide();
|
|
1081
|
-
}
|
|
1082
|
-
const selectedCommit = historyState?.selectedCommit ?? null;
|
|
1083
|
-
const commitDiff = historyState?.commitDiff ?? null;
|
|
1084
|
-
const { content, totalRows } = formatHistoryDiff(selectedCommit, commitDiff, width, state.diffScrollOffset, this.layout.dimensions.bottomPaneHeight, this.currentTheme, state.wrapMode);
|
|
1085
|
-
this.bottomPaneTotalRows = totalRows;
|
|
1086
|
-
this.layout.bottomPane.setContent(content);
|
|
1087
|
-
}
|
|
1088
|
-
else if (state.bottomTab === 'compare') {
|
|
1089
|
-
// Hide commit textarea when not on commit tab
|
|
1090
|
-
if (this.commitTextarea) {
|
|
1091
|
-
this.commitTextarea.hide();
|
|
1092
|
-
}
|
|
1093
|
-
const compareSelectionState = this.gitManager?.compareSelectionState;
|
|
1094
|
-
const compareDiff = compareSelectionState?.diff ?? null;
|
|
1095
|
-
if (compareDiff) {
|
|
1096
|
-
const { content, totalRows } = formatDiff(compareDiff, width, state.diffScrollOffset, this.layout.dimensions.bottomPaneHeight, this.currentTheme, state.wrapMode);
|
|
1097
|
-
this.bottomPaneTotalRows = totalRows;
|
|
1098
|
-
this.layout.bottomPane.setContent(content);
|
|
867
|
+
const { content, totalRows } = renderBottomPane(state, this.gitManager?.state.diff ?? null, this.gitManager?.historyState, this.gitManager?.compareSelectionState, this.explorerManager?.state?.selectedFile ?? null, this.commitFlowState.state, stagedCount, this.currentTheme, width, this.layout.dimensions.bottomPaneHeight);
|
|
868
|
+
this.bottomPaneTotalRows = totalRows;
|
|
869
|
+
this.layout.bottomPane.setContent(content);
|
|
870
|
+
// Manage commit textarea visibility
|
|
871
|
+
if (this.commitTextarea) {
|
|
872
|
+
if (state.bottomTab === 'commit' && this.commitFlowState.state.inputFocused) {
|
|
873
|
+
this.commitTextarea.show();
|
|
1099
874
|
}
|
|
1100
875
|
else {
|
|
1101
|
-
this.bottomPaneTotalRows = 0;
|
|
1102
|
-
this.layout.bottomPane.setContent('{gray-fg}Select a commit or file to view diff{/gray-fg}');
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
else if (state.bottomTab === 'explorer') {
|
|
1106
|
-
// Hide commit textarea when not on commit tab
|
|
1107
|
-
if (this.commitTextarea) {
|
|
1108
|
-
this.commitTextarea.hide();
|
|
1109
|
-
}
|
|
1110
|
-
const explorerState = this.explorerManager?.state;
|
|
1111
|
-
const selectedFile = explorerState?.selectedFile ?? null;
|
|
1112
|
-
const content = formatExplorerContent(selectedFile?.path ?? null, selectedFile?.content ?? null, width, state.explorerFileScrollOffset, this.layout.dimensions.bottomPaneHeight, selectedFile?.truncated ?? false, state.wrapMode, state.showMiddleDots);
|
|
1113
|
-
// TODO: formatExplorerContent should also return totalRows
|
|
1114
|
-
this.layout.bottomPane.setContent(content);
|
|
1115
|
-
}
|
|
1116
|
-
else {
|
|
1117
|
-
// Hide commit textarea when not on commit tab
|
|
1118
|
-
if (this.commitTextarea) {
|
|
1119
876
|
this.commitTextarea.hide();
|
|
1120
877
|
}
|
|
1121
|
-
const { content, totalRows } = formatDiff(diff, width, state.diffScrollOffset, this.layout.dimensions.bottomPaneHeight, this.currentTheme, state.wrapMode);
|
|
1122
|
-
this.bottomPaneTotalRows = totalRows;
|
|
1123
|
-
this.layout.bottomPane.setContent(content);
|
|
1124
878
|
}
|
|
1125
879
|
}
|
|
1126
880
|
updateFooter() {
|
|
1127
881
|
const state = this.uiState.state;
|
|
1128
882
|
const width = this.screen.width || 80;
|
|
1129
|
-
const content = formatFooter(state.bottomTab, state.mouseEnabled, state.autoTabEnabled, state.wrapMode,
|
|
883
|
+
const content = formatFooter(state.bottomTab, state.mouseEnabled, state.autoTabEnabled, state.wrapMode, this.followMode?.isEnabled ?? false, this.explorerManager?.showOnlyChanges ?? false, width);
|
|
1130
884
|
this.layout.footerBox.setContent(content);
|
|
1131
885
|
}
|
|
1132
886
|
/**
|
|
@@ -1140,8 +894,8 @@ export class App {
|
|
|
1140
894
|
if (this.explorerManager) {
|
|
1141
895
|
this.explorerManager.dispose();
|
|
1142
896
|
}
|
|
1143
|
-
if (this.
|
|
1144
|
-
this.
|
|
897
|
+
if (this.followMode) {
|
|
898
|
+
this.followMode.stop();
|
|
1145
899
|
}
|
|
1146
900
|
if (this.commandServer) {
|
|
1147
901
|
this.commandServer.stop();
|