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,21 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
import { ScrollableList } from './ScrollableList.js';
|
|
4
|
-
import { formatDate } from '../utils/formatDate.js';
|
|
5
|
-
import { formatCommitDisplay } from '../utils/commitFormat.js';
|
|
6
|
-
// Re-export from utils for backwards compatibility
|
|
7
|
-
export { getCommitIndexFromRow, getHistoryTotalRows, getHistoryRowOffset, } from '../utils/rowCalculations.js';
|
|
8
|
-
export function HistoryView({ commits, selectedIndex, scrollOffset, maxHeight, isActive, width, onSelectCommit: _onSelectCommit, }) {
|
|
9
|
-
if (commits.length === 0) {
|
|
10
|
-
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "No commits yet" }) }));
|
|
11
|
-
}
|
|
12
|
-
return (_jsx(ScrollableList, { items: commits, maxHeight: maxHeight, scrollOffset: scrollOffset, getKey: (commit) => commit.hash, renderItem: (commit, actualIndex) => {
|
|
13
|
-
const isSelected = actualIndex === selectedIndex && isActive;
|
|
14
|
-
const dateStr = formatDate(commit.date);
|
|
15
|
-
// Fixed parts: hash(7) + spaces(4) + date + parens(2)
|
|
16
|
-
const baseWidth = 7 + 4 + dateStr.length + 2;
|
|
17
|
-
const remainingWidth = width - baseWidth;
|
|
18
|
-
const { displayMessage, displayRefs } = formatCommitDisplay(commit.message, commit.refs, remainingWidth);
|
|
19
|
-
return (_jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: commit.shortHash }), _jsx(Text, { children: " " }), _jsx(Text, { color: isSelected ? 'cyan' : undefined, bold: isSelected, inverse: isSelected, children: displayMessage }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: ["(", dateStr, ")"] }), displayRefs && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: "green", children: displayRefs })] }))] }));
|
|
20
|
-
} }));
|
|
21
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text, useInput } from 'ink';
|
|
3
|
-
import { Modal, centerModal } from './Modal.js';
|
|
4
|
-
const hotkeyGroups = [
|
|
5
|
-
{
|
|
6
|
-
title: 'Navigation',
|
|
7
|
-
entries: [
|
|
8
|
-
{ key: '↑/k', description: 'Move up' },
|
|
9
|
-
{ key: '↓/j', description: 'Move down' },
|
|
10
|
-
{ key: 'Tab', description: 'Toggle pane focus' },
|
|
11
|
-
],
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
title: 'Staging',
|
|
15
|
-
entries: [
|
|
16
|
-
{ key: '^S', description: 'Stage file' },
|
|
17
|
-
{ key: '^U', description: 'Unstage file' },
|
|
18
|
-
{ key: '^A', description: 'Stage all' },
|
|
19
|
-
{ key: '^Z', description: 'Unstage all' },
|
|
20
|
-
{ key: 'Space/Enter', description: 'Toggle stage' },
|
|
21
|
-
],
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
title: 'Actions',
|
|
25
|
-
entries: [
|
|
26
|
-
{ key: 'c', description: 'Open commit panel' },
|
|
27
|
-
{ key: 'r', description: 'Refresh' },
|
|
28
|
-
{ key: 'q', description: 'Quit' },
|
|
29
|
-
],
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
title: 'Pane Resize',
|
|
33
|
-
entries: [
|
|
34
|
-
{ key: '[', description: 'Shrink top pane' },
|
|
35
|
-
{ key: ']', description: 'Grow top pane' },
|
|
36
|
-
],
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
title: 'Tabs',
|
|
40
|
-
entries: [
|
|
41
|
-
{ key: '1', description: 'Diff view' },
|
|
42
|
-
{ key: '2', description: 'Commit panel' },
|
|
43
|
-
{ key: '3', description: 'History view' },
|
|
44
|
-
{ key: '4', description: 'Compare view' },
|
|
45
|
-
{ key: '5', description: 'Explorer view' },
|
|
46
|
-
{ key: 'a', description: 'Toggle auto-tab mode' },
|
|
47
|
-
],
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
title: 'Explorer',
|
|
51
|
-
entries: [
|
|
52
|
-
{ key: '.', description: 'Toggle middle-dots' },
|
|
53
|
-
{ key: '^H', description: 'Toggle hidden files' },
|
|
54
|
-
{ key: '^G', description: 'Toggle gitignored' },
|
|
55
|
-
{ key: 'Enter', description: 'Enter directory' },
|
|
56
|
-
{ key: 'Backspace/h', description: 'Go up' },
|
|
57
|
-
],
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
title: 'Other',
|
|
61
|
-
entries: [
|
|
62
|
-
{ key: 'm', description: 'Toggle scroll/select' },
|
|
63
|
-
{ key: 'f', description: 'Toggle follow mode' },
|
|
64
|
-
{ key: 'w', description: 'Toggle wrap mode' },
|
|
65
|
-
{ key: 't', description: 'Theme picker' },
|
|
66
|
-
{ key: 'b', description: 'Base branch picker' },
|
|
67
|
-
{ key: 'u', description: 'Toggle uncommitted' },
|
|
68
|
-
{ key: '?', description: 'This help' },
|
|
69
|
-
],
|
|
70
|
-
},
|
|
71
|
-
];
|
|
72
|
-
export function HotkeysModal({ onClose, width, height }) {
|
|
73
|
-
useInput((input, key) => {
|
|
74
|
-
if (key.escape || key.return || input === '?') {
|
|
75
|
-
onClose();
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
// Determine if we should use 2 columns (need at least 90 chars width)
|
|
79
|
-
const useTwoColumns = width >= 90;
|
|
80
|
-
const columnWidth = useTwoColumns ? 38 : 30;
|
|
81
|
-
const boxWidth = useTwoColumns ? Math.min(82, width - 4) : Math.min(40, width - 4);
|
|
82
|
-
// Calculate height based on layout
|
|
83
|
-
let boxHeight;
|
|
84
|
-
if (useTwoColumns) {
|
|
85
|
-
// Split groups into two columns
|
|
86
|
-
const midpoint = Math.ceil(hotkeyGroups.length / 2);
|
|
87
|
-
const leftGroups = hotkeyGroups.slice(0, midpoint);
|
|
88
|
-
const rightGroups = hotkeyGroups.slice(midpoint);
|
|
89
|
-
const leftLines = leftGroups.reduce((sum, g) => sum + g.entries.length + 2, 0);
|
|
90
|
-
const rightLines = rightGroups.reduce((sum, g) => sum + g.entries.length + 2, 0);
|
|
91
|
-
boxHeight = Math.min(Math.max(leftLines, rightLines) + 5, height - 4);
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
const totalLines = hotkeyGroups.reduce((sum, g) => sum + g.entries.length + 2, 0) + 4;
|
|
95
|
-
boxHeight = Math.min(totalLines, height - 4);
|
|
96
|
-
}
|
|
97
|
-
// Center the modal
|
|
98
|
-
const { x, y } = centerModal(boxWidth, boxHeight, width, height);
|
|
99
|
-
// Render a single group
|
|
100
|
-
const renderGroup = (group, colWidth) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: group.title }), group.entries.map((entry) => (_jsxs(Box, { children: [_jsx(Box, { width: 13, children: _jsx(Text, { color: "cyan", children: entry.key }) }), _jsx(Box, { width: colWidth - 13, children: _jsx(Text, { children: entry.description }) })] }, entry.key)))] }, group.title));
|
|
101
|
-
if (useTwoColumns) {
|
|
102
|
-
const midpoint = Math.ceil(hotkeyGroups.length / 2);
|
|
103
|
-
const leftGroups = hotkeyGroups.slice(0, midpoint);
|
|
104
|
-
const rightGroups = hotkeyGroups.slice(midpoint);
|
|
105
|
-
return (_jsx(Modal, { x: x, y: y, width: boxWidth, height: boxHeight, children: _jsxs(Box, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", width: boxWidth, children: [_jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsxs(Text, { bold: true, color: "cyan", children: [' ', "Keyboard Shortcuts", ' '] }) }), _jsxs(Box, { children: [_jsx(Box, { flexDirection: "column", width: columnWidth, marginRight: 2, children: leftGroups.map((g) => renderGroup(g, columnWidth)) }), _jsx(Box, { flexDirection: "column", width: columnWidth, children: rightGroups.map((g) => renderGroup(g, columnWidth)) })] }), _jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsx(Text, { dimColor: true, children: "Press Esc, Enter, or ? to close" }) })] }) }));
|
|
106
|
-
}
|
|
107
|
-
return (_jsx(Modal, { x: x, y: y, width: boxWidth, height: boxHeight, children: _jsxs(Box, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", width: boxWidth, children: [_jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsxs(Text, { bold: true, color: "cyan", children: [' ', "Keyboard Shortcuts", ' '] }) }), hotkeyGroups.map((group) => renderGroup(group, columnWidth)), _jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsx(Text, { dimColor: true, children: "Press Esc, Enter, or ? to close" }) })] }) }));
|
|
108
|
-
}
|
package/dist/components/Modal.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
/**
|
|
4
|
-
* A modal overlay that blankets only its own area before rendering children.
|
|
5
|
-
* Use this to create popups that cover the content behind them.
|
|
6
|
-
*/
|
|
7
|
-
export function Modal({ x, y, width, height, children }) {
|
|
8
|
-
const blankLine = ' '.repeat(width);
|
|
9
|
-
return (_jsxs(Box, { position: "absolute", marginLeft: x, marginTop: y, flexDirection: "column", children: [Array.from({ length: height }).map((_, i) => (_jsx(Text, { children: blankLine }, `blank-${i}`))), _jsx(Box, { position: "absolute", flexDirection: "column", children: children })] }));
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Helper to calculate centered modal position.
|
|
13
|
-
*/
|
|
14
|
-
export function centerModal(modalWidth, modalHeight, screenWidth, screenHeight) {
|
|
15
|
-
return {
|
|
16
|
-
x: Math.floor((screenWidth - modalWidth) / 2),
|
|
17
|
-
y: Math.floor((screenHeight - modalHeight) / 2),
|
|
18
|
-
};
|
|
19
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo } from 'react';
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
4
|
-
/**
|
|
5
|
-
* A generic scrollable list component that properly handles:
|
|
6
|
-
* - Scroll indicators (↑/↓) taking up space
|
|
7
|
-
* - Consistent height calculations
|
|
8
|
-
* - Proper React keys for re-rendering
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* ```tsx
|
|
12
|
-
* <ScrollableList
|
|
13
|
-
* items={myItems}
|
|
14
|
-
* renderItem={(item, i) => <MyRow item={item} />}
|
|
15
|
-
* maxHeight={20}
|
|
16
|
-
* scrollOffset={offset}
|
|
17
|
-
* getKey={(item, i) => item.id}
|
|
18
|
-
* />
|
|
19
|
-
* ```
|
|
20
|
-
*/
|
|
21
|
-
export function ScrollableList({ items, renderItem, maxHeight, scrollOffset, getKey, header, showIndicators = true, getItemHeight, }) {
|
|
22
|
-
// If getItemHeight is not provided, use simple item-based scrolling
|
|
23
|
-
const hasVariableHeight = !!getItemHeight;
|
|
24
|
-
// Calculate cumulative row positions for variable height items
|
|
25
|
-
const { itemRowStarts, totalRows } = useMemo(() => {
|
|
26
|
-
if (!hasVariableHeight) {
|
|
27
|
-
return { itemRowStarts: [], totalRows: items.length };
|
|
28
|
-
}
|
|
29
|
-
const starts = [];
|
|
30
|
-
let cumulative = 0;
|
|
31
|
-
for (let i = 0; i < items.length; i++) {
|
|
32
|
-
starts.push(cumulative);
|
|
33
|
-
cumulative += getItemHeight(items[i], i);
|
|
34
|
-
}
|
|
35
|
-
return { itemRowStarts: starts, totalRows: cumulative };
|
|
36
|
-
}, [items, getItemHeight, hasVariableHeight]);
|
|
37
|
-
// Calculate available space for actual content
|
|
38
|
-
let availableHeight = maxHeight;
|
|
39
|
-
// Reserve space for header if present
|
|
40
|
-
if (header) {
|
|
41
|
-
availableHeight--;
|
|
42
|
-
}
|
|
43
|
-
const hasPrevious = scrollOffset > 0;
|
|
44
|
-
const contentTotal = hasVariableHeight ? totalRows : items.length;
|
|
45
|
-
const needsScrolling = contentTotal > maxHeight;
|
|
46
|
-
// Simple rule: if content needs scrolling, ALWAYS reserve 2 rows for indicators
|
|
47
|
-
// No clever predictions - just consistent, predictable behavior
|
|
48
|
-
if (showIndicators && needsScrolling) {
|
|
49
|
-
availableHeight -= 2;
|
|
50
|
-
}
|
|
51
|
-
// Ensure we have at least 1 line for content
|
|
52
|
-
availableHeight = Math.max(1, availableHeight);
|
|
53
|
-
// Find visible items based on scroll offset (in rows)
|
|
54
|
-
const visibleItems = [];
|
|
55
|
-
let usedRows = 0;
|
|
56
|
-
let rowsAbove = 0;
|
|
57
|
-
let rowsBelow = 0;
|
|
58
|
-
if (hasVariableHeight) {
|
|
59
|
-
// Find first visible item (the one that contains scrollOffset row)
|
|
60
|
-
let startIdx = 0;
|
|
61
|
-
for (let i = 0; i < items.length; i++) {
|
|
62
|
-
const itemHeight = getItemHeight(items[i], i);
|
|
63
|
-
if (itemRowStarts[i] + itemHeight > scrollOffset) {
|
|
64
|
-
startIdx = i;
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
// Collect items that fit in available height
|
|
69
|
-
for (let i = startIdx; i < items.length && usedRows < availableHeight; i++) {
|
|
70
|
-
const itemHeight = getItemHeight(items[i], i);
|
|
71
|
-
visibleItems.push({ item: items[i], index: i });
|
|
72
|
-
usedRows += itemHeight;
|
|
73
|
-
}
|
|
74
|
-
rowsAbove = scrollOffset;
|
|
75
|
-
// Simple calculation: total rows minus what we've scrolled past minus what we're showing
|
|
76
|
-
rowsBelow = Math.max(0, totalRows - scrollOffset - usedRows);
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
// Simple item-based scrolling (1 item = 1 row)
|
|
80
|
-
const endIdx = Math.min(scrollOffset + availableHeight, items.length);
|
|
81
|
-
for (let i = scrollOffset; i < endIdx; i++) {
|
|
82
|
-
visibleItems.push({ item: items[i], index: i });
|
|
83
|
-
usedRows++;
|
|
84
|
-
}
|
|
85
|
-
rowsAbove = scrollOffset;
|
|
86
|
-
rowsBelow = Math.max(0, items.length - scrollOffset - usedRows);
|
|
87
|
-
}
|
|
88
|
-
return (_jsxs(Box, { flexDirection: "column", overflowX: "hidden", height: maxHeight, overflow: "hidden", children: [header, showIndicators &&
|
|
89
|
-
needsScrolling &&
|
|
90
|
-
(hasPrevious ? _jsxs(Text, { dimColor: true, children: ["\u2191 ", rowsAbove, " more above"] }) : _jsx(Text, { children: " " })), visibleItems.map(({ item, index }) => (_jsx(Box, { children: renderItem(item, index) }, `${scrollOffset}-${index}-${getKey(item, index)}`))), showIndicators &&
|
|
91
|
-
needsScrolling &&
|
|
92
|
-
(rowsBelow > 0 ? _jsxs(Text, { dimColor: true, children: ["\u2193 ", rowsBelow, " more below"] }) : _jsx(Text, { children: " " }))] }));
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Calculate the maximum scroll offset for a list.
|
|
96
|
-
*/
|
|
97
|
-
export function getMaxScrollOffset(totalItems, maxHeight, hasHeader = false, showIndicators = true) {
|
|
98
|
-
let availableHeight = maxHeight;
|
|
99
|
-
if (hasHeader)
|
|
100
|
-
availableHeight--;
|
|
101
|
-
// When scrolled, we always show "↑ above" indicator
|
|
102
|
-
// and usually "↓ below" indicator, so subtract 2
|
|
103
|
-
if (showIndicators && totalItems > availableHeight) {
|
|
104
|
-
availableHeight -= 2;
|
|
105
|
-
}
|
|
106
|
-
availableHeight = Math.max(1, availableHeight);
|
|
107
|
-
return Math.max(0, totalItems - availableHeight);
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Calculate visible item count for a given configuration.
|
|
111
|
-
* Useful for scroll calculations in parent components.
|
|
112
|
-
*/
|
|
113
|
-
export function getVisibleItemCount(totalItems, maxHeight, scrollOffset, hasHeader = false, showIndicators = true) {
|
|
114
|
-
let availableHeight = maxHeight;
|
|
115
|
-
if (hasHeader)
|
|
116
|
-
availableHeight--;
|
|
117
|
-
if (showIndicators) {
|
|
118
|
-
if (scrollOffset > 0)
|
|
119
|
-
availableHeight--;
|
|
120
|
-
if (totalItems > scrollOffset + availableHeight)
|
|
121
|
-
availableHeight--;
|
|
122
|
-
}
|
|
123
|
-
availableHeight = Math.max(1, availableHeight);
|
|
124
|
-
return Math.min(availableHeight, totalItems - scrollOffset);
|
|
125
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import { Box, Text, useInput } from 'ink';
|
|
4
|
-
import { themes, themeOrder, getTheme } from '../themes.js';
|
|
5
|
-
import { Modal, centerModal } from './Modal.js';
|
|
6
|
-
// Preview sample for theme visualization
|
|
7
|
-
function ThemePreview({ theme }) {
|
|
8
|
-
const { colors } = theme;
|
|
9
|
-
return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { backgroundColor: colors.delBg, color: colors.delLineNum, children: ' 5 ' }), _jsx(Text, { backgroundColor: colors.delBg, color: colors.delSymbol, bold: true, children: '- ' }), _jsx(Text, { backgroundColor: colors.delBg, color: colors.text, children: 'const ' }), _jsx(Text, { backgroundColor: colors.delHighlight, color: colors.text, children: 'old' }), _jsx(Text, { backgroundColor: colors.delBg, color: colors.text, children: ' = value;' })] }), _jsxs(Box, { children: [_jsx(Text, { backgroundColor: colors.addBg, color: colors.addLineNum, children: ' 5 ' }), _jsx(Text, { backgroundColor: colors.addBg, color: colors.addSymbol, bold: true, children: '+ ' }), _jsx(Text, { backgroundColor: colors.addBg, color: colors.text, children: 'const ' }), _jsx(Text, { backgroundColor: colors.addHighlight, color: colors.text, children: 'new' }), _jsx(Text, { backgroundColor: colors.addBg, color: colors.text, children: ' = value;' })] })] }));
|
|
10
|
-
}
|
|
11
|
-
export function ThemePicker({ currentTheme, onSelect, onCancel, width, height, }) {
|
|
12
|
-
const [selectedIndex, setSelectedIndex] = useState(() => {
|
|
13
|
-
const idx = themeOrder.indexOf(currentTheme);
|
|
14
|
-
return idx >= 0 ? idx : 0;
|
|
15
|
-
});
|
|
16
|
-
const previewTheme = getTheme(themeOrder[selectedIndex]);
|
|
17
|
-
useInput((input, key) => {
|
|
18
|
-
if (key.escape) {
|
|
19
|
-
onCancel();
|
|
20
|
-
}
|
|
21
|
-
else if (key.return) {
|
|
22
|
-
onSelect(themeOrder[selectedIndex]);
|
|
23
|
-
}
|
|
24
|
-
else if (key.upArrow || input === 'k') {
|
|
25
|
-
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
26
|
-
}
|
|
27
|
-
else if (key.downArrow || input === 'j') {
|
|
28
|
-
setSelectedIndex((prev) => Math.min(themeOrder.length - 1, prev + 1));
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
// Calculate box dimensions
|
|
32
|
-
const boxWidth = Math.min(50, width - 4);
|
|
33
|
-
const boxHeight = Math.min(themeOrder.length + 10, height - 4); // +10 for header, preview, footer, borders
|
|
34
|
-
// Center the modal
|
|
35
|
-
const { x, y } = centerModal(boxWidth, boxHeight, width, height);
|
|
36
|
-
return (_jsx(Modal, { x: x, y: y, width: boxWidth, height: boxHeight, children: _jsxs(Box, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", width: boxWidth, children: [_jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsxs(Text, { bold: true, color: "cyan", children: [' ', "Select Theme", ' '] }) }), themeOrder.map((themeName, index) => {
|
|
37
|
-
const theme = themes[themeName];
|
|
38
|
-
const isSelected = index === selectedIndex;
|
|
39
|
-
const isCurrent = themeName === currentTheme;
|
|
40
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? 'cyan' : undefined, children: isSelected ? '▸ ' : ' ' }), _jsx(Text, { bold: isSelected, color: isSelected ? 'cyan' : undefined, children: theme.displayName }), isCurrent && _jsx(Text, { dimColor: true, children: " (current)" })] }, themeName));
|
|
41
|
-
}), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Preview:" }), _jsx(ThemePreview, { theme: previewTheme })] }), _jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate \u2022 Enter select \u2022 Esc cancel" }) })] }) }));
|
|
42
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
4
|
-
import { FileList } from './FileList.js';
|
|
5
|
-
import { HistoryView } from './HistoryView.js';
|
|
6
|
-
import { CompareListView } from './CompareListView.js';
|
|
7
|
-
import { ExplorerView, buildBreadcrumbs } from './ExplorerView.js';
|
|
8
|
-
import { categorizeFiles } from '../utils/fileCategories.js';
|
|
9
|
-
export function TopPane({ bottomTab, currentPane, terminalWidth, topPaneHeight, files, selectedIndex, fileListScrollOffset, stagedCount, onStage, onUnstage, commits, historySelectedIndex, historyScrollOffset, onSelectHistoryCommit, compareDiff, compareListSelection, compareScrollOffset, includeUncommitted, explorerCurrentPath = '', explorerItems = [], explorerSelectedIndex = 0, explorerScrollOffset = 0, explorerIsLoading = false, explorerError = null, hideHiddenFiles = true, hideGitignored = true, }) {
|
|
10
|
-
const { modified, untracked } = categorizeFiles(files);
|
|
11
|
-
const modifiedCount = modified.length;
|
|
12
|
-
const untrackedCount = untracked.length;
|
|
13
|
-
return (_jsxs(Box, { flexDirection: "column", height: topPaneHeight, width: terminalWidth, overflowX: "hidden", overflowY: "hidden", children: [(bottomTab === 'diff' || bottomTab === 'commit') && (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: currentPane === 'files' ? 'cyan' : undefined, children: "STAGING AREA" }), _jsxs(Text, { dimColor: true, children: [' ', "(", modifiedCount, " modified, ", untrackedCount, " untracked, ", stagedCount, " staged)"] })] }), _jsx(FileList, { files: files, selectedIndex: selectedIndex, isFocused: currentPane === 'files', scrollOffset: fileListScrollOffset, maxHeight: topPaneHeight - 1, width: terminalWidth, onStage: onStage, onUnstage: onUnstage })] })), bottomTab === 'history' && (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: currentPane === 'history' ? 'cyan' : undefined, children: "COMMITS" }), _jsxs(Text, { dimColor: true, children: [" (", commits.length, " commits)"] })] }), _jsx(HistoryView, { commits: commits, selectedIndex: historySelectedIndex, scrollOffset: historyScrollOffset, maxHeight: topPaneHeight - 1, isActive: currentPane === 'history', width: terminalWidth, onSelectCommit: onSelectHistoryCommit })] })), bottomTab === 'compare' && (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: currentPane === 'compare' ? 'cyan' : undefined, children: "COMPARE" }), _jsx(Text, { dimColor: true, children: " (vs " }), _jsx(Text, { color: "cyan", children: compareDiff?.baseBranch ?? '...' }), _jsxs(Text, { dimColor: true, children: [": ", compareDiff?.commits.length ?? 0, " commits, ", compareDiff?.files.length ?? 0, " files) (b)"] }), compareDiff && compareDiff.uncommittedCount > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " | " }), _jsxs(Text, { color: includeUncommitted ? 'magenta' : 'yellow', children: ["[", includeUncommitted ? 'x' : ' ', "] uncommitted"] }), _jsx(Text, { dimColor: true, children: " (u)" })] }))] }), _jsx(CompareListView, { commits: compareDiff?.commits ?? [], files: compareDiff?.files ?? [], selectedItem: compareListSelection, scrollOffset: compareScrollOffset, maxHeight: topPaneHeight - 1, isActive: currentPane === 'compare', width: terminalWidth })] })), bottomTab === 'explorer' && (_jsxs(_Fragment, { children: [_jsxs(Box, { justifyContent: "space-between", width: terminalWidth, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: currentPane === 'explorer' ? 'cyan' : undefined, children: "EXPLORER" }), _jsx(Text, { dimColor: true, children: " " }), buildBreadcrumbs(explorerCurrentPath).map((segment, i, arr) => (_jsxs(React.Fragment, { children: [_jsx(Text, { color: "blue", children: segment }), i < arr.length - 1 && _jsx(Text, { dimColor: true, children: " / " })] }, i))), explorerCurrentPath && _jsx(Text, { dimColor: true, children: " /" }), !explorerCurrentPath && _jsx(Text, { dimColor: true, children: "(root)" })] }), _jsx(Box, { children: (hideHiddenFiles || hideGitignored) && (_jsxs(Text, { dimColor: true, children: [hideHiddenFiles && 'H', hideGitignored && 'G'] })) })] }), _jsx(ExplorerView, { currentPath: explorerCurrentPath, items: explorerItems, selectedIndex: explorerSelectedIndex, scrollOffset: explorerScrollOffset, maxHeight: topPaneHeight - 1, isActive: currentPane === 'explorer', width: terminalWidth, isLoading: explorerIsLoading, error: explorerError })] }))] }));
|
|
14
|
-
}
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo } from 'react';
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
4
|
-
import { getTheme } from '../themes.js';
|
|
5
|
-
import { ScrollableList } from './ScrollableList.js';
|
|
6
|
-
import { getDisplayRowsLineNumWidth } from '../utils/displayRows.js';
|
|
7
|
-
// Truncate string to fit within maxWidth, adding ellipsis if needed
|
|
8
|
-
function truncate(str, maxWidth) {
|
|
9
|
-
if (maxWidth <= 0 || str.length <= maxWidth)
|
|
10
|
-
return str;
|
|
11
|
-
if (maxWidth <= 1)
|
|
12
|
-
return '\u2026';
|
|
13
|
-
return str.slice(0, maxWidth - 1) + '\u2026';
|
|
14
|
-
}
|
|
15
|
-
// Format line number with padding
|
|
16
|
-
function formatLineNum(lineNum, width) {
|
|
17
|
-
if (lineNum === undefined)
|
|
18
|
-
return ' '.repeat(width);
|
|
19
|
-
return String(lineNum).padStart(width, ' ');
|
|
20
|
-
}
|
|
21
|
-
function DisplayRowRenderer({ row, lineNumWidth, width, theme, wrapMode, }) {
|
|
22
|
-
const { colors } = theme;
|
|
23
|
-
// Available width for content: width - paddingX(1) - lineNum - space(1) - symbol(1) - space(1) - paddingX(1)
|
|
24
|
-
const contentWidth = width - lineNumWidth - 5;
|
|
25
|
-
// Width for headers (just subtract paddingX on each side)
|
|
26
|
-
const headerWidth = width - 2;
|
|
27
|
-
switch (row.type) {
|
|
28
|
-
case 'diff-header': {
|
|
29
|
-
// Extract file path from diff --git and show as clean separator
|
|
30
|
-
const content = row.content;
|
|
31
|
-
if (content.startsWith('diff --git')) {
|
|
32
|
-
const match = content.match(/diff --git a\/.+ b\/(.+)$/);
|
|
33
|
-
if (match) {
|
|
34
|
-
const maxPathLen = headerWidth - 6; // "── " + " ──"
|
|
35
|
-
const path = truncate(match[1], maxPathLen);
|
|
36
|
-
return (_jsxs(Text, { color: "cyan", bold: true, children: ["\u2500\u2500 ", path, " \u2500\u2500"] }));
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return _jsx(Text, { dimColor: true, children: truncate(content, headerWidth) });
|
|
40
|
-
}
|
|
41
|
-
case 'diff-hunk': {
|
|
42
|
-
// Parse hunk header: @@ -oldStart,oldCount +newStart,newCount @@ context
|
|
43
|
-
const match = row.content.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/);
|
|
44
|
-
if (match) {
|
|
45
|
-
const oldStart = parseInt(match[1], 10);
|
|
46
|
-
const oldCount = match[2] ? parseInt(match[2], 10) : 1;
|
|
47
|
-
const newStart = parseInt(match[3], 10);
|
|
48
|
-
const newCount = match[4] ? parseInt(match[4], 10) : 1;
|
|
49
|
-
const context = match[5].trim();
|
|
50
|
-
const oldEnd = oldStart + oldCount - 1;
|
|
51
|
-
const newEnd = newStart + newCount - 1;
|
|
52
|
-
const oldRange = oldCount === 1 ? `${oldStart}` : `${oldStart}-${oldEnd}`;
|
|
53
|
-
const newRange = newCount === 1 ? `${newStart}` : `${newStart}-${newEnd}`;
|
|
54
|
-
const rangeText = `Lines ${oldRange} \u2192 ${newRange}`;
|
|
55
|
-
const contextMaxLen = headerWidth - rangeText.length - 1;
|
|
56
|
-
const truncatedContext = context && contextMaxLen > 3 ? ' ' + truncate(context, contextMaxLen) : '';
|
|
57
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", dimColor: true, children: rangeText }), truncatedContext && _jsx(Text, { color: "gray", children: truncatedContext })] }));
|
|
58
|
-
}
|
|
59
|
-
return (_jsx(Text, { color: "cyan", dimColor: true, children: truncate(row.content, headerWidth) }));
|
|
60
|
-
}
|
|
61
|
-
case 'diff-add': {
|
|
62
|
-
const isCont = row.isContinuation;
|
|
63
|
-
// Use » for continuation - it's single-width and renders background correctly
|
|
64
|
-
const symbol = isCont ? '\u00bb' : '+';
|
|
65
|
-
const rawContent = wrapMode ? row.content || '' : truncate(row.content, contentWidth) || '';
|
|
66
|
-
// Always prepend space to content
|
|
67
|
-
const content = ' ' + rawContent || ' ';
|
|
68
|
-
return (_jsxs(Box, { children: [_jsx(Text, { backgroundColor: colors.addBg, color: colors.addLineNum, children: formatLineNum(row.lineNum, lineNumWidth) + ' ' }), _jsx(Text, { backgroundColor: colors.addBg, color: isCont ? colors.addLineNum : colors.addSymbol, bold: !isCont, children: symbol }), _jsx(Text, { backgroundColor: colors.addBg, color: colors.text, children: content })] }));
|
|
69
|
-
}
|
|
70
|
-
case 'diff-del': {
|
|
71
|
-
const isCont = row.isContinuation;
|
|
72
|
-
// Use » for continuation - it's single-width and renders background correctly
|
|
73
|
-
const symbol = isCont ? '\u00bb' : '-';
|
|
74
|
-
const rawContent = wrapMode ? row.content || '' : truncate(row.content, contentWidth) || '';
|
|
75
|
-
// Always prepend space to content
|
|
76
|
-
const content = ' ' + rawContent || ' ';
|
|
77
|
-
return (_jsxs(Box, { children: [_jsx(Text, { backgroundColor: colors.delBg, color: colors.delLineNum, children: formatLineNum(row.lineNum, lineNumWidth) + ' ' }), _jsx(Text, { backgroundColor: colors.delBg, color: isCont ? colors.delLineNum : colors.delSymbol, bold: !isCont, children: symbol }), _jsx(Text, { backgroundColor: colors.delBg, color: colors.text, children: content })] }));
|
|
78
|
-
}
|
|
79
|
-
case 'diff-context': {
|
|
80
|
-
const isCont = row.isContinuation;
|
|
81
|
-
// Use » for continuation - it's single-width and renders correctly
|
|
82
|
-
const symbol = isCont ? '\u00bb ' : ' ';
|
|
83
|
-
const content = wrapMode ? row.content : truncate(row.content, contentWidth);
|
|
84
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { color: colors.contextLineNum, children: [formatLineNum(row.lineNum, lineNumWidth), " "] }), _jsx(Text, { dimColor: true, children: symbol }), _jsx(Text, { children: content })] }));
|
|
85
|
-
}
|
|
86
|
-
case 'commit-header':
|
|
87
|
-
return _jsx(Text, { color: "yellow", children: truncate(row.content, headerWidth) });
|
|
88
|
-
case 'commit-message':
|
|
89
|
-
return _jsx(Text, { children: truncate(row.content, headerWidth) });
|
|
90
|
-
case 'spacer':
|
|
91
|
-
return _jsx(Text, { children: " " });
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* The ONE diff renderer used by all tabs.
|
|
96
|
-
* Every row = exactly 1 terminal row.
|
|
97
|
-
* No variable heights, no complexity.
|
|
98
|
-
*/
|
|
99
|
-
export function UnifiedDiffView({ rows, maxHeight, scrollOffset, theme: themeName, width, wrapMode = false, }) {
|
|
100
|
-
const theme = useMemo(() => getTheme(themeName), [themeName]);
|
|
101
|
-
const lineNumWidth = useMemo(() => getDisplayRowsLineNumWidth(rows), [rows]);
|
|
102
|
-
if (rows.length === 0) {
|
|
103
|
-
return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "No diff to display" }) }));
|
|
104
|
-
}
|
|
105
|
-
return (_jsx(Box, { flexDirection: "column", paddingX: 1, width: width, children: _jsx(ScrollableList, { items: rows, maxHeight: maxHeight, scrollOffset: scrollOffset, getKey: (_, i) => `row-${i}`,
|
|
106
|
-
// NO getItemHeight - all rows are 1 line
|
|
107
|
-
renderItem: (row) => (_jsx(DisplayRowRenderer, { row: row, lineNumWidth: lineNumWidth, width: width, theme: theme, wrapMode: wrapMode })) }) }));
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Get total row count for scroll calculation.
|
|
111
|
-
* Since every row = 1 terminal row, this is just rows.length.
|
|
112
|
-
*/
|
|
113
|
-
export function getUnifiedDiffTotalRows(rows) {
|
|
114
|
-
return rows.length;
|
|
115
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { useState, useCallback, useEffect } from 'react';
|
|
2
|
-
import { validateCommit, formatCommitMessage } from '../services/commitService.js';
|
|
3
|
-
/**
|
|
4
|
-
* Hook that manages the commit flow state and logic.
|
|
5
|
-
* Extracted from CommitPanel to separate concerns.
|
|
6
|
-
*/
|
|
7
|
-
export function useCommitFlow(options) {
|
|
8
|
-
const { stagedCount, onCommit, onSuccess, getHeadMessage } = options;
|
|
9
|
-
const [message, setMessage] = useState('');
|
|
10
|
-
const [amend, setAmend] = useState(false);
|
|
11
|
-
const [isCommitting, setIsCommitting] = useState(false);
|
|
12
|
-
const [error, setError] = useState(null);
|
|
13
|
-
const [inputFocused, setInputFocused] = useState(false);
|
|
14
|
-
// Load HEAD message when amend is toggled
|
|
15
|
-
useEffect(() => {
|
|
16
|
-
if (amend) {
|
|
17
|
-
getHeadMessage().then((msg) => {
|
|
18
|
-
if (msg && !message) {
|
|
19
|
-
setMessage(msg);
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
}, [amend, getHeadMessage]);
|
|
24
|
-
const toggleAmend = useCallback(() => {
|
|
25
|
-
setAmend((prev) => !prev);
|
|
26
|
-
}, []);
|
|
27
|
-
const handleSubmit = useCallback(async () => {
|
|
28
|
-
const validation = validateCommit(message, stagedCount, amend);
|
|
29
|
-
if (!validation.valid) {
|
|
30
|
-
setError(validation.error);
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
setIsCommitting(true);
|
|
34
|
-
setError(null);
|
|
35
|
-
try {
|
|
36
|
-
await onCommit(formatCommitMessage(message), amend);
|
|
37
|
-
setMessage('');
|
|
38
|
-
setAmend(false);
|
|
39
|
-
onSuccess();
|
|
40
|
-
}
|
|
41
|
-
catch (err) {
|
|
42
|
-
setError(err instanceof Error ? err.message : 'Commit failed');
|
|
43
|
-
}
|
|
44
|
-
finally {
|
|
45
|
-
setIsCommitting(false);
|
|
46
|
-
}
|
|
47
|
-
}, [message, stagedCount, amend, onCommit, onSuccess]);
|
|
48
|
-
const reset = useCallback(() => {
|
|
49
|
-
setMessage('');
|
|
50
|
-
setAmend(false);
|
|
51
|
-
setError(null);
|
|
52
|
-
setInputFocused(false);
|
|
53
|
-
}, []);
|
|
54
|
-
return {
|
|
55
|
-
message,
|
|
56
|
-
amend,
|
|
57
|
-
isCommitting,
|
|
58
|
-
error,
|
|
59
|
-
inputFocused,
|
|
60
|
-
setMessage,
|
|
61
|
-
toggleAmend,
|
|
62
|
-
setInputFocused,
|
|
63
|
-
handleSubmit,
|
|
64
|
-
reset,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
2
|
-
import { getCompareItemIndexFromRow, getFileScrollOffset } from '../utils/rowCalculations.js';
|
|
3
|
-
import { buildCompareDisplayRows, getDisplayRowsLineNumWidth, getWrappedRowCount, } from '../utils/displayRows.js';
|
|
4
|
-
export function useCompareState({ repoPath, isActive, compareDiff, refreshCompareDiff, getCandidateBaseBranches, setCompareBaseBranch, selectCompareCommit, topPaneHeight, compareScrollOffset, setCompareScrollOffset, setDiffScrollOffset, status, wrapMode, terminalWidth, }) {
|
|
5
|
-
const [includeUncommitted, setIncludeUncommitted] = useState(true);
|
|
6
|
-
const [compareListSelection, setCompareListSelection] = useState(null);
|
|
7
|
-
const [compareSelectedIndex, setCompareSelectedIndex] = useState(0);
|
|
8
|
-
const compareSelectionInitialized = useRef(false);
|
|
9
|
-
const [baseBranchCandidates, setBaseBranchCandidates] = useState([]);
|
|
10
|
-
const [showBaseBranchPicker, setShowBaseBranchPicker] = useState(false);
|
|
11
|
-
// Fetch compare diff when tab becomes active
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
if (repoPath && isActive) {
|
|
14
|
-
refreshCompareDiff(includeUncommitted);
|
|
15
|
-
}
|
|
16
|
-
}, [repoPath, isActive, status, refreshCompareDiff, includeUncommitted]);
|
|
17
|
-
// Fetch base branch candidates when entering compare view
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
if (repoPath && isActive) {
|
|
20
|
-
getCandidateBaseBranches().then(setBaseBranchCandidates);
|
|
21
|
-
}
|
|
22
|
-
}, [repoPath, isActive, getCandidateBaseBranches]);
|
|
23
|
-
// Reset compare selection state when entering compare tab
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
if (isActive) {
|
|
26
|
-
compareSelectionInitialized.current = false;
|
|
27
|
-
setCompareListSelection(null);
|
|
28
|
-
setDiffScrollOffset(0);
|
|
29
|
-
}
|
|
30
|
-
}, [isActive, setDiffScrollOffset]);
|
|
31
|
-
// Update compare selection when compareSelectedIndex changes (only after user interaction)
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
if (isActive && compareDiff && compareSelectionInitialized.current) {
|
|
34
|
-
const commitCount = compareDiff.commits.length;
|
|
35
|
-
const fileCount = compareDiff.files.length;
|
|
36
|
-
if (compareSelectedIndex < commitCount) {
|
|
37
|
-
setCompareListSelection({ type: 'commit', index: compareSelectedIndex });
|
|
38
|
-
selectCompareCommit(compareSelectedIndex);
|
|
39
|
-
setDiffScrollOffset(0);
|
|
40
|
-
}
|
|
41
|
-
else if (compareSelectedIndex < commitCount + fileCount) {
|
|
42
|
-
const fileIndex = compareSelectedIndex - commitCount;
|
|
43
|
-
setCompareListSelection({ type: 'file', index: fileIndex });
|
|
44
|
-
const scrollTo = getFileScrollOffset(compareDiff, fileIndex);
|
|
45
|
-
setDiffScrollOffset(scrollTo);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}, [isActive, compareDiff, compareSelectedIndex, selectCompareCommit, setDiffScrollOffset]);
|
|
49
|
-
// Computed values
|
|
50
|
-
const compareTotalItems = useMemo(() => {
|
|
51
|
-
if (!compareDiff)
|
|
52
|
-
return 0;
|
|
53
|
-
return compareDiff.commits.length + compareDiff.files.length;
|
|
54
|
-
}, [compareDiff]);
|
|
55
|
-
// When wrap mode is enabled, account for wrapped lines
|
|
56
|
-
const compareDiffTotalRows = useMemo(() => {
|
|
57
|
-
const displayRows = buildCompareDisplayRows(compareDiff);
|
|
58
|
-
if (!wrapMode)
|
|
59
|
-
return displayRows.length;
|
|
60
|
-
const lineNumWidth = getDisplayRowsLineNumWidth(displayRows);
|
|
61
|
-
const contentWidth = terminalWidth - lineNumWidth - 5;
|
|
62
|
-
return getWrappedRowCount(displayRows, contentWidth, true);
|
|
63
|
-
}, [compareDiff, wrapMode, terminalWidth]);
|
|
64
|
-
// Handlers
|
|
65
|
-
const toggleIncludeUncommitted = useCallback(() => {
|
|
66
|
-
setIncludeUncommitted((prev) => !prev);
|
|
67
|
-
}, []);
|
|
68
|
-
const openBaseBranchPicker = useCallback(() => {
|
|
69
|
-
setShowBaseBranchPicker(true);
|
|
70
|
-
}, []);
|
|
71
|
-
const closeBaseBranchPicker = useCallback(() => {
|
|
72
|
-
setShowBaseBranchPicker(false);
|
|
73
|
-
}, []);
|
|
74
|
-
const selectBaseBranch = useCallback((branch) => {
|
|
75
|
-
setShowBaseBranchPicker(false);
|
|
76
|
-
setCompareBaseBranch(branch, includeUncommitted);
|
|
77
|
-
}, [setCompareBaseBranch, includeUncommitted]);
|
|
78
|
-
const markSelectionInitialized = useCallback(() => {
|
|
79
|
-
compareSelectionInitialized.current = true;
|
|
80
|
-
}, []);
|
|
81
|
-
const navigateCompareUp = useCallback(() => {
|
|
82
|
-
compareSelectionInitialized.current = true;
|
|
83
|
-
setCompareSelectedIndex((prev) => {
|
|
84
|
-
const newIndex = Math.max(0, prev - 1);
|
|
85
|
-
if (newIndex < compareScrollOffset)
|
|
86
|
-
setCompareScrollOffset(newIndex);
|
|
87
|
-
return newIndex;
|
|
88
|
-
});
|
|
89
|
-
}, [compareScrollOffset, setCompareScrollOffset]);
|
|
90
|
-
const navigateCompareDown = useCallback(() => {
|
|
91
|
-
compareSelectionInitialized.current = true;
|
|
92
|
-
setCompareSelectedIndex((prev) => {
|
|
93
|
-
const newIndex = Math.min(compareTotalItems - 1, prev + 1);
|
|
94
|
-
const visibleEnd = compareScrollOffset + topPaneHeight - 2;
|
|
95
|
-
if (newIndex >= visibleEnd)
|
|
96
|
-
setCompareScrollOffset(compareScrollOffset + 1);
|
|
97
|
-
return newIndex;
|
|
98
|
-
});
|
|
99
|
-
}, [compareTotalItems, compareScrollOffset, topPaneHeight, setCompareScrollOffset]);
|
|
100
|
-
const getItemIndexFromRow = useCallback((visualRow) => {
|
|
101
|
-
if (!compareDiff)
|
|
102
|
-
return -1;
|
|
103
|
-
return getCompareItemIndexFromRow(visualRow, compareDiff.commits.length, compareDiff.files.length);
|
|
104
|
-
}, [compareDiff]);
|
|
105
|
-
return {
|
|
106
|
-
includeUncommitted,
|
|
107
|
-
compareListSelection,
|
|
108
|
-
compareSelectedIndex,
|
|
109
|
-
baseBranchCandidates,
|
|
110
|
-
showBaseBranchPicker,
|
|
111
|
-
compareTotalItems,
|
|
112
|
-
compareDiffTotalRows,
|
|
113
|
-
setCompareSelectedIndex,
|
|
114
|
-
toggleIncludeUncommitted,
|
|
115
|
-
openBaseBranchPicker,
|
|
116
|
-
closeBaseBranchPicker,
|
|
117
|
-
selectBaseBranch,
|
|
118
|
-
navigateCompareUp,
|
|
119
|
-
navigateCompareDown,
|
|
120
|
-
markSelectionInitialized,
|
|
121
|
-
getItemIndexFromRow,
|
|
122
|
-
};
|
|
123
|
-
}
|