diffstalker 0.1.6 → 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/.github/workflows/release.yml +5 -3
- package/CHANGELOG.md +36 -0
- package/bun.lock +378 -0
- package/dist/App.js +1162 -1
- package/dist/config.js +83 -2
- package/dist/core/ExplorerStateManager.js +266 -0
- package/dist/core/FilePathWatcher.js +133 -0
- package/dist/core/GitOperationQueue.js +109 -1
- package/dist/core/GitStateManager.js +525 -1
- package/dist/git/diff.js +471 -10
- package/dist/git/ignoreUtils.js +30 -0
- package/dist/git/status.js +237 -5
- package/dist/index.js +70 -16
- package/dist/ipc/CommandClient.js +165 -0
- package/dist/ipc/CommandServer.js +152 -0
- package/dist/services/commitService.js +22 -1
- package/dist/state/CommitFlowState.js +86 -0
- package/dist/state/UIState.js +182 -0
- package/dist/themes.js +127 -1
- 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/ansiTruncate.js +108 -0
- package/dist/utils/baseBranchCache.js +44 -2
- package/dist/utils/commitFormat.js +38 -1
- package/dist/utils/diffFilters.js +21 -1
- package/dist/utils/diffRowCalculations.js +113 -1
- package/dist/utils/displayRows.js +351 -2
- package/dist/utils/explorerDisplayRows.js +169 -0
- package/dist/utils/fileCategories.js +26 -1
- package/dist/utils/formatDate.js +39 -1
- package/dist/utils/formatPath.js +58 -1
- package/dist/utils/languageDetection.js +236 -0
- package/dist/utils/layoutCalculations.js +98 -1
- package/dist/utils/lineBreaking.js +88 -5
- package/dist/utils/mouseCoordinates.js +165 -1
- package/dist/utils/pathUtils.js +27 -0
- package/dist/utils/rowCalculations.js +246 -4
- package/dist/utils/wordDiff.js +50 -0
- package/package.json +15 -19
- package/dist/components/BaseBranchPicker.js +0 -1
- package/dist/components/BottomPane.js +0 -1
- package/dist/components/CommitPanel.js +0 -1
- package/dist/components/CompareListView.js +0 -1
- package/dist/components/ExplorerContentView.js +0 -3
- package/dist/components/ExplorerView.js +0 -1
- package/dist/components/FileList.js +0 -1
- package/dist/components/Footer.js +0 -1
- package/dist/components/Header.js +0 -1
- package/dist/components/HistoryView.js +0 -1
- package/dist/components/HotkeysModal.js +0 -1
- package/dist/components/Modal.js +0 -1
- package/dist/components/ScrollableList.js +0 -1
- package/dist/components/ThemePicker.js +0 -1
- package/dist/components/TopPane.js +0 -1
- package/dist/components/UnifiedDiffView.js +0 -1
- package/dist/hooks/useCommitFlow.js +0 -1
- package/dist/hooks/useCompareState.js +0 -1
- package/dist/hooks/useExplorerState.js +0 -9
- package/dist/hooks/useGit.js +0 -1
- package/dist/hooks/useHistoryState.js +0 -1
- package/dist/hooks/useKeymap.js +0 -1
- package/dist/hooks/useLayout.js +0 -1
- package/dist/hooks/useMouse.js +0 -1
- package/dist/hooks/useTerminalSize.js +0 -1
- package/dist/hooks/useWatcher.js +0 -11
|
@@ -1 +1,165 @@
|
|
|
1
|
-
import{categorizeFiles
|
|
1
|
+
import { categorizeFiles } from './fileCategories.js';
|
|
2
|
+
/**
|
|
3
|
+
* Calculate the row boundaries for each pane in the layout.
|
|
4
|
+
* Layout: Header (headerHeight) + sep (1) + top pane + sep (1) + bottom pane + sep (1) + footer (1)
|
|
5
|
+
*/
|
|
6
|
+
export function calculatePaneBoundaries(topPaneHeight, bottomPaneHeight, terminalHeight, headerHeight = 1) {
|
|
7
|
+
// Layout (1-indexed rows):
|
|
8
|
+
// Rows 1 to headerHeight: Header
|
|
9
|
+
// Row headerHeight+1: Separator
|
|
10
|
+
// Row headerHeight+2: Top pane header ("STAGING AREA" or "COMMITS")
|
|
11
|
+
// Rows headerHeight+3 to headerHeight+1+topPaneHeight: Top pane content
|
|
12
|
+
const stagingPaneStart = headerHeight + 2; // First row of top pane (the header row)
|
|
13
|
+
const fileListEnd = headerHeight + 1 + topPaneHeight; // Last row of top pane
|
|
14
|
+
const separatorRow = fileListEnd + 1; // Separator between panes
|
|
15
|
+
const diffPaneStart = fileListEnd + 2; // First row of bottom pane content
|
|
16
|
+
const diffPaneEnd = diffPaneStart + bottomPaneHeight - 1;
|
|
17
|
+
const footerRow = terminalHeight;
|
|
18
|
+
return {
|
|
19
|
+
stagingPaneStart,
|
|
20
|
+
fileListEnd,
|
|
21
|
+
separatorRow,
|
|
22
|
+
diffPaneStart,
|
|
23
|
+
diffPaneEnd,
|
|
24
|
+
footerRow,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Given a y-coordinate in the file list area, calculate which file index was clicked.
|
|
29
|
+
* Returns -1 if the click is not on a file.
|
|
30
|
+
*
|
|
31
|
+
* FileList layout: Modified → Untracked → Staged (with headers and spacers)
|
|
32
|
+
*/
|
|
33
|
+
export function getClickedFileIndex(y, scrollOffset, files, stagingPaneStart, fileListEnd) {
|
|
34
|
+
if (y < stagingPaneStart + 1 || y > fileListEnd)
|
|
35
|
+
return -1;
|
|
36
|
+
// Calculate which row in the list was clicked (0-indexed)
|
|
37
|
+
// Use stagingPaneStart + 1 to account for the "STAGING AREA" header row
|
|
38
|
+
const listRow = y - (stagingPaneStart + 1) + scrollOffset;
|
|
39
|
+
// Split files into 3 categories (same order as FileList)
|
|
40
|
+
const { modified: modifiedFiles, untracked: untrackedFiles, staged: stagedFiles, } = categorizeFiles(files);
|
|
41
|
+
// Build row map (same structure as FileList builds)
|
|
42
|
+
// Each section: header (1) + files (n)
|
|
43
|
+
// Spacer (1) between sections if previous section exists
|
|
44
|
+
let currentRow = 0;
|
|
45
|
+
let currentFileIndex = 0;
|
|
46
|
+
// Modified section
|
|
47
|
+
if (modifiedFiles.length > 0) {
|
|
48
|
+
currentRow++; // "Modified:" header
|
|
49
|
+
for (let i = 0; i < modifiedFiles.length; i++) {
|
|
50
|
+
if (listRow === currentRow) {
|
|
51
|
+
return currentFileIndex;
|
|
52
|
+
}
|
|
53
|
+
currentRow++;
|
|
54
|
+
currentFileIndex++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Untracked section
|
|
58
|
+
if (untrackedFiles.length > 0) {
|
|
59
|
+
if (modifiedFiles.length > 0) {
|
|
60
|
+
currentRow++; // spacer
|
|
61
|
+
}
|
|
62
|
+
currentRow++; // "Untracked:" header
|
|
63
|
+
for (let i = 0; i < untrackedFiles.length; i++) {
|
|
64
|
+
if (listRow === currentRow) {
|
|
65
|
+
return currentFileIndex;
|
|
66
|
+
}
|
|
67
|
+
currentRow++;
|
|
68
|
+
currentFileIndex++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Staged section
|
|
72
|
+
if (stagedFiles.length > 0) {
|
|
73
|
+
if (modifiedFiles.length > 0 || untrackedFiles.length > 0) {
|
|
74
|
+
currentRow++; // spacer
|
|
75
|
+
}
|
|
76
|
+
currentRow++; // "Staged:" header
|
|
77
|
+
for (let i = 0; i < stagedFiles.length; i++) {
|
|
78
|
+
if (listRow === currentRow) {
|
|
79
|
+
return currentFileIndex;
|
|
80
|
+
}
|
|
81
|
+
currentRow++;
|
|
82
|
+
currentFileIndex++;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return -1;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Calculate the x-coordinate boundaries for each tab in the footer.
|
|
89
|
+
* Tab layout (right-aligned): [1]Diff [2]Commit [3]History [4]Compare [5]Explorer (51 chars total)
|
|
90
|
+
*/
|
|
91
|
+
export function getTabBoundaries(terminalWidth) {
|
|
92
|
+
const tabsStart = terminalWidth - 51; // 1-indexed start of tabs section
|
|
93
|
+
return {
|
|
94
|
+
diffStart: tabsStart,
|
|
95
|
+
diffEnd: tabsStart + 6,
|
|
96
|
+
commitStart: tabsStart + 8,
|
|
97
|
+
commitEnd: tabsStart + 16,
|
|
98
|
+
historyStart: tabsStart + 18,
|
|
99
|
+
historyEnd: tabsStart + 27,
|
|
100
|
+
compareStart: tabsStart + 29,
|
|
101
|
+
compareEnd: tabsStart + 38,
|
|
102
|
+
explorerStart: tabsStart + 40,
|
|
103
|
+
explorerEnd: tabsStart + 50,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Given an x-coordinate in the footer row, determine which tab was clicked.
|
|
108
|
+
* Returns null if no tab was clicked.
|
|
109
|
+
*/
|
|
110
|
+
export function getClickedTab(x, terminalWidth) {
|
|
111
|
+
const bounds = getTabBoundaries(terminalWidth);
|
|
112
|
+
if (x >= bounds.diffStart && x <= bounds.diffEnd) {
|
|
113
|
+
return 'diff';
|
|
114
|
+
}
|
|
115
|
+
else if (x >= bounds.commitStart && x <= bounds.commitEnd) {
|
|
116
|
+
return 'commit';
|
|
117
|
+
}
|
|
118
|
+
else if (x >= bounds.historyStart && x <= bounds.historyEnd) {
|
|
119
|
+
return 'history';
|
|
120
|
+
}
|
|
121
|
+
else if (x >= bounds.compareStart && x <= bounds.compareEnd) {
|
|
122
|
+
return 'compare';
|
|
123
|
+
}
|
|
124
|
+
else if (x >= bounds.explorerStart && x <= bounds.explorerEnd) {
|
|
125
|
+
return 'explorer';
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Check if a click is in the file button area (first 6 columns for stage/unstage toggle).
|
|
131
|
+
*/
|
|
132
|
+
export function isButtonAreaClick(x) {
|
|
133
|
+
return x <= 6;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Check if a y-coordinate is within a given pane.
|
|
137
|
+
*/
|
|
138
|
+
export function isInPane(y, paneStart, paneEnd) {
|
|
139
|
+
return y >= paneStart && y <= paneEnd;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Given an x-coordinate in the footer row, determine which left indicator was clicked.
|
|
143
|
+
* Layout (scroll mode): "? [scroll] [auto] [wrap]"
|
|
144
|
+
* 1 3 10 12 17 19 24
|
|
145
|
+
* (In select mode, mouse tracking is disabled so clicks don't register)
|
|
146
|
+
*/
|
|
147
|
+
export function getFooterLeftClick(x) {
|
|
148
|
+
// "?" area: column 1
|
|
149
|
+
if (x === 1) {
|
|
150
|
+
return 'hotkeys';
|
|
151
|
+
}
|
|
152
|
+
// "[scroll]" or "[select]" area: columns 3-10
|
|
153
|
+
if (x >= 3 && x <= 10) {
|
|
154
|
+
return 'mouse-mode';
|
|
155
|
+
}
|
|
156
|
+
// "[auto]" area: columns 12-17
|
|
157
|
+
if (x >= 12 && x <= 17) {
|
|
158
|
+
return 'auto-tab';
|
|
159
|
+
}
|
|
160
|
+
// "[wrap]" area: columns 19-24
|
|
161
|
+
if (x >= 19 && x <= 24) {
|
|
162
|
+
return 'wrap';
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
/**
|
|
4
|
+
* Expand tilde (~) to home directory.
|
|
5
|
+
*/
|
|
6
|
+
export function expandPath(p) {
|
|
7
|
+
if (p.startsWith('~/')) {
|
|
8
|
+
return path.join(os.homedir(), p.slice(2));
|
|
9
|
+
}
|
|
10
|
+
if (p === '~') {
|
|
11
|
+
return os.homedir();
|
|
12
|
+
}
|
|
13
|
+
return p;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Get the last non-empty line from content.
|
|
17
|
+
* Supports append-only files by reading only the last non-empty line.
|
|
18
|
+
*/
|
|
19
|
+
export function getLastNonEmptyLine(content) {
|
|
20
|
+
const lines = content.split('\n');
|
|
21
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
22
|
+
const line = lines[i].trim();
|
|
23
|
+
if (line)
|
|
24
|
+
return line;
|
|
25
|
+
}
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
@@ -1,4 +1,246 @@
|
|
|
1
|
-
import{isDisplayableDiffLine
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { isDisplayableDiffLine } from './diffFilters.js';
|
|
2
|
+
import { formatDateAbsolute } from './formatDate.js';
|
|
3
|
+
import { getDiffTotalRows, getDiffLineRowCount } from './diffRowCalculations.js';
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// History View Row Calculations
|
|
6
|
+
// ============================================================================
|
|
7
|
+
/**
|
|
8
|
+
* Map a visual row index to the commit index in HistoryView.
|
|
9
|
+
* Since each commit takes 1 row, this is simply visualRow + scrollOffset.
|
|
10
|
+
*/
|
|
11
|
+
export function getCommitIndexFromRow(visualRow, commits, _terminalWidth, scrollOffset = 0) {
|
|
12
|
+
const index = visualRow + scrollOffset;
|
|
13
|
+
if (index < 0 || index >= commits.length) {
|
|
14
|
+
return -1;
|
|
15
|
+
}
|
|
16
|
+
return index;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Get the total number of visual rows for all commits in HistoryView.
|
|
20
|
+
* Since each commit takes 1 row, this equals commits.length.
|
|
21
|
+
*/
|
|
22
|
+
export function getHistoryTotalRows(commits, _terminalWidth) {
|
|
23
|
+
return commits.length;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get the visual row offset for a given commit index in HistoryView.
|
|
27
|
+
* Since each commit takes 1 row, this equals commitIndex.
|
|
28
|
+
*/
|
|
29
|
+
export function getHistoryRowOffset(_commits, commitIndex, _terminalWidth) {
|
|
30
|
+
return commitIndex;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Build all displayable rows for the history diff view.
|
|
34
|
+
* This includes commit metadata, message, and diff lines.
|
|
35
|
+
* Single source of truth for both rendering and row counting.
|
|
36
|
+
*/
|
|
37
|
+
export function buildHistoryDiffRows(commit, diff) {
|
|
38
|
+
const rows = [];
|
|
39
|
+
if (commit) {
|
|
40
|
+
// Commit header: hash, author, date
|
|
41
|
+
rows.push({
|
|
42
|
+
type: 'commit-header',
|
|
43
|
+
content: `commit ${commit.hash}`,
|
|
44
|
+
});
|
|
45
|
+
rows.push({
|
|
46
|
+
type: 'commit-header',
|
|
47
|
+
content: `Author: ${commit.author}`,
|
|
48
|
+
});
|
|
49
|
+
rows.push({
|
|
50
|
+
type: 'commit-header',
|
|
51
|
+
content: `Date: ${formatDateAbsolute(commit.date)}`,
|
|
52
|
+
});
|
|
53
|
+
// Blank line before message
|
|
54
|
+
rows.push({ type: 'spacer' });
|
|
55
|
+
// Commit message (can be multi-line)
|
|
56
|
+
const messageLines = commit.message.split('\n');
|
|
57
|
+
for (const line of messageLines) {
|
|
58
|
+
rows.push({
|
|
59
|
+
type: 'commit-message',
|
|
60
|
+
content: ` ${line}`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// Blank line after message, before diff
|
|
64
|
+
rows.push({ type: 'spacer' });
|
|
65
|
+
}
|
|
66
|
+
// Diff lines (filter same as DiffView)
|
|
67
|
+
if (diff) {
|
|
68
|
+
for (const line of diff.lines) {
|
|
69
|
+
if (isDisplayableDiffLine(line)) {
|
|
70
|
+
rows.push({ type: 'diff-line', diffLine: line });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return rows;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get the visual row count for a single HistoryDiffRow.
|
|
78
|
+
* Headers, spacers, and commit messages are always 1 row.
|
|
79
|
+
* Diff lines may wrap based on terminal width.
|
|
80
|
+
*/
|
|
81
|
+
export function getHistoryDiffRowHeight(row, lineNumWidth, terminalWidth) {
|
|
82
|
+
if (row.type !== 'diff-line' || !row.diffLine) {
|
|
83
|
+
return 1; // Headers, spacers, commit messages don't wrap
|
|
84
|
+
}
|
|
85
|
+
return getDiffLineRowCount(row.diffLine, lineNumWidth, terminalWidth);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get total displayable rows for history diff scroll calculation.
|
|
89
|
+
* Uses getDiffTotalRows for the diff portion to account for line wrapping.
|
|
90
|
+
*/
|
|
91
|
+
export function getHistoryDiffTotalRows(commit, diff, terminalWidth) {
|
|
92
|
+
// Count header rows (these don't wrap - they're short metadata)
|
|
93
|
+
let headerRows = 0;
|
|
94
|
+
if (commit) {
|
|
95
|
+
headerRows += 3; // hash, author, date
|
|
96
|
+
headerRows += 1; // spacer before message
|
|
97
|
+
headerRows += commit.message.split('\n').length; // message lines
|
|
98
|
+
headerRows += 1; // spacer after message
|
|
99
|
+
}
|
|
100
|
+
// Use getDiffTotalRows for diff portion (handles line wrapping)
|
|
101
|
+
const diffRows = getDiffTotalRows(diff, terminalWidth);
|
|
102
|
+
return headerRows + diffRows;
|
|
103
|
+
}
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// Compare View Row Calculations
|
|
106
|
+
// ============================================================================
|
|
107
|
+
/**
|
|
108
|
+
* Build a combined DiffResult from all compare files.
|
|
109
|
+
* This is the single source of truth for compare diff content.
|
|
110
|
+
*/
|
|
111
|
+
export function buildCombinedCompareDiff(compareDiff) {
|
|
112
|
+
if (!compareDiff || compareDiff.files.length === 0) {
|
|
113
|
+
return { raw: '', lines: [] };
|
|
114
|
+
}
|
|
115
|
+
const allLines = [];
|
|
116
|
+
const rawParts = [];
|
|
117
|
+
for (const file of compareDiff.files) {
|
|
118
|
+
// Include all lines from each file's diff (including headers)
|
|
119
|
+
for (const line of file.diff.lines) {
|
|
120
|
+
allLines.push(line);
|
|
121
|
+
}
|
|
122
|
+
rawParts.push(file.diff.raw);
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
raw: rawParts.join('\n'),
|
|
126
|
+
lines: allLines,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Calculate the total number of displayable lines in the compare diff.
|
|
131
|
+
* This accounts for header filtering done by DiffView.
|
|
132
|
+
*/
|
|
133
|
+
export function getCompareDiffTotalRows(compareDiff) {
|
|
134
|
+
const combined = buildCombinedCompareDiff(compareDiff);
|
|
135
|
+
return combined.lines.filter(isDisplayableDiffLine).length;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Calculate the row offset to scroll to a specific file in the compare diff.
|
|
139
|
+
* Returns the row index where the file's diff --git header starts.
|
|
140
|
+
*/
|
|
141
|
+
export function getFileScrollOffset(compareDiff, fileIndex) {
|
|
142
|
+
if (!compareDiff || fileIndex < 0 || fileIndex >= compareDiff.files.length)
|
|
143
|
+
return 0;
|
|
144
|
+
const combined = buildCombinedCompareDiff(compareDiff);
|
|
145
|
+
let displayableRow = 0;
|
|
146
|
+
let currentFileIndex = 0;
|
|
147
|
+
for (const line of combined.lines) {
|
|
148
|
+
// Check if this is a file boundary
|
|
149
|
+
if (line.type === 'header' && line.content.startsWith('diff --git')) {
|
|
150
|
+
if (currentFileIndex === fileIndex) {
|
|
151
|
+
return displayableRow;
|
|
152
|
+
}
|
|
153
|
+
currentFileIndex++;
|
|
154
|
+
}
|
|
155
|
+
// Skip lines that DiffView filters out
|
|
156
|
+
if (!isDisplayableDiffLine(line)) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
displayableRow++;
|
|
160
|
+
}
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
// ============================================================================
|
|
164
|
+
// Compare List View Row Calculations
|
|
165
|
+
// ============================================================================
|
|
166
|
+
/**
|
|
167
|
+
* Map a visual row index to the item index in CompareListView.
|
|
168
|
+
* Returns the commit index for commits, or commitCount + fileIndex for files.
|
|
169
|
+
* Returns -1 if the row is a header, spacer, or out of bounds.
|
|
170
|
+
*
|
|
171
|
+
* Row structure (when both commits and files exist, both expanded):
|
|
172
|
+
* Row 0: "▼ Commits" header
|
|
173
|
+
* Row 1..N: commits
|
|
174
|
+
* Row N+1: spacer
|
|
175
|
+
* Row N+2: "▼ Files" header
|
|
176
|
+
* Rows N+3..: files
|
|
177
|
+
*/
|
|
178
|
+
export function getCompareItemIndexFromRow(row, commitCount, fileCount, commitsExpanded = true, filesExpanded = true) {
|
|
179
|
+
let currentRow = 0;
|
|
180
|
+
// Commits section
|
|
181
|
+
if (commitCount > 0) {
|
|
182
|
+
if (row === currentRow)
|
|
183
|
+
return -1; // "▼ Commits" header
|
|
184
|
+
currentRow++;
|
|
185
|
+
if (commitsExpanded) {
|
|
186
|
+
if (row < currentRow + commitCount) {
|
|
187
|
+
return row - currentRow; // Commit index
|
|
188
|
+
}
|
|
189
|
+
currentRow += commitCount;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Files section
|
|
193
|
+
if (fileCount > 0) {
|
|
194
|
+
if (commitCount > 0) {
|
|
195
|
+
if (row === currentRow)
|
|
196
|
+
return -1; // Spacer
|
|
197
|
+
currentRow++;
|
|
198
|
+
}
|
|
199
|
+
if (row === currentRow)
|
|
200
|
+
return -1; // "▼ Files" header
|
|
201
|
+
currentRow++;
|
|
202
|
+
if (filesExpanded) {
|
|
203
|
+
if (row < currentRow + fileCount) {
|
|
204
|
+
return commitCount + (row - currentRow); // File index (offset by commit count)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return -1; // Out of bounds
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Map an item index to its row position in CompareListView.
|
|
212
|
+
* This is the inverse of getCompareItemIndexFromRow.
|
|
213
|
+
*
|
|
214
|
+
* For itemIndex in 0..(commitCount-1): row = 1 + itemIndex
|
|
215
|
+
* For itemIndex in commitCount..(commitCount+fileCount-1): row = 1 + commitCount + 1 + 1 + (itemIndex - commitCount)
|
|
216
|
+
*/
|
|
217
|
+
export function getCompareRowFromItemIndex(itemIndex, commitCount, fileCount, commitsExpanded = true, filesExpanded = true) {
|
|
218
|
+
if (itemIndex < 0)
|
|
219
|
+
return 0;
|
|
220
|
+
// Handle commit items
|
|
221
|
+
if (itemIndex < commitCount) {
|
|
222
|
+
// Row 0 is commits header, so commit i is at row 1 + i
|
|
223
|
+
return commitsExpanded ? 1 + itemIndex : 0;
|
|
224
|
+
}
|
|
225
|
+
// Handle file items
|
|
226
|
+
const fileIndex = itemIndex - commitCount;
|
|
227
|
+
if (fileIndex >= fileCount)
|
|
228
|
+
return 0;
|
|
229
|
+
// Calculate row for file:
|
|
230
|
+
// - commits header (1 row if commits exist)
|
|
231
|
+
// - commits (commitCount rows if expanded)
|
|
232
|
+
// - spacer (1 row if commits exist)
|
|
233
|
+
// - files header (1 row)
|
|
234
|
+
// - file items
|
|
235
|
+
let row = 0;
|
|
236
|
+
if (commitCount > 0) {
|
|
237
|
+
row += 1; // commits header
|
|
238
|
+
if (commitsExpanded)
|
|
239
|
+
row += commitCount;
|
|
240
|
+
row += 1; // spacer
|
|
241
|
+
}
|
|
242
|
+
row += 1; // files header
|
|
243
|
+
if (filesExpanded)
|
|
244
|
+
row += fileIndex;
|
|
245
|
+
return row;
|
|
246
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Word-level diff utility using fast-diff
|
|
2
|
+
import fastDiff from 'fast-diff';
|
|
3
|
+
/**
|
|
4
|
+
* Check if two lines are similar enough to warrant word-level diffing.
|
|
5
|
+
* Returns true if they share at least 30% common content.
|
|
6
|
+
*/
|
|
7
|
+
export function areSimilarEnough(oldText, newText) {
|
|
8
|
+
if (!oldText || !newText)
|
|
9
|
+
return false;
|
|
10
|
+
const diffs = fastDiff(oldText, newText);
|
|
11
|
+
let commonLength = 0;
|
|
12
|
+
let totalLength = 0;
|
|
13
|
+
for (const [type, text] of diffs) {
|
|
14
|
+
totalLength += text.length;
|
|
15
|
+
if (type === fastDiff.EQUAL) {
|
|
16
|
+
commonLength += text.length;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (totalLength === 0)
|
|
20
|
+
return false;
|
|
21
|
+
// Require at least 50% similarity for word-level highlighting to be useful
|
|
22
|
+
const similarity = commonLength / totalLength;
|
|
23
|
+
return similarity >= 0.5;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Compute word-level diff between two strings.
|
|
27
|
+
* Returns segments for both the old (deleted) and new (added) lines,
|
|
28
|
+
* marking which portions changed.
|
|
29
|
+
*/
|
|
30
|
+
export function computeWordDiff(oldText, newText) {
|
|
31
|
+
const diffs = fastDiff(oldText, newText);
|
|
32
|
+
const oldSegments = [];
|
|
33
|
+
const newSegments = [];
|
|
34
|
+
for (const [type, text] of diffs) {
|
|
35
|
+
if (type === fastDiff.EQUAL) {
|
|
36
|
+
// Same in both - add to both segment lists
|
|
37
|
+
oldSegments.push({ text, type: 'same' });
|
|
38
|
+
newSegments.push({ text, type: 'same' });
|
|
39
|
+
}
|
|
40
|
+
else if (type === fastDiff.DELETE) {
|
|
41
|
+
// Deleted from old - only in old segments
|
|
42
|
+
oldSegments.push({ text, type: 'changed' });
|
|
43
|
+
}
|
|
44
|
+
else if (type === fastDiff.INSERT) {
|
|
45
|
+
// Inserted in new - only in new segments
|
|
46
|
+
newSegments.push({ text, type: 'changed' });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { oldSegments, newSegments };
|
|
50
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "diffstalker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Terminal application that displays git diff/status for directories",
|
|
5
5
|
"author": "yogh-io",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,19 +18,20 @@
|
|
|
18
18
|
"diffstalker": "bin/diffstalker"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
|
-
"
|
|
21
|
+
"postinstall": "node scripts/patch-neo-blessed.cjs",
|
|
22
|
+
"dev": "bun --watch src/index.ts",
|
|
22
23
|
"build": "tsc",
|
|
23
|
-
"build:prod": "tsc &&
|
|
24
|
-
"bundle": "
|
|
25
|
-
"start": "
|
|
26
|
-
"start:bundle": "
|
|
27
|
-
"test": "
|
|
28
|
-
"test:watch": "
|
|
24
|
+
"build:prod": "tsc && bun build dist/index.js --outfile dist/index.js --minify --target node --packages external",
|
|
25
|
+
"bundle": "bun run build:prod && bun build dist/index.js --outdir dist/bundle --minify --target node --external neo-blessed",
|
|
26
|
+
"start": "bun dist/index.js",
|
|
27
|
+
"start:bundle": "bun dist/bundle/index.js",
|
|
28
|
+
"test": "bun test src/*.test.ts src/**/*.test.ts",
|
|
29
|
+
"test:watch": "bun test --watch src/*.test.ts src/**/*.test.ts",
|
|
29
30
|
"lint": "eslint src/",
|
|
30
31
|
"lint:fix": "eslint src/ --fix",
|
|
31
32
|
"format": "prettier --write src/",
|
|
32
33
|
"format:check": "prettier --check src/",
|
|
33
|
-
"prepublishOnly": "
|
|
34
|
+
"prepublishOnly": "bun run build:prod"
|
|
34
35
|
},
|
|
35
36
|
"keywords": [
|
|
36
37
|
"git",
|
|
@@ -46,25 +47,20 @@
|
|
|
46
47
|
"chokidar": "^4.0.3",
|
|
47
48
|
"emphasize": "^7.0.0",
|
|
48
49
|
"fast-diff": "^1.3.0",
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"react": "^19.2.0",
|
|
50
|
+
"ignore": "^7.0.5",
|
|
51
|
+
"neo-blessed": "^0.2.0",
|
|
52
52
|
"simple-git": "^3.27.0",
|
|
53
53
|
"string-width": "^8.1.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@eslint/js": "^9.39.2",
|
|
57
|
+
"@types/blessed": "^0.1.27",
|
|
57
58
|
"@types/node": "^22.10.7",
|
|
58
|
-
"@types/react": "^19.2.0",
|
|
59
|
-
"@vercel/ncc": "^0.38.4",
|
|
60
|
-
"esbuild": "^0.27.2",
|
|
61
59
|
"eslint": "^9.39.2",
|
|
62
60
|
"eslint-config-prettier": "^10.1.8",
|
|
63
|
-
"
|
|
61
|
+
"patch-package": "^8.0.1",
|
|
64
62
|
"prettier": "^3.8.0",
|
|
65
|
-
"tsx": "^4.19.2",
|
|
66
63
|
"typescript": "^5.7.3",
|
|
67
|
-
"typescript-eslint": "^8.53.1"
|
|
68
|
-
"vitest": "^2.1.0"
|
|
64
|
+
"typescript-eslint": "^8.53.1"
|
|
69
65
|
}
|
|
70
66
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{jsxs as d,jsx as e}from"react/jsx-runtime";import{useState as I,useMemo as v}from"react";import{Box as i,Text as n,useInput as A}from"ink";import{Modal as D,centerModal as H}from"./Modal.js";export function BaseBranchPicker({candidates:m,currentBranch:b,onSelect:g,onCancel:j,width:p,height:f}){const[r,M]=I(""),[y,h]=I(0),o=v(()=>{if(!r)return m;const l=r.toLowerCase();return m.filter(t=>t.toLowerCase().includes(l))},[m,r]),x=Math.min(y,Math.max(0,o.length-1));A((l,t)=>{t.escape?j():t.return?o.length===0&&r?g(r):o.length>0&&g(o[x]):t.upArrow?h(c=>Math.max(0,c-1)):t.downArrow?h(c=>Math.min(o.length-1,c+1)):t.backspace||t.delete?(M(c=>c.slice(0,-1)),h(0)):l&&!t.ctrl&&!t.meta&&(M(c=>c+l),h(0))});const C=Math.min(60,p-4),s=Math.min(10,f-10),w=Math.min(s+9,f-4),{x:S,y:E}=H(C,w,p,f),a=Math.max(0,x-s+1),B=o.slice(a,a+s);return e(D,{x:S,y:E,width:C,height:w,children:d(i,{borderStyle:"round",borderColor:"cyan",flexDirection:"column",width:C,children:[e(i,{justifyContent:"center",marginBottom:1,children:d(n,{bold:!0,color:"cyan",children:[" ","Select Base Branch"," "]})}),d(i,{marginBottom:1,children:[e(n,{dimColor:!0,children:"Filter: "}),e(n,{color:"cyan",children:r}),e(n,{color:"cyan",children:"\u258C"})]}),e(i,{flexDirection:"column",height:s,children:B.length>0?B.map((l,t)=>{const u=a+t===x,L=l===b;return d(i,{children:[e(n,{color:u?"cyan":void 0,children:u?"\u25B8 ":" "}),e(n,{bold:u,color:u?"cyan":void 0,children:l}),L&&e(n,{dimColor:!0,children:" (current)"})]},l)}):r?d(i,{children:[e(n,{dimColor:!0,children:" No matches. Press Enter to use: "}),e(n,{color:"yellow",children:r})]}):e(n,{dimColor:!0,children:" No candidates found"})}),o.length>s&&e(i,{children:d(n,{dimColor:!0,children:[a>0?"\u2191 ":" ",a+s<o.length?"\u2193 more":""]})}),e(i,{marginTop:1,justifyContent:"center",children:e(n,{dimColor:!0,children:"\u2191\u2193 navigate \u2022 Enter select \u2022 Esc cancel"})})]})})}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{jsx as r,jsxs as s}from"react/jsx-runtime";import{useMemo as v}from"react";import{Box as l,Text as i}from"ink";import{UnifiedDiffView as U}from"./UnifiedDiffView.js";import{CommitPanel as X}from"./CommitPanel.js";import{ExplorerContentView as k}from"./ExplorerContentView.js";import{shortenPath as g}from"../utils/formatPath.js";import{buildDiffDisplayRows as H,buildHistoryDisplayRows as q,buildCompareDisplayRows as z,getDisplayRowsLineNumWidth as F,wrapDisplayRows as J}from"../utils/displayRows.js";export function BottomPane({bottomTab:n,currentPane:t,terminalWidth:o,bottomPaneHeight:w,diffScrollOffset:I,currentTheme:N,diff:m,selectedFile:C,stagedCount:B,onCommit:O,onCommitCancel:D,getHeadCommitMessage:G,onCommitInputFocusChange:M,historySelectedCommit:f,historyCommitDiff:y,compareDiff:e,compareLoading:V,compareError:R,compareListSelection:u,compareSelectionDiff:x,wrapMode:p,explorerSelectedFile:h=null,explorerFileScrollOffset:Y=0}){const a=t!=="files"&&t!=="history"&&t!=="compare"&&t!=="explorer",c=v(()=>n==="diff"?H(m):n==="history"?q(f,y):n==="compare"?u?.type==="commit"&&x?H(x):z(e):[],[n,m,f,y,u,x,e]),_=v(()=>{if(!p||c.length===0)return c;const d=F(c),E=o-d-5;return J(c,E,p)},[c,o,p]),j=()=>{if(C&&n==="diff")return r(i,{dimColor:!0,children:g(C.path,o-10)});if(n==="history"&&f)return s(i,{dimColor:!0,children:[f.shortHash," - ",f.message.slice(0,50)]});if(n==="compare"&&u)if(u.type==="commit"){const d=e?.commits[u.index];return s(i,{dimColor:!0,children:[d?.shortHash??""," - ",d?.message.slice(0,40)??""]})}else{const d=e?.files[u.index]?.path??"";return r(i,{dimColor:!0,children:g(d,o-10)})}return n==="explorer"&&h?r(i,{dimColor:!0,children:g(h.path,o-10)}):null},A=()=>{if(n==="commit")return r(X,{isActive:t==="commit",stagedCount:B,onCommit:O,onCancel:D,getHeadMessage:G,onInputFocusChange:M});if(n==="compare"){if(V)return r(i,{dimColor:!0,children:"Loading compare diff..."});if(R)return r(i,{color:"red",children:R});if(!e)return r(i,{dimColor:!0,children:"No base branch found (no origin/main or origin/master)"});if(e.files.length===0)return s(i,{dimColor:!0,children:["No changes compared to ",e.baseBranch]})}return r(U,{rows:_,maxHeight:w-1,scrollOffset:I,theme:N,width:o,wrapMode:p})};return n==="explorer"?s(l,{flexDirection:"column",height:w,width:o,overflowY:"hidden",children:[s(l,{width:o,children:[r(i,{bold:!0,color:a?"cyan":void 0,children:"FILE"}),r(l,{flexGrow:1,justifyContent:"flex-end",children:j()})]}),r(k,{filePath:h?.path??null,content:h?.content??null,maxHeight:w-1,scrollOffset:Y,truncated:h?.truncated})]}):s(l,{flexDirection:"column",height:w,width:o,overflowX:"hidden",overflowY:"hidden",children:[s(l,{width:o,children:[r(i,{bold:!0,color:a?"cyan":void 0,children:n==="commit"?"COMMIT":"DIFF"}),r(l,{flexGrow:1,justifyContent:"flex-end",children:j()})]}),A()]})}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{jsx as e,jsxs as n}from"react/jsx-runtime";import{useEffect as T}from"react";import{Box as o,Text as r,useInput as S}from"ink";import j from"ink-text-input";import{useCommitFlow as w}from"../hooks/useCommitFlow.js";export function CommitPanel({isActive:l,stagedCount:c,onCommit:p,onCancel:s,getHeadMessage:x,onInputFocusChange:a}){const{message:t,amend:m,isCommitting:C,error:u,inputFocused:i,setMessage:E,toggleAmend:f,setInputFocused:g,handleSubmit:b}=w({stagedCount:c,onCommit:p,onSuccess:s,getHeadMessage:x});return T(()=>{a?.(i)},[i,a]),S((d,h)=>{if(l){if(h.escape){i?g(!1):s();return}if(!i){if(d==="i"||h.return){g(!0);return}if(d==="a"){f();return}return}if(d==="a"&&!t){f();return}}},{isActive:l}),l?n(o,{flexDirection:"column",paddingX:1,children:[n(o,{marginBottom:1,children:[e(r,{bold:!0,children:"Commit Message"}),m&&e(r,{color:"yellow",children:" (amending)"})]}),e(o,{borderStyle:"round",borderColor:i?"cyan":void 0,paddingX:1,children:i?e(j,{value:t,onChange:E,onSubmit:b,placeholder:"Enter commit message..."}):e(r,{dimColor:!t,children:t||"Press i or Enter to edit..."})}),n(o,{marginTop:1,gap:2,children:[n(r,{color:m?"green":"gray",children:["[",m?"x":" ","] Amend"]}),e(r,{dimColor:!0,children:"(a)"})]}),u&&e(o,{marginTop:1,children:e(r,{color:"red",children:u})}),C&&e(o,{marginTop:1,children:e(r,{color:"yellow",children:"Committing..."})}),e(o,{marginTop:1,children:n(r,{dimColor:!0,children:["Staged: ",c," file(s) |"," ",i?"Enter: commit | Esc: unfocus":"i/Enter: edit | Esc: cancel | 1/3: switch tab"]})})]}):e(o,{paddingX:1,children:e(r,{dimColor:!0,children:"Press '2' or 'c' to open commit panel"})})}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{jsx as o,jsxs as l,Fragment as E}from"react/jsx-runtime";import{useMemo as R}from"react";import{Box as f,Text as n}from"ink";import{shortenPath as F}from"../utils/formatPath.js";import{formatDate as D}from"../utils/formatDate.js";import{formatCommitDisplay as I}from"../utils/commitFormat.js";export{getCompareItemIndexFromRow}from"../utils/rowCalculations.js";function L({commit:e,isSelected:r,isActive:i,width:s}){const d=D(e.date),m=13+d.length+2,c=s-m,{displayMessage:g,displayRefs:u}=I(e.message,e.refs,c);return l(f,{children:[o(n,{children:" "}),o(n,{color:"yellow",children:e.shortHash}),o(n,{children:" "}),o(n,{color:r&&i?"cyan":void 0,bold:r&&i,inverse:r&&i,children:g}),o(n,{children:" "}),l(n,{dimColor:!0,children:["(",d,")"]}),u&&l(E,{children:[o(n,{children:" "}),o(n,{color:"green",children:u})]})]})}function T({file:e,isSelected:r,isActive:i,maxPathLength:s}){const d={added:"green",modified:"yellow",deleted:"red",renamed:"blue"},m={added:"A",modified:"M",deleted:"D",renamed:"R"},c=e.isUncommitted??!1,g=5+String(e.additions).length+String(e.deletions).length,u=c?14:0,x=s-g-u;return l(f,{children:[o(n,{children:" "}),c&&o(n,{color:"magenta",bold:!0,children:"*"}),o(n,{color:c?"magenta":d[e.status],bold:!0,children:m[e.status]}),l(n,{bold:r&&i,color:r&&i?"cyan":c?"magenta":void 0,inverse:r&&i,children:[" ",F(e.path,x)]}),o(n,{dimColor:!0,children:" ("}),l(n,{color:"green",children:["+",e.additions]}),o(n,{dimColor:!0,children:" "}),l(n,{color:"red",children:["-",e.deletions]}),o(n,{dimColor:!0,children:")"}),c&&l(n,{color:"magenta",dimColor:!0,children:[" ","[uncommitted]"]})]})}export function CompareListView({commits:e,files:r,selectedItem:i,scrollOffset:s,maxHeight:d,isActive:m,width:c}){const y=R(()=>{const t=[];return e.length>0&&(t.push({type:"section-header",sectionType:"commits"}),e.forEach((p,a)=>{t.push({type:"commit",commitIndex:a,commit:p})})),r.length>0&&(e.length>0&&t.push({type:"spacer"}),t.push({type:"section-header",sectionType:"files"}),r.forEach((p,a)=>{t.push({type:"file",fileIndex:a,file:p})})),t},[e,r,!0,!0]).slice(s,s+d);return e.length===0&&r.length===0?o(f,{flexDirection:"column",children:o(n,{dimColor:!0,children:"No changes compared to base branch"})}):o(f,{flexDirection:"column",children:y.map((t,p)=>{const a=`row-${s+p}`;if(t.type==="section-header"){const h=t.sectionType==="commits",C=!0,b=h?e.length:r.length;return l(f,{children:[l(n,{bold:!0,color:"cyan",children:[C?"\u25BC":"\u25B6"," ",h?"Commits":"Files"]}),l(n,{dimColor:!0,children:[" (",b,")"]})]},a)}if(t.type==="spacer")return o(n,{children:" "},a);if(t.type==="commit"&&t.commit!==void 0&&t.commitIndex!==void 0){const h=i?.type==="commit"&&i.index===t.commitIndex;return o(L,{commit:t.commit,isSelected:h,isActive:m,width:c},a)}if(t.type==="file"&&t.file!==void 0&&t.fileIndex!==void 0){const h=i?.type==="file"&&i.index===t.fileIndex;return o(T,{file:t.file,isSelected:h,isActive:m,maxPathLength:c-5},a)}return null})})}export function getCompareListTotalRows(e,r,i=!0,s=!0){let d=0;return e.length>0&&(d+=1,i&&(d+=e.length)),r.length>0&&(e.length>0&&(d+=1),d+=1,s&&(d+=r.length)),d}
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import{jsx as e,jsxs as m}from"react/jsx-runtime";import{useMemo as u}from"react";import{Box as t,Text as n}from"ink";import{ScrollableList as f}from"./ScrollableList.js";export function ExplorerContentView({filePath:i,content:l,maxHeight:c,scrollOffset:s,truncated:h=!1}){const o=u(()=>l?l.split(`
|
|
2
|
-
`).map((r,d)=>({lineNum:d+1,content:r})):[],[l]),a=u(()=>{const r=o.length;return Math.max(3,String(r).length)},[o.length]);return i?l?o.length===0?e(t,{paddingX:1,children:e(n,{dimColor:!0,children:"(empty file)"})}):m(t,{flexDirection:"column",paddingX:1,children:[e(f,{items:o,maxHeight:c,scrollOffset:s,getKey:r=>`${r.lineNum}`,renderItem:r=>{const d=String(r.lineNum).padStart(a," ");return m(t,{children:[m(n,{dimColor:!0,children:[d," "]}),e(n,{children:r.content||" "})]})}}),h&&e(t,{children:e(n,{color:"yellow",dimColor:!0,children:"(file truncated)"})})]}):e(t,{paddingX:1,children:e(n,{dimColor:!0,children:"Loading..."})}):e(t,{paddingX:1,children:e(n,{dimColor:!0,children:"Select a file to view its contents"})})}export function getExplorerContentTotalRows(i){return i?i.split(`
|
|
3
|
-
`).length:0}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{jsxs as y,jsx as e}from"react/jsx-runtime";import{Box as t,Text as n}from"ink";import{ScrollableList as g}from"./ScrollableList.js";export function ExplorerView({currentPath:o,items:i,selectedIndex:a,scrollOffset:s,maxHeight:u,isActive:m,width:f,isLoading:h=!1,error:c=null}){if(c)return e(t,{flexDirection:"column",children:y(n,{color:"red",children:["Error: ",c]})});if(h)return e(t,{flexDirection:"column",children:e(n,{dimColor:!0,children:"Loading..."})});if(i.length===0)return e(t,{flexDirection:"column",children:e(n,{dimColor:!0,children:"(empty directory)"})});const x=Math.min(Math.max(...i.map(r=>r.name.length+(r.isDirectory?1:0))),f-10);return e(g,{items:i,maxHeight:u,scrollOffset:s,getKey:r=>r.path||r.name,renderItem:(r,p)=>{const l=p===a&&m,d=(r.isDirectory?`${r.name}/`:r.name).padEnd(x+1);return e(t,{children:e(n,{color:l?"cyan":void 0,bold:l,inverse:l,children:r.isDirectory?e(n,{color:l?"cyan":"blue",children:d}):e(n,{color:l?"cyan":void 0,children:d})})})}})}export function buildBreadcrumbs(o){return o?o.split("/").filter(Boolean):[]}export function getExplorerTotalRows(o){return o.length}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{jsx as d,jsxs as l}from"react/jsx-runtime";import{Box as x,Text as t}from"ink";import{shortenPath as C}from"../utils/formatPath.js";import{categorizeFiles as F}from"../utils/fileCategories.js";function P(e){switch(e){case"modified":return"M";case"added":return"A";case"deleted":return"D";case"untracked":return"?";case"renamed":return"R";case"copied":return"C";default:return" "}}function b(e){switch(e){case"modified":return"yellow";case"added":return"green";case"deleted":return"red";case"untracked":return"gray";case"renamed":return"blue";case"copied":return"cyan";default:return"white"}}function k(e,s){if(e===void 0&&s===void 0)return null;const i=e??0,o=s??0;if(i===0&&o===0)return null;const c=[];return i>0&&c.push(`+${i}`),o>0&&c.push(`-${o}`),c.join(" ")}function j({file:e,isSelected:s,isFocused:i,maxPathLength:o}){const c=P(e.status),f=b(e.status),g=e.staged?"[-]":"[+]",h=e.staged?"red":"green",u=k(e.insertions,e.deletions),a=s&&i,r=u?u.length+1:0,p=o-r,m=C(e.path,p);return l(x,{children:[d(t,{color:a?"cyan":void 0,bold:a,children:a?"\u25B8 ":" "}),l(t,{color:h,children:[g," "]}),l(t,{color:f,children:[c," "]}),d(t,{color:a?"cyan":void 0,inverse:a,children:m}),e.originalPath&&l(t,{dimColor:!0,children:[" \u2190 ",C(e.originalPath,30)]}),u&&l(t,{children:[d(t,{dimColor:!0,children:" "}),e.insertions!==void 0&&e.insertions>0&&l(t,{color:"green",children:["+",e.insertions]}),e.insertions!==void 0&&e.insertions>0&&e.deletions!==void 0&&e.deletions>0&&d(t,{dimColor:!0,children:" "}),e.deletions!==void 0&&e.deletions>0&&l(t,{color:"red",children:["-",e.deletions]})]})]})}export function FileList({files:e,selectedIndex:s,isFocused:i,scrollOffset:o=0,maxHeight:c,width:f=80}){const g=f-10,{modified:h,untracked:u,staged:a}=F(e);if(e.length===0)return d(x,{flexDirection:"column",children:d(t,{dimColor:!0,children:" No changes"})});const r=[];let p=0;h.length>0&&(r.push({type:"header",content:"Modified:",headerColor:"yellow"}),h.forEach(n=>{r.push({type:"file",file:n,fileIndex:p++})})),u.length>0&&(h.length>0&&r.push({type:"spacer"}),r.push({type:"header",content:"Untracked:",headerColor:"gray"}),u.forEach(n=>{r.push({type:"file",file:n,fileIndex:p++})})),a.length>0&&((h.length>0||u.length>0)&&r.push({type:"spacer"}),r.push({type:"header",content:"Staged:",headerColor:"green"}),a.forEach(n=>{r.push({type:"file",file:n,fileIndex:p++})}));const m=c?r.slice(o,o+c):r.slice(o);return d(x,{flexDirection:"column",children:m.map((n,I)=>{const y=`row-${o+I}`;return n.type==="header"?d(t,{bold:!0,color:n.headerColor,children:n.content},y):n.type==="spacer"?d(t,{children:" "},y):n.type==="file"&&n.file!==void 0&&n.fileIndex!==void 0?d(j,{file:n.file,isSelected:n.fileIndex===s,isFocused:i,maxPathLength:g},y):null})})}export function getFileAtIndex(e,s){const{ordered:i}=F(e);return i[s]??null}export function getTotalFileCount(e){return e.length}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{jsx as e,jsxs as l}from"react/jsx-runtime";import{Box as c,Text as o}from"ink";export function Footer({activeTab:r,mouseEnabled:i=!0,autoTabEnabled:n=!1,wrapMode:d=!1}){return l(c,{justifyContent:"space-between",children:[l(o,{children:[e(o,{dimColor:!0,children:"?"})," ",e(o,{color:"yellow",children:i?"[scroll]":"m:[select]"})," ",e(o,{color:n?"blue":void 0,dimColor:!n,children:"[auto]"})," ",e(o,{color:d?"blue":void 0,dimColor:!d,children:"[wrap]"})]}),l(o,{children:[e(o,{color:r==="diff"?"cyan":void 0,bold:r==="diff",children:"[1]Diff"})," ",e(o,{color:r==="commit"?"cyan":void 0,bold:r==="commit",children:"[2]Commit"})," ",e(o,{color:r==="history"?"cyan":void 0,bold:r==="history",children:"[3]History"})," ",e(o,{color:r==="compare"?"cyan":void 0,bold:r==="compare",children:"[4]Compare"})]})]})}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{jsx as n,Fragment as W,jsxs as i}from"react/jsx-runtime";import{Box as d,Text as e}from"ink";import{abbreviateHomePath as p}from"../config.js";export function getHeaderHeight(t,l,s,c,g=null,o=!1){if(!t)return 1;const y=p(t),a=g==="Not a git repository";let r=0;l&&(r=l.current.length,l.tracking&&(r+=3+l.tracking.length),l.ahead>0&&(r+=3+String(l.ahead).length),l.behind>0&&(r+=3+String(l.behind).length));let h=y.length;if(o&&(h+=2),a&&(h+=24),g&&!a&&(h+=g.length+3),s?.enabled&&s.sourceFile){const u=` (follow: ${p(s.sourceFile)})`,f=c-h-r-4;if(u.length>f){const C=c-h-2;if(u.length<=C)return 2}}return 1}function j({branch:t}){return i(d,{children:[n(e,{color:"green",bold:!0,children:t.current}),t.tracking&&i(W,{children:[n(e,{dimColor:!0,children:" \u2192 "}),n(e,{color:"blue",children:t.tracking})]}),(t.ahead>0||t.behind>0)&&i(e,{children:[t.ahead>0&&i(e,{color:"green",children:[" \u2191",t.ahead]}),t.behind>0&&i(e,{color:"red",children:[" \u2193",t.behind]})]})]})}export function Header({repoPath:t,branch:l,isLoading:s,error:c,debug:g,watcherState:o,width:y=80}){if(!t)return i(d,{flexDirection:"column",children:[i(d,{children:[n(e,{dimColor:!0,children:"Waiting for target path..."}),n(e,{dimColor:!0,children:" (write path to ~/.cache/diffstalker/target)"})]}),g&&o&&o.enabled&&o.sourceFile&&i(d,{children:[i(e,{dimColor:!0,children:["[debug] source: ",p(o.sourceFile)]}),o.rawContent&&i(e,{dimColor:!0,children:[' | raw: "',o.rawContent,'"']})]})]});const a=p(t),r=c==="Not a git repository",h=x=>x?x.toLocaleTimeString():"";let m=0;l&&(m=l.current.length,l.tracking&&(m+=3+l.tracking.length),l.ahead>0&&(m+=3+String(l.ahead).length),l.behind>0&&(m+=3+String(l.behind).length));let u=a.length;s&&(u+=2),r&&(u+=24),c&&!r&&(u+=c.length+3);let f=null,C=!1;if(o?.enabled&&o.sourceFile){const F=` (follow: ${p(o.sourceFile)})`,k=y-u-m-4;if(F.length<=k)f=F;else{const v=y-u-2;F.length<=v&&(f=F,C=!0)}}return i(d,{flexDirection:"column",width:y,children:[C?i(W,{children:[n(d,{justifyContent:"space-between",children:i(d,{children:[n(e,{bold:!0,color:"cyan",children:a}),s&&n(e,{color:"yellow",children:" \u27F3"}),r&&n(e,{color:"yellow",children:" (not a git repository)"}),c&&!r&&i(e,{color:"red",children:[" (",c,")"]}),f&&n(e,{dimColor:!0,children:f})]})}),n(d,{justifyContent:"flex-end",children:l&&n(j,{branch:l})})]}):i(d,{justifyContent:"space-between",children:[i(d,{children:[n(e,{bold:!0,color:"cyan",children:a}),s&&n(e,{color:"yellow",children:" \u27F3"}),r&&n(e,{color:"yellow",children:" (not a git repository)"}),c&&!r&&i(e,{color:"red",children:[" (",c,")"]}),f&&n(e,{dimColor:!0,children:f})]}),l&&n(j,{branch:l})]}),g&&o&&o.enabled&&o.sourceFile&&i(d,{children:[i(e,{dimColor:!0,children:["[debug] source: ",p(o.sourceFile)]}),i(e,{dimColor:!0,children:[' | raw: "',o.rawContent,'"']}),o.lastUpdate&&i(e,{dimColor:!0,children:[" | updated: ",h(o.lastUpdate)]})]})]})}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{jsx as e,jsxs as i,Fragment as d}from"react/jsx-runtime";import{Box as u,Text as r}from"ink";import{ScrollableList as C}from"./ScrollableList.js";import{formatDate as w}from"../utils/formatDate.js";import{formatCommitDisplay as H}from"../utils/commitFormat.js";export{getCommitIndexFromRow,getHistoryTotalRows,getHistoryRowOffset}from"../utils/rowCalculations.js";export function HistoryView({commits:s,selectedIndex:m,scrollOffset:a,maxHeight:c,isActive:h,width:f,onSelectCommit:S}){return s.length===0?e(u,{children:e(r,{dimColor:!0,children:"No commits yet"})}):e(C,{items:s,maxHeight:c,scrollOffset:a,getKey:t=>t.hash,renderItem:(t,g)=>{const o=g===m&&h,n=w(t.date),p=11+n.length+2,x=f-p,{displayMessage:y,displayRefs:l}=H(t.message,t.refs,x);return i(d,{children:[e(r,{color:"yellow",children:t.shortHash}),e(r,{children:" "}),e(r,{color:o?"cyan":void 0,bold:o,inverse:o,children:y}),e(r,{children:" "}),i(r,{dimColor:!0,children:["(",n,")"]}),l&&i(d,{children:[e(r,{children:" "}),e(r,{color:"green",children:l})]})]})}})}
|