diffstalker 0.1.7 → 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/CHANGELOG.md +36 -0
- package/bun.lock +89 -306
- package/dist/App.js +895 -520
- package/dist/FollowMode.js +85 -0
- package/dist/KeyBindings.js +178 -0
- package/dist/MouseHandlers.js +156 -0
- package/dist/core/ExplorerStateManager.js +632 -0
- package/dist/core/FilePathWatcher.js +133 -0
- package/dist/core/GitStateManager.js +221 -86
- package/dist/git/diff.js +4 -0
- package/dist/git/ignoreUtils.js +30 -0
- package/dist/git/status.js +2 -34
- package/dist/index.js +68 -53
- package/dist/ipc/CommandClient.js +165 -0
- package/dist/ipc/CommandServer.js +152 -0
- package/dist/state/CommitFlowState.js +86 -0
- package/dist/state/UIState.js +195 -0
- package/dist/types/tabs.js +4 -0
- package/dist/ui/Layout.js +252 -0
- package/dist/ui/PaneRenderers.js +56 -0
- package/dist/ui/modals/BaseBranchPicker.js +110 -0
- package/dist/ui/modals/DiscardConfirm.js +77 -0
- package/dist/ui/modals/FileFinder.js +232 -0
- package/dist/ui/modals/HotkeysModal.js +209 -0
- package/dist/ui/modals/ThemePicker.js +107 -0
- package/dist/ui/widgets/CommitPanel.js +58 -0
- package/dist/ui/widgets/CompareListView.js +238 -0
- package/dist/ui/widgets/DiffView.js +281 -0
- package/dist/ui/widgets/ExplorerContent.js +89 -0
- package/dist/ui/widgets/ExplorerView.js +204 -0
- package/dist/ui/widgets/FileList.js +185 -0
- package/dist/ui/widgets/Footer.js +50 -0
- package/dist/ui/widgets/Header.js +68 -0
- package/dist/ui/widgets/HistoryView.js +69 -0
- package/dist/utils/displayRows.js +185 -6
- package/dist/utils/explorerDisplayRows.js +1 -1
- package/dist/utils/fileCategories.js +37 -0
- package/dist/utils/fileTree.js +148 -0
- package/dist/utils/languageDetection.js +56 -0
- package/dist/utils/pathUtils.js +27 -0
- package/dist/utils/wordDiff.js +50 -0
- package/eslint.metrics.js +16 -0
- package/metrics/.gitkeep +0 -0
- package/metrics/v0.2.1.json +268 -0
- package/package.json +14 -12
- package/dist/components/BaseBranchPicker.js +0 -60
- package/dist/components/BottomPane.js +0 -101
- package/dist/components/CommitPanel.js +0 -58
- package/dist/components/CompareListView.js +0 -110
- package/dist/components/ExplorerContentView.js +0 -80
- package/dist/components/ExplorerView.js +0 -37
- package/dist/components/FileList.js +0 -131
- package/dist/components/Footer.js +0 -6
- package/dist/components/Header.js +0 -107
- package/dist/components/HistoryView.js +0 -21
- package/dist/components/HotkeysModal.js +0 -108
- package/dist/components/Modal.js +0 -19
- package/dist/components/ScrollableList.js +0 -125
- package/dist/components/ThemePicker.js +0 -42
- package/dist/components/TopPane.js +0 -14
- package/dist/components/UnifiedDiffView.js +0 -115
- package/dist/hooks/useCommitFlow.js +0 -66
- package/dist/hooks/useCompareState.js +0 -123
- package/dist/hooks/useExplorerState.js +0 -248
- package/dist/hooks/useGit.js +0 -156
- package/dist/hooks/useHistoryState.js +0 -62
- package/dist/hooks/useKeymap.js +0 -167
- package/dist/hooks/useLayout.js +0 -154
- package/dist/hooks/useMouse.js +0 -87
- package/dist/hooks/useTerminalSize.js +0 -20
- package/dist/hooks/useWatcher.js +0 -137
- package/dist/utils/mouseCoordinates.js +0 -165
- package/dist/utils/rowCalculations.js +0 -209
package/dist/App.js
CHANGED
|
@@ -1,541 +1,916 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
1
|
+
import blessed from 'neo-blessed';
|
|
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';
|
|
7
|
+
import { formatHeader } from './ui/widgets/Header.js';
|
|
8
|
+
import { formatFooter } from './ui/widgets/Footer.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
|
+
import { ExplorerStateManager, } from './core/ExplorerStateManager.js';
|
|
13
|
+
import { ThemePicker } from './ui/modals/ThemePicker.js';
|
|
14
|
+
import { HotkeysModal } from './ui/modals/HotkeysModal.js';
|
|
15
|
+
import { BaseBranchPicker } from './ui/modals/BaseBranchPicker.js';
|
|
16
|
+
import { DiscardConfirm } from './ui/modals/DiscardConfirm.js';
|
|
17
|
+
import { FileFinder } from './ui/modals/FileFinder.js';
|
|
18
|
+
import { CommitFlowState } from './state/CommitFlowState.js';
|
|
19
|
+
import { UIState } from './state/UIState.js';
|
|
20
|
+
import { getManagerForRepo, removeManagerForRepo, } from './core/GitStateManager.js';
|
|
20
21
|
import { saveConfig } from './config.js';
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
22
|
+
import { getCategoryForIndex, getIndexForCategoryPosition, } from './utils/fileCategories.js';
|
|
23
|
+
/**
|
|
24
|
+
* Main application controller.
|
|
25
|
+
* Coordinates between GitStateManager, UIState, and blessed widgets.
|
|
26
|
+
*/
|
|
27
|
+
export class App {
|
|
28
|
+
screen;
|
|
29
|
+
layout;
|
|
30
|
+
uiState;
|
|
31
|
+
gitManager = null;
|
|
32
|
+
followMode = null;
|
|
33
|
+
explorerManager = null;
|
|
34
|
+
config;
|
|
35
|
+
commandServer;
|
|
36
|
+
// Current state
|
|
37
|
+
repoPath;
|
|
38
|
+
currentTheme;
|
|
39
|
+
// Commit flow state
|
|
40
|
+
commitFlowState;
|
|
41
|
+
commitTextarea = null;
|
|
42
|
+
// Active modals
|
|
43
|
+
activeModal = null;
|
|
44
|
+
// Cached total rows for scroll bounds (single source of truth from render)
|
|
45
|
+
bottomPaneTotalRows = 0;
|
|
46
|
+
// Selection anchor: remembers category + position before stage/unstage
|
|
47
|
+
pendingSelectionAnchor = null;
|
|
48
|
+
constructor(options) {
|
|
49
|
+
this.config = options.config;
|
|
50
|
+
this.commandServer = options.commandServer ?? null;
|
|
51
|
+
this.repoPath = options.initialPath ?? process.cwd();
|
|
52
|
+
this.currentTheme = options.config.theme;
|
|
53
|
+
// Initialize UI state with config values
|
|
54
|
+
this.uiState = new UIState({
|
|
55
|
+
splitRatio: options.config.splitRatio ?? 0.4,
|
|
56
|
+
});
|
|
57
|
+
// Create blessed screen
|
|
58
|
+
this.screen = blessed.screen({
|
|
59
|
+
smartCSR: true,
|
|
60
|
+
fullUnicode: true,
|
|
61
|
+
title: 'diffstalker',
|
|
62
|
+
mouse: true,
|
|
63
|
+
terminal: 'xterm-256color',
|
|
64
|
+
});
|
|
65
|
+
// Force 256-color support (terminfo detection can be unreliable)
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
67
|
+
const screenAny = this.screen;
|
|
68
|
+
if (screenAny.tput) {
|
|
69
|
+
screenAny.tput.colors = 256;
|
|
70
|
+
}
|
|
71
|
+
if (screenAny.program?.tput) {
|
|
72
|
+
screenAny.program.tput.colors = 256;
|
|
73
|
+
}
|
|
74
|
+
// Create layout
|
|
75
|
+
this.layout = new LayoutManager(this.screen, this.uiState.state.splitRatio);
|
|
76
|
+
// Handle screen resize - re-render content
|
|
77
|
+
// Use setImmediate to ensure screen dimensions are fully updated
|
|
78
|
+
this.screen.on('resize', () => {
|
|
79
|
+
setImmediate(() => this.render());
|
|
80
|
+
});
|
|
81
|
+
// Initialize commit flow state
|
|
82
|
+
this.commitFlowState = new CommitFlowState({
|
|
83
|
+
getHeadMessage: () => this.gitManager?.getHeadCommitMessage() ?? Promise.resolve(''),
|
|
84
|
+
onCommit: async (message, amend) => {
|
|
85
|
+
await this.gitManager?.commit(message, amend);
|
|
86
|
+
},
|
|
87
|
+
onSuccess: () => {
|
|
88
|
+
this.uiState.setTab('diff');
|
|
89
|
+
this.render();
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
// Create commit textarea (hidden initially)
|
|
93
|
+
this.commitTextarea = blessed.textarea({
|
|
94
|
+
parent: this.layout.bottomPane,
|
|
95
|
+
top: 3,
|
|
96
|
+
left: 1,
|
|
97
|
+
width: '100%-4',
|
|
98
|
+
height: 1,
|
|
99
|
+
inputOnFocus: true,
|
|
100
|
+
hidden: true,
|
|
101
|
+
style: {
|
|
102
|
+
fg: 'white',
|
|
103
|
+
bg: 'default',
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
// Handle textarea submission
|
|
107
|
+
this.commitTextarea.on('submit', () => {
|
|
108
|
+
this.commitFlowState.submit();
|
|
109
|
+
});
|
|
110
|
+
// Sync textarea value with commit state
|
|
111
|
+
this.commitTextarea.on('keypress', () => {
|
|
112
|
+
// Defer to next tick to get updated value
|
|
113
|
+
setImmediate(() => {
|
|
114
|
+
const value = this.commitTextarea?.getValue() ?? '';
|
|
115
|
+
this.commitFlowState.setMessage(value);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
// Setup keyboard handlers
|
|
119
|
+
this.setupKeyboardHandlers();
|
|
120
|
+
// Setup mouse handlers
|
|
121
|
+
this.setupMouseEventHandlers();
|
|
122
|
+
// Setup state change listeners
|
|
123
|
+
this.setupStateListeners();
|
|
124
|
+
// Setup follow mode if enabled
|
|
125
|
+
if (this.config.watcherEnabled) {
|
|
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();
|
|
131
|
+
}
|
|
132
|
+
// Setup IPC command handler if command server provided
|
|
133
|
+
if (this.commandServer) {
|
|
134
|
+
this.setupCommandHandler();
|
|
135
|
+
}
|
|
136
|
+
// Initialize git manager for current repo
|
|
137
|
+
this.initGitManager();
|
|
138
|
+
// Initial render
|
|
139
|
+
this.render();
|
|
140
|
+
}
|
|
141
|
+
setupKeyboardHandlers() {
|
|
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);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
await this.gitManager?.stage(file);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
setupStateListeners() {
|
|
209
|
+
// Update footer when UI state changes
|
|
210
|
+
this.uiState.on('change', () => {
|
|
211
|
+
this.render();
|
|
212
|
+
});
|
|
213
|
+
// Load data when switching tabs
|
|
214
|
+
this.uiState.on('tab-change', (tab) => {
|
|
215
|
+
if (tab === 'history') {
|
|
216
|
+
this.gitManager?.loadHistory();
|
|
206
217
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (bottomTab === 'diff' || bottomTab === 'commit') {
|
|
215
|
-
const clickedIndex = getClickedFileIndex(y, fileListScrollOffset, files, stagingPaneStart, fileListEnd);
|
|
216
|
-
if (clickedIndex >= 0 && clickedIndex < totalFiles) {
|
|
217
|
-
setSelectedIndex(clickedIndex);
|
|
218
|
-
setCurrentPane('files');
|
|
219
|
-
const file = getFileAtIndex(files, clickedIndex);
|
|
220
|
-
if (file) {
|
|
221
|
-
if (button === 'right' && !file.staged && file.status !== 'untracked') {
|
|
222
|
-
setPendingDiscard(file);
|
|
223
|
-
}
|
|
224
|
-
else if (button === 'left' && isButtonAreaClick(x)) {
|
|
225
|
-
if (file.staged) {
|
|
226
|
-
unstage(file);
|
|
227
|
-
}
|
|
228
|
-
else {
|
|
229
|
-
stage(file);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
218
|
+
else if (tab === 'compare') {
|
|
219
|
+
this.gitManager?.refreshCompareDiff(this.uiState.state.includeUncommitted);
|
|
220
|
+
}
|
|
221
|
+
else if (tab === 'explorer') {
|
|
222
|
+
// Explorer is already loaded on init, but refresh if needed
|
|
223
|
+
if (!this.explorerManager?.state.displayRows.length) {
|
|
224
|
+
this.explorerManager?.loadDirectory('');
|
|
235
225
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
// Handle modal opening/closing
|
|
229
|
+
this.uiState.on('modal-change', (modal) => {
|
|
230
|
+
// Close any existing modal
|
|
231
|
+
if (this.activeModal) {
|
|
232
|
+
this.activeModal = null;
|
|
233
|
+
}
|
|
234
|
+
// Open new modal if requested
|
|
235
|
+
if (modal === 'theme') {
|
|
236
|
+
this.activeModal = new ThemePicker(this.screen, this.currentTheme, (theme) => {
|
|
237
|
+
this.currentTheme = theme;
|
|
238
|
+
saveConfig({ theme });
|
|
239
|
+
this.activeModal = null;
|
|
240
|
+
this.uiState.closeModal();
|
|
241
|
+
this.render();
|
|
242
|
+
}, () => {
|
|
243
|
+
this.activeModal = null;
|
|
244
|
+
this.uiState.closeModal();
|
|
245
|
+
});
|
|
246
|
+
this.activeModal.focus();
|
|
247
|
+
}
|
|
248
|
+
else if (modal === 'hotkeys') {
|
|
249
|
+
this.activeModal = new HotkeysModal(this.screen, () => {
|
|
250
|
+
this.activeModal = null;
|
|
251
|
+
this.uiState.closeModal();
|
|
252
|
+
});
|
|
253
|
+
this.activeModal.focus();
|
|
254
|
+
}
|
|
255
|
+
else if (modal === 'baseBranch') {
|
|
256
|
+
// Load candidate branches and show picker
|
|
257
|
+
this.gitManager?.getCandidateBaseBranches().then((branches) => {
|
|
258
|
+
const currentBranch = this.gitManager?.compareState.compareBaseBranch ?? null;
|
|
259
|
+
this.activeModal = new BaseBranchPicker(this.screen, branches, currentBranch, (branch) => {
|
|
260
|
+
this.activeModal = null;
|
|
261
|
+
this.uiState.closeModal();
|
|
262
|
+
// Set base branch and refresh compare view
|
|
263
|
+
const includeUncommitted = this.uiState.state.includeUncommitted;
|
|
264
|
+
this.gitManager?.setCompareBaseBranch(branch, includeUncommitted);
|
|
265
|
+
}, () => {
|
|
266
|
+
this.activeModal = null;
|
|
267
|
+
this.uiState.closeModal();
|
|
268
|
+
});
|
|
269
|
+
this.activeModal.focus();
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
// Save split ratio to config when it changes
|
|
274
|
+
let saveTimer = null;
|
|
275
|
+
this.uiState.on('change', (state) => {
|
|
276
|
+
if (saveTimer)
|
|
277
|
+
clearTimeout(saveTimer);
|
|
278
|
+
saveTimer = setTimeout(() => {
|
|
279
|
+
if (state.splitRatio !== this.config.splitRatio) {
|
|
280
|
+
saveConfig({ splitRatio: state.splitRatio });
|
|
246
281
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
282
|
+
}, 500);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
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();
|
|
296
|
+
}
|
|
297
|
+
initGitManager(oldRepoPath) {
|
|
298
|
+
// Clean up existing manager
|
|
299
|
+
if (this.gitManager) {
|
|
300
|
+
this.gitManager.removeAllListeners();
|
|
301
|
+
// Use oldRepoPath if provided (when switching repos), otherwise use current path
|
|
302
|
+
removeManagerForRepo(oldRepoPath ?? this.repoPath);
|
|
303
|
+
}
|
|
304
|
+
// Get or create manager for this repo
|
|
305
|
+
this.gitManager = getManagerForRepo(this.repoPath);
|
|
306
|
+
// Listen to state changes
|
|
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);
|
|
257
322
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
323
|
+
}
|
|
324
|
+
// Update explorer git status when git state changes
|
|
325
|
+
this.updateExplorerGitStatus();
|
|
326
|
+
this.render();
|
|
327
|
+
});
|
|
328
|
+
this.gitManager.on('history-state-change', (historyState) => {
|
|
329
|
+
// Auto-select first commit when history loads
|
|
330
|
+
if (historyState.commits.length > 0 && !historyState.selectedCommit) {
|
|
331
|
+
const state = this.uiState.state;
|
|
332
|
+
if (state.bottomTab === 'history') {
|
|
333
|
+
this.selectHistoryCommitByIndex(state.historySelectedIndex);
|
|
266
334
|
}
|
|
267
335
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
336
|
+
this.render();
|
|
337
|
+
});
|
|
338
|
+
this.gitManager.on('compare-state-change', () => {
|
|
339
|
+
this.render();
|
|
340
|
+
});
|
|
341
|
+
this.gitManager.on('compare-selection-change', () => {
|
|
342
|
+
this.render();
|
|
343
|
+
});
|
|
344
|
+
// Start watching and do initial refresh
|
|
345
|
+
this.gitManager.startWatching();
|
|
346
|
+
this.gitManager.refresh();
|
|
347
|
+
// Initialize explorer manager
|
|
348
|
+
this.initExplorerManager();
|
|
349
|
+
}
|
|
350
|
+
initExplorerManager() {
|
|
351
|
+
// Clean up existing manager
|
|
352
|
+
if (this.explorerManager) {
|
|
353
|
+
this.explorerManager.dispose();
|
|
354
|
+
}
|
|
355
|
+
// Create new manager with options
|
|
356
|
+
const options = {
|
|
357
|
+
hideHidden: true,
|
|
358
|
+
hideGitignored: true,
|
|
359
|
+
showOnlyChanges: false,
|
|
360
|
+
};
|
|
361
|
+
this.explorerManager = new ExplorerStateManager(this.repoPath, options);
|
|
362
|
+
// Listen to state changes
|
|
363
|
+
this.explorerManager.on('state-change', () => {
|
|
364
|
+
this.render();
|
|
365
|
+
});
|
|
366
|
+
// Load root directory
|
|
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()
|
|
420
|
+
}
|
|
421
|
+
setupCommandHandler() {
|
|
422
|
+
if (!this.commandServer)
|
|
423
|
+
return;
|
|
424
|
+
const handler = {
|
|
425
|
+
navigateUp: () => this.navigateUp(),
|
|
426
|
+
navigateDown: () => this.navigateDown(),
|
|
427
|
+
switchTab: (tab) => this.uiState.setTab(tab),
|
|
428
|
+
togglePane: () => this.uiState.togglePane(),
|
|
429
|
+
stage: async () => this.stageSelected(),
|
|
430
|
+
unstage: async () => this.unstageSelected(),
|
|
431
|
+
stageAll: async () => this.stageAll(),
|
|
432
|
+
unstageAll: async () => this.unstageAll(),
|
|
433
|
+
commit: async (message) => this.commit(message),
|
|
434
|
+
refresh: async () => this.refresh(),
|
|
435
|
+
getState: () => this.getAppState(),
|
|
436
|
+
quit: () => this.exit(),
|
|
437
|
+
};
|
|
438
|
+
this.commandServer.setHandler(handler);
|
|
439
|
+
this.commandServer.notifyReady();
|
|
440
|
+
}
|
|
441
|
+
getAppState() {
|
|
442
|
+
const state = this.uiState.state;
|
|
443
|
+
const gitState = this.gitManager?.state;
|
|
444
|
+
const historyState = this.gitManager?.historyState;
|
|
445
|
+
const files = gitState?.status?.files ?? [];
|
|
446
|
+
const commits = historyState?.commits ?? [];
|
|
447
|
+
return {
|
|
448
|
+
currentTab: state.bottomTab,
|
|
449
|
+
currentPane: state.currentPane,
|
|
450
|
+
selectedIndex: state.selectedIndex,
|
|
451
|
+
totalFiles: files.length,
|
|
452
|
+
stagedCount: files.filter((f) => f.staged).length,
|
|
453
|
+
files: files.map((f) => ({
|
|
454
|
+
path: f.path,
|
|
455
|
+
status: f.status,
|
|
456
|
+
staged: f.staged,
|
|
457
|
+
})),
|
|
458
|
+
historySelectedIndex: state.historySelectedIndex,
|
|
459
|
+
historyCommitCount: commits.length,
|
|
460
|
+
compareSelectedIndex: state.compareSelectedIndex,
|
|
461
|
+
compareTotalItems: 0,
|
|
462
|
+
includeUncommitted: state.includeUncommitted,
|
|
463
|
+
explorerPath: this.repoPath,
|
|
464
|
+
explorerSelectedIndex: state.explorerSelectedIndex,
|
|
465
|
+
explorerItemCount: 0,
|
|
466
|
+
wrapMode: state.wrapMode,
|
|
467
|
+
mouseEnabled: state.mouseEnabled,
|
|
468
|
+
autoTabEnabled: state.autoTabEnabled,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
// Navigation methods
|
|
472
|
+
navigateUp() {
|
|
473
|
+
const state = this.uiState.state;
|
|
474
|
+
if (state.bottomTab === 'history') {
|
|
475
|
+
if (state.currentPane === 'history') {
|
|
476
|
+
this.navigateHistoryUp();
|
|
477
|
+
}
|
|
478
|
+
else if (state.currentPane === 'diff') {
|
|
479
|
+
this.uiState.setDiffScrollOffset(Math.max(0, state.diffScrollOffset - 3));
|
|
271
480
|
}
|
|
481
|
+
return;
|
|
272
482
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (bottomTab === 'diff' || bottomTab === 'commit') {
|
|
277
|
-
scrollFileList(direction);
|
|
278
|
-
}
|
|
279
|
-
else if (bottomTab === 'history') {
|
|
280
|
-
scrollHistory(direction, historyTotalRows);
|
|
281
|
-
}
|
|
282
|
-
else if (bottomTab === 'compare') {
|
|
283
|
-
scrollCompare(direction, compareTotalItems);
|
|
284
|
-
}
|
|
285
|
-
else if (bottomTab === 'explorer') {
|
|
286
|
-
// Scroll explorer list (maxHeight is topPaneHeight - 1 for "EXPLORER" header)
|
|
287
|
-
const scrollAmount = direction === 'up' ? -3 : 3;
|
|
288
|
-
const maxOffset = getMaxScrollOffset(explorerTotalRows, topPaneHeight - 1);
|
|
289
|
-
setExplorerScrollOffset((prev) => Math.max(0, Math.min(prev + scrollAmount, maxOffset)));
|
|
290
|
-
}
|
|
483
|
+
if (state.bottomTab === 'compare') {
|
|
484
|
+
if (state.currentPane === 'compare') {
|
|
485
|
+
this.navigateCompareUp();
|
|
291
486
|
}
|
|
292
|
-
else {
|
|
293
|
-
|
|
294
|
-
// Scroll file content with proper bounds
|
|
295
|
-
const scrollAmount = direction === 'up' ? -3 : 3;
|
|
296
|
-
const maxOffset = getMaxScrollOffset(explorerContentTotalRows, bottomPaneHeight - 1);
|
|
297
|
-
setExplorerFileScrollOffset((prev) => Math.max(0, Math.min(prev + scrollAmount, maxOffset)));
|
|
298
|
-
}
|
|
299
|
-
else {
|
|
300
|
-
let maxRows;
|
|
301
|
-
if (bottomTab === 'compare' && compareListSelection?.type !== 'commit') {
|
|
302
|
-
maxRows = compareDiffTotalRows;
|
|
303
|
-
}
|
|
304
|
-
else if (bottomTab === 'history') {
|
|
305
|
-
maxRows = historyDiffTotalRows;
|
|
306
|
-
}
|
|
307
|
-
else if (bottomTab === 'diff') {
|
|
308
|
-
maxRows = diffTotalRows;
|
|
309
|
-
}
|
|
310
|
-
scrollDiff(direction, 3, maxRows);
|
|
311
|
-
}
|
|
487
|
+
else if (state.currentPane === 'diff') {
|
|
488
|
+
this.uiState.setDiffScrollOffset(Math.max(0, state.diffScrollOffset - 3));
|
|
312
489
|
}
|
|
490
|
+
return;
|
|
313
491
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
compareDiff,
|
|
322
|
-
compareTotalItems,
|
|
323
|
-
stage,
|
|
324
|
-
unstage,
|
|
325
|
-
scrollDiff,
|
|
326
|
-
scrollFileList,
|
|
327
|
-
scrollHistory,
|
|
328
|
-
scrollCompare,
|
|
329
|
-
historyScrollOffset,
|
|
330
|
-
compareScrollOffset,
|
|
331
|
-
setDiffScrollOffset,
|
|
332
|
-
setHistorySelectedIndex,
|
|
333
|
-
setCompareSelectedIndex,
|
|
334
|
-
markSelectionInitialized,
|
|
335
|
-
getItemIndexFromRow,
|
|
336
|
-
compareListSelection?.type,
|
|
337
|
-
compareDiffTotalRows,
|
|
338
|
-
diffTotalRows,
|
|
339
|
-
historyDiffTotalRows,
|
|
340
|
-
historyTotalRows,
|
|
341
|
-
activeModal,
|
|
342
|
-
explorerItems,
|
|
343
|
-
explorerScrollOffset,
|
|
344
|
-
explorerTotalRows,
|
|
345
|
-
explorerContentTotalRows,
|
|
346
|
-
topPaneHeight,
|
|
347
|
-
bottomPaneHeight,
|
|
348
|
-
setExplorerSelectedIndex,
|
|
349
|
-
setExplorerScrollOffset,
|
|
350
|
-
setExplorerFileScrollOffset,
|
|
351
|
-
]);
|
|
352
|
-
// Disable mouse when inputs are focused
|
|
353
|
-
const mouseDisabled = commitInputFocused || showBaseBranchPicker;
|
|
354
|
-
const { mouseEnabled, toggleMouse } = useMouse(handleMouseEvent, mouseDisabled);
|
|
355
|
-
toggleMouseRef.current = toggleMouse;
|
|
356
|
-
// Auto-tab mode: switch tabs based on file count transitions
|
|
357
|
-
const prevTotalFilesRef = useRef(totalFiles);
|
|
358
|
-
useEffect(() => {
|
|
359
|
-
if (!autoTabEnabled) {
|
|
360
|
-
prevTotalFilesRef.current = totalFiles;
|
|
492
|
+
if (state.bottomTab === 'explorer') {
|
|
493
|
+
if (state.currentPane === 'explorer') {
|
|
494
|
+
this.navigateExplorerUp();
|
|
495
|
+
}
|
|
496
|
+
else if (state.currentPane === 'diff') {
|
|
497
|
+
this.uiState.setExplorerFileScrollOffset(Math.max(0, state.explorerFileScrollOffset - 3));
|
|
498
|
+
}
|
|
361
499
|
return;
|
|
362
500
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
else if (currentPane === 'diff') {
|
|
389
|
-
let maxRows;
|
|
390
|
-
if (bottomTab === 'compare' && compareListSelection?.type !== 'commit') {
|
|
391
|
-
maxRows = compareDiffTotalRows;
|
|
392
|
-
}
|
|
393
|
-
else if (bottomTab === 'diff') {
|
|
394
|
-
maxRows = diffTotalRows;
|
|
395
|
-
}
|
|
396
|
-
scrollDiff('up', 3, maxRows);
|
|
397
|
-
}
|
|
398
|
-
else if (currentPane === 'history') {
|
|
399
|
-
navigateHistoryUp();
|
|
400
|
-
}
|
|
401
|
-
else if (currentPane === 'compare') {
|
|
402
|
-
navigateCompareUp();
|
|
403
|
-
}
|
|
404
|
-
else if (currentPane === 'explorer') {
|
|
405
|
-
navigateExplorerUp();
|
|
406
|
-
}
|
|
407
|
-
}, [
|
|
408
|
-
currentPane,
|
|
409
|
-
bottomTab,
|
|
410
|
-
compareListSelection?.type,
|
|
411
|
-
compareDiffTotalRows,
|
|
412
|
-
diffTotalRows,
|
|
413
|
-
scrollDiff,
|
|
414
|
-
navigateHistoryUp,
|
|
415
|
-
navigateCompareUp,
|
|
416
|
-
navigateExplorerUp,
|
|
417
|
-
]);
|
|
418
|
-
const handleNavigateDown = useCallback(() => {
|
|
419
|
-
if (currentPane === 'files') {
|
|
420
|
-
setSelectedIndex((prev) => Math.min(totalFiles - 1, prev + 1));
|
|
421
|
-
}
|
|
422
|
-
else if (currentPane === 'diff') {
|
|
423
|
-
let maxRows;
|
|
424
|
-
if (bottomTab === 'compare' && compareListSelection?.type !== 'commit') {
|
|
425
|
-
maxRows = compareDiffTotalRows;
|
|
426
|
-
}
|
|
427
|
-
else if (bottomTab === 'diff') {
|
|
428
|
-
maxRows = diffTotalRows;
|
|
429
|
-
}
|
|
430
|
-
scrollDiff('down', 3, maxRows);
|
|
431
|
-
}
|
|
432
|
-
else if (currentPane === 'history') {
|
|
433
|
-
navigateHistoryDown();
|
|
434
|
-
}
|
|
435
|
-
else if (currentPane === 'compare') {
|
|
436
|
-
navigateCompareDown();
|
|
437
|
-
}
|
|
438
|
-
else if (currentPane === 'explorer') {
|
|
439
|
-
navigateExplorerDown();
|
|
440
|
-
}
|
|
441
|
-
}, [
|
|
442
|
-
currentPane,
|
|
443
|
-
bottomTab,
|
|
444
|
-
compareListSelection?.type,
|
|
445
|
-
compareDiffTotalRows,
|
|
446
|
-
diffTotalRows,
|
|
447
|
-
totalFiles,
|
|
448
|
-
scrollDiff,
|
|
449
|
-
navigateHistoryDown,
|
|
450
|
-
navigateCompareDown,
|
|
451
|
-
navigateExplorerDown,
|
|
452
|
-
]);
|
|
453
|
-
const handleTogglePane = useCallback(() => {
|
|
454
|
-
if (bottomTab === 'diff' || bottomTab === 'commit') {
|
|
455
|
-
setCurrentPane((prev) => (prev === 'files' ? 'diff' : 'files'));
|
|
456
|
-
}
|
|
457
|
-
else if (bottomTab === 'history') {
|
|
458
|
-
setCurrentPane((prev) => (prev === 'history' ? 'diff' : 'history'));
|
|
459
|
-
}
|
|
460
|
-
else if (bottomTab === 'compare') {
|
|
461
|
-
setCurrentPane((prev) => (prev === 'compare' ? 'diff' : 'compare'));
|
|
462
|
-
}
|
|
463
|
-
else if (bottomTab === 'explorer') {
|
|
464
|
-
setCurrentPane((prev) => (prev === 'explorer' ? 'diff' : 'explorer'));
|
|
465
|
-
}
|
|
466
|
-
}, [bottomTab]);
|
|
467
|
-
// File operations
|
|
468
|
-
const handleStage = useCallback(async () => {
|
|
469
|
-
if (currentFile && !currentFile.staged)
|
|
470
|
-
await stage(currentFile);
|
|
471
|
-
}, [currentFile, stage]);
|
|
472
|
-
const handleUnstage = useCallback(async () => {
|
|
473
|
-
if (currentFile?.staged)
|
|
474
|
-
await unstage(currentFile);
|
|
475
|
-
}, [currentFile, unstage]);
|
|
476
|
-
const handleSelect = useCallback(async () => {
|
|
477
|
-
if (!currentFile)
|
|
501
|
+
if (state.currentPane === 'files') {
|
|
502
|
+
const files = this.gitManager?.state.status?.files ?? [];
|
|
503
|
+
const newIndex = Math.max(0, state.selectedIndex - 1);
|
|
504
|
+
this.uiState.setSelectedIndex(newIndex);
|
|
505
|
+
this.selectFileByIndex(newIndex);
|
|
506
|
+
// Keep selection visible - scroll up if needed
|
|
507
|
+
const row = getRowFromFileIndex(newIndex, files);
|
|
508
|
+
if (row < state.fileListScrollOffset) {
|
|
509
|
+
this.uiState.setFileListScrollOffset(row);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
else if (state.currentPane === 'diff') {
|
|
513
|
+
this.uiState.setDiffScrollOffset(Math.max(0, state.diffScrollOffset - 3));
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
navigateDown() {
|
|
517
|
+
const state = this.uiState.state;
|
|
518
|
+
const files = this.gitManager?.state.status?.files ?? [];
|
|
519
|
+
if (state.bottomTab === 'history') {
|
|
520
|
+
if (state.currentPane === 'history') {
|
|
521
|
+
this.navigateHistoryDown();
|
|
522
|
+
}
|
|
523
|
+
else if (state.currentPane === 'diff') {
|
|
524
|
+
this.uiState.setDiffScrollOffset(state.diffScrollOffset + 3);
|
|
525
|
+
}
|
|
478
526
|
return;
|
|
479
|
-
|
|
480
|
-
|
|
527
|
+
}
|
|
528
|
+
if (state.bottomTab === 'compare') {
|
|
529
|
+
if (state.currentPane === 'compare') {
|
|
530
|
+
this.navigateCompareDown();
|
|
531
|
+
}
|
|
532
|
+
else if (state.currentPane === 'diff') {
|
|
533
|
+
this.uiState.setDiffScrollOffset(state.diffScrollOffset + 3);
|
|
534
|
+
}
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (state.bottomTab === 'explorer') {
|
|
538
|
+
if (state.currentPane === 'explorer') {
|
|
539
|
+
this.navigateExplorerDown();
|
|
540
|
+
}
|
|
541
|
+
else if (state.currentPane === 'diff') {
|
|
542
|
+
this.uiState.setExplorerFileScrollOffset(state.explorerFileScrollOffset + 3);
|
|
543
|
+
}
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (state.currentPane === 'files') {
|
|
547
|
+
const newIndex = Math.min(files.length - 1, state.selectedIndex + 1);
|
|
548
|
+
this.uiState.setSelectedIndex(newIndex);
|
|
549
|
+
this.selectFileByIndex(newIndex);
|
|
550
|
+
// Keep selection visible - scroll down if needed
|
|
551
|
+
const row = getRowFromFileIndex(newIndex, files);
|
|
552
|
+
const visibleEnd = state.fileListScrollOffset + this.layout.dimensions.topPaneHeight - 1;
|
|
553
|
+
if (row >= visibleEnd) {
|
|
554
|
+
this.uiState.setFileListScrollOffset(state.fileListScrollOffset + (row - visibleEnd + 1));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
else if (state.currentPane === 'diff') {
|
|
558
|
+
this.uiState.setDiffScrollOffset(state.diffScrollOffset + 3);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
navigateHistoryUp() {
|
|
562
|
+
const state = this.uiState.state;
|
|
563
|
+
const newIndex = Math.max(0, state.historySelectedIndex - 1);
|
|
564
|
+
if (newIndex !== state.historySelectedIndex) {
|
|
565
|
+
this.uiState.setHistorySelectedIndex(newIndex);
|
|
566
|
+
// Keep selection visible
|
|
567
|
+
if (newIndex < state.historyScrollOffset) {
|
|
568
|
+
this.uiState.setHistoryScrollOffset(newIndex);
|
|
569
|
+
}
|
|
570
|
+
this.selectHistoryCommitByIndex(newIndex);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
navigateHistoryDown() {
|
|
574
|
+
const state = this.uiState.state;
|
|
575
|
+
const commits = this.gitManager?.historyState.commits ?? [];
|
|
576
|
+
const newIndex = Math.min(commits.length - 1, state.historySelectedIndex + 1);
|
|
577
|
+
if (newIndex !== state.historySelectedIndex) {
|
|
578
|
+
this.uiState.setHistorySelectedIndex(newIndex);
|
|
579
|
+
// Keep selection visible
|
|
580
|
+
const visibleEnd = state.historyScrollOffset + this.layout.dimensions.topPaneHeight - 1;
|
|
581
|
+
if (newIndex >= visibleEnd) {
|
|
582
|
+
this.uiState.setHistoryScrollOffset(state.historyScrollOffset + 1);
|
|
583
|
+
}
|
|
584
|
+
this.selectHistoryCommitByIndex(newIndex);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
selectHistoryCommitByIndex(index) {
|
|
588
|
+
const commits = this.gitManager?.historyState.commits ?? [];
|
|
589
|
+
const commit = getCommitAtIndex(commits, index);
|
|
590
|
+
if (commit) {
|
|
591
|
+
this.uiState.setDiffScrollOffset(0);
|
|
592
|
+
this.gitManager?.selectHistoryCommit(commit);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
// Compare navigation
|
|
596
|
+
compareSelection = null;
|
|
597
|
+
navigateCompareUp() {
|
|
598
|
+
const compareState = this.gitManager?.compareState;
|
|
599
|
+
const commits = compareState?.compareDiff?.commits ?? [];
|
|
600
|
+
const files = compareState?.compareDiff?.files ?? [];
|
|
601
|
+
if (commits.length === 0 && files.length === 0)
|
|
602
|
+
return;
|
|
603
|
+
const next = getNextCompareSelection(this.compareSelection, commits, files, 'up');
|
|
604
|
+
if (next &&
|
|
605
|
+
(next.type !== this.compareSelection?.type || next.index !== this.compareSelection?.index)) {
|
|
606
|
+
this.selectCompareItem(next);
|
|
607
|
+
// Keep selection visible - scroll up if needed
|
|
608
|
+
const state = this.uiState.state;
|
|
609
|
+
const row = getRowFromCompareSelection(next, commits, files);
|
|
610
|
+
if (row < state.compareScrollOffset) {
|
|
611
|
+
this.uiState.setCompareScrollOffset(row);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
navigateCompareDown() {
|
|
616
|
+
const compareState = this.gitManager?.compareState;
|
|
617
|
+
const commits = compareState?.compareDiff?.commits ?? [];
|
|
618
|
+
const files = compareState?.compareDiff?.files ?? [];
|
|
619
|
+
if (commits.length === 0 && files.length === 0)
|
|
620
|
+
return;
|
|
621
|
+
// Auto-select first item if nothing selected
|
|
622
|
+
if (!this.compareSelection) {
|
|
623
|
+
// Select first commit if available, otherwise first file
|
|
624
|
+
if (commits.length > 0) {
|
|
625
|
+
this.selectCompareItem({ type: 'commit', index: 0 });
|
|
626
|
+
}
|
|
627
|
+
else if (files.length > 0) {
|
|
628
|
+
this.selectCompareItem({ type: 'file', index: 0 });
|
|
629
|
+
}
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const next = getNextCompareSelection(this.compareSelection, commits, files, 'down');
|
|
633
|
+
if (next &&
|
|
634
|
+
(next.type !== this.compareSelection?.type || next.index !== this.compareSelection?.index)) {
|
|
635
|
+
this.selectCompareItem(next);
|
|
636
|
+
// Keep selection visible - scroll down if needed
|
|
637
|
+
const state = this.uiState.state;
|
|
638
|
+
const row = getRowFromCompareSelection(next, commits, files);
|
|
639
|
+
const visibleEnd = state.compareScrollOffset + this.layout.dimensions.topPaneHeight - 1;
|
|
640
|
+
if (row >= visibleEnd) {
|
|
641
|
+
this.uiState.setCompareScrollOffset(state.compareScrollOffset + (row - visibleEnd + 1));
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
selectCompareItem(selection) {
|
|
646
|
+
this.compareSelection = selection;
|
|
647
|
+
this.uiState.setDiffScrollOffset(0);
|
|
648
|
+
if (selection.type === 'commit') {
|
|
649
|
+
this.gitManager?.selectCompareCommit(selection.index);
|
|
481
650
|
}
|
|
482
651
|
else {
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
onUnstageAll: unstageAll,
|
|
503
|
-
onCommit: handleCommit,
|
|
504
|
-
onQuit: exit,
|
|
505
|
-
onRefresh: refresh,
|
|
506
|
-
onNavigateUp: handleNavigateUp,
|
|
507
|
-
onNavigateDown: handleNavigateDown,
|
|
508
|
-
onTogglePane: handleTogglePane,
|
|
509
|
-
onSwitchTab: handleSwitchTab,
|
|
510
|
-
onSelect: handleSelect,
|
|
511
|
-
onToggleIncludeUncommitted: toggleIncludeUncommitted,
|
|
512
|
-
onCycleBaseBranch: openBaseBranchPicker,
|
|
513
|
-
onOpenThemePicker: () => setActiveModal('theme'),
|
|
514
|
-
onShrinkTopPane: () => adjustSplitRatio(-SPLIT_RATIO_STEP),
|
|
515
|
-
onGrowTopPane: () => adjustSplitRatio(SPLIT_RATIO_STEP),
|
|
516
|
-
onOpenHotkeysModal: () => setActiveModal('hotkeys'),
|
|
517
|
-
onToggleMouse: toggleMouse,
|
|
518
|
-
onToggleFollow: () => setWatcherEnabled((prev) => !prev),
|
|
519
|
-
onToggleAutoTab: () => setAutoTabEnabled((prev) => !prev),
|
|
520
|
-
onToggleWrap: () => setWrapMode((prev) => !prev),
|
|
521
|
-
onToggleMiddleDots: bottomTab === 'explorer' ? () => setShowMiddleDots((prev) => !prev) : undefined,
|
|
522
|
-
onToggleHideHiddenFiles: bottomTab === 'explorer' ? () => setHideHiddenFiles((prev) => !prev) : undefined,
|
|
523
|
-
onToggleHideGitignored: bottomTab === 'explorer' ? () => setHideGitignored((prev) => !prev) : undefined,
|
|
524
|
-
onExplorerEnter: bottomTab === 'explorer' ? explorerEnterDirectory : undefined,
|
|
525
|
-
onExplorerBack: bottomTab === 'explorer' ? explorerGoUp : undefined,
|
|
526
|
-
}, currentPane, commitInputFocused || activeModal !== null || showBaseBranchPicker);
|
|
527
|
-
// Discard confirmation
|
|
528
|
-
useInput((input, key) => {
|
|
529
|
-
if (!pendingDiscard)
|
|
652
|
+
this.gitManager?.selectCompareFile(selection.index);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
// Explorer navigation
|
|
656
|
+
navigateExplorerUp() {
|
|
657
|
+
const state = this.uiState.state;
|
|
658
|
+
const rows = this.explorerManager?.state.displayRows ?? [];
|
|
659
|
+
if (rows.length === 0)
|
|
660
|
+
return;
|
|
661
|
+
const newScrollOffset = this.explorerManager?.navigateUp(state.explorerScrollOffset);
|
|
662
|
+
if (newScrollOffset !== null && newScrollOffset !== undefined) {
|
|
663
|
+
this.uiState.setExplorerScrollOffset(newScrollOffset);
|
|
664
|
+
}
|
|
665
|
+
this.uiState.setExplorerSelectedIndex(this.explorerManager?.state.selectedIndex ?? 0);
|
|
666
|
+
}
|
|
667
|
+
navigateExplorerDown() {
|
|
668
|
+
const state = this.uiState.state;
|
|
669
|
+
const rows = this.explorerManager?.state.displayRows ?? [];
|
|
670
|
+
if (rows.length === 0)
|
|
530
671
|
return;
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
672
|
+
const visibleHeight = this.layout.dimensions.topPaneHeight;
|
|
673
|
+
const newScrollOffset = this.explorerManager?.navigateDown(state.explorerScrollOffset, visibleHeight);
|
|
674
|
+
if (newScrollOffset !== null && newScrollOffset !== undefined) {
|
|
675
|
+
this.uiState.setExplorerScrollOffset(newScrollOffset);
|
|
676
|
+
}
|
|
677
|
+
this.uiState.setExplorerSelectedIndex(this.explorerManager?.state.selectedIndex ?? 0);
|
|
678
|
+
}
|
|
679
|
+
async enterExplorerDirectory() {
|
|
680
|
+
await this.explorerManager?.enterDirectory();
|
|
681
|
+
// Reset file content scroll when expanding/collapsing
|
|
682
|
+
this.uiState.setExplorerFileScrollOffset(0);
|
|
683
|
+
// Sync selected index from explorer manager (it maintains selection by path)
|
|
684
|
+
this.uiState.setExplorerSelectedIndex(this.explorerManager?.state.selectedIndex ?? 0);
|
|
685
|
+
}
|
|
686
|
+
async goExplorerUp() {
|
|
687
|
+
await this.explorerManager?.goUp();
|
|
688
|
+
// Reset file content scroll when collapsing
|
|
689
|
+
this.uiState.setExplorerFileScrollOffset(0);
|
|
690
|
+
// Sync selected index from explorer manager
|
|
691
|
+
this.uiState.setExplorerSelectedIndex(this.explorerManager?.state.selectedIndex ?? 0);
|
|
692
|
+
}
|
|
693
|
+
selectFileByIndex(index) {
|
|
694
|
+
const files = this.gitManager?.state.status?.files ?? [];
|
|
695
|
+
const file = getFileAtIndex(files, index);
|
|
696
|
+
if (file) {
|
|
697
|
+
// Reset diff scroll when changing files
|
|
698
|
+
this.uiState.setDiffScrollOffset(0);
|
|
699
|
+
this.gitManager?.selectFile(file);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Navigate to a file given its absolute path.
|
|
704
|
+
* Extracts the relative path and finds the file in the current file list.
|
|
705
|
+
*/
|
|
706
|
+
navigateToFile(absolutePath) {
|
|
707
|
+
if (!absolutePath || !this.repoPath)
|
|
708
|
+
return;
|
|
709
|
+
// Check if the path is within the current repo
|
|
710
|
+
const repoPrefix = this.repoPath.endsWith('/') ? this.repoPath : this.repoPath + '/';
|
|
711
|
+
if (!absolutePath.startsWith(repoPrefix))
|
|
712
|
+
return;
|
|
713
|
+
// Extract relative path
|
|
714
|
+
const relativePath = absolutePath.slice(repoPrefix.length);
|
|
715
|
+
if (!relativePath)
|
|
716
|
+
return;
|
|
717
|
+
// Find the file in the list
|
|
718
|
+
const files = this.gitManager?.state.status?.files ?? [];
|
|
719
|
+
const fileIndex = files.findIndex((f) => f.path === relativePath);
|
|
720
|
+
if (fileIndex >= 0) {
|
|
721
|
+
this.uiState.setSelectedIndex(fileIndex);
|
|
722
|
+
this.selectFileByIndex(fileIndex);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
// Git operations
|
|
726
|
+
async stageSelected() {
|
|
727
|
+
const files = this.gitManager?.state.status?.files ?? [];
|
|
728
|
+
const index = this.uiState.state.selectedIndex;
|
|
729
|
+
const selectedFile = getFileAtIndex(files, index);
|
|
730
|
+
if (selectedFile && !selectedFile.staged) {
|
|
731
|
+
this.pendingSelectionAnchor = getCategoryForIndex(files, index);
|
|
732
|
+
await this.gitManager?.stage(selectedFile);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
async unstageSelected() {
|
|
736
|
+
const files = this.gitManager?.state.status?.files ?? [];
|
|
737
|
+
const index = this.uiState.state.selectedIndex;
|
|
738
|
+
const selectedFile = getFileAtIndex(files, index);
|
|
739
|
+
if (selectedFile?.staged) {
|
|
740
|
+
this.pendingSelectionAnchor = getCategoryForIndex(files, index);
|
|
741
|
+
await this.gitManager?.unstage(selectedFile);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
async toggleSelected() {
|
|
745
|
+
const files = this.gitManager?.state.status?.files ?? [];
|
|
746
|
+
const index = this.uiState.state.selectedIndex;
|
|
747
|
+
const selectedFile = getFileAtIndex(files, index);
|
|
748
|
+
if (selectedFile) {
|
|
749
|
+
this.pendingSelectionAnchor = getCategoryForIndex(files, index);
|
|
750
|
+
if (selectedFile.staged) {
|
|
751
|
+
await this.gitManager?.unstage(selectedFile);
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
await this.gitManager?.stage(selectedFile);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
async stageAll() {
|
|
759
|
+
await this.gitManager?.stageAll();
|
|
760
|
+
}
|
|
761
|
+
async unstageAll() {
|
|
762
|
+
await this.gitManager?.unstageAll();
|
|
763
|
+
}
|
|
764
|
+
showDiscardConfirm(file) {
|
|
765
|
+
this.activeModal = new DiscardConfirm(this.screen, file.path, async () => {
|
|
766
|
+
this.activeModal = null;
|
|
767
|
+
await this.gitManager?.discard(file);
|
|
768
|
+
}, () => {
|
|
769
|
+
this.activeModal = null;
|
|
770
|
+
});
|
|
771
|
+
this.activeModal.focus();
|
|
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
|
+
}
|
|
793
|
+
async commit(message) {
|
|
794
|
+
await this.gitManager?.commit(message);
|
|
795
|
+
}
|
|
796
|
+
async refresh() {
|
|
797
|
+
await this.gitManager?.refresh();
|
|
798
|
+
}
|
|
799
|
+
toggleMouseMode() {
|
|
800
|
+
const willEnable = !this.uiState.state.mouseEnabled;
|
|
801
|
+
this.uiState.toggleMouse();
|
|
802
|
+
// Access program for terminal mouse control (not on screen's TS types)
|
|
803
|
+
const program = this.screen.program;
|
|
804
|
+
if (willEnable) {
|
|
805
|
+
program.enableMouse();
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
808
|
+
program.disableMouse();
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
toggleFollow() {
|
|
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
|
+
});
|
|
817
|
+
}
|
|
818
|
+
this.followMode.toggle();
|
|
819
|
+
this.render();
|
|
820
|
+
}
|
|
821
|
+
focusCommitInput() {
|
|
822
|
+
if (this.commitTextarea) {
|
|
823
|
+
this.commitTextarea.show();
|
|
824
|
+
this.commitTextarea.focus();
|
|
825
|
+
this.commitTextarea.setValue(this.commitFlowState.state.message);
|
|
826
|
+
this.commitFlowState.setInputFocused(true);
|
|
827
|
+
this.render();
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
unfocusCommitInput() {
|
|
831
|
+
if (this.commitTextarea) {
|
|
832
|
+
const value = this.commitTextarea.getValue() ?? '';
|
|
833
|
+
this.commitFlowState.setMessage(value);
|
|
834
|
+
this.commitTextarea.hide();
|
|
835
|
+
this.commitFlowState.setInputFocused(false);
|
|
836
|
+
this.screen.focusPush(this.layout.bottomPane);
|
|
837
|
+
this.render();
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
// Render methods
|
|
841
|
+
render() {
|
|
842
|
+
this.updateHeader();
|
|
843
|
+
this.updateTopPane();
|
|
844
|
+
this.updateBottomPane();
|
|
845
|
+
this.updateFooter();
|
|
846
|
+
this.screen.render();
|
|
847
|
+
}
|
|
848
|
+
updateHeader() {
|
|
849
|
+
const gitState = this.gitManager?.state;
|
|
850
|
+
const width = this.screen.width || 80;
|
|
851
|
+
const content = formatHeader(this.repoPath, gitState?.status?.branch ?? null, gitState?.isLoading ?? false, gitState?.error ?? null, width);
|
|
852
|
+
this.layout.headerBox.setContent(content);
|
|
853
|
+
}
|
|
854
|
+
updateTopPane() {
|
|
855
|
+
const state = this.uiState.state;
|
|
856
|
+
const width = this.screen.width || 80;
|
|
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);
|
|
858
|
+
this.layout.topPane.setContent(content);
|
|
859
|
+
}
|
|
860
|
+
updateBottomPane() {
|
|
861
|
+
const state = this.uiState.state;
|
|
862
|
+
const width = this.screen.width || 80;
|
|
863
|
+
const files = this.gitManager?.state.status?.files ?? [];
|
|
864
|
+
const stagedCount = files.filter((f) => f.staged).length;
|
|
865
|
+
// Update staged count for commit validation
|
|
866
|
+
this.commitFlowState.setStagedCount(stagedCount);
|
|
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();
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
this.commitTextarea.hide();
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
updateFooter() {
|
|
881
|
+
const state = this.uiState.state;
|
|
882
|
+
const width = this.screen.width || 80;
|
|
883
|
+
const content = formatFooter(state.bottomTab, state.mouseEnabled, state.autoTabEnabled, state.wrapMode, this.followMode?.isEnabled ?? false, this.explorerManager?.showOnlyChanges ?? false, width);
|
|
884
|
+
this.layout.footerBox.setContent(content);
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Exit the application cleanly.
|
|
888
|
+
*/
|
|
889
|
+
exit() {
|
|
890
|
+
// Clean up
|
|
891
|
+
if (this.gitManager) {
|
|
892
|
+
removeManagerForRepo(this.repoPath);
|
|
893
|
+
}
|
|
894
|
+
if (this.explorerManager) {
|
|
895
|
+
this.explorerManager.dispose();
|
|
896
|
+
}
|
|
897
|
+
if (this.followMode) {
|
|
898
|
+
this.followMode.stop();
|
|
534
899
|
}
|
|
535
|
-
|
|
536
|
-
|
|
900
|
+
if (this.commandServer) {
|
|
901
|
+
this.commandServer.stop();
|
|
537
902
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
903
|
+
// Destroy screen (this will clean up terminal)
|
|
904
|
+
this.screen.destroy();
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Start the application (returns when app exits).
|
|
908
|
+
*/
|
|
909
|
+
start() {
|
|
910
|
+
return new Promise((resolve) => {
|
|
911
|
+
this.screen.on('destroy', () => {
|
|
912
|
+
resolve();
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
}
|
|
541
916
|
}
|