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
|
@@ -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
|
-
}
|