diffstalker 0.1.7 → 0.2.1
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 +8 -0
- package/CHANGELOG.md +36 -0
- package/bun.lock +89 -306
- package/dist/App.js +895 -520
- package/dist/FollowMode.js +85 -0
- package/dist/KeyBindings.js +178 -0
- package/dist/MouseHandlers.js +156 -0
- package/dist/core/ExplorerStateManager.js +632 -0
- package/dist/core/FilePathWatcher.js +133 -0
- package/dist/core/GitStateManager.js +221 -86
- package/dist/git/diff.js +4 -0
- package/dist/git/ignoreUtils.js +30 -0
- package/dist/git/status.js +2 -34
- package/dist/index.js +68 -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 +195 -0
- package/dist/types/tabs.js +4 -0
- package/dist/ui/Layout.js +252 -0
- package/dist/ui/PaneRenderers.js +56 -0
- package/dist/ui/modals/BaseBranchPicker.js +110 -0
- package/dist/ui/modals/DiscardConfirm.js +77 -0
- package/dist/ui/modals/FileFinder.js +232 -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 +238 -0
- package/dist/ui/widgets/DiffView.js +281 -0
- package/dist/ui/widgets/ExplorerContent.js +89 -0
- package/dist/ui/widgets/ExplorerView.js +204 -0
- package/dist/ui/widgets/FileList.js +185 -0
- package/dist/ui/widgets/Footer.js +50 -0
- package/dist/ui/widgets/Header.js +68 -0
- package/dist/ui/widgets/HistoryView.js +69 -0
- package/dist/utils/displayRows.js +185 -6
- package/dist/utils/explorerDisplayRows.js +1 -1
- package/dist/utils/fileCategories.js +37 -0
- package/dist/utils/fileTree.js +148 -0
- package/dist/utils/languageDetection.js +56 -0
- package/dist/utils/pathUtils.js +27 -0
- package/dist/utils/wordDiff.js +50 -0
- package/eslint.metrics.js +16 -0
- package/metrics/.gitkeep +0 -0
- package/metrics/v0.2.1.json +268 -0
- package/package.json +14 -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
- package/dist/utils/mouseCoordinates.js +0 -165
- package/dist/utils/rowCalculations.js +0 -209
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
{
|
|
2
|
+
"timestamp": "2026-01-30T15:45:56.951Z",
|
|
3
|
+
"gitRef": "v0.2.1",
|
|
4
|
+
"gitSha": "3d0506b",
|
|
5
|
+
"summary": {
|
|
6
|
+
"files": 62,
|
|
7
|
+
"lines": 12327,
|
|
8
|
+
"functions": 754,
|
|
9
|
+
"avgCyclomaticComplexity": 5.6,
|
|
10
|
+
"maxCyclomaticComplexity": {
|
|
11
|
+
"value": 64,
|
|
12
|
+
"function": "formatDisplayRow",
|
|
13
|
+
"file": "src/ui/widgets/DiffView.ts:68"
|
|
14
|
+
},
|
|
15
|
+
"avgCognitiveComplexity": 7.6,
|
|
16
|
+
"maxCognitiveComplexity": {
|
|
17
|
+
"value": 96,
|
|
18
|
+
"function": "formatDisplayRow",
|
|
19
|
+
"file": "src/ui/widgets/DiffView.ts:68"
|
|
20
|
+
},
|
|
21
|
+
"smells": 44
|
|
22
|
+
},
|
|
23
|
+
"hotspots": [
|
|
24
|
+
{
|
|
25
|
+
"file": "src/App.ts",
|
|
26
|
+
"lines": 1131,
|
|
27
|
+
"cyclomaticMax": 19,
|
|
28
|
+
"cognitiveMax": 16,
|
|
29
|
+
"smells": 7
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"file": "src/core/ExplorerStateManager.ts",
|
|
33
|
+
"lines": 769,
|
|
34
|
+
"cyclomaticMax": 18,
|
|
35
|
+
"cognitiveMax": 34,
|
|
36
|
+
"smells": 7
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"file": "src/ui/widgets/Header.ts",
|
|
40
|
+
"lines": 89,
|
|
41
|
+
"cyclomaticMax": 14,
|
|
42
|
+
"cognitiveMax": 22,
|
|
43
|
+
"smells": 3
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"file": "src/state/UIState.ts",
|
|
47
|
+
"lines": 280,
|
|
48
|
+
"cyclomaticMax": 10,
|
|
49
|
+
"cognitiveMax": 12,
|
|
50
|
+
"smells": 3
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"file": "src/ipc/CommandClient.ts",
|
|
54
|
+
"lines": 202,
|
|
55
|
+
"cyclomaticMax": 7,
|
|
56
|
+
"cognitiveMax": 6,
|
|
57
|
+
"smells": 3
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"file": "src/utils/displayRows.ts",
|
|
61
|
+
"lines": 459,
|
|
62
|
+
"cyclomaticMax": 42,
|
|
63
|
+
"cognitiveMax": 68,
|
|
64
|
+
"smells": 2
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"file": "src/index.ts",
|
|
68
|
+
"lines": 178,
|
|
69
|
+
"cyclomaticMax": 16,
|
|
70
|
+
"cognitiveMax": 17,
|
|
71
|
+
"smells": 2
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"file": "src/utils/ansiTruncate.ts",
|
|
75
|
+
"lines": 124,
|
|
76
|
+
"cyclomaticMax": 16,
|
|
77
|
+
"cognitiveMax": 24,
|
|
78
|
+
"smells": 2
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"file": "src/utils/languageDetection.ts",
|
|
82
|
+
"lines": 258,
|
|
83
|
+
"cyclomaticMax": 10,
|
|
84
|
+
"cognitiveMax": 8,
|
|
85
|
+
"smells": 2
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"file": "src/ui/modals/HotkeysModal.ts",
|
|
89
|
+
"lines": 242,
|
|
90
|
+
"cyclomaticMax": 7,
|
|
91
|
+
"cognitiveMax": 9,
|
|
92
|
+
"smells": 2
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"file": "src/ui/modals/ThemePicker.ts",
|
|
96
|
+
"lines": 133,
|
|
97
|
+
"cyclomaticMax": 5,
|
|
98
|
+
"cognitiveMax": 7,
|
|
99
|
+
"smells": 2
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"file": "src/ui/widgets/DiffView.ts",
|
|
103
|
+
"lines": 366,
|
|
104
|
+
"cyclomaticMax": 64,
|
|
105
|
+
"cognitiveMax": 96,
|
|
106
|
+
"smells": 1
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"file": "src/git/diff.ts",
|
|
110
|
+
"lines": 567,
|
|
111
|
+
"cyclomaticMax": 27,
|
|
112
|
+
"cognitiveMax": 43,
|
|
113
|
+
"smells": 1
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"file": "src/ui/widgets/ExplorerView.ts",
|
|
117
|
+
"lines": 243,
|
|
118
|
+
"cyclomaticMax": 24,
|
|
119
|
+
"cognitiveMax": 37,
|
|
120
|
+
"smells": 1
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"file": "src/ui/widgets/ExplorerContent.ts",
|
|
124
|
+
"lines": 131,
|
|
125
|
+
"cyclomaticMax": 20,
|
|
126
|
+
"cognitiveMax": 24,
|
|
127
|
+
"smells": 1
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"file": "src/ui/PaneRenderers.ts",
|
|
131
|
+
"lines": 170,
|
|
132
|
+
"cyclomaticMax": 18,
|
|
133
|
+
"cognitiveMax": 6,
|
|
134
|
+
"smells": 1
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"file": "src/ui/widgets/FileList.ts",
|
|
138
|
+
"lines": 225,
|
|
139
|
+
"cyclomaticMax": 16,
|
|
140
|
+
"cognitiveMax": 26,
|
|
141
|
+
"smells": 1
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"file": "src/ui/widgets/CommitPanel.ts",
|
|
145
|
+
"lines": 83,
|
|
146
|
+
"cyclomaticMax": 14,
|
|
147
|
+
"cognitiveMax": 12,
|
|
148
|
+
"smells": 1
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"file": "src/ui/widgets/Footer.ts",
|
|
152
|
+
"lines": 67,
|
|
153
|
+
"cyclomaticMax": 7,
|
|
154
|
+
"cognitiveMax": 7,
|
|
155
|
+
"smells": 1
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"file": "src/core/GitOperationQueue.test.ts",
|
|
159
|
+
"lines": 276,
|
|
160
|
+
"cyclomaticMax": 0,
|
|
161
|
+
"cognitiveMax": 0,
|
|
162
|
+
"smells": 1
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"file": "src/git/status.ts",
|
|
166
|
+
"lines": 310,
|
|
167
|
+
"cyclomaticMax": 28,
|
|
168
|
+
"cognitiveMax": 32,
|
|
169
|
+
"smells": 0
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"file": "src/ui/widgets/CompareListView.ts",
|
|
173
|
+
"lines": 350,
|
|
174
|
+
"cyclomaticMax": 23,
|
|
175
|
+
"cognitiveMax": 21,
|
|
176
|
+
"smells": 0
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"file": "src/MouseHandlers.ts",
|
|
180
|
+
"lines": 212,
|
|
181
|
+
"cyclomaticMax": 17,
|
|
182
|
+
"cognitiveMax": 15,
|
|
183
|
+
"smells": 0
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"file": "src/ipc/CommandServer.ts",
|
|
187
|
+
"lines": 266,
|
|
188
|
+
"cyclomaticMax": 17,
|
|
189
|
+
"cognitiveMax": 6,
|
|
190
|
+
"smells": 0
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"file": "src/utils/diffRowCalculations.ts",
|
|
194
|
+
"lines": 136,
|
|
195
|
+
"cyclomaticMax": 13,
|
|
196
|
+
"cognitiveMax": 24,
|
|
197
|
+
"smells": 0
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"file": "src/utils/lineBreaking.ts",
|
|
201
|
+
"lines": 114,
|
|
202
|
+
"cyclomaticMax": 12,
|
|
203
|
+
"cognitiveMax": 17,
|
|
204
|
+
"smells": 0
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"file": "src/config.ts",
|
|
208
|
+
"lines": 107,
|
|
209
|
+
"cyclomaticMax": 10,
|
|
210
|
+
"cognitiveMax": 13,
|
|
211
|
+
"smells": 0
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"file": "src/core/GitStateManager.ts",
|
|
215
|
+
"lines": 720,
|
|
216
|
+
"cyclomaticMax": 9,
|
|
217
|
+
"cognitiveMax": 13,
|
|
218
|
+
"smells": 0
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"file": "src/ui/modals/FileFinder.ts",
|
|
222
|
+
"lines": 280,
|
|
223
|
+
"cyclomaticMax": 9,
|
|
224
|
+
"cognitiveMax": 13,
|
|
225
|
+
"smells": 0
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"file": "src/ui/widgets/HistoryView.ts",
|
|
229
|
+
"lines": 97,
|
|
230
|
+
"cyclomaticMax": 9,
|
|
231
|
+
"cognitiveMax": 12,
|
|
232
|
+
"smells": 0
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"file": "src/utils/formatPath.ts",
|
|
236
|
+
"lines": 72,
|
|
237
|
+
"cyclomaticMax": 9,
|
|
238
|
+
"cognitiveMax": 11,
|
|
239
|
+
"smells": 0
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"file": "src/utils/explorerDisplayRows.ts",
|
|
243
|
+
"lines": 206,
|
|
244
|
+
"cyclomaticMax": 8,
|
|
245
|
+
"cognitiveMax": 15,
|
|
246
|
+
"smells": 0
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"file": "src/ui/modals/BaseBranchPicker.ts",
|
|
250
|
+
"lines": 134,
|
|
251
|
+
"cyclomaticMax": 6,
|
|
252
|
+
"cognitiveMax": 13,
|
|
253
|
+
"smells": 0
|
|
254
|
+
}
|
|
255
|
+
],
|
|
256
|
+
"smellsByRule": {
|
|
257
|
+
"@typescript-eslint/no-unused-vars": 22,
|
|
258
|
+
"@typescript-eslint/no-explicit-any": 1,
|
|
259
|
+
"sonarjs/no-ignored-exceptions": 4,
|
|
260
|
+
"sonarjs/slow-regex": 4,
|
|
261
|
+
"sonarjs/updated-loop-counter": 2,
|
|
262
|
+
"prefer-const": 1,
|
|
263
|
+
"sonarjs/no-dead-store": 2,
|
|
264
|
+
"sonarjs/no-all-duplicated-branches": 1,
|
|
265
|
+
"sonarjs/no-nested-conditional": 3,
|
|
266
|
+
"no-control-regex": 4
|
|
267
|
+
}
|
|
268
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "diffstalker",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Terminal application that displays git diff/status for directories",
|
|
5
5
|
"author": "yogh-io",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,18 +18,21 @@
|
|
|
18
18
|
"diffstalker": "bin/diffstalker"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
|
-
"
|
|
21
|
+
"postinstall": "node scripts/patch-neo-blessed.cjs",
|
|
22
|
+
"dev": "bun --watch src/index.ts",
|
|
22
23
|
"build": "tsc",
|
|
23
24
|
"build:prod": "tsc && bun build dist/index.js --outfile dist/index.js --minify --target node --packages external",
|
|
24
|
-
"bundle": "bun run build:prod && bun build dist/index.js --outdir dist/bundle --minify --target node --external
|
|
25
|
+
"bundle": "bun run build:prod && bun build dist/index.js --outdir dist/bundle --minify --target node --external neo-blessed",
|
|
25
26
|
"start": "bun dist/index.js",
|
|
26
27
|
"start:bundle": "bun dist/bundle/index.js",
|
|
27
|
-
"test": "
|
|
28
|
-
"test:watch": "
|
|
28
|
+
"test": "bun test src/*.test.ts src/**/*.test.ts",
|
|
29
|
+
"test:watch": "bun test --watch src/*.test.ts src/**/*.test.ts",
|
|
29
30
|
"lint": "eslint src/",
|
|
30
31
|
"lint:fix": "eslint src/ --fix",
|
|
31
32
|
"format": "prettier --write src/",
|
|
32
33
|
"format:check": "prettier --check src/",
|
|
34
|
+
"metrics": "bun scripts/collect-metrics.ts",
|
|
35
|
+
"metrics:snapshot": "bun scripts/collect-metrics.ts --save",
|
|
33
36
|
"prepublishOnly": "bun run build:prod"
|
|
34
37
|
},
|
|
35
38
|
"keywords": [
|
|
@@ -46,22 +49,21 @@
|
|
|
46
49
|
"chokidar": "^4.0.3",
|
|
47
50
|
"emphasize": "^7.0.0",
|
|
48
51
|
"fast-diff": "^1.3.0",
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"react": "^19.2.0",
|
|
52
|
+
"ignore": "^7.0.5",
|
|
53
|
+
"neo-blessed": "^0.2.0",
|
|
52
54
|
"simple-git": "^3.27.0",
|
|
53
55
|
"string-width": "^8.1.0"
|
|
54
56
|
},
|
|
55
57
|
"devDependencies": {
|
|
56
58
|
"@eslint/js": "^9.39.2",
|
|
59
|
+
"@types/blessed": "^0.1.27",
|
|
57
60
|
"@types/node": "^22.10.7",
|
|
58
|
-
"@types/react": "^19.2.0",
|
|
59
61
|
"eslint": "^9.39.2",
|
|
60
62
|
"eslint-config-prettier": "^10.1.8",
|
|
61
|
-
"eslint-plugin-
|
|
63
|
+
"eslint-plugin-sonarjs": "^3.0.6",
|
|
64
|
+
"patch-package": "^8.0.1",
|
|
62
65
|
"prettier": "^3.8.0",
|
|
63
66
|
"typescript": "^5.7.3",
|
|
64
|
-
"typescript-eslint": "^8.53.1"
|
|
65
|
-
"vitest": "^2.1.0"
|
|
67
|
+
"typescript-eslint": "^8.53.1"
|
|
66
68
|
}
|
|
67
69
|
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useMemo } from 'react';
|
|
3
|
-
import { Box, Text, useInput } from 'ink';
|
|
4
|
-
import { Modal, centerModal } from './Modal.js';
|
|
5
|
-
export function BaseBranchPicker({ candidates, currentBranch, onSelect, onCancel, width, height, }) {
|
|
6
|
-
const [inputValue, setInputValue] = useState('');
|
|
7
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
8
|
-
// Filter candidates based on input
|
|
9
|
-
const filteredCandidates = useMemo(() => {
|
|
10
|
-
if (!inputValue)
|
|
11
|
-
return candidates;
|
|
12
|
-
const lower = inputValue.toLowerCase();
|
|
13
|
-
return candidates.filter((c) => c.toLowerCase().includes(lower));
|
|
14
|
-
}, [candidates, inputValue]);
|
|
15
|
-
// Clamp selected index to valid range
|
|
16
|
-
const clampedIndex = Math.min(selectedIndex, Math.max(0, filteredCandidates.length - 1));
|
|
17
|
-
useInput((input, key) => {
|
|
18
|
-
if (key.escape) {
|
|
19
|
-
onCancel();
|
|
20
|
-
}
|
|
21
|
-
else if (key.return) {
|
|
22
|
-
// If input matches no candidates but has value, use the input as custom branch
|
|
23
|
-
if (filteredCandidates.length === 0 && inputValue) {
|
|
24
|
-
onSelect(inputValue);
|
|
25
|
-
}
|
|
26
|
-
else if (filteredCandidates.length > 0) {
|
|
27
|
-
onSelect(filteredCandidates[clampedIndex]);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
else if (key.upArrow) {
|
|
31
|
-
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
32
|
-
}
|
|
33
|
-
else if (key.downArrow) {
|
|
34
|
-
setSelectedIndex((prev) => Math.min(filteredCandidates.length - 1, prev + 1));
|
|
35
|
-
}
|
|
36
|
-
else if (key.backspace || key.delete) {
|
|
37
|
-
setInputValue((prev) => prev.slice(0, -1));
|
|
38
|
-
setSelectedIndex(0);
|
|
39
|
-
}
|
|
40
|
-
else if (input && !key.ctrl && !key.meta) {
|
|
41
|
-
setInputValue((prev) => prev + input);
|
|
42
|
-
setSelectedIndex(0);
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
// Calculate box dimensions
|
|
46
|
-
const boxWidth = Math.min(60, width - 4);
|
|
47
|
-
const maxListHeight = Math.min(10, height - 10);
|
|
48
|
-
const boxHeight = Math.min(maxListHeight + 9, height - 4); // +9 for header, input, footer, borders
|
|
49
|
-
// Center the modal
|
|
50
|
-
const { x, y } = centerModal(boxWidth, boxHeight, width, height);
|
|
51
|
-
// Visible candidates (with scroll)
|
|
52
|
-
const scrollOffset = Math.max(0, clampedIndex - maxListHeight + 1);
|
|
53
|
-
const visibleCandidates = filteredCandidates.slice(scrollOffset, scrollOffset + maxListHeight);
|
|
54
|
-
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 Base Branch", ' '] }) }), _jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Filter: " }), _jsx(Text, { color: "cyan", children: inputValue }), _jsx(Text, { color: "cyan", children: "\u258C" })] }), _jsx(Box, { flexDirection: "column", height: maxListHeight, children: visibleCandidates.length > 0 ? (visibleCandidates.map((branch, index) => {
|
|
55
|
-
const actualIndex = scrollOffset + index;
|
|
56
|
-
const isSelected = actualIndex === clampedIndex;
|
|
57
|
-
const isCurrent = branch === currentBranch;
|
|
58
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? 'cyan' : undefined, children: isSelected ? '▸ ' : ' ' }), _jsx(Text, { bold: isSelected, color: isSelected ? 'cyan' : undefined, children: branch }), isCurrent && _jsx(Text, { dimColor: true, children: " (current)" })] }, branch));
|
|
59
|
-
})) : inputValue ? (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " No matches. Press Enter to use: " }), _jsx(Text, { color: "yellow", children: inputValue })] })) : (_jsx(Text, { dimColor: true, children: " No candidates found" })) }), filteredCandidates.length > maxListHeight && (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [scrollOffset > 0 ? '↑ ' : ' ', scrollOffset + maxListHeight < filteredCandidates.length ? '↓ more' : ''] }) })), _jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate \u2022 Enter select \u2022 Esc cancel" }) })] }) }));
|
|
60
|
-
}
|
|
@@ -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
|
-
}
|