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.
Files changed (74) hide show
  1. package/.github/workflows/release.yml +8 -0
  2. package/CHANGELOG.md +36 -0
  3. package/bun.lock +89 -306
  4. package/dist/App.js +895 -520
  5. package/dist/FollowMode.js +85 -0
  6. package/dist/KeyBindings.js +178 -0
  7. package/dist/MouseHandlers.js +156 -0
  8. package/dist/core/ExplorerStateManager.js +632 -0
  9. package/dist/core/FilePathWatcher.js +133 -0
  10. package/dist/core/GitStateManager.js +221 -86
  11. package/dist/git/diff.js +4 -0
  12. package/dist/git/ignoreUtils.js +30 -0
  13. package/dist/git/status.js +2 -34
  14. package/dist/index.js +68 -53
  15. package/dist/ipc/CommandClient.js +165 -0
  16. package/dist/ipc/CommandServer.js +152 -0
  17. package/dist/state/CommitFlowState.js +86 -0
  18. package/dist/state/UIState.js +195 -0
  19. package/dist/types/tabs.js +4 -0
  20. package/dist/ui/Layout.js +252 -0
  21. package/dist/ui/PaneRenderers.js +56 -0
  22. package/dist/ui/modals/BaseBranchPicker.js +110 -0
  23. package/dist/ui/modals/DiscardConfirm.js +77 -0
  24. package/dist/ui/modals/FileFinder.js +232 -0
  25. package/dist/ui/modals/HotkeysModal.js +209 -0
  26. package/dist/ui/modals/ThemePicker.js +107 -0
  27. package/dist/ui/widgets/CommitPanel.js +58 -0
  28. package/dist/ui/widgets/CompareListView.js +238 -0
  29. package/dist/ui/widgets/DiffView.js +281 -0
  30. package/dist/ui/widgets/ExplorerContent.js +89 -0
  31. package/dist/ui/widgets/ExplorerView.js +204 -0
  32. package/dist/ui/widgets/FileList.js +185 -0
  33. package/dist/ui/widgets/Footer.js +50 -0
  34. package/dist/ui/widgets/Header.js +68 -0
  35. package/dist/ui/widgets/HistoryView.js +69 -0
  36. package/dist/utils/displayRows.js +185 -6
  37. package/dist/utils/explorerDisplayRows.js +1 -1
  38. package/dist/utils/fileCategories.js +37 -0
  39. package/dist/utils/fileTree.js +148 -0
  40. package/dist/utils/languageDetection.js +56 -0
  41. package/dist/utils/pathUtils.js +27 -0
  42. package/dist/utils/wordDiff.js +50 -0
  43. package/eslint.metrics.js +16 -0
  44. package/metrics/.gitkeep +0 -0
  45. package/metrics/v0.2.1.json +268 -0
  46. package/package.json +14 -12
  47. package/dist/components/BaseBranchPicker.js +0 -60
  48. package/dist/components/BottomPane.js +0 -101
  49. package/dist/components/CommitPanel.js +0 -58
  50. package/dist/components/CompareListView.js +0 -110
  51. package/dist/components/ExplorerContentView.js +0 -80
  52. package/dist/components/ExplorerView.js +0 -37
  53. package/dist/components/FileList.js +0 -131
  54. package/dist/components/Footer.js +0 -6
  55. package/dist/components/Header.js +0 -107
  56. package/dist/components/HistoryView.js +0 -21
  57. package/dist/components/HotkeysModal.js +0 -108
  58. package/dist/components/Modal.js +0 -19
  59. package/dist/components/ScrollableList.js +0 -125
  60. package/dist/components/ThemePicker.js +0 -42
  61. package/dist/components/TopPane.js +0 -14
  62. package/dist/components/UnifiedDiffView.js +0 -115
  63. package/dist/hooks/useCommitFlow.js +0 -66
  64. package/dist/hooks/useCompareState.js +0 -123
  65. package/dist/hooks/useExplorerState.js +0 -248
  66. package/dist/hooks/useGit.js +0 -156
  67. package/dist/hooks/useHistoryState.js +0 -62
  68. package/dist/hooks/useKeymap.js +0 -167
  69. package/dist/hooks/useLayout.js +0 -154
  70. package/dist/hooks/useMouse.js +0 -87
  71. package/dist/hooks/useTerminalSize.js +0 -20
  72. package/dist/hooks/useWatcher.js +0 -137
  73. package/dist/utils/mouseCoordinates.js +0 -165
  74. package/dist/utils/rowCalculations.js +0 -209
@@ -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
- }
@@ -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
- }
@@ -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
- }