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
package/dist/themes.js
CHANGED
|
@@ -1 +1,127 @@
|
|
|
1
|
-
|
|
1
|
+
// Theme definitions for diffstalker
|
|
2
|
+
// Dark theme - sampled from Claude Code's dark mode
|
|
3
|
+
const darkTheme = {
|
|
4
|
+
name: 'dark',
|
|
5
|
+
displayName: 'Dark',
|
|
6
|
+
colors: {
|
|
7
|
+
addBg: '#022800', // sampled: rgb(2,40,0)
|
|
8
|
+
delBg: '#3D0100', // sampled: rgb(61,1,0)
|
|
9
|
+
addHighlight: '#044700', // sampled: rgb(4,71,0)
|
|
10
|
+
delHighlight: '#5C0200', // sampled: rgb(92,2,0)
|
|
11
|
+
text: 'white',
|
|
12
|
+
addLineNum: '#368F35', // sampled: rgb(54,143,53)
|
|
13
|
+
delLineNum: '#A14040', // sampled: rgb(161,64,64)
|
|
14
|
+
contextLineNum: 'gray',
|
|
15
|
+
addSymbol: 'greenBright',
|
|
16
|
+
delSymbol: 'redBright',
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
// Light theme - matches Claude Code's light mode colors
|
|
20
|
+
const lightTheme = {
|
|
21
|
+
name: 'light',
|
|
22
|
+
displayName: 'Light',
|
|
23
|
+
colors: {
|
|
24
|
+
addBg: '#69db7c', // rgb(105,219,124)
|
|
25
|
+
delBg: '#ffa8b4', // rgb(255,168,180)
|
|
26
|
+
addHighlight: '#2f9d44', // rgb(47,157,68)
|
|
27
|
+
delHighlight: '#d1454b', // rgb(209,69,75)
|
|
28
|
+
text: 'black',
|
|
29
|
+
addLineNum: '#2f9d44',
|
|
30
|
+
delLineNum: '#d1454b',
|
|
31
|
+
contextLineNum: '#6c757d',
|
|
32
|
+
addSymbol: 'green',
|
|
33
|
+
delSymbol: 'red',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
// Dark colorblind theme - matches Claude Code's dark-daltonized colors
|
|
37
|
+
const darkColorblindTheme = {
|
|
38
|
+
name: 'dark-colorblind',
|
|
39
|
+
displayName: 'Dark (colorblind)',
|
|
40
|
+
colors: {
|
|
41
|
+
addBg: '#004466', // rgb(0,68,102)
|
|
42
|
+
delBg: '#660000', // rgb(102,0,0)
|
|
43
|
+
addHighlight: '#0077b3', // rgb(0,119,179)
|
|
44
|
+
delHighlight: '#b30000', // rgb(179,0,0)
|
|
45
|
+
text: 'white',
|
|
46
|
+
addLineNum: '#0077b3',
|
|
47
|
+
delLineNum: '#b30000',
|
|
48
|
+
contextLineNum: 'gray',
|
|
49
|
+
addSymbol: 'cyanBright',
|
|
50
|
+
delSymbol: 'redBright',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
// Light colorblind theme - matches Claude Code's light-daltonized colors
|
|
54
|
+
const lightColorblindTheme = {
|
|
55
|
+
name: 'light-colorblind',
|
|
56
|
+
displayName: 'Light (colorblind)',
|
|
57
|
+
colors: {
|
|
58
|
+
addBg: '#99ccff', // rgb(153,204,255)
|
|
59
|
+
delBg: '#ffcccc', // rgb(255,204,204)
|
|
60
|
+
addHighlight: '#3366cc', // rgb(51,102,204)
|
|
61
|
+
delHighlight: '#993333', // rgb(153,51,51)
|
|
62
|
+
text: 'black',
|
|
63
|
+
addLineNum: '#3366cc',
|
|
64
|
+
delLineNum: '#993333',
|
|
65
|
+
contextLineNum: '#6c757d',
|
|
66
|
+
addSymbol: 'blue',
|
|
67
|
+
delSymbol: 'red',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
// Dark ANSI theme - uses terminal's native 16 ANSI colors
|
|
71
|
+
const darkAnsiTheme = {
|
|
72
|
+
name: 'dark-ansi',
|
|
73
|
+
displayName: 'Dark (ANSI)',
|
|
74
|
+
colors: {
|
|
75
|
+
addBg: 'green',
|
|
76
|
+
delBg: 'red',
|
|
77
|
+
addHighlight: 'greenBright',
|
|
78
|
+
delHighlight: 'redBright',
|
|
79
|
+
text: 'white',
|
|
80
|
+
addLineNum: 'greenBright',
|
|
81
|
+
delLineNum: 'redBright',
|
|
82
|
+
contextLineNum: 'gray',
|
|
83
|
+
addSymbol: 'greenBright',
|
|
84
|
+
delSymbol: 'redBright',
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
// Light ANSI theme - uses terminal's native 16 ANSI colors
|
|
88
|
+
const lightAnsiTheme = {
|
|
89
|
+
name: 'light-ansi',
|
|
90
|
+
displayName: 'Light (ANSI)',
|
|
91
|
+
colors: {
|
|
92
|
+
addBg: 'green',
|
|
93
|
+
delBg: 'red',
|
|
94
|
+
addHighlight: 'greenBright',
|
|
95
|
+
delHighlight: 'redBright',
|
|
96
|
+
text: 'black',
|
|
97
|
+
addLineNum: 'green',
|
|
98
|
+
delLineNum: 'red',
|
|
99
|
+
contextLineNum: 'gray',
|
|
100
|
+
addSymbol: 'green',
|
|
101
|
+
delSymbol: 'red',
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
export const themes = {
|
|
105
|
+
dark: darkTheme,
|
|
106
|
+
light: lightTheme,
|
|
107
|
+
'dark-colorblind': darkColorblindTheme,
|
|
108
|
+
'light-colorblind': lightColorblindTheme,
|
|
109
|
+
'dark-ansi': darkAnsiTheme,
|
|
110
|
+
'light-ansi': lightAnsiTheme,
|
|
111
|
+
};
|
|
112
|
+
export const themeOrder = [
|
|
113
|
+
'dark',
|
|
114
|
+
'light',
|
|
115
|
+
'dark-colorblind',
|
|
116
|
+
'light-colorblind',
|
|
117
|
+
'dark-ansi',
|
|
118
|
+
'light-ansi',
|
|
119
|
+
];
|
|
120
|
+
export function getTheme(name) {
|
|
121
|
+
return themes[name] ?? themes['dark'];
|
|
122
|
+
}
|
|
123
|
+
export function getNextTheme(current) {
|
|
124
|
+
const currentIndex = themeOrder.indexOf(current);
|
|
125
|
+
const nextIndex = (currentIndex + 1) % themeOrder.length;
|
|
126
|
+
return themeOrder[nextIndex];
|
|
127
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import blessed from 'neo-blessed';
|
|
2
|
+
/**
|
|
3
|
+
* Layout constants matching the React/Ink implementation.
|
|
4
|
+
*/
|
|
5
|
+
export const LAYOUT_OVERHEAD = 5; // Header (1-2) + 3 separators + footer (1)
|
|
6
|
+
export const SPLIT_RATIO_STEP = 0.05;
|
|
7
|
+
/**
|
|
8
|
+
* Calculate layout dimensions based on terminal size and split ratio.
|
|
9
|
+
*/
|
|
10
|
+
export function calculateLayout(terminalHeight, terminalWidth, splitRatio, headerHeight = 1) {
|
|
11
|
+
// Total overhead: header + 3 separators + footer
|
|
12
|
+
const overhead = headerHeight + 4; // 3 separators + 1 footer
|
|
13
|
+
const availableHeight = terminalHeight - overhead;
|
|
14
|
+
const topPaneHeight = Math.floor(availableHeight * splitRatio);
|
|
15
|
+
const bottomPaneHeight = availableHeight - topPaneHeight;
|
|
16
|
+
return {
|
|
17
|
+
width: terminalWidth,
|
|
18
|
+
height: terminalHeight,
|
|
19
|
+
headerHeight,
|
|
20
|
+
topPaneHeight,
|
|
21
|
+
bottomPaneHeight,
|
|
22
|
+
footerRow: terminalHeight - 1,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Calculate pane boundaries for mouse click detection.
|
|
27
|
+
*/
|
|
28
|
+
export function calculatePaneBoundaries(terminalHeight, headerHeight, topPaneHeight, bottomPaneHeight) {
|
|
29
|
+
const stagingPaneStart = headerHeight + 1; // After header + separator
|
|
30
|
+
const fileListEnd = stagingPaneStart + topPaneHeight;
|
|
31
|
+
const diffPaneStart = fileListEnd + 1; // After separator
|
|
32
|
+
const diffPaneEnd = diffPaneStart + bottomPaneHeight;
|
|
33
|
+
const footerRow = terminalHeight - 1;
|
|
34
|
+
return {
|
|
35
|
+
stagingPaneStart,
|
|
36
|
+
fileListEnd,
|
|
37
|
+
diffPaneStart,
|
|
38
|
+
diffPaneEnd,
|
|
39
|
+
footerRow,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* LayoutManager creates and manages blessed boxes for the two-pane layout.
|
|
44
|
+
*/
|
|
45
|
+
export class LayoutManager {
|
|
46
|
+
screen;
|
|
47
|
+
headerBox;
|
|
48
|
+
topSeparator;
|
|
49
|
+
topPane;
|
|
50
|
+
middleSeparator;
|
|
51
|
+
bottomPane;
|
|
52
|
+
bottomSeparator;
|
|
53
|
+
footerBox;
|
|
54
|
+
_dimensions;
|
|
55
|
+
_splitRatio;
|
|
56
|
+
constructor(screen, splitRatio = 0.4) {
|
|
57
|
+
this.screen = screen;
|
|
58
|
+
this._splitRatio = splitRatio;
|
|
59
|
+
this._dimensions = this.calculateDimensions();
|
|
60
|
+
// Create all layout boxes
|
|
61
|
+
this.headerBox = this.createHeaderBox();
|
|
62
|
+
this.topSeparator = this.createSeparator(this._dimensions.headerHeight);
|
|
63
|
+
this.topPane = this.createTopPane();
|
|
64
|
+
this.middleSeparator = this.createSeparator(this._dimensions.headerHeight + 1 + this._dimensions.topPaneHeight);
|
|
65
|
+
this.bottomPane = this.createBottomPane();
|
|
66
|
+
this.bottomSeparator = this.createSeparator(this._dimensions.headerHeight +
|
|
67
|
+
2 +
|
|
68
|
+
this._dimensions.topPaneHeight +
|
|
69
|
+
this._dimensions.bottomPaneHeight);
|
|
70
|
+
this.footerBox = this.createFooterBox();
|
|
71
|
+
// Handle screen resize
|
|
72
|
+
screen.on('resize', () => this.onResize());
|
|
73
|
+
}
|
|
74
|
+
get dimensions() {
|
|
75
|
+
return this._dimensions;
|
|
76
|
+
}
|
|
77
|
+
get splitRatio() {
|
|
78
|
+
return this._splitRatio;
|
|
79
|
+
}
|
|
80
|
+
setSplitRatio(ratio) {
|
|
81
|
+
this._splitRatio = Math.min(0.85, Math.max(0.15, ratio));
|
|
82
|
+
this.updateLayout();
|
|
83
|
+
}
|
|
84
|
+
adjustSplitRatio(delta) {
|
|
85
|
+
this.setSplitRatio(this._splitRatio + delta);
|
|
86
|
+
}
|
|
87
|
+
calculateDimensions() {
|
|
88
|
+
const height = this.screen.height || 24;
|
|
89
|
+
const width = this.screen.width || 80;
|
|
90
|
+
return calculateLayout(height, width, this._splitRatio);
|
|
91
|
+
}
|
|
92
|
+
createHeaderBox() {
|
|
93
|
+
return blessed.box({
|
|
94
|
+
parent: this.screen,
|
|
95
|
+
top: 0,
|
|
96
|
+
left: 0,
|
|
97
|
+
width: '100%',
|
|
98
|
+
height: this._dimensions.headerHeight,
|
|
99
|
+
tags: true,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
createSeparator(top) {
|
|
103
|
+
const width = this.screen.width || 80;
|
|
104
|
+
return blessed.box({
|
|
105
|
+
parent: this.screen,
|
|
106
|
+
top,
|
|
107
|
+
left: 0,
|
|
108
|
+
width: '100%',
|
|
109
|
+
height: 1,
|
|
110
|
+
content: '\u2500'.repeat(width),
|
|
111
|
+
style: {
|
|
112
|
+
fg: 'gray',
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
createTopPane() {
|
|
117
|
+
return blessed.box({
|
|
118
|
+
parent: this.screen,
|
|
119
|
+
top: this._dimensions.headerHeight + 1,
|
|
120
|
+
left: 0,
|
|
121
|
+
width: '100%',
|
|
122
|
+
height: this._dimensions.topPaneHeight,
|
|
123
|
+
tags: true,
|
|
124
|
+
scrollable: true,
|
|
125
|
+
alwaysScroll: true,
|
|
126
|
+
wrap: false, // Disable blessed's built-in wrapping - we handle wrapping ourselves
|
|
127
|
+
scrollbar: {
|
|
128
|
+
ch: ' ',
|
|
129
|
+
track: {
|
|
130
|
+
bg: 'gray',
|
|
131
|
+
},
|
|
132
|
+
style: {
|
|
133
|
+
inverse: true,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
createBottomPane() {
|
|
139
|
+
return blessed.box({
|
|
140
|
+
parent: this.screen,
|
|
141
|
+
top: this._dimensions.headerHeight + 2 + this._dimensions.topPaneHeight,
|
|
142
|
+
left: 0,
|
|
143
|
+
width: '100%',
|
|
144
|
+
height: this._dimensions.bottomPaneHeight,
|
|
145
|
+
tags: true,
|
|
146
|
+
scrollable: true,
|
|
147
|
+
alwaysScroll: true,
|
|
148
|
+
wrap: false, // Disable blessed's built-in wrapping - we handle wrapping ourselves
|
|
149
|
+
scrollbar: {
|
|
150
|
+
ch: ' ',
|
|
151
|
+
track: {
|
|
152
|
+
bg: 'gray',
|
|
153
|
+
},
|
|
154
|
+
style: {
|
|
155
|
+
inverse: true,
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
createFooterBox() {
|
|
161
|
+
return blessed.box({
|
|
162
|
+
parent: this.screen,
|
|
163
|
+
top: this._dimensions.footerRow,
|
|
164
|
+
left: 0,
|
|
165
|
+
width: '100%',
|
|
166
|
+
height: 1,
|
|
167
|
+
tags: true,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
onResize() {
|
|
171
|
+
this._dimensions = this.calculateDimensions();
|
|
172
|
+
this.updateLayout();
|
|
173
|
+
// Don't call screen.render() here - App's resize handler will render
|
|
174
|
+
// with properly recalculated content
|
|
175
|
+
}
|
|
176
|
+
updateLayout() {
|
|
177
|
+
this._dimensions = this.calculateDimensions();
|
|
178
|
+
const width = this.screen.width || 80;
|
|
179
|
+
// Update header
|
|
180
|
+
this.headerBox.height = this._dimensions.headerHeight;
|
|
181
|
+
this.headerBox.width = width;
|
|
182
|
+
// Update top separator
|
|
183
|
+
this.topSeparator.top = this._dimensions.headerHeight;
|
|
184
|
+
this.topSeparator.width = width;
|
|
185
|
+
this.topSeparator.setContent('\u2500'.repeat(width));
|
|
186
|
+
// Update top pane
|
|
187
|
+
this.topPane.top = this._dimensions.headerHeight + 1;
|
|
188
|
+
this.topPane.height = this._dimensions.topPaneHeight;
|
|
189
|
+
this.topPane.width = width;
|
|
190
|
+
// Update middle separator
|
|
191
|
+
this.middleSeparator.top = this._dimensions.headerHeight + 1 + this._dimensions.topPaneHeight;
|
|
192
|
+
this.middleSeparator.width = width;
|
|
193
|
+
this.middleSeparator.setContent('\u2500'.repeat(width));
|
|
194
|
+
// Update bottom pane
|
|
195
|
+
this.bottomPane.top = this._dimensions.headerHeight + 2 + this._dimensions.topPaneHeight;
|
|
196
|
+
this.bottomPane.height = this._dimensions.bottomPaneHeight;
|
|
197
|
+
this.bottomPane.width = width;
|
|
198
|
+
// Update bottom separator
|
|
199
|
+
this.bottomSeparator.top =
|
|
200
|
+
this._dimensions.headerHeight +
|
|
201
|
+
2 +
|
|
202
|
+
this._dimensions.topPaneHeight +
|
|
203
|
+
this._dimensions.bottomPaneHeight;
|
|
204
|
+
this.bottomSeparator.width = width;
|
|
205
|
+
this.bottomSeparator.setContent('\u2500'.repeat(width));
|
|
206
|
+
// Update footer
|
|
207
|
+
this.footerBox.top = this._dimensions.footerRow;
|
|
208
|
+
this.footerBox.width = width;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Get pane boundaries for mouse click detection.
|
|
212
|
+
*/
|
|
213
|
+
getPaneBoundaries() {
|
|
214
|
+
return calculatePaneBoundaries(this._dimensions.height, this._dimensions.headerHeight, this._dimensions.topPaneHeight, this._dimensions.bottomPaneHeight);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Convert screen Y coordinate to content row within the top pane.
|
|
218
|
+
* Returns the 0-based row index of the content, or -1 if outside the pane.
|
|
219
|
+
*/
|
|
220
|
+
screenYToTopPaneRow(screenY) {
|
|
221
|
+
const paneTop = this._dimensions.headerHeight + 1; // header + separator
|
|
222
|
+
const paneBottom = paneTop + this._dimensions.topPaneHeight;
|
|
223
|
+
if (screenY < paneTop || screenY >= paneBottom) {
|
|
224
|
+
return -1;
|
|
225
|
+
}
|
|
226
|
+
return screenY - paneTop;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Convert screen Y coordinate to content row within the bottom pane.
|
|
230
|
+
* Returns the 0-based row index of the content, or -1 if outside the pane.
|
|
231
|
+
*/
|
|
232
|
+
screenYToBottomPaneRow(screenY) {
|
|
233
|
+
const paneTop = this._dimensions.headerHeight + 2 + this._dimensions.topPaneHeight; // header + 2 separators + top pane
|
|
234
|
+
const paneBottom = paneTop + this._dimensions.bottomPaneHeight;
|
|
235
|
+
if (screenY < paneTop || screenY >= paneBottom) {
|
|
236
|
+
return -1;
|
|
237
|
+
}
|
|
238
|
+
return screenY - paneTop;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Get the top position of the top pane (for reference).
|
|
242
|
+
*/
|
|
243
|
+
get topPaneTop() {
|
|
244
|
+
return this._dimensions.headerHeight + 1;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Get the top position of the bottom pane (for reference).
|
|
248
|
+
*/
|
|
249
|
+
get bottomPaneTop() {
|
|
250
|
+
return this._dimensions.headerHeight + 2 + this._dimensions.topPaneHeight;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import blessed from 'neo-blessed';
|
|
2
|
+
/**
|
|
3
|
+
* BaseBranchPicker modal for selecting the base branch for PR comparison.
|
|
4
|
+
*/
|
|
5
|
+
export class BaseBranchPicker {
|
|
6
|
+
box;
|
|
7
|
+
screen;
|
|
8
|
+
branches;
|
|
9
|
+
selectedIndex;
|
|
10
|
+
currentBranch;
|
|
11
|
+
onSelect;
|
|
12
|
+
onCancel;
|
|
13
|
+
constructor(screen, branches, currentBranch, onSelect, onCancel) {
|
|
14
|
+
this.screen = screen;
|
|
15
|
+
this.branches = branches;
|
|
16
|
+
this.currentBranch = currentBranch;
|
|
17
|
+
this.onSelect = onSelect;
|
|
18
|
+
this.onCancel = onCancel;
|
|
19
|
+
// Find current branch index
|
|
20
|
+
this.selectedIndex = currentBranch ? branches.indexOf(currentBranch) : 0;
|
|
21
|
+
if (this.selectedIndex < 0)
|
|
22
|
+
this.selectedIndex = 0;
|
|
23
|
+
// Create modal box
|
|
24
|
+
const width = 50;
|
|
25
|
+
const maxVisibleBranches = Math.min(branches.length, 15);
|
|
26
|
+
const height = maxVisibleBranches + 6; // branches + header + footer + borders + padding
|
|
27
|
+
this.box = blessed.box({
|
|
28
|
+
parent: screen,
|
|
29
|
+
top: 'center',
|
|
30
|
+
left: 'center',
|
|
31
|
+
width,
|
|
32
|
+
height,
|
|
33
|
+
border: {
|
|
34
|
+
type: 'line',
|
|
35
|
+
},
|
|
36
|
+
style: {
|
|
37
|
+
border: {
|
|
38
|
+
fg: 'cyan',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
tags: true,
|
|
42
|
+
keys: true,
|
|
43
|
+
scrollable: true,
|
|
44
|
+
alwaysScroll: true,
|
|
45
|
+
});
|
|
46
|
+
// Setup key handlers
|
|
47
|
+
this.setupKeyHandlers();
|
|
48
|
+
// Initial render
|
|
49
|
+
this.render();
|
|
50
|
+
}
|
|
51
|
+
setupKeyHandlers() {
|
|
52
|
+
this.box.key(['escape', 'q'], () => {
|
|
53
|
+
this.close();
|
|
54
|
+
this.onCancel();
|
|
55
|
+
});
|
|
56
|
+
this.box.key(['enter', 'space'], () => {
|
|
57
|
+
const selected = this.branches[this.selectedIndex];
|
|
58
|
+
if (selected) {
|
|
59
|
+
this.close();
|
|
60
|
+
this.onSelect(selected);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
this.box.key(['up', 'k'], () => {
|
|
64
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
65
|
+
this.render();
|
|
66
|
+
});
|
|
67
|
+
this.box.key(['down', 'j'], () => {
|
|
68
|
+
this.selectedIndex = Math.min(this.branches.length - 1, this.selectedIndex + 1);
|
|
69
|
+
this.render();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
render() {
|
|
73
|
+
const lines = [];
|
|
74
|
+
// Header
|
|
75
|
+
lines.push('{bold}{cyan-fg} Select Base Branch{/cyan-fg}{/bold}');
|
|
76
|
+
lines.push('');
|
|
77
|
+
if (this.branches.length === 0) {
|
|
78
|
+
lines.push('{gray-fg}No branches found{/gray-fg}');
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Branch list
|
|
82
|
+
for (let i = 0; i < this.branches.length; i++) {
|
|
83
|
+
const branch = this.branches[i];
|
|
84
|
+
const isSelected = i === this.selectedIndex;
|
|
85
|
+
const isCurrent = branch === this.currentBranch;
|
|
86
|
+
let line = isSelected ? '{cyan-fg}{bold}> ' : ' ';
|
|
87
|
+
line += branch;
|
|
88
|
+
if (isSelected)
|
|
89
|
+
line += '{/bold}{/cyan-fg}';
|
|
90
|
+
if (isCurrent)
|
|
91
|
+
line += ' {gray-fg}(current){/gray-fg}';
|
|
92
|
+
lines.push(line);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Footer
|
|
96
|
+
lines.push('');
|
|
97
|
+
lines.push('{gray-fg}j/k: navigate | Enter: select | Esc: cancel{/gray-fg}');
|
|
98
|
+
this.box.setContent(lines.join('\n'));
|
|
99
|
+
this.screen.render();
|
|
100
|
+
}
|
|
101
|
+
close() {
|
|
102
|
+
this.box.destroy();
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Focus the modal.
|
|
106
|
+
*/
|
|
107
|
+
focus() {
|
|
108
|
+
this.box.focus();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import blessed from 'neo-blessed';
|
|
2
|
+
/**
|
|
3
|
+
* DiscardConfirm modal for confirming discard of file changes.
|
|
4
|
+
*/
|
|
5
|
+
export class DiscardConfirm {
|
|
6
|
+
box;
|
|
7
|
+
screen;
|
|
8
|
+
filePath;
|
|
9
|
+
onConfirm;
|
|
10
|
+
onCancel;
|
|
11
|
+
constructor(screen, filePath, onConfirm, onCancel) {
|
|
12
|
+
this.screen = screen;
|
|
13
|
+
this.filePath = filePath;
|
|
14
|
+
this.onConfirm = onConfirm;
|
|
15
|
+
this.onCancel = onCancel;
|
|
16
|
+
// Create modal box - small confirmation dialog
|
|
17
|
+
const width = Math.min(60, Math.max(40, filePath.length + 20));
|
|
18
|
+
const height = 7;
|
|
19
|
+
this.box = blessed.box({
|
|
20
|
+
parent: screen,
|
|
21
|
+
top: 'center',
|
|
22
|
+
left: 'center',
|
|
23
|
+
width,
|
|
24
|
+
height,
|
|
25
|
+
border: {
|
|
26
|
+
type: 'line',
|
|
27
|
+
},
|
|
28
|
+
style: {
|
|
29
|
+
border: {
|
|
30
|
+
fg: 'yellow',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
tags: true,
|
|
34
|
+
keys: true,
|
|
35
|
+
});
|
|
36
|
+
// Setup key handlers
|
|
37
|
+
this.setupKeyHandlers();
|
|
38
|
+
// Render content
|
|
39
|
+
this.render();
|
|
40
|
+
}
|
|
41
|
+
setupKeyHandlers() {
|
|
42
|
+
this.box.key(['y', 'Y'], () => {
|
|
43
|
+
this.close();
|
|
44
|
+
this.onConfirm();
|
|
45
|
+
});
|
|
46
|
+
this.box.key(['n', 'N', 'escape', 'q'], () => {
|
|
47
|
+
this.close();
|
|
48
|
+
this.onCancel();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
render() {
|
|
52
|
+
const lines = [];
|
|
53
|
+
// Header
|
|
54
|
+
lines.push('{bold}{yellow-fg} Discard Changes?{/yellow-fg}{/bold}');
|
|
55
|
+
lines.push('');
|
|
56
|
+
// File path (truncate if needed)
|
|
57
|
+
const maxPathLen = this.box.width - 6;
|
|
58
|
+
const displayPath = this.filePath.length > maxPathLen
|
|
59
|
+
? '...' + this.filePath.slice(-(maxPathLen - 3))
|
|
60
|
+
: this.filePath;
|
|
61
|
+
lines.push(`{white-fg}${displayPath}{/white-fg}`);
|
|
62
|
+
lines.push('');
|
|
63
|
+
// Prompt
|
|
64
|
+
lines.push('{gray-fg}Press {/gray-fg}{green-fg}y{/green-fg}{gray-fg} to confirm, {/gray-fg}{red-fg}n{/red-fg}{gray-fg} or Esc to cancel{/gray-fg}');
|
|
65
|
+
this.box.setContent(lines.join('\n'));
|
|
66
|
+
this.screen.render();
|
|
67
|
+
}
|
|
68
|
+
close() {
|
|
69
|
+
this.box.destroy();
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Focus the modal.
|
|
73
|
+
*/
|
|
74
|
+
focus() {
|
|
75
|
+
this.box.focus();
|
|
76
|
+
}
|
|
77
|
+
}
|