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/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
|
-
}
|
package/dist/hooks/useLayout.js
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
2
|
-
import { getRowForFileIndex, calculateScrollOffset, getFileListSectionCounts, getFileListTotalRows, } from '../utils/layoutCalculations.js';
|
|
3
|
-
import { calculatePaneBoundaries } from '../utils/mouseCoordinates.js';
|
|
4
|
-
import { getMaxScrollOffset } from '../components/ScrollableList.js';
|
|
5
|
-
// Layout constants (compact: single-line separators)
|
|
6
|
-
// Header (1) + sep (1) + sep (1) + sep (1) + footer (1) = 5 lines overhead
|
|
7
|
-
// Note: Header can be 2 lines when follow indicator causes branch to wrap
|
|
8
|
-
export const LAYOUT_OVERHEAD = 5;
|
|
9
|
-
// Default split ratios for different modes
|
|
10
|
-
const DEFAULT_SPLIT_RATIOS = {
|
|
11
|
-
diff: 0.4, // 40% top pane for staging area
|
|
12
|
-
commit: 0.4, // 40% top pane for staging area
|
|
13
|
-
history: 0.5, // 50% top pane for commit list (larger default)
|
|
14
|
-
compare: 0.5, // 50% top pane for compare list (larger default)
|
|
15
|
-
explorer: 0.4, // 40% top pane for file listing
|
|
16
|
-
};
|
|
17
|
-
// Step size for keyboard-based pane resizing (5% per keypress)
|
|
18
|
-
export const SPLIT_RATIO_STEP = 0.05;
|
|
19
|
-
export function useLayout(terminalHeight, terminalWidth, files, selectedIndex, diff, mode = 'diff', historySelectedIndex, initialSplitRatio, extraOverhead = 0) {
|
|
20
|
-
// Calculate content height (terminal minus overhead)
|
|
21
|
-
const contentHeight = terminalHeight - LAYOUT_OVERHEAD - extraOverhead;
|
|
22
|
-
// Custom split ratio state (null means use default for mode)
|
|
23
|
-
const [customSplitRatio, setCustomSplitRatio] = useState(initialSplitRatio ?? null);
|
|
24
|
-
// Get the effective split ratio
|
|
25
|
-
const effectiveSplitRatio = customSplitRatio ?? DEFAULT_SPLIT_RATIOS[mode];
|
|
26
|
-
// Calculate pane heights based on custom ratio or mode default
|
|
27
|
-
const { topPaneHeight, bottomPaneHeight } = useMemo(() => {
|
|
28
|
-
// Apply the split ratio directly
|
|
29
|
-
const minHeight = 5;
|
|
30
|
-
const maxHeight = contentHeight - minHeight; // Leave at least minHeight for bottom pane
|
|
31
|
-
const targetHeight = Math.floor(contentHeight * effectiveSplitRatio);
|
|
32
|
-
const topHeight = Math.max(minHeight, Math.min(targetHeight, maxHeight));
|
|
33
|
-
const bottomHeight = contentHeight - topHeight;
|
|
34
|
-
return { topPaneHeight: topHeight, bottomPaneHeight: bottomHeight };
|
|
35
|
-
}, [contentHeight, effectiveSplitRatio]);
|
|
36
|
-
// Setter for split ratio with bounds checking
|
|
37
|
-
const setSplitRatio = useCallback((ratio) => {
|
|
38
|
-
// Clamp ratio between 0.15 and 0.85 to ensure both panes remain usable
|
|
39
|
-
const clampedRatio = Math.max(0.15, Math.min(0.85, ratio));
|
|
40
|
-
setCustomSplitRatio(clampedRatio);
|
|
41
|
-
}, []);
|
|
42
|
-
// Adjust split ratio by delta (for keyboard-based resizing)
|
|
43
|
-
const adjustSplitRatio = useCallback((delta) => {
|
|
44
|
-
const currentRatio = customSplitRatio ?? DEFAULT_SPLIT_RATIOS[mode];
|
|
45
|
-
const newRatio = Math.max(0.15, Math.min(0.85, currentRatio + delta));
|
|
46
|
-
setCustomSplitRatio(newRatio);
|
|
47
|
-
}, [customSplitRatio, mode]);
|
|
48
|
-
// Expose current split ratio
|
|
49
|
-
const splitRatio = effectiveSplitRatio;
|
|
50
|
-
// Calculate pane boundaries for mouse handling
|
|
51
|
-
// extraOverhead = headerHeight - 1, so headerHeight = extraOverhead + 1
|
|
52
|
-
const headerHeight = extraOverhead + 1;
|
|
53
|
-
const paneBoundaries = useMemo(() => calculatePaneBoundaries(topPaneHeight, bottomPaneHeight, terminalHeight, headerHeight), [topPaneHeight, bottomPaneHeight, terminalHeight, headerHeight]);
|
|
54
|
-
// Scroll state
|
|
55
|
-
const [fileListScrollOffset, setFileListScrollOffset] = useState(0);
|
|
56
|
-
const [diffScrollOffset, setDiffScrollOffset] = useState(0);
|
|
57
|
-
const [historyScrollOffset, setHistoryScrollOffset] = useState(0);
|
|
58
|
-
const [compareScrollOffset, setCompareScrollOffset] = useState(0);
|
|
59
|
-
// Reset file list scroll when files change
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
setFileListScrollOffset(0);
|
|
62
|
-
}, [files.length]);
|
|
63
|
-
// Reset diff scroll when diff changes
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
setDiffScrollOffset(0);
|
|
66
|
-
}, [diff]);
|
|
67
|
-
// Auto-scroll file list to keep selected item visible (only when selection changes)
|
|
68
|
-
useEffect(() => {
|
|
69
|
-
const { modifiedCount, untrackedCount, stagedCount } = getFileListSectionCounts(files);
|
|
70
|
-
const selectedRow = getRowForFileIndex(selectedIndex, modifiedCount, untrackedCount, stagedCount);
|
|
71
|
-
const visibleHeight = topPaneHeight - 1;
|
|
72
|
-
setFileListScrollOffset((prev) => {
|
|
73
|
-
const newOffset = calculateScrollOffset(selectedRow, prev, visibleHeight);
|
|
74
|
-
return newOffset;
|
|
75
|
-
});
|
|
76
|
-
}, [selectedIndex, files, topPaneHeight]);
|
|
77
|
-
// Scroll helpers
|
|
78
|
-
const scrollDiff = useCallback((direction, amount = 3, totalRows = 0) => {
|
|
79
|
-
// Simple: totalRows = displayRows.length (every row = 1 terminal row)
|
|
80
|
-
// Match ScrollableList's available height calculation:
|
|
81
|
-
// - DiffView receives maxHeight = bottomPaneHeight - 1 (BottomPane header)
|
|
82
|
-
// - ScrollableList reserves 2 rows for indicators when content needs scrolling
|
|
83
|
-
const diffViewMaxHeight = bottomPaneHeight - 1;
|
|
84
|
-
const needsScrolling = totalRows > diffViewMaxHeight;
|
|
85
|
-
const availableAtMaxScroll = needsScrolling ? diffViewMaxHeight - 2 : diffViewMaxHeight;
|
|
86
|
-
const maxOffset = Math.max(0, totalRows - availableAtMaxScroll);
|
|
87
|
-
setDiffScrollOffset((prev) => {
|
|
88
|
-
if (direction === 'up') {
|
|
89
|
-
return Math.max(0, prev - amount);
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
return Math.min(maxOffset, prev + amount);
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
}, [bottomPaneHeight]);
|
|
96
|
-
const scrollFileList = useCallback((direction, amount = 3) => {
|
|
97
|
-
const totalRows = getFileListTotalRows(files);
|
|
98
|
-
const visibleRows = topPaneHeight - 1;
|
|
99
|
-
const maxScroll = Math.max(0, totalRows - visibleRows);
|
|
100
|
-
setFileListScrollOffset((prev) => {
|
|
101
|
-
if (direction === 'up') {
|
|
102
|
-
return Math.max(0, prev - amount);
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
return Math.min(maxScroll, prev + amount);
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
}, [files, topPaneHeight]);
|
|
109
|
-
const scrollHistory = useCallback((direction, totalItems = 0, amount = 3) => {
|
|
110
|
-
// History is in top pane, maxHeight is topPaneHeight - 1 for "COMMITS" header
|
|
111
|
-
const maxOffset = getMaxScrollOffset(totalItems, topPaneHeight - 1);
|
|
112
|
-
setHistoryScrollOffset((prev) => {
|
|
113
|
-
if (direction === 'up') {
|
|
114
|
-
return Math.max(0, prev - amount);
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
return Math.min(maxOffset, prev + amount);
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
}, [topPaneHeight]);
|
|
121
|
-
const scrollCompare = useCallback((direction, totalRows, amount = 3) => {
|
|
122
|
-
// Compare list is in top pane, maxHeight is topPaneHeight - 1 for "COMPARE" header
|
|
123
|
-
const maxOffset = getMaxScrollOffset(totalRows, topPaneHeight - 1);
|
|
124
|
-
setCompareScrollOffset((prev) => {
|
|
125
|
-
if (direction === 'up') {
|
|
126
|
-
return Math.max(0, prev - amount);
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
return Math.min(maxOffset, prev + amount);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
}, [topPaneHeight]);
|
|
133
|
-
return {
|
|
134
|
-
topPaneHeight,
|
|
135
|
-
bottomPaneHeight,
|
|
136
|
-
contentHeight,
|
|
137
|
-
paneBoundaries,
|
|
138
|
-
splitRatio,
|
|
139
|
-
setSplitRatio,
|
|
140
|
-
adjustSplitRatio,
|
|
141
|
-
fileListScrollOffset,
|
|
142
|
-
diffScrollOffset,
|
|
143
|
-
historyScrollOffset,
|
|
144
|
-
compareScrollOffset,
|
|
145
|
-
setFileListScrollOffset,
|
|
146
|
-
setDiffScrollOffset,
|
|
147
|
-
setHistoryScrollOffset,
|
|
148
|
-
setCompareScrollOffset,
|
|
149
|
-
scrollDiff,
|
|
150
|
-
scrollFileList,
|
|
151
|
-
scrollHistory,
|
|
152
|
-
scrollCompare,
|
|
153
|
-
};
|
|
154
|
-
}
|
package/dist/hooks/useMouse.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState, useCallback, useRef } from 'react';
|
|
2
|
-
import { useStdin } from 'ink';
|
|
3
|
-
export function useMouse(onEvent, disabled = false) {
|
|
4
|
-
const { stdin, setRawMode } = useStdin();
|
|
5
|
-
const [mouseEnabled, setMouseEnabled] = useState(true);
|
|
6
|
-
const onEventRef = useRef(onEvent);
|
|
7
|
-
useEffect(() => {
|
|
8
|
-
onEventRef.current = onEvent;
|
|
9
|
-
});
|
|
10
|
-
const toggleMouse = useCallback(() => {
|
|
11
|
-
setMouseEnabled((prev) => !prev);
|
|
12
|
-
}, []);
|
|
13
|
-
// Store mouseEnabled in ref for use in event handler
|
|
14
|
-
const mouseEnabledRef = useRef(mouseEnabled);
|
|
15
|
-
useEffect(() => {
|
|
16
|
-
mouseEnabledRef.current = mouseEnabled;
|
|
17
|
-
}, [mouseEnabled]);
|
|
18
|
-
// Handle mouse mode changes
|
|
19
|
-
// When disabled (text input focused) or in select mode, disable mouse tracking for text selection
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
if (!disabled && mouseEnabled) {
|
|
22
|
-
process.stdout.write('\x1b[?1000h');
|
|
23
|
-
process.stdout.write('\x1b[?1006h');
|
|
24
|
-
}
|
|
25
|
-
else {
|
|
26
|
-
process.stdout.write('\x1b[?1006l');
|
|
27
|
-
process.stdout.write('\x1b[?1000l');
|
|
28
|
-
}
|
|
29
|
-
}, [disabled, mouseEnabled]);
|
|
30
|
-
// Set up event listener (separate from mode toggle)
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
if (!stdin || !setRawMode)
|
|
33
|
-
return;
|
|
34
|
-
const handleData = (data) => {
|
|
35
|
-
const str = data.toString();
|
|
36
|
-
// Parse SGR mouse events: \x1b[<button;x;y[Mm]
|
|
37
|
-
// eslint-disable-next-line no-control-regex
|
|
38
|
-
const sgrMatch = str.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
|
|
39
|
-
if (sgrMatch) {
|
|
40
|
-
const buttonCode = parseInt(sgrMatch[1], 10);
|
|
41
|
-
const x = parseInt(sgrMatch[2], 10);
|
|
42
|
-
const y = parseInt(sgrMatch[3], 10);
|
|
43
|
-
const isRelease = sgrMatch[4] === 'm';
|
|
44
|
-
// Scroll wheel events (button codes 64-67) - only when in scroll mode
|
|
45
|
-
if (buttonCode >= 64 && buttonCode < 96) {
|
|
46
|
-
if (mouseEnabledRef.current) {
|
|
47
|
-
const type = buttonCode === 64 ? 'scroll-up' : 'scroll-down';
|
|
48
|
-
onEventRef.current({ x, y, type, button: 'none' });
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
// Click events (button codes 0-2) - only on release to avoid double-firing
|
|
52
|
-
else if (isRelease && buttonCode >= 0 && buttonCode < 3) {
|
|
53
|
-
const button = buttonCode === 0 ? 'left' : buttonCode === 1 ? 'middle' : 'right';
|
|
54
|
-
onEventRef.current({ x, y, type: 'click', button });
|
|
55
|
-
}
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
// Parse legacy mouse events
|
|
59
|
-
// eslint-disable-next-line no-control-regex
|
|
60
|
-
const legacyMatch = str.match(/\x1b\[M(.)(.)(.)/);
|
|
61
|
-
if (legacyMatch) {
|
|
62
|
-
const buttonCode = legacyMatch[1].charCodeAt(0) - 32;
|
|
63
|
-
const x = legacyMatch[2].charCodeAt(0) - 32;
|
|
64
|
-
const y = legacyMatch[3].charCodeAt(0) - 32;
|
|
65
|
-
if (buttonCode >= 64) {
|
|
66
|
-
if (mouseEnabledRef.current) {
|
|
67
|
-
const type = buttonCode === 64 ? 'scroll-up' : 'scroll-down';
|
|
68
|
-
onEventRef.current({ x, y, type, button: 'none' });
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// Legacy click events (button codes 0-2)
|
|
72
|
-
else if (buttonCode >= 0 && buttonCode < 3) {
|
|
73
|
-
const button = buttonCode === 0 ? 'left' : buttonCode === 1 ? 'middle' : 'right';
|
|
74
|
-
onEventRef.current({ x, y, type: 'click', button });
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
stdin.on('data', handleData);
|
|
79
|
-
return () => {
|
|
80
|
-
stdin.off('data', handleData);
|
|
81
|
-
// Disable mouse tracking on unmount
|
|
82
|
-
process.stdout.write('\x1b[?1006l');
|
|
83
|
-
process.stdout.write('\x1b[?1000l');
|
|
84
|
-
};
|
|
85
|
-
}, [stdin, setRawMode]);
|
|
86
|
-
return { mouseEnabled, toggleMouse };
|
|
87
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
2
|
-
export function useTerminalSize() {
|
|
3
|
-
const [size, setSize] = useState({
|
|
4
|
-
rows: process.stdout.rows ?? 24,
|
|
5
|
-
columns: process.stdout.columns ?? 80,
|
|
6
|
-
});
|
|
7
|
-
useEffect(() => {
|
|
8
|
-
const handleResize = () => {
|
|
9
|
-
setSize({
|
|
10
|
-
rows: process.stdout.rows ?? 24,
|
|
11
|
-
columns: process.stdout.columns ?? 80,
|
|
12
|
-
});
|
|
13
|
-
};
|
|
14
|
-
process.stdout.on('resize', handleResize);
|
|
15
|
-
return () => {
|
|
16
|
-
process.stdout.off('resize', handleResize);
|
|
17
|
-
};
|
|
18
|
-
}, []);
|
|
19
|
-
return size;
|
|
20
|
-
}
|