diffstalker 0.1.7 → 0.2.0
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/CHANGELOG.md +36 -0
- package/bun.lock +72 -312
- package/dist/App.js +1136 -515
- package/dist/core/ExplorerStateManager.js +266 -0
- package/dist/core/FilePathWatcher.js +133 -0
- package/dist/core/GitStateManager.js +75 -16
- package/dist/git/ignoreUtils.js +30 -0
- package/dist/git/status.js +2 -34
- package/dist/index.js +67 -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 +182 -0
- package/dist/types/tabs.js +4 -0
- package/dist/ui/Layout.js +252 -0
- package/dist/ui/modals/BaseBranchPicker.js +110 -0
- package/dist/ui/modals/DiscardConfirm.js +77 -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 +216 -0
- package/dist/ui/widgets/DiffView.js +279 -0
- package/dist/ui/widgets/ExplorerContent.js +102 -0
- package/dist/ui/widgets/ExplorerView.js +95 -0
- package/dist/ui/widgets/FileList.js +185 -0
- package/dist/ui/widgets/Footer.js +46 -0
- package/dist/ui/widgets/Header.js +111 -0
- package/dist/ui/widgets/HistoryView.js +69 -0
- package/dist/utils/ansiToBlessed.js +125 -0
- package/dist/utils/displayRows.js +185 -6
- package/dist/utils/explorerDisplayRows.js +1 -1
- package/dist/utils/languageDetection.js +56 -0
- package/dist/utils/pathUtils.js +27 -0
- package/dist/utils/rowCalculations.js +37 -0
- package/dist/utils/wordDiff.js +50 -0
- package/package.json +11 -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
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
2
|
-
import * as fs from 'node:fs';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
|
-
import { simpleGit } from 'simple-git';
|
|
5
|
-
const MAX_FILE_SIZE = 1024 * 1024; // 1MB
|
|
6
|
-
const WARN_FILE_SIZE = 100 * 1024; // 100KB
|
|
7
|
-
// Check if content appears to be binary
|
|
8
|
-
function isBinaryContent(buffer) {
|
|
9
|
-
// Check first 8KB for null bytes (common in binary files)
|
|
10
|
-
const checkLength = Math.min(buffer.length, 8192);
|
|
11
|
-
for (let i = 0; i < checkLength; i++) {
|
|
12
|
-
if (buffer[i] === 0)
|
|
13
|
-
return true;
|
|
14
|
-
}
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
// Get ignored files using git check-ignore
|
|
18
|
-
async function getIgnoredFiles(repoPath, files) {
|
|
19
|
-
if (files.length === 0)
|
|
20
|
-
return new Set();
|
|
21
|
-
const git = simpleGit(repoPath);
|
|
22
|
-
const ignoredFiles = new Set();
|
|
23
|
-
const batchSize = 100;
|
|
24
|
-
for (let i = 0; i < files.length; i += batchSize) {
|
|
25
|
-
const batch = files.slice(i, i + batchSize);
|
|
26
|
-
try {
|
|
27
|
-
const result = await git.raw(['check-ignore', ...batch]);
|
|
28
|
-
const ignored = result
|
|
29
|
-
.trim()
|
|
30
|
-
.split('\n')
|
|
31
|
-
.filter((f) => f.length > 0);
|
|
32
|
-
for (const f of ignored) {
|
|
33
|
-
ignoredFiles.add(f);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
// check-ignore exits with code 1 if no files are ignored
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return ignoredFiles;
|
|
41
|
-
}
|
|
42
|
-
export function useExplorerState({ repoPath, isActive, topPaneHeight, explorerScrollOffset, setExplorerScrollOffset, fileScrollOffset, setFileScrollOffset, hideHiddenFiles, hideGitignored, }) {
|
|
43
|
-
const [currentPath, setCurrentPath] = useState('');
|
|
44
|
-
const [items, setItems] = useState([]);
|
|
45
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
46
|
-
const [selectedFile, setSelectedFile] = useState(null);
|
|
47
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
48
|
-
const [error, setError] = useState(null);
|
|
49
|
-
// Load directory contents when path changes or tab becomes active
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
if (!isActive || !repoPath)
|
|
52
|
-
return;
|
|
53
|
-
const loadDirectory = async () => {
|
|
54
|
-
setIsLoading(true);
|
|
55
|
-
setError(null);
|
|
56
|
-
try {
|
|
57
|
-
const fullPath = path.join(repoPath, currentPath);
|
|
58
|
-
// Read directory
|
|
59
|
-
const entries = await fs.promises.readdir(fullPath, { withFileTypes: true });
|
|
60
|
-
// Build list of paths for gitignore check
|
|
61
|
-
const pathsToCheck = entries.map((e) => currentPath ? path.join(currentPath, e.name) : e.name);
|
|
62
|
-
// Get ignored files (only if we need to filter them)
|
|
63
|
-
const ignoredFiles = hideGitignored
|
|
64
|
-
? await getIgnoredFiles(repoPath, pathsToCheck)
|
|
65
|
-
: new Set();
|
|
66
|
-
// Filter and map entries
|
|
67
|
-
const explorerItems = entries
|
|
68
|
-
.filter((entry) => {
|
|
69
|
-
// Filter dot-prefixed hidden files (e.g., .env, .gitignore)
|
|
70
|
-
if (hideHiddenFiles && entry.name.startsWith('.')) {
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
// Filter gitignored files
|
|
74
|
-
if (hideGitignored) {
|
|
75
|
-
const relativePath = currentPath ? path.join(currentPath, entry.name) : entry.name;
|
|
76
|
-
if (ignoredFiles.has(relativePath)) {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return true;
|
|
81
|
-
})
|
|
82
|
-
.map((entry) => ({
|
|
83
|
-
name: entry.name,
|
|
84
|
-
path: currentPath ? path.join(currentPath, entry.name) : entry.name,
|
|
85
|
-
isDirectory: entry.isDirectory(),
|
|
86
|
-
}));
|
|
87
|
-
// Sort: directories first (alphabetical), then files (alphabetical)
|
|
88
|
-
explorerItems.sort((a, b) => {
|
|
89
|
-
if (a.isDirectory && !b.isDirectory)
|
|
90
|
-
return -1;
|
|
91
|
-
if (!a.isDirectory && b.isDirectory)
|
|
92
|
-
return 1;
|
|
93
|
-
return a.name.localeCompare(b.name);
|
|
94
|
-
});
|
|
95
|
-
// Add ".." at the beginning if not at root
|
|
96
|
-
if (currentPath) {
|
|
97
|
-
explorerItems.unshift({
|
|
98
|
-
name: '..',
|
|
99
|
-
path: path.dirname(currentPath) || '',
|
|
100
|
-
isDirectory: true,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
setItems(explorerItems);
|
|
104
|
-
setSelectedIndex(0);
|
|
105
|
-
setExplorerScrollOffset(0);
|
|
106
|
-
}
|
|
107
|
-
catch (err) {
|
|
108
|
-
setError(err instanceof Error ? err.message : 'Failed to read directory');
|
|
109
|
-
setItems([]);
|
|
110
|
-
}
|
|
111
|
-
finally {
|
|
112
|
-
setIsLoading(false);
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
loadDirectory();
|
|
116
|
-
}, [repoPath, currentPath, isActive, setExplorerScrollOffset, hideHiddenFiles, hideGitignored]);
|
|
117
|
-
// Load file content when selection changes to a file
|
|
118
|
-
useEffect(() => {
|
|
119
|
-
if (!isActive || !repoPath || items.length === 0) {
|
|
120
|
-
setSelectedFile(null);
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
const selected = items[selectedIndex];
|
|
124
|
-
if (!selected || selected.isDirectory) {
|
|
125
|
-
setSelectedFile(null);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
const loadFile = async () => {
|
|
129
|
-
try {
|
|
130
|
-
const fullPath = path.join(repoPath, selected.path);
|
|
131
|
-
const stats = await fs.promises.stat(fullPath);
|
|
132
|
-
// Check file size
|
|
133
|
-
if (stats.size > MAX_FILE_SIZE) {
|
|
134
|
-
setSelectedFile({
|
|
135
|
-
path: selected.path,
|
|
136
|
-
content: `File too large to display (${(stats.size / 1024 / 1024).toFixed(2)} MB).\nMaximum size: 1 MB`,
|
|
137
|
-
truncated: true,
|
|
138
|
-
});
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
const buffer = await fs.promises.readFile(fullPath);
|
|
142
|
-
// Check if binary
|
|
143
|
-
if (isBinaryContent(buffer)) {
|
|
144
|
-
setSelectedFile({
|
|
145
|
-
path: selected.path,
|
|
146
|
-
content: 'Binary file - cannot display',
|
|
147
|
-
});
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
let content = buffer.toString('utf-8');
|
|
151
|
-
let truncated = false;
|
|
152
|
-
// Warn about large files
|
|
153
|
-
if (stats.size > WARN_FILE_SIZE) {
|
|
154
|
-
const warning = `⚠ Large file (${(stats.size / 1024).toFixed(1)} KB)\n\n`;
|
|
155
|
-
content = warning + content;
|
|
156
|
-
}
|
|
157
|
-
// Truncate if needed (shouldn't happen given MAX_FILE_SIZE, but just in case)
|
|
158
|
-
const maxLines = 5000;
|
|
159
|
-
const lines = content.split('\n');
|
|
160
|
-
if (lines.length > maxLines) {
|
|
161
|
-
content =
|
|
162
|
-
lines.slice(0, maxLines).join('\n') +
|
|
163
|
-
`\n\n... (truncated, ${lines.length - maxLines} more lines)`;
|
|
164
|
-
truncated = true;
|
|
165
|
-
}
|
|
166
|
-
setSelectedFile({
|
|
167
|
-
path: selected.path,
|
|
168
|
-
content,
|
|
169
|
-
truncated,
|
|
170
|
-
});
|
|
171
|
-
setFileScrollOffset(0);
|
|
172
|
-
}
|
|
173
|
-
catch (err) {
|
|
174
|
-
setSelectedFile({
|
|
175
|
-
path: selected.path,
|
|
176
|
-
content: err instanceof Error ? `Error: ${err.message}` : 'Failed to read file',
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
loadFile();
|
|
181
|
-
}, [repoPath, items, selectedIndex, isActive, setFileScrollOffset]);
|
|
182
|
-
// Total rows for scroll calculations (item count)
|
|
183
|
-
const explorerTotalRows = useMemo(() => items.length, [items]);
|
|
184
|
-
// Navigation handlers
|
|
185
|
-
const navigateUp = useCallback(() => {
|
|
186
|
-
setSelectedIndex((prev) => {
|
|
187
|
-
const newIndex = Math.max(0, prev - 1);
|
|
188
|
-
// When scrolled, the top indicator takes a row, so first visible item is scrollOffset
|
|
189
|
-
// but we want to keep item visible above the indicator when scrolling up
|
|
190
|
-
if (newIndex < explorerScrollOffset) {
|
|
191
|
-
setExplorerScrollOffset(newIndex);
|
|
192
|
-
}
|
|
193
|
-
return newIndex;
|
|
194
|
-
});
|
|
195
|
-
}, [explorerScrollOffset, setExplorerScrollOffset]);
|
|
196
|
-
const navigateDown = useCallback(() => {
|
|
197
|
-
setSelectedIndex((prev) => {
|
|
198
|
-
const newIndex = Math.min(items.length - 1, prev + 1);
|
|
199
|
-
// Calculate visible area: topPaneHeight - 1 for "EXPLORER" header
|
|
200
|
-
// When content needs scrolling, ScrollableList reserves 2 more rows for indicators
|
|
201
|
-
const maxHeight = topPaneHeight - 1;
|
|
202
|
-
const needsScrolling = items.length > maxHeight;
|
|
203
|
-
const availableHeight = needsScrolling ? maxHeight - 2 : maxHeight;
|
|
204
|
-
const visibleEnd = explorerScrollOffset + availableHeight;
|
|
205
|
-
if (newIndex >= visibleEnd) {
|
|
206
|
-
setExplorerScrollOffset(explorerScrollOffset + 1);
|
|
207
|
-
}
|
|
208
|
-
return newIndex;
|
|
209
|
-
});
|
|
210
|
-
}, [items.length, explorerScrollOffset, topPaneHeight, setExplorerScrollOffset]);
|
|
211
|
-
const enterDirectory = useCallback(() => {
|
|
212
|
-
const selected = items[selectedIndex];
|
|
213
|
-
if (!selected)
|
|
214
|
-
return;
|
|
215
|
-
if (selected.isDirectory) {
|
|
216
|
-
if (selected.name === '..') {
|
|
217
|
-
// Go to parent directory
|
|
218
|
-
setCurrentPath(path.dirname(currentPath) || '');
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
// Enter the directory
|
|
222
|
-
setCurrentPath(selected.path);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
// If it's a file, do nothing (file content is already shown in bottom pane)
|
|
226
|
-
}, [items, selectedIndex, currentPath]);
|
|
227
|
-
const goUp = useCallback(() => {
|
|
228
|
-
if (currentPath) {
|
|
229
|
-
setCurrentPath(path.dirname(currentPath) || '');
|
|
230
|
-
}
|
|
231
|
-
}, [currentPath]);
|
|
232
|
-
return {
|
|
233
|
-
currentPath,
|
|
234
|
-
items,
|
|
235
|
-
selectedIndex,
|
|
236
|
-
setSelectedIndex,
|
|
237
|
-
selectedFile,
|
|
238
|
-
fileScrollOffset,
|
|
239
|
-
setFileScrollOffset,
|
|
240
|
-
navigateUp,
|
|
241
|
-
navigateDown,
|
|
242
|
-
enterDirectory,
|
|
243
|
-
goUp,
|
|
244
|
-
isLoading,
|
|
245
|
-
error,
|
|
246
|
-
explorerTotalRows,
|
|
247
|
-
};
|
|
248
|
-
}
|
package/dist/hooks/useGit.js
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
-
import { getManagerForRepo, removeManagerForRepo, } from '../core/GitStateManager.js';
|
|
3
|
-
/**
|
|
4
|
-
* React hook that wraps GitStateManager.
|
|
5
|
-
* Subscribes to state changes and provides React-friendly interface.
|
|
6
|
-
*/
|
|
7
|
-
export function useGit(repoPath) {
|
|
8
|
-
const [gitState, setGitState] = useState({
|
|
9
|
-
status: null,
|
|
10
|
-
diff: null,
|
|
11
|
-
stagedDiff: '',
|
|
12
|
-
selectedFile: null,
|
|
13
|
-
isLoading: false,
|
|
14
|
-
error: null,
|
|
15
|
-
});
|
|
16
|
-
const [compareState, setCompareState] = useState({
|
|
17
|
-
compareDiff: null,
|
|
18
|
-
compareBaseBranch: null,
|
|
19
|
-
compareLoading: false,
|
|
20
|
-
compareError: null,
|
|
21
|
-
});
|
|
22
|
-
const [historyState, setHistoryState] = useState({
|
|
23
|
-
selectedCommit: null,
|
|
24
|
-
commitDiff: null,
|
|
25
|
-
});
|
|
26
|
-
const [compareSelectionState, setCompareSelectionState] = useState({
|
|
27
|
-
type: null,
|
|
28
|
-
index: 0,
|
|
29
|
-
diff: null,
|
|
30
|
-
});
|
|
31
|
-
const managerRef = useRef(null);
|
|
32
|
-
// Setup manager and subscribe to events
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
if (!repoPath) {
|
|
35
|
-
setGitState({
|
|
36
|
-
status: null,
|
|
37
|
-
diff: null,
|
|
38
|
-
stagedDiff: '',
|
|
39
|
-
selectedFile: null,
|
|
40
|
-
isLoading: false,
|
|
41
|
-
error: null,
|
|
42
|
-
});
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
const manager = getManagerForRepo(repoPath);
|
|
46
|
-
managerRef.current = manager;
|
|
47
|
-
// Subscribe to state changes
|
|
48
|
-
const handleStateChange = (state) => {
|
|
49
|
-
setGitState(state);
|
|
50
|
-
};
|
|
51
|
-
const handleCompareStateChange = (state) => {
|
|
52
|
-
setCompareState(state);
|
|
53
|
-
};
|
|
54
|
-
const handleHistoryStateChange = (state) => {
|
|
55
|
-
setHistoryState(state);
|
|
56
|
-
};
|
|
57
|
-
const handleCompareSelectionChange = (state) => {
|
|
58
|
-
setCompareSelectionState(state);
|
|
59
|
-
};
|
|
60
|
-
manager.on('state-change', handleStateChange);
|
|
61
|
-
manager.on('compare-state-change', handleCompareStateChange);
|
|
62
|
-
manager.on('history-state-change', handleHistoryStateChange);
|
|
63
|
-
manager.on('compare-selection-change', handleCompareSelectionChange);
|
|
64
|
-
// Start watching and do initial refresh
|
|
65
|
-
manager.startWatching();
|
|
66
|
-
manager.refresh();
|
|
67
|
-
return () => {
|
|
68
|
-
manager.off('state-change', handleStateChange);
|
|
69
|
-
manager.off('compare-state-change', handleCompareStateChange);
|
|
70
|
-
manager.off('history-state-change', handleHistoryStateChange);
|
|
71
|
-
manager.off('compare-selection-change', handleCompareSelectionChange);
|
|
72
|
-
removeManagerForRepo(repoPath);
|
|
73
|
-
managerRef.current = null;
|
|
74
|
-
};
|
|
75
|
-
}, [repoPath]);
|
|
76
|
-
// Wrapped methods that delegate to manager
|
|
77
|
-
const selectFile = useCallback((file) => {
|
|
78
|
-
managerRef.current?.selectFile(file);
|
|
79
|
-
}, []);
|
|
80
|
-
const stage = useCallback(async (file) => {
|
|
81
|
-
await managerRef.current?.stage(file);
|
|
82
|
-
}, []);
|
|
83
|
-
const unstage = useCallback(async (file) => {
|
|
84
|
-
await managerRef.current?.unstage(file);
|
|
85
|
-
}, []);
|
|
86
|
-
const discard = useCallback(async (file) => {
|
|
87
|
-
await managerRef.current?.discard(file);
|
|
88
|
-
}, []);
|
|
89
|
-
const stageAll = useCallback(async () => {
|
|
90
|
-
await managerRef.current?.stageAll();
|
|
91
|
-
}, []);
|
|
92
|
-
const unstageAll = useCallback(async () => {
|
|
93
|
-
await managerRef.current?.unstageAll();
|
|
94
|
-
}, []);
|
|
95
|
-
const commit = useCallback(async (message, amend = false) => {
|
|
96
|
-
await managerRef.current?.commit(message, amend);
|
|
97
|
-
}, []);
|
|
98
|
-
const refresh = useCallback(async () => {
|
|
99
|
-
await managerRef.current?.refresh();
|
|
100
|
-
}, []);
|
|
101
|
-
const getHeadCommitMessage = useCallback(async () => {
|
|
102
|
-
return managerRef.current?.getHeadCommitMessage() ?? '';
|
|
103
|
-
}, []);
|
|
104
|
-
const refreshCompareDiff = useCallback(async (includeUncommitted = false) => {
|
|
105
|
-
await managerRef.current?.refreshCompareDiff(includeUncommitted);
|
|
106
|
-
}, []);
|
|
107
|
-
const getCandidateBaseBranches = useCallback(async () => {
|
|
108
|
-
return managerRef.current?.getCandidateBaseBranches() ?? [];
|
|
109
|
-
}, []);
|
|
110
|
-
const setCompareBaseBranch = useCallback(async (branch, includeUncommitted = false) => {
|
|
111
|
-
await managerRef.current?.setCompareBaseBranch(branch, includeUncommitted);
|
|
112
|
-
}, []);
|
|
113
|
-
const selectHistoryCommit = useCallback(async (commit) => {
|
|
114
|
-
await managerRef.current?.selectHistoryCommit(commit);
|
|
115
|
-
}, []);
|
|
116
|
-
const selectCompareCommit = useCallback(async (index) => {
|
|
117
|
-
await managerRef.current?.selectCompareCommit(index);
|
|
118
|
-
}, []);
|
|
119
|
-
const selectCompareFile = useCallback((index) => {
|
|
120
|
-
managerRef.current?.selectCompareFile(index);
|
|
121
|
-
}, []);
|
|
122
|
-
return {
|
|
123
|
-
status: gitState.status,
|
|
124
|
-
diff: gitState.diff,
|
|
125
|
-
stagedDiff: gitState.stagedDiff,
|
|
126
|
-
selectedFile: gitState.selectedFile,
|
|
127
|
-
isLoading: gitState.isLoading,
|
|
128
|
-
error: gitState.error,
|
|
129
|
-
selectFile,
|
|
130
|
-
stage,
|
|
131
|
-
unstage,
|
|
132
|
-
discard,
|
|
133
|
-
stageAll,
|
|
134
|
-
unstageAll,
|
|
135
|
-
commit,
|
|
136
|
-
refresh,
|
|
137
|
-
getHeadCommitMessage,
|
|
138
|
-
compareDiff: compareState.compareDiff,
|
|
139
|
-
compareBaseBranch: compareState.compareBaseBranch,
|
|
140
|
-
compareLoading: compareState.compareLoading,
|
|
141
|
-
compareError: compareState.compareError,
|
|
142
|
-
refreshCompareDiff,
|
|
143
|
-
getCandidateBaseBranches,
|
|
144
|
-
setCompareBaseBranch,
|
|
145
|
-
// History state
|
|
146
|
-
historySelectedCommit: historyState.selectedCommit,
|
|
147
|
-
historyCommitDiff: historyState.commitDiff,
|
|
148
|
-
selectHistoryCommit,
|
|
149
|
-
// Compare selection state
|
|
150
|
-
compareSelectionType: compareSelectionState.type,
|
|
151
|
-
compareSelectionIndex: compareSelectionState.index,
|
|
152
|
-
compareSelectionDiff: compareSelectionState.diff,
|
|
153
|
-
selectCompareCommit,
|
|
154
|
-
selectCompareFile,
|
|
155
|
-
};
|
|
156
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
2
|
-
import { getCommitHistory } from '../git/status.js';
|
|
3
|
-
import { buildHistoryDisplayRows, getDisplayRowsLineNumWidth, getWrappedRowCount, } from '../utils/displayRows.js';
|
|
4
|
-
export function useHistoryState({ repoPath, isActive, selectHistoryCommit, historyCommitDiff, historySelectedCommit, topPaneHeight, historyScrollOffset, setHistoryScrollOffset, setDiffScrollOffset, status, wrapMode, terminalWidth, }) {
|
|
5
|
-
const [commits, setCommits] = useState([]);
|
|
6
|
-
const [historySelectedIndex, setHistorySelectedIndex] = useState(0);
|
|
7
|
-
// Fetch commit history when tab becomes active
|
|
8
|
-
useEffect(() => {
|
|
9
|
-
if (repoPath && isActive) {
|
|
10
|
-
getCommitHistory(repoPath, 100).then(setCommits);
|
|
11
|
-
}
|
|
12
|
-
}, [repoPath, isActive, status]);
|
|
13
|
-
// Update selected history commit when index changes
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
if (isActive && commits.length > 0) {
|
|
16
|
-
const commit = commits[historySelectedIndex];
|
|
17
|
-
if (commit) {
|
|
18
|
-
selectHistoryCommit(commit);
|
|
19
|
-
setDiffScrollOffset(0);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}, [isActive, commits, historySelectedIndex, selectHistoryCommit, setDiffScrollOffset]);
|
|
23
|
-
// Calculate history diff total rows for scrolling
|
|
24
|
-
// When wrap mode is enabled, account for wrapped lines
|
|
25
|
-
const historyDiffTotalRows = useMemo(() => {
|
|
26
|
-
const displayRows = buildHistoryDisplayRows(historySelectedCommit, historyCommitDiff);
|
|
27
|
-
if (!wrapMode)
|
|
28
|
-
return displayRows.length;
|
|
29
|
-
const lineNumWidth = getDisplayRowsLineNumWidth(displayRows);
|
|
30
|
-
const contentWidth = terminalWidth - lineNumWidth - 5;
|
|
31
|
-
return getWrappedRowCount(displayRows, contentWidth, true);
|
|
32
|
-
}, [historySelectedCommit, historyCommitDiff, wrapMode, terminalWidth]);
|
|
33
|
-
// Calculate total commits for scroll limits (1 commit = 1 row)
|
|
34
|
-
const historyTotalRows = useMemo(() => commits.length, [commits]);
|
|
35
|
-
// Navigation handlers
|
|
36
|
-
const navigateHistoryUp = useCallback(() => {
|
|
37
|
-
setHistorySelectedIndex((prev) => {
|
|
38
|
-
const newIndex = Math.max(0, prev - 1);
|
|
39
|
-
if (newIndex < historyScrollOffset)
|
|
40
|
-
setHistoryScrollOffset(newIndex);
|
|
41
|
-
return newIndex;
|
|
42
|
-
});
|
|
43
|
-
}, [historyScrollOffset, setHistoryScrollOffset]);
|
|
44
|
-
const navigateHistoryDown = useCallback(() => {
|
|
45
|
-
setHistorySelectedIndex((prev) => {
|
|
46
|
-
const newIndex = Math.min(commits.length - 1, prev + 1);
|
|
47
|
-
const visibleEnd = historyScrollOffset + topPaneHeight - 2;
|
|
48
|
-
if (newIndex >= visibleEnd)
|
|
49
|
-
setHistoryScrollOffset(historyScrollOffset + 1);
|
|
50
|
-
return newIndex;
|
|
51
|
-
});
|
|
52
|
-
}, [commits.length, historyScrollOffset, topPaneHeight, setHistoryScrollOffset]);
|
|
53
|
-
return {
|
|
54
|
-
commits,
|
|
55
|
-
historySelectedIndex,
|
|
56
|
-
setHistorySelectedIndex,
|
|
57
|
-
historyDiffTotalRows,
|
|
58
|
-
navigateHistoryUp,
|
|
59
|
-
navigateHistoryDown,
|
|
60
|
-
historyTotalRows,
|
|
61
|
-
};
|
|
62
|
-
}
|
package/dist/hooks/useKeymap.js
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { useInput } from 'ink';
|
|
2
|
-
export function useKeymap(actions, currentPane, isCommitInputActive) {
|
|
3
|
-
useInput((input, key) => {
|
|
4
|
-
// Don't handle keys when commit input is active - let CommitPanel handle them
|
|
5
|
-
if (isCommitInputActive) {
|
|
6
|
-
return;
|
|
7
|
-
}
|
|
8
|
-
// Quit: Ctrl+C or q
|
|
9
|
-
if (key.ctrl && input === 'c') {
|
|
10
|
-
actions.onQuit();
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
if (input === 'q') {
|
|
14
|
-
actions.onQuit();
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
// Navigation (these can stay simple - j/k or arrows)
|
|
18
|
-
if (input === 'j' || key.downArrow) {
|
|
19
|
-
actions.onNavigateDown();
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
if (input === 'k' || key.upArrow) {
|
|
23
|
-
actions.onNavigateUp();
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
// Pane switching: Tab
|
|
27
|
-
if (key.tab) {
|
|
28
|
-
actions.onTogglePane();
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
// Tab switching: 1/2/3
|
|
32
|
-
if (input === '1') {
|
|
33
|
-
actions.onSwitchTab('diff');
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
if (input === '2') {
|
|
37
|
-
actions.onSwitchTab('commit');
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
if (input === '3') {
|
|
41
|
-
actions.onSwitchTab('history');
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
if (input === '4') {
|
|
45
|
-
actions.onSwitchTab('compare');
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
if (input === '5') {
|
|
49
|
-
actions.onSwitchTab('explorer');
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
// Toggle include uncommitted in compare view: u
|
|
53
|
-
if (input === 'u' && actions.onToggleIncludeUncommitted) {
|
|
54
|
-
actions.onToggleIncludeUncommitted();
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
// Cycle base branch in compare view: b
|
|
58
|
-
if (input === 'b' && actions.onCycleBaseBranch) {
|
|
59
|
-
actions.onCycleBaseBranch();
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
// Open theme picker: t
|
|
63
|
-
if (input === 't' && actions.onOpenThemePicker) {
|
|
64
|
-
actions.onOpenThemePicker();
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
// Open hotkeys modal: ?
|
|
68
|
-
if (input === '?' && actions.onOpenHotkeysModal) {
|
|
69
|
-
actions.onOpenHotkeysModal();
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
// Shrink top pane: [
|
|
73
|
-
if (input === '[' && actions.onShrinkTopPane) {
|
|
74
|
-
actions.onShrinkTopPane();
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
// Grow top pane: ]
|
|
78
|
-
if (input === ']' && actions.onGrowTopPane) {
|
|
79
|
-
actions.onGrowTopPane();
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
// Toggle mouse mode: m
|
|
83
|
-
if (input === 'm' && actions.onToggleMouse) {
|
|
84
|
-
actions.onToggleMouse();
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
// Toggle follow mode: f
|
|
88
|
-
if (input === 'f' && actions.onToggleFollow) {
|
|
89
|
-
actions.onToggleFollow();
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
// Toggle auto-tab mode: a
|
|
93
|
-
if (input === 'a' && actions.onToggleAutoTab) {
|
|
94
|
-
actions.onToggleAutoTab();
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
// Toggle wrap mode: w
|
|
98
|
-
if (input === 'w' && actions.onToggleWrap) {
|
|
99
|
-
actions.onToggleWrap();
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
// Toggle middle-dots (indentation visualization): .
|
|
103
|
-
if (input === '.' && actions.onToggleMiddleDots) {
|
|
104
|
-
actions.onToggleMiddleDots();
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
// Toggle hide hidden files: Ctrl+H
|
|
108
|
-
if (key.ctrl && input === 'h' && actions.onToggleHideHiddenFiles) {
|
|
109
|
-
actions.onToggleHideHiddenFiles();
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
// Toggle hide gitignored files: Ctrl+G
|
|
113
|
-
if (key.ctrl && input === 'g' && actions.onToggleHideGitignored) {
|
|
114
|
-
actions.onToggleHideGitignored();
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
// Stage: Ctrl+S or Enter/Space on file
|
|
118
|
-
if (key.ctrl && input === 's') {
|
|
119
|
-
actions.onStage();
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
// Unstage: Ctrl+U
|
|
123
|
-
if (key.ctrl && input === 'u') {
|
|
124
|
-
actions.onUnstage();
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
// Stage all: Ctrl+A
|
|
128
|
-
if (key.ctrl && input === 'a') {
|
|
129
|
-
actions.onStageAll();
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
// Unstage all: Ctrl+Shift+A (detected as Ctrl+A with shift, but terminal might not support)
|
|
133
|
-
// Use Ctrl+Z as alternative for unstage all
|
|
134
|
-
if (key.ctrl && input === 'z') {
|
|
135
|
-
actions.onUnstageAll();
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
// Commit: Ctrl+Enter (c as fallback since Ctrl+Enter is hard in terminals)
|
|
139
|
-
if (input === 'c') {
|
|
140
|
-
actions.onCommit();
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
// Refresh: Ctrl+R or r
|
|
144
|
-
if (key.ctrl && input === 'r') {
|
|
145
|
-
actions.onRefresh();
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
if (input === 'r') {
|
|
149
|
-
actions.onRefresh();
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
// Explorer: Enter to enter directory, Backspace/h to go up
|
|
153
|
-
if (actions.onExplorerEnter && key.return) {
|
|
154
|
-
actions.onExplorerEnter();
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
if (actions.onExplorerBack && (key.backspace || key.delete || input === 'h')) {
|
|
158
|
-
actions.onExplorerBack();
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
// Enter/Space to toggle stage/unstage for selected file
|
|
162
|
-
if (key.return || input === ' ') {
|
|
163
|
-
actions.onSelect();
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
}
|