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.
Files changed (62) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/bun.lock +72 -312
  3. package/dist/App.js +1136 -515
  4. package/dist/core/ExplorerStateManager.js +266 -0
  5. package/dist/core/FilePathWatcher.js +133 -0
  6. package/dist/core/GitStateManager.js +75 -16
  7. package/dist/git/ignoreUtils.js +30 -0
  8. package/dist/git/status.js +2 -34
  9. package/dist/index.js +67 -53
  10. package/dist/ipc/CommandClient.js +165 -0
  11. package/dist/ipc/CommandServer.js +152 -0
  12. package/dist/state/CommitFlowState.js +86 -0
  13. package/dist/state/UIState.js +182 -0
  14. package/dist/types/tabs.js +4 -0
  15. package/dist/ui/Layout.js +252 -0
  16. package/dist/ui/modals/BaseBranchPicker.js +110 -0
  17. package/dist/ui/modals/DiscardConfirm.js +77 -0
  18. package/dist/ui/modals/HotkeysModal.js +209 -0
  19. package/dist/ui/modals/ThemePicker.js +107 -0
  20. package/dist/ui/widgets/CommitPanel.js +58 -0
  21. package/dist/ui/widgets/CompareListView.js +216 -0
  22. package/dist/ui/widgets/DiffView.js +279 -0
  23. package/dist/ui/widgets/ExplorerContent.js +102 -0
  24. package/dist/ui/widgets/ExplorerView.js +95 -0
  25. package/dist/ui/widgets/FileList.js +185 -0
  26. package/dist/ui/widgets/Footer.js +46 -0
  27. package/dist/ui/widgets/Header.js +111 -0
  28. package/dist/ui/widgets/HistoryView.js +69 -0
  29. package/dist/utils/ansiToBlessed.js +125 -0
  30. package/dist/utils/displayRows.js +185 -6
  31. package/dist/utils/explorerDisplayRows.js +1 -1
  32. package/dist/utils/languageDetection.js +56 -0
  33. package/dist/utils/pathUtils.js +27 -0
  34. package/dist/utils/rowCalculations.js +37 -0
  35. package/dist/utils/wordDiff.js +50 -0
  36. package/package.json +11 -12
  37. package/dist/components/BaseBranchPicker.js +0 -60
  38. package/dist/components/BottomPane.js +0 -101
  39. package/dist/components/CommitPanel.js +0 -58
  40. package/dist/components/CompareListView.js +0 -110
  41. package/dist/components/ExplorerContentView.js +0 -80
  42. package/dist/components/ExplorerView.js +0 -37
  43. package/dist/components/FileList.js +0 -131
  44. package/dist/components/Footer.js +0 -6
  45. package/dist/components/Header.js +0 -107
  46. package/dist/components/HistoryView.js +0 -21
  47. package/dist/components/HotkeysModal.js +0 -108
  48. package/dist/components/Modal.js +0 -19
  49. package/dist/components/ScrollableList.js +0 -125
  50. package/dist/components/ThemePicker.js +0 -42
  51. package/dist/components/TopPane.js +0 -14
  52. package/dist/components/UnifiedDiffView.js +0 -115
  53. package/dist/hooks/useCommitFlow.js +0 -66
  54. package/dist/hooks/useCompareState.js +0 -123
  55. package/dist/hooks/useExplorerState.js +0 -248
  56. package/dist/hooks/useGit.js +0 -156
  57. package/dist/hooks/useHistoryState.js +0 -62
  58. package/dist/hooks/useKeymap.js +0 -167
  59. package/dist/hooks/useLayout.js +0 -154
  60. package/dist/hooks/useMouse.js +0 -87
  61. package/dist/hooks/useTerminalSize.js +0 -20
  62. 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
- }
@@ -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
- }
@@ -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
- }