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,101 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo } from 'react';
3
- import { Box, Text } from 'ink';
4
- import { UnifiedDiffView } from './UnifiedDiffView.js';
5
- import { CommitPanel } from './CommitPanel.js';
6
- import { ExplorerContentView } from './ExplorerContentView.js';
7
- import { shortenPath } from '../utils/formatPath.js';
8
- import { buildDiffDisplayRows, buildHistoryDisplayRows, buildCompareDisplayRows, getDisplayRowsLineNumWidth, wrapDisplayRows, } from '../utils/displayRows.js';
9
- export function BottomPane({ bottomTab, currentPane, terminalWidth, bottomPaneHeight, diffScrollOffset, currentTheme, diff, selectedFile, stagedCount, onCommit, onCommitCancel, getHeadCommitMessage, onCommitInputFocusChange, historySelectedCommit, historyCommitDiff, compareDiff, compareLoading, compareError, compareListSelection, compareSelectionDiff, wrapMode, explorerSelectedFile = null, explorerFileScrollOffset = 0, showMiddleDots = false, }) {
10
- const isDiffFocused = currentPane !== 'files' &&
11
- currentPane !== 'history' &&
12
- currentPane !== 'compare' &&
13
- currentPane !== 'explorer';
14
- // Build display rows based on current tab
15
- const displayRows = useMemo(() => {
16
- if (bottomTab === 'diff') {
17
- return buildDiffDisplayRows(diff);
18
- }
19
- if (bottomTab === 'history') {
20
- return buildHistoryDisplayRows(historySelectedCommit, historyCommitDiff);
21
- }
22
- if (bottomTab === 'compare') {
23
- // If a specific commit is selected, show that commit's diff
24
- if (compareListSelection?.type === 'commit' && compareSelectionDiff) {
25
- return buildDiffDisplayRows(compareSelectionDiff);
26
- }
27
- // Otherwise show combined compare diff
28
- return buildCompareDisplayRows(compareDiff);
29
- }
30
- return [];
31
- }, [
32
- bottomTab,
33
- diff,
34
- historySelectedCommit,
35
- historyCommitDiff,
36
- compareListSelection,
37
- compareSelectionDiff,
38
- compareDiff,
39
- ]);
40
- // Wrap display rows if wrap mode is enabled
41
- const wrappedRows = useMemo(() => {
42
- if (!wrapMode || displayRows.length === 0)
43
- return displayRows;
44
- // Calculate content width: width - paddingX(1) - lineNum - space(1) - symbol(1) - space(1) - paddingX(1)
45
- const lineNumWidth = getDisplayRowsLineNumWidth(displayRows);
46
- const contentWidth = terminalWidth - lineNumWidth - 5;
47
- return wrapDisplayRows(displayRows, contentWidth, wrapMode);
48
- }, [displayRows, terminalWidth, wrapMode]);
49
- // Build header right-side content
50
- const renderHeaderRight = () => {
51
- if (selectedFile && bottomTab === 'diff') {
52
- return _jsx(Text, { dimColor: true, children: shortenPath(selectedFile.path, terminalWidth - 10) });
53
- }
54
- if (bottomTab === 'history' && historySelectedCommit) {
55
- return (_jsxs(Text, { dimColor: true, children: [historySelectedCommit.shortHash, " - ", historySelectedCommit.message.slice(0, 50)] }));
56
- }
57
- if (bottomTab === 'compare' && compareListSelection) {
58
- if (compareListSelection.type === 'commit') {
59
- const commit = compareDiff?.commits[compareListSelection.index];
60
- return (_jsxs(Text, { dimColor: true, children: [commit?.shortHash ?? '', " - ", commit?.message.slice(0, 40) ?? ''] }));
61
- }
62
- else {
63
- const path = compareDiff?.files[compareListSelection.index]?.path ?? '';
64
- return _jsx(Text, { dimColor: true, children: shortenPath(path, terminalWidth - 10) });
65
- }
66
- }
67
- if (bottomTab === 'explorer' && explorerSelectedFile) {
68
- return _jsx(Text, { dimColor: true, children: shortenPath(explorerSelectedFile.path, terminalWidth - 10) });
69
- }
70
- return null;
71
- };
72
- // Render content based on tab
73
- const renderContent = () => {
74
- // Commit tab is special - not a diff view
75
- if (bottomTab === 'commit') {
76
- return (_jsx(CommitPanel, { isActive: currentPane === 'commit', stagedCount: stagedCount, onCommit: onCommit, onCancel: onCommitCancel, getHeadMessage: getHeadCommitMessage, onInputFocusChange: onCommitInputFocusChange }));
77
- }
78
- // Compare tab loading/error states
79
- if (bottomTab === 'compare') {
80
- if (compareLoading) {
81
- return _jsx(Text, { dimColor: true, children: "Loading compare diff..." });
82
- }
83
- if (compareError) {
84
- return _jsx(Text, { color: "red", children: compareError });
85
- }
86
- if (!compareDiff) {
87
- return _jsx(Text, { dimColor: true, children: "No base branch found (no origin/main or origin/master)" });
88
- }
89
- if (compareDiff.files.length === 0) {
90
- return _jsxs(Text, { dimColor: true, children: ["No changes compared to ", compareDiff.baseBranch] });
91
- }
92
- }
93
- // All diff views use UnifiedDiffView
94
- return (_jsx(UnifiedDiffView, { rows: wrappedRows, maxHeight: bottomPaneHeight - 1, scrollOffset: diffScrollOffset, theme: currentTheme, width: terminalWidth, wrapMode: wrapMode }));
95
- };
96
- // Explorer tab content
97
- if (bottomTab === 'explorer') {
98
- return (_jsxs(Box, { flexDirection: "column", height: bottomPaneHeight, width: terminalWidth, overflowY: "hidden", children: [_jsxs(Box, { width: terminalWidth, children: [_jsx(Text, { bold: true, color: isDiffFocused ? 'cyan' : undefined, children: "FILE" }), _jsx(Box, { flexGrow: 1, justifyContent: "flex-end", children: renderHeaderRight() })] }), _jsx(ExplorerContentView, { filePath: explorerSelectedFile?.path ?? null, content: explorerSelectedFile?.content ?? null, maxHeight: bottomPaneHeight - 1, scrollOffset: explorerFileScrollOffset, truncated: explorerSelectedFile?.truncated, wrapMode: wrapMode, width: terminalWidth, showMiddleDots: showMiddleDots })] }));
99
- }
100
- return (_jsxs(Box, { flexDirection: "column", height: bottomPaneHeight, width: terminalWidth, overflowX: "hidden", overflowY: "hidden", children: [_jsxs(Box, { width: terminalWidth, children: [_jsx(Text, { bold: true, color: isDiffFocused ? 'cyan' : undefined, children: bottomTab === 'commit' ? 'COMMIT' : 'DIFF' }), _jsx(Box, { flexGrow: 1, justifyContent: "flex-end", children: renderHeaderRight() })] }), renderContent()] }));
101
- }
@@ -1,58 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect } from 'react';
3
- import { Box, Text, useInput } from 'ink';
4
- import TextInput from 'ink-text-input';
5
- import { useCommitFlow } from '../hooks/useCommitFlow.js';
6
- export function CommitPanel({ isActive, stagedCount, onCommit, onCancel, getHeadMessage, onInputFocusChange, }) {
7
- const { message, amend, isCommitting, error, inputFocused, setMessage, toggleAmend, setInputFocused, handleSubmit, } = useCommitFlow({
8
- stagedCount,
9
- onCommit,
10
- onSuccess: onCancel,
11
- getHeadMessage,
12
- });
13
- // Notify parent of focus state changes
14
- useEffect(() => {
15
- onInputFocusChange?.(inputFocused);
16
- }, [inputFocused, onInputFocusChange]);
17
- // Keyboard handling
18
- useInput((input, key) => {
19
- if (!isActive)
20
- return;
21
- // When input is focused, Escape unfocuses it (but stays on commit tab)
22
- // When input is unfocused, Escape cancels and goes back to diff
23
- if (key.escape) {
24
- if (inputFocused) {
25
- setInputFocused(false);
26
- }
27
- else {
28
- onCancel();
29
- }
30
- return;
31
- }
32
- // When input is unfocused, allow refocusing with 'i' or Enter
33
- if (!inputFocused) {
34
- if (input === 'i' || key.return) {
35
- setInputFocused(true);
36
- return;
37
- }
38
- // Toggle amend with 'a' when unfocused
39
- if (input === 'a') {
40
- toggleAmend();
41
- return;
42
- }
43
- return; // Don't handle other keys - let them bubble up to useKeymap
44
- }
45
- // When input is focused, only handle special keys
46
- // Toggle amend with 'a' when message is empty
47
- if (input === 'a' && !message) {
48
- toggleAmend();
49
- return;
50
- }
51
- }, { isActive });
52
- if (!isActive) {
53
- return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "Press '2' or 'c' to open commit panel" }) }));
54
- }
55
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Commit Message" }), amend && _jsx(Text, { color: "yellow", children: " (amending)" })] }), _jsx(Box, { borderStyle: "round", borderColor: inputFocused ? 'cyan' : undefined, paddingX: 1, children: inputFocused ? (_jsx(TextInput, { value: message, onChange: setMessage, onSubmit: handleSubmit, placeholder: "Enter commit message..." })) : (_jsx(Text, { dimColor: !message, children: message || 'Press i or Enter to edit...' })) }), _jsxs(Box, { marginTop: 1, gap: 2, children: [_jsxs(Text, { color: amend ? 'green' : 'gray', children: ["[", amend ? 'x' : ' ', "] Amend"] }), _jsx(Text, { dimColor: true, children: "(a)" })] }), error && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", children: error }) })), isCommitting && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: "Committing..." }) })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Staged: ", stagedCount, " file(s) |", ' ', inputFocused
56
- ? 'Enter: commit | Esc: unfocus'
57
- : 'i/Enter: edit | Esc: cancel | 1/3: switch tab'] }) })] }));
58
- }
@@ -1,110 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useMemo } from 'react';
3
- import { Box, Text } from 'ink';
4
- import { shortenPath } from '../utils/formatPath.js';
5
- import { formatDate } from '../utils/formatDate.js';
6
- import { formatCommitDisplay } from '../utils/commitFormat.js';
7
- // Re-export from utils for backwards compatibility
8
- export { getCompareItemIndexFromRow } from '../utils/rowCalculations.js';
9
- function CommitRow({ commit, isSelected, isActive, width, }) {
10
- const dateStr = formatDate(commit.date);
11
- // Fixed parts: indent(2) + hash(7) + spaces(4) + date + parens(2)
12
- const baseWidth = 2 + 7 + 4 + dateStr.length + 2;
13
- const remainingWidth = width - baseWidth;
14
- const { displayMessage, displayRefs } = formatCommitDisplay(commit.message, commit.refs, remainingWidth);
15
- return (_jsxs(Box, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: "yellow", children: commit.shortHash }), _jsx(Text, { children: " " }), _jsx(Text, { color: isSelected && isActive ? 'cyan' : undefined, bold: isSelected && isActive, inverse: isSelected && isActive, children: displayMessage }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: ["(", dateStr, ")"] }), displayRefs && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: "green", children: displayRefs })] }))] }));
16
- }
17
- function FileRow({ file, isSelected, isActive, maxPathLength, }) {
18
- const statusColors = {
19
- added: 'green',
20
- modified: 'yellow',
21
- deleted: 'red',
22
- renamed: 'blue',
23
- };
24
- const statusChars = {
25
- added: 'A',
26
- modified: 'M',
27
- deleted: 'D',
28
- renamed: 'R',
29
- };
30
- const isUncommitted = file.isUncommitted ?? false;
31
- // Account for stats: " (+123 -456)" and possible "[uncommitted]"
32
- const statsLength = 5 + String(file.additions).length + String(file.deletions).length;
33
- const uncommittedLength = isUncommitted ? 14 : 0;
34
- const availableForPath = maxPathLength - statsLength - uncommittedLength;
35
- return (_jsxs(Box, { children: [_jsx(Text, { children: " " }), isUncommitted && (_jsx(Text, { color: "magenta", bold: true, children: "*" })), _jsx(Text, { color: isUncommitted ? 'magenta' : statusColors[file.status], bold: true, children: statusChars[file.status] }), _jsxs(Text, { bold: isSelected && isActive, color: isSelected && isActive ? 'cyan' : isUncommitted ? 'magenta' : undefined, inverse: isSelected && isActive, children: [' ', shortenPath(file.path, availableForPath)] }), _jsx(Text, { dimColor: true, children: " (" }), _jsxs(Text, { color: "green", children: ["+", file.additions] }), _jsx(Text, { dimColor: true, children: " " }), _jsxs(Text, { color: "red", children: ["-", file.deletions] }), _jsx(Text, { dimColor: true, children: ")" }), isUncommitted && (_jsxs(Text, { color: "magenta", dimColor: true, children: [' ', "[uncommitted]"] }))] }));
36
- }
37
- export function CompareListView({ commits, files, selectedItem, scrollOffset, maxHeight, isActive, width, }) {
38
- // Note: expand/collapse functionality is prepared but not exposed yet
39
- const commitsExpanded = true;
40
- const filesExpanded = true;
41
- // Build flat list of rows
42
- const rows = useMemo(() => {
43
- const result = [];
44
- // Commits section
45
- if (commits.length > 0) {
46
- result.push({ type: 'section-header', sectionType: 'commits' });
47
- if (commitsExpanded) {
48
- commits.forEach((commit, i) => {
49
- result.push({ type: 'commit', commitIndex: i, commit });
50
- });
51
- }
52
- }
53
- // Files section
54
- if (files.length > 0) {
55
- if (commits.length > 0) {
56
- result.push({ type: 'spacer' });
57
- }
58
- result.push({ type: 'section-header', sectionType: 'files' });
59
- if (filesExpanded) {
60
- files.forEach((file, i) => {
61
- result.push({ type: 'file', fileIndex: i, file });
62
- });
63
- }
64
- }
65
- return result;
66
- }, [commits, files, commitsExpanded, filesExpanded]);
67
- const visibleRows = rows.slice(scrollOffset, scrollOffset + maxHeight);
68
- if (commits.length === 0 && files.length === 0) {
69
- return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "No changes compared to base branch" }) }));
70
- }
71
- return (_jsx(Box, { flexDirection: "column", children: visibleRows.map((row, i) => {
72
- const key = `row-${scrollOffset + i}`;
73
- if (row.type === 'section-header') {
74
- const isCommits = row.sectionType === 'commits';
75
- const expanded = isCommits ? commitsExpanded : filesExpanded;
76
- const count = isCommits ? commits.length : files.length;
77
- const label = isCommits ? 'Commits' : 'Files';
78
- return (_jsxs(Box, { children: [_jsxs(Text, { bold: true, color: "cyan", children: [expanded ? '▼' : '▶', " ", label] }), _jsxs(Text, { dimColor: true, children: [" (", count, ")"] })] }, key));
79
- }
80
- if (row.type === 'spacer') {
81
- return _jsx(Text, { children: " " }, key);
82
- }
83
- if (row.type === 'commit' && row.commit !== undefined && row.commitIndex !== undefined) {
84
- const isSelected = selectedItem?.type === 'commit' && selectedItem.index === row.commitIndex;
85
- return (_jsx(CommitRow, { commit: row.commit, isSelected: isSelected, isActive: isActive, width: width }, key));
86
- }
87
- if (row.type === 'file' && row.file !== undefined && row.fileIndex !== undefined) {
88
- const isSelected = selectedItem?.type === 'file' && selectedItem.index === row.fileIndex;
89
- return (_jsx(FileRow, { file: row.file, isSelected: isSelected, isActive: isActive, maxPathLength: width - 5 }, key));
90
- }
91
- return null;
92
- }) }));
93
- }
94
- // Helper to get total row count for scrolling
95
- export function getCompareListTotalRows(commits, files, commitsExpanded = true, filesExpanded = true) {
96
- let count = 0;
97
- if (commits.length > 0) {
98
- count += 1; // header
99
- if (commitsExpanded)
100
- count += commits.length;
101
- }
102
- if (files.length > 0) {
103
- if (commits.length > 0)
104
- count += 1; // spacer
105
- count += 1; // header
106
- if (filesExpanded)
107
- count += files.length;
108
- }
109
- return count;
110
- }
@@ -1,80 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo } from 'react';
3
- import { Box, Text } from 'ink';
4
- import { ScrollableList } from './ScrollableList.js';
5
- import { buildExplorerContentRows, wrapExplorerContentRows, getExplorerContentRowCount, getExplorerContentLineNumWidth, applyMiddleDots, } from '../utils/explorerDisplayRows.js';
6
- import { truncateAnsi } from '../utils/ansiTruncate.js';
7
- export function ExplorerContentView({ filePath, content, maxHeight, scrollOffset, truncated = false, wrapMode = false, width, showMiddleDots = false, }) {
8
- // Build base rows with syntax highlighting
9
- const baseRows = useMemo(() => buildExplorerContentRows(content, filePath, truncated), [content, filePath, truncated]);
10
- // Calculate line number width
11
- const lineNumWidth = useMemo(() => getExplorerContentLineNumWidth(baseRows), [baseRows]);
12
- // Calculate content width for wrapping
13
- // Layout: paddingX(1) + lineNum + space(1) + content + paddingX(1)
14
- const contentWidth = width - lineNumWidth - 3;
15
- // Apply wrapping if enabled
16
- const displayRows = useMemo(() => wrapExplorerContentRows(baseRows, contentWidth, wrapMode), [baseRows, contentWidth, wrapMode]);
17
- if (!filePath) {
18
- return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "Select a file to view its contents" }) }));
19
- }
20
- if (!content) {
21
- return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "Loading..." }) }));
22
- }
23
- if (displayRows.length === 0) {
24
- return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "(empty file)" }) }));
25
- }
26
- return (_jsx(Box, { flexDirection: "column", paddingX: 1, children: _jsx(ScrollableList, { items: displayRows, maxHeight: maxHeight, scrollOffset: scrollOffset, getKey: (row, index) => `${index}`, renderItem: (row) => {
27
- if (row.type === 'truncation') {
28
- return (_jsx(Box, { children: _jsx(Text, { color: "yellow", dimColor: true, children: row.content }) }));
29
- }
30
- // Code row
31
- const isContinuation = row.isContinuation ?? false;
32
- // Line number display
33
- let lineNumDisplay;
34
- if (isContinuation) {
35
- // Show continuation marker instead of line number
36
- lineNumDisplay = '>>'.padStart(lineNumWidth, ' ');
37
- }
38
- else {
39
- lineNumDisplay = String(row.lineNum).padStart(lineNumWidth, ' ');
40
- }
41
- // Determine what content to display
42
- const rawContent = row.content;
43
- const shouldTruncate = !wrapMode && rawContent.length > contentWidth;
44
- // Use highlighted content if available and not a continuation or middle-dots mode
45
- const canUseHighlighting = row.highlighted && !isContinuation && !showMiddleDots;
46
- let displayContent;
47
- if (canUseHighlighting && row.highlighted) {
48
- // Use ANSI-aware truncation to preserve syntax highlighting
49
- displayContent = shouldTruncate
50
- ? truncateAnsi(row.highlighted, contentWidth)
51
- : row.highlighted;
52
- }
53
- else {
54
- // Plain content path
55
- let content = rawContent;
56
- // Apply middle-dots to raw content
57
- if (showMiddleDots && !isContinuation) {
58
- content = applyMiddleDots(content, true);
59
- }
60
- // Simple truncation for plain content
61
- if (shouldTruncate) {
62
- content = content.slice(0, Math.max(0, contentWidth - 1)) + '…';
63
- }
64
- displayContent = content;
65
- }
66
- return (_jsxs(Box, { children: [_jsxs(Text, { dimColor: !isContinuation, color: isContinuation ? 'cyan' : undefined, children: [lineNumDisplay, ' '] }), _jsx(Text, { dimColor: showMiddleDots && !canUseHighlighting, children: displayContent || ' ' })] }));
67
- } }) }));
68
- }
69
- /**
70
- * Get total rows for scroll calculations.
71
- * Accounts for wrap mode when calculating.
72
- */
73
- export function getExplorerContentTotalRows(content, filePath, truncated, width, wrapMode) {
74
- if (!content)
75
- return 0;
76
- const rows = buildExplorerContentRows(content, filePath, truncated);
77
- const lineNumWidth = getExplorerContentLineNumWidth(rows);
78
- const contentWidth = width - lineNumWidth - 3;
79
- return getExplorerContentRowCount(rows, contentWidth, wrapMode);
80
- }
@@ -1,37 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- import { ScrollableList } from './ScrollableList.js';
4
- export function ExplorerView({ currentPath: _currentPath, items, selectedIndex, scrollOffset, maxHeight, isActive, width, isLoading = false, error = null, }) {
5
- if (error) {
6
- return (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) }));
7
- }
8
- if (isLoading) {
9
- return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "Loading..." }) }));
10
- }
11
- if (items.length === 0) {
12
- return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "(empty directory)" }) }));
13
- }
14
- // Calculate max name width for alignment
15
- const maxNameWidth = Math.min(Math.max(...items.map((item) => item.name.length + (item.isDirectory ? 1 : 0))), width - 10);
16
- return (_jsx(ScrollableList, { items: items, maxHeight: maxHeight, scrollOffset: scrollOffset, getKey: (item) => item.path || item.name, renderItem: (item, actualIndex) => {
17
- const isSelected = actualIndex === selectedIndex && isActive;
18
- const displayName = item.isDirectory ? `${item.name}/` : item.name;
19
- const paddedName = displayName.padEnd(maxNameWidth + 1);
20
- return (_jsx(Box, { children: _jsx(Text, { color: isSelected ? 'cyan' : undefined, bold: isSelected, inverse: isSelected, children: item.isDirectory ? (_jsx(Text, { color: isSelected ? 'cyan' : 'blue', children: paddedName })) : (_jsx(Text, { color: isSelected ? 'cyan' : undefined, children: paddedName })) }) }));
21
- } }));
22
- }
23
- /**
24
- * Build breadcrumb segments from a path.
25
- * Returns segments like ["src", "components"] for "src/components"
26
- */
27
- export function buildBreadcrumbs(currentPath) {
28
- if (!currentPath)
29
- return [];
30
- return currentPath.split('/').filter(Boolean);
31
- }
32
- /**
33
- * Get total rows in explorer for scroll calculations.
34
- */
35
- export function getExplorerTotalRows(items) {
36
- return items.length;
37
- }
@@ -1,131 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- import { shortenPath } from '../utils/formatPath.js';
4
- import { categorizeFiles } from '../utils/fileCategories.js';
5
- function getStatusChar(status) {
6
- switch (status) {
7
- case 'modified':
8
- return 'M';
9
- case 'added':
10
- return 'A';
11
- case 'deleted':
12
- return 'D';
13
- case 'untracked':
14
- return '?';
15
- case 'renamed':
16
- return 'R';
17
- case 'copied':
18
- return 'C';
19
- default:
20
- return ' ';
21
- }
22
- }
23
- function getStatusColor(status) {
24
- switch (status) {
25
- case 'modified':
26
- return 'yellow';
27
- case 'added':
28
- return 'green';
29
- case 'deleted':
30
- return 'red';
31
- case 'untracked':
32
- return 'gray';
33
- case 'renamed':
34
- return 'blue';
35
- case 'copied':
36
- return 'cyan';
37
- default:
38
- return 'white';
39
- }
40
- }
41
- function formatStats(insertions, deletions) {
42
- if (insertions === undefined && deletions === undefined)
43
- return null;
44
- const add = insertions ?? 0;
45
- const del = deletions ?? 0;
46
- if (add === 0 && del === 0)
47
- return null;
48
- const parts = [];
49
- if (add > 0)
50
- parts.push(`+${add}`);
51
- if (del > 0)
52
- parts.push(`-${del}`);
53
- return parts.join(' ');
54
- }
55
- function FileRow({ file, isSelected, isFocused, maxPathLength }) {
56
- const statusChar = getStatusChar(file.status);
57
- const statusColor = getStatusColor(file.status);
58
- const actionButton = file.staged ? '[-]' : '[+]';
59
- const buttonColor = file.staged ? 'red' : 'green';
60
- const stats = formatStats(file.insertions, file.deletions);
61
- const isHighlighted = isSelected && isFocused;
62
- // Calculate available space for path (account for stats if present)
63
- const statsLength = stats ? stats.length + 1 : 0;
64
- const availableForPath = maxPathLength - statsLength;
65
- const displayPath = shortenPath(file.path, availableForPath);
66
- return (_jsxs(Box, { children: [_jsx(Text, { color: isHighlighted ? 'cyan' : undefined, bold: isHighlighted, children: isHighlighted ? '▸ ' : ' ' }), _jsxs(Text, { color: buttonColor, children: [actionButton, " "] }), _jsxs(Text, { color: statusColor, children: [statusChar, " "] }), _jsx(Text, { color: isHighlighted ? 'cyan' : undefined, inverse: isHighlighted, children: displayPath }), file.originalPath && _jsxs(Text, { dimColor: true, children: [" \u2190 ", shortenPath(file.originalPath, 30)] }), stats && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: " " }), file.insertions !== undefined && file.insertions > 0 && (_jsxs(Text, { color: "green", children: ["+", file.insertions] })), file.insertions !== undefined &&
67
- file.insertions > 0 &&
68
- file.deletions !== undefined &&
69
- file.deletions > 0 && _jsx(Text, { dimColor: true, children: " " }), file.deletions !== undefined && file.deletions > 0 && (_jsxs(Text, { color: "red", children: ["-", file.deletions] }))] }))] }));
70
- }
71
- export function FileList({ files, selectedIndex, isFocused, scrollOffset = 0, maxHeight, width = 80, }) {
72
- // Calculate max path length: width minus prefix chars (▸/space + [+]/[-] + status + spaces = ~10)
73
- const maxPathLength = width - 10;
74
- // Split files into 3 categories: Modified, Untracked, Staged
75
- const { modified: modifiedFiles, untracked: untrackedFiles, staged: stagedFiles, } = categorizeFiles(files);
76
- if (files.length === 0) {
77
- return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { dimColor: true, children: " No changes" }) }));
78
- }
79
- // Build a flat list of all rows
80
- // Order: Modified → Untracked → Staged
81
- const rows = [];
82
- let currentFileIndex = 0;
83
- if (modifiedFiles.length > 0) {
84
- rows.push({ type: 'header', content: 'Modified:', headerColor: 'yellow' });
85
- modifiedFiles.forEach((file) => {
86
- rows.push({ type: 'file', file, fileIndex: currentFileIndex++ });
87
- });
88
- }
89
- if (untrackedFiles.length > 0) {
90
- if (modifiedFiles.length > 0) {
91
- rows.push({ type: 'spacer' });
92
- }
93
- rows.push({ type: 'header', content: 'Untracked:', headerColor: 'gray' });
94
- untrackedFiles.forEach((file) => {
95
- rows.push({ type: 'file', file, fileIndex: currentFileIndex++ });
96
- });
97
- }
98
- if (stagedFiles.length > 0) {
99
- if (modifiedFiles.length > 0 || untrackedFiles.length > 0) {
100
- rows.push({ type: 'spacer' });
101
- }
102
- rows.push({ type: 'header', content: 'Staged:', headerColor: 'green' });
103
- stagedFiles.forEach((file) => {
104
- rows.push({ type: 'file', file, fileIndex: currentFileIndex++ });
105
- });
106
- }
107
- // Apply scroll offset and max height
108
- const visibleRows = maxHeight
109
- ? rows.slice(scrollOffset, scrollOffset + maxHeight)
110
- : rows.slice(scrollOffset);
111
- return (_jsx(Box, { flexDirection: "column", children: visibleRows.map((row, i) => {
112
- const key = `row-${scrollOffset + i}`;
113
- if (row.type === 'header') {
114
- return (_jsx(Text, { bold: true, color: row.headerColor, children: row.content }, key));
115
- }
116
- if (row.type === 'spacer') {
117
- return _jsx(Text, { children: " " }, key);
118
- }
119
- if (row.type === 'file' && row.file !== undefined && row.fileIndex !== undefined) {
120
- return (_jsx(FileRow, { file: row.file, isSelected: row.fileIndex === selectedIndex, isFocused: isFocused, maxPathLength: maxPathLength }, key));
121
- }
122
- return null;
123
- }) }));
124
- }
125
- export function getFileAtIndex(files, index) {
126
- const { ordered } = categorizeFiles(files);
127
- return ordered[index] ?? null;
128
- }
129
- export function getTotalFileCount(files) {
130
- return files.length;
131
- }
@@ -1,6 +0,0 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- export function Footer({ activeTab, mouseEnabled = true, autoTabEnabled = false, wrapMode = false, showMiddleDots = false, }) {
4
- // Layout: "? [scroll] [auto] [wrap] [dots]" with spaces between
5
- return (_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "?" }), ' ', _jsx(Text, { color: "yellow", children: mouseEnabled ? '[scroll]' : 'm:[select]' }), ' ', _jsx(Text, { color: autoTabEnabled ? 'blue' : undefined, dimColor: !autoTabEnabled, children: "[auto]" }), ' ', _jsx(Text, { color: wrapMode ? 'blue' : undefined, dimColor: !wrapMode, children: "[wrap]" }), activeTab === 'explorer' && (_jsxs(_Fragment, { children: [' ', _jsx(Text, { color: showMiddleDots ? 'blue' : undefined, dimColor: !showMiddleDots, children: "[dots]" })] }))] }), _jsxs(Text, { children: [_jsx(Text, { color: activeTab === 'diff' ? 'cyan' : undefined, bold: activeTab === 'diff', children: "[1]Diff" }), ' ', _jsx(Text, { color: activeTab === 'commit' ? 'cyan' : undefined, bold: activeTab === 'commit', children: "[2]Commit" }), ' ', _jsx(Text, { color: activeTab === 'history' ? 'cyan' : undefined, bold: activeTab === 'history', children: "[3]History" }), ' ', _jsx(Text, { color: activeTab === 'compare' ? 'cyan' : undefined, bold: activeTab === 'compare', children: "[4]Compare" })] })] }));
6
- }
@@ -1,107 +0,0 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- import { abbreviateHomePath } from '../config.js';
4
- /**
5
- * Calculate the header height based on whether content needs to wrap.
6
- * Returns 1 for single line, 2 if branch wraps to second line.
7
- */
8
- export function getHeaderHeight(repoPath, branch, watcherState, width, error = null, isLoading = false) {
9
- if (!repoPath)
10
- return 1;
11
- const displayPath = abbreviateHomePath(repoPath);
12
- const isNotGitRepo = error === 'Not a git repository';
13
- // Calculate branch width
14
- let branchWidth = 0;
15
- if (branch) {
16
- branchWidth = branch.current.length;
17
- if (branch.tracking)
18
- branchWidth += 3 + branch.tracking.length;
19
- if (branch.ahead > 0)
20
- branchWidth += 3 + String(branch.ahead).length;
21
- if (branch.behind > 0)
22
- branchWidth += 3 + String(branch.behind).length;
23
- }
24
- // Calculate left side width
25
- let leftWidth = displayPath.length;
26
- if (isLoading)
27
- leftWidth += 2;
28
- if (isNotGitRepo)
29
- leftWidth += 24;
30
- if (error && !isNotGitRepo)
31
- leftWidth += error.length + 3;
32
- // Check if follow indicator causes wrap
33
- if (watcherState?.enabled && watcherState.sourceFile) {
34
- const followPath = abbreviateHomePath(watcherState.sourceFile);
35
- const fullFollow = ` (follow: ${followPath})`;
36
- const availableOneLine = width - leftWidth - branchWidth - 4;
37
- if (fullFollow.length > availableOneLine) {
38
- // Would need to wrap
39
- const availableWithWrap = width - leftWidth - 2;
40
- if (fullFollow.length <= availableWithWrap) {
41
- return 2; // Branch wraps to second line
42
- }
43
- }
44
- }
45
- return 1;
46
- }
47
- function BranchDisplay({ branch }) {
48
- return (_jsxs(Box, { children: [_jsx(Text, { color: "green", bold: true, children: branch.current }), branch.tracking && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " \u2192 " }), _jsx(Text, { color: "blue", children: branch.tracking })] })), (branch.ahead > 0 || branch.behind > 0) && (_jsxs(Text, { children: [branch.ahead > 0 && _jsxs(Text, { color: "green", children: [" \u2191", branch.ahead] }), branch.behind > 0 && _jsxs(Text, { color: "red", children: [" \u2193", branch.behind] })] }))] }));
49
- }
50
- export function Header({ repoPath, branch, isLoading, error, debug, watcherState, width = 80, }) {
51
- if (!repoPath) {
52
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Waiting for target path..." }), _jsx(Text, { dimColor: true, children: " (write path to ~/.cache/diffstalker/target)" })] }), debug && watcherState && watcherState.enabled && watcherState.sourceFile && (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: ["[debug] source: ", abbreviateHomePath(watcherState.sourceFile)] }), watcherState.rawContent && _jsxs(Text, { dimColor: true, children: [" | raw: \"", watcherState.rawContent, "\""] })] }))] }));
53
- }
54
- const displayPath = abbreviateHomePath(repoPath);
55
- const isNotGitRepo = error === 'Not a git repository';
56
- const formatTime = (date) => {
57
- if (!date)
58
- return '';
59
- return date.toLocaleTimeString();
60
- };
61
- // Calculate branch info width for layout
62
- let branchWidth = 0;
63
- if (branch) {
64
- branchWidth = branch.current.length;
65
- if (branch.tracking) {
66
- branchWidth += 3 + branch.tracking.length; // " → tracking"
67
- }
68
- if (branch.ahead > 0)
69
- branchWidth += 3 + String(branch.ahead).length;
70
- if (branch.behind > 0)
71
- branchWidth += 3 + String(branch.behind).length;
72
- }
73
- // Calculate left side content width (without follow)
74
- let leftWidth = displayPath.length;
75
- if (isLoading)
76
- leftWidth += 2;
77
- if (isNotGitRepo)
78
- leftWidth += 24;
79
- if (error && !isNotGitRepo)
80
- leftWidth += error.length + 3;
81
- // Determine follow indicator display and layout
82
- let followText = null;
83
- let wrapBranch = false;
84
- if (watcherState?.enabled && watcherState.sourceFile) {
85
- const followPath = abbreviateHomePath(watcherState.sourceFile);
86
- const fullFollow = ` (follow: ${followPath})`;
87
- const availableOneLine = width - leftWidth - branchWidth - 4; // 4 for spacing
88
- if (fullFollow.length <= availableOneLine) {
89
- // Everything fits on one line
90
- followText = fullFollow;
91
- }
92
- else {
93
- // Need to wrap branch to second line
94
- const availableWithWrap = width - leftWidth - 2;
95
- if (fullFollow.length <= availableWithWrap) {
96
- followText = fullFollow;
97
- wrapBranch = true;
98
- }
99
- // If it doesn't fit, don't show follow at all
100
- }
101
- }
102
- return (_jsxs(Box, { flexDirection: "column", width: width, children: [wrapBranch ? (
103
- // Two-line layout: path + follow on first line, branch on second
104
- _jsxs(_Fragment, { children: [_jsx(Box, { justifyContent: "space-between", children: _jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: displayPath }), isLoading && _jsx(Text, { color: "yellow", children: " \u27F3" }), isNotGitRepo && _jsx(Text, { color: "yellow", children: " (not a git repository)" }), error && !isNotGitRepo && _jsxs(Text, { color: "red", children: [" (", error, ")"] }), followText && _jsx(Text, { dimColor: true, children: followText })] }) }), _jsx(Box, { justifyContent: "flex-end", children: branch && _jsx(BranchDisplay, { branch: branch }) })] })) : (
105
- // Single-line layout
106
- _jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: displayPath }), isLoading && _jsx(Text, { color: "yellow", children: " \u27F3" }), isNotGitRepo && _jsx(Text, { color: "yellow", children: " (not a git repository)" }), error && !isNotGitRepo && _jsxs(Text, { color: "red", children: [" (", error, ")"] }), followText && _jsx(Text, { dimColor: true, children: followText })] }), branch && _jsx(BranchDisplay, { branch: branch })] })), debug && watcherState && watcherState.enabled && watcherState.sourceFile && (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: ["[debug] source: ", abbreviateHomePath(watcherState.sourceFile)] }), _jsxs(Text, { dimColor: true, children: [" | raw: \"", watcherState.rawContent, "\""] }), watcherState.lastUpdate && (_jsxs(Text, { dimColor: true, children: [" | updated: ", formatTime(watcherState.lastUpdate)] }))] }))] }));
107
- }