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
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
|
-
}
|
package/dist/hooks/useWatcher.js
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useRef } from 'react';
|
|
2
|
-
import * as fs from 'node:fs';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
|
-
import * as os from 'node:os';
|
|
5
|
-
import { watch } from 'chokidar';
|
|
6
|
-
import { ensureTargetDir } from '../config.js';
|
|
7
|
-
function expandPath(p) {
|
|
8
|
-
// Expand ~ to home directory
|
|
9
|
-
if (p.startsWith('~/')) {
|
|
10
|
-
return path.join(os.homedir(), p.slice(2));
|
|
11
|
-
}
|
|
12
|
-
if (p === '~') {
|
|
13
|
-
return os.homedir();
|
|
14
|
-
}
|
|
15
|
-
return p;
|
|
16
|
-
}
|
|
17
|
-
function getLastNonEmptyLine(content) {
|
|
18
|
-
// Support append-only files by reading only the last non-empty line
|
|
19
|
-
const lines = content.split('\n');
|
|
20
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
21
|
-
const line = lines[i].trim();
|
|
22
|
-
if (line)
|
|
23
|
-
return line;
|
|
24
|
-
}
|
|
25
|
-
return '';
|
|
26
|
-
}
|
|
27
|
-
export function useWatcher(initialEnabled, targetFile, debug = false) {
|
|
28
|
-
const [enabled, setEnabled] = useState(initialEnabled);
|
|
29
|
-
const [state, setState] = useState({
|
|
30
|
-
path: null,
|
|
31
|
-
lastUpdate: null,
|
|
32
|
-
rawContent: null,
|
|
33
|
-
sourceFile: initialEnabled ? targetFile : null,
|
|
34
|
-
enabled: initialEnabled,
|
|
35
|
-
});
|
|
36
|
-
const debounceTimer = useRef(null);
|
|
37
|
-
const lastReadPath = useRef(null);
|
|
38
|
-
// Update state when enabled changes
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
setState((prev) => ({
|
|
41
|
-
...prev,
|
|
42
|
-
enabled,
|
|
43
|
-
sourceFile: enabled ? targetFile : null,
|
|
44
|
-
}));
|
|
45
|
-
}, [enabled, targetFile]);
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
// If watcher is disabled, do nothing
|
|
48
|
-
if (!enabled) {
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
// Ensure the directory exists
|
|
52
|
-
ensureTargetDir(targetFile);
|
|
53
|
-
// Create the file if it doesn't exist
|
|
54
|
-
if (!fs.existsSync(targetFile)) {
|
|
55
|
-
fs.writeFileSync(targetFile, '');
|
|
56
|
-
}
|
|
57
|
-
// Read and set target path with debouncing
|
|
58
|
-
const readTarget = () => {
|
|
59
|
-
// Clear any pending debounce
|
|
60
|
-
if (debounceTimer.current) {
|
|
61
|
-
clearTimeout(debounceTimer.current);
|
|
62
|
-
}
|
|
63
|
-
debounceTimer.current = setTimeout(() => {
|
|
64
|
-
try {
|
|
65
|
-
const raw = fs.readFileSync(targetFile, 'utf-8');
|
|
66
|
-
const content = getLastNonEmptyLine(raw);
|
|
67
|
-
if (content && content !== lastReadPath.current) {
|
|
68
|
-
// Expand ~ and resolve to absolute path
|
|
69
|
-
const expanded = expandPath(content);
|
|
70
|
-
const resolved = path.isAbsolute(expanded) ? expanded : path.resolve(expanded);
|
|
71
|
-
const now = new Date();
|
|
72
|
-
if (debug) {
|
|
73
|
-
process.stderr.write(`[diffstalker ${now.toISOString()}] Path change detected\n`);
|
|
74
|
-
process.stderr.write(` Source file: ${targetFile}\n`);
|
|
75
|
-
process.stderr.write(` Raw content: "${content}"\n`);
|
|
76
|
-
process.stderr.write(` Previous: "${lastReadPath.current ?? '(none)'}"\n`);
|
|
77
|
-
process.stderr.write(` Resolved: "${resolved}"\n`);
|
|
78
|
-
}
|
|
79
|
-
lastReadPath.current = resolved;
|
|
80
|
-
setState({
|
|
81
|
-
path: resolved,
|
|
82
|
-
lastUpdate: now,
|
|
83
|
-
rawContent: content,
|
|
84
|
-
sourceFile: targetFile,
|
|
85
|
-
enabled: true,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
// Ignore read errors
|
|
91
|
-
}
|
|
92
|
-
}, 100);
|
|
93
|
-
};
|
|
94
|
-
// Read initial value immediately (no debounce for first read)
|
|
95
|
-
try {
|
|
96
|
-
const raw = fs.readFileSync(targetFile, 'utf-8');
|
|
97
|
-
const content = getLastNonEmptyLine(raw);
|
|
98
|
-
if (content) {
|
|
99
|
-
// Expand ~ and resolve to absolute path
|
|
100
|
-
const expanded = expandPath(content);
|
|
101
|
-
const resolved = path.isAbsolute(expanded) ? expanded : path.resolve(expanded);
|
|
102
|
-
const now = new Date();
|
|
103
|
-
if (debug) {
|
|
104
|
-
process.stderr.write(`[diffstalker ${now.toISOString()}] Initial path read\n`);
|
|
105
|
-
process.stderr.write(` Source file: ${targetFile}\n`);
|
|
106
|
-
process.stderr.write(` Raw content: "${content}"\n`);
|
|
107
|
-
process.stderr.write(` Resolved: "${resolved}"\n`);
|
|
108
|
-
}
|
|
109
|
-
lastReadPath.current = resolved;
|
|
110
|
-
setState({
|
|
111
|
-
path: resolved,
|
|
112
|
-
lastUpdate: now,
|
|
113
|
-
rawContent: content,
|
|
114
|
-
sourceFile: targetFile,
|
|
115
|
-
enabled: true,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
// Ignore read errors
|
|
121
|
-
}
|
|
122
|
-
// Watch for changes
|
|
123
|
-
const watcher = watch(targetFile, {
|
|
124
|
-
persistent: true,
|
|
125
|
-
ignoreInitial: true,
|
|
126
|
-
});
|
|
127
|
-
watcher.on('change', readTarget);
|
|
128
|
-
watcher.on('add', readTarget);
|
|
129
|
-
return () => {
|
|
130
|
-
if (debounceTimer.current) {
|
|
131
|
-
clearTimeout(debounceTimer.current);
|
|
132
|
-
}
|
|
133
|
-
watcher.close();
|
|
134
|
-
};
|
|
135
|
-
}, [enabled, targetFile, debug]);
|
|
136
|
-
return { state, setEnabled };
|
|
137
|
-
}
|