diffstalker 0.2.0 → 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/bun.lock +23 -0
- package/dist/App.js +225 -471
- package/dist/FollowMode.js +85 -0
- package/dist/KeyBindings.js +178 -0
- package/dist/MouseHandlers.js +156 -0
- package/dist/core/ExplorerStateManager.js +444 -78
- package/dist/core/GitStateManager.js +169 -93
- package/dist/git/diff.js +4 -0
- package/dist/index.js +54 -53
- package/dist/state/UIState.js +17 -4
- package/dist/ui/PaneRenderers.js +56 -0
- package/dist/ui/modals/FileFinder.js +232 -0
- package/dist/ui/widgets/CompareListView.js +86 -64
- package/dist/ui/widgets/DiffView.js +19 -17
- package/dist/ui/widgets/ExplorerContent.js +15 -28
- package/dist/ui/widgets/ExplorerView.js +140 -31
- package/dist/ui/widgets/Footer.js +6 -2
- package/dist/ui/widgets/Header.js +3 -46
- package/dist/utils/fileCategories.js +37 -0
- package/dist/utils/fileTree.js +148 -0
- package/eslint.metrics.js +16 -0
- package/metrics/.gitkeep +0 -0
- package/metrics/v0.2.1.json +268 -0
- package/package.json +4 -1
- package/dist/utils/ansiToBlessed.js +0 -125
- package/dist/utils/mouseCoordinates.js +0 -165
- package/dist/utils/rowCalculations.js +0 -246
|
@@ -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.2.
|
|
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",
|
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
"lint:fix": "eslint src/ --fix",
|
|
32
32
|
"format": "prettier --write src/",
|
|
33
33
|
"format:check": "prettier --check src/",
|
|
34
|
+
"metrics": "bun scripts/collect-metrics.ts",
|
|
35
|
+
"metrics:snapshot": "bun scripts/collect-metrics.ts --save",
|
|
34
36
|
"prepublishOnly": "bun run build:prod"
|
|
35
37
|
},
|
|
36
38
|
"keywords": [
|
|
@@ -58,6 +60,7 @@
|
|
|
58
60
|
"@types/node": "^22.10.7",
|
|
59
61
|
"eslint": "^9.39.2",
|
|
60
62
|
"eslint-config-prettier": "^10.1.8",
|
|
63
|
+
"eslint-plugin-sonarjs": "^3.0.6",
|
|
61
64
|
"patch-package": "^8.0.1",
|
|
62
65
|
"prettier": "^3.8.0",
|
|
63
66
|
"typescript": "^5.7.3",
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Convert ANSI escape codes to blessed tags.
|
|
3
|
-
* Supports basic foreground colors and styles.
|
|
4
|
-
*/
|
|
5
|
-
// ANSI color code to blessed color name mapping
|
|
6
|
-
const ANSI_FG_COLORS = {
|
|
7
|
-
30: 'black',
|
|
8
|
-
31: 'red',
|
|
9
|
-
32: 'green',
|
|
10
|
-
33: 'yellow',
|
|
11
|
-
34: 'blue',
|
|
12
|
-
35: 'magenta',
|
|
13
|
-
36: 'cyan',
|
|
14
|
-
37: 'white',
|
|
15
|
-
90: 'gray',
|
|
16
|
-
91: 'red',
|
|
17
|
-
92: 'green',
|
|
18
|
-
93: 'yellow',
|
|
19
|
-
94: 'blue',
|
|
20
|
-
95: 'magenta',
|
|
21
|
-
96: 'cyan',
|
|
22
|
-
97: 'white',
|
|
23
|
-
};
|
|
24
|
-
/**
|
|
25
|
-
* Escape blessed tags in plain text.
|
|
26
|
-
*/
|
|
27
|
-
function escapeBlessed(text) {
|
|
28
|
-
return text.replace(/\{/g, '{{').replace(/\}/g, '}}');
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Convert ANSI escape sequences to blessed tags.
|
|
32
|
-
*
|
|
33
|
-
* @param input - String containing ANSI escape codes
|
|
34
|
-
* @returns String with blessed tags
|
|
35
|
-
*/
|
|
36
|
-
export function ansiToBlessed(input) {
|
|
37
|
-
if (!input)
|
|
38
|
-
return '';
|
|
39
|
-
// Track current styles
|
|
40
|
-
const activeStyles = [];
|
|
41
|
-
let result = '';
|
|
42
|
-
let i = 0;
|
|
43
|
-
while (i < input.length) {
|
|
44
|
-
// Check for ANSI escape sequence
|
|
45
|
-
if (input[i] === '\x1b' && input[i + 1] === '[') {
|
|
46
|
-
// Find the end of the sequence (look for 'm')
|
|
47
|
-
let j = i + 2;
|
|
48
|
-
while (j < input.length && input[j] !== 'm') {
|
|
49
|
-
j++;
|
|
50
|
-
}
|
|
51
|
-
if (input[j] === 'm') {
|
|
52
|
-
// Parse the codes
|
|
53
|
-
const codes = input
|
|
54
|
-
.slice(i + 2, j)
|
|
55
|
-
.split(';')
|
|
56
|
-
.map(Number);
|
|
57
|
-
for (const code of codes) {
|
|
58
|
-
if (code === 0) {
|
|
59
|
-
// Reset - close all active styles
|
|
60
|
-
while (activeStyles.length > 0) {
|
|
61
|
-
const style = activeStyles.pop();
|
|
62
|
-
if (style) {
|
|
63
|
-
result += `{/${style}}`;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
else if (code === 1) {
|
|
68
|
-
// Bold
|
|
69
|
-
activeStyles.push('bold');
|
|
70
|
-
result += '{bold}';
|
|
71
|
-
}
|
|
72
|
-
else if (code === 2) {
|
|
73
|
-
// Dim/faint - blessed doesn't have direct support, use gray
|
|
74
|
-
activeStyles.push('gray-fg');
|
|
75
|
-
result += '{gray-fg}';
|
|
76
|
-
}
|
|
77
|
-
else if (code === 3) {
|
|
78
|
-
// Italic - not well supported in terminals, skip
|
|
79
|
-
}
|
|
80
|
-
else if (code === 4) {
|
|
81
|
-
// Underline
|
|
82
|
-
activeStyles.push('underline');
|
|
83
|
-
result += '{underline}';
|
|
84
|
-
}
|
|
85
|
-
else if (code >= 30 && code <= 37) {
|
|
86
|
-
// Standard foreground colors
|
|
87
|
-
const color = ANSI_FG_COLORS[code];
|
|
88
|
-
if (color) {
|
|
89
|
-
activeStyles.push(`${color}-fg`);
|
|
90
|
-
result += `{${color}-fg}`;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
else if (code >= 90 && code <= 97) {
|
|
94
|
-
// Bright foreground colors
|
|
95
|
-
const color = ANSI_FG_COLORS[code];
|
|
96
|
-
if (color) {
|
|
97
|
-
activeStyles.push(`${color}-fg`);
|
|
98
|
-
result += `{${color}-fg}`;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
// Note: We ignore background colors (40-47, 100-107) for simplicity
|
|
102
|
-
}
|
|
103
|
-
i = j + 1;
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
// Regular character - escape if needed and append
|
|
108
|
-
const char = input[i];
|
|
109
|
-
if (char === '{' || char === '}') {
|
|
110
|
-
result += char + char;
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
result += char;
|
|
114
|
-
}
|
|
115
|
-
i++;
|
|
116
|
-
}
|
|
117
|
-
// Close any remaining active styles
|
|
118
|
-
while (activeStyles.length > 0) {
|
|
119
|
-
const style = activeStyles.pop();
|
|
120
|
-
if (style) {
|
|
121
|
-
result += `{/${style}}`;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return result;
|
|
125
|
-
}
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import { categorizeFiles } from './fileCategories.js';
|
|
2
|
-
/**
|
|
3
|
-
* Calculate the row boundaries for each pane in the layout.
|
|
4
|
-
* Layout: Header (headerHeight) + sep (1) + top pane + sep (1) + bottom pane + sep (1) + footer (1)
|
|
5
|
-
*/
|
|
6
|
-
export function calculatePaneBoundaries(topPaneHeight, bottomPaneHeight, terminalHeight, headerHeight = 1) {
|
|
7
|
-
// Layout (1-indexed rows):
|
|
8
|
-
// Rows 1 to headerHeight: Header
|
|
9
|
-
// Row headerHeight+1: Separator
|
|
10
|
-
// Row headerHeight+2: Top pane header ("STAGING AREA" or "COMMITS")
|
|
11
|
-
// Rows headerHeight+3 to headerHeight+1+topPaneHeight: Top pane content
|
|
12
|
-
const stagingPaneStart = headerHeight + 2; // First row of top pane (the header row)
|
|
13
|
-
const fileListEnd = headerHeight + 1 + topPaneHeight; // Last row of top pane
|
|
14
|
-
const separatorRow = fileListEnd + 1; // Separator between panes
|
|
15
|
-
const diffPaneStart = fileListEnd + 2; // First row of bottom pane content
|
|
16
|
-
const diffPaneEnd = diffPaneStart + bottomPaneHeight - 1;
|
|
17
|
-
const footerRow = terminalHeight;
|
|
18
|
-
return {
|
|
19
|
-
stagingPaneStart,
|
|
20
|
-
fileListEnd,
|
|
21
|
-
separatorRow,
|
|
22
|
-
diffPaneStart,
|
|
23
|
-
diffPaneEnd,
|
|
24
|
-
footerRow,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Given a y-coordinate in the file list area, calculate which file index was clicked.
|
|
29
|
-
* Returns -1 if the click is not on a file.
|
|
30
|
-
*
|
|
31
|
-
* FileList layout: Modified → Untracked → Staged (with headers and spacers)
|
|
32
|
-
*/
|
|
33
|
-
export function getClickedFileIndex(y, scrollOffset, files, stagingPaneStart, fileListEnd) {
|
|
34
|
-
if (y < stagingPaneStart + 1 || y > fileListEnd)
|
|
35
|
-
return -1;
|
|
36
|
-
// Calculate which row in the list was clicked (0-indexed)
|
|
37
|
-
// Use stagingPaneStart + 1 to account for the "STAGING AREA" header row
|
|
38
|
-
const listRow = y - (stagingPaneStart + 1) + scrollOffset;
|
|
39
|
-
// Split files into 3 categories (same order as FileList)
|
|
40
|
-
const { modified: modifiedFiles, untracked: untrackedFiles, staged: stagedFiles, } = categorizeFiles(files);
|
|
41
|
-
// Build row map (same structure as FileList builds)
|
|
42
|
-
// Each section: header (1) + files (n)
|
|
43
|
-
// Spacer (1) between sections if previous section exists
|
|
44
|
-
let currentRow = 0;
|
|
45
|
-
let currentFileIndex = 0;
|
|
46
|
-
// Modified section
|
|
47
|
-
if (modifiedFiles.length > 0) {
|
|
48
|
-
currentRow++; // "Modified:" header
|
|
49
|
-
for (let i = 0; i < modifiedFiles.length; i++) {
|
|
50
|
-
if (listRow === currentRow) {
|
|
51
|
-
return currentFileIndex;
|
|
52
|
-
}
|
|
53
|
-
currentRow++;
|
|
54
|
-
currentFileIndex++;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
// Untracked section
|
|
58
|
-
if (untrackedFiles.length > 0) {
|
|
59
|
-
if (modifiedFiles.length > 0) {
|
|
60
|
-
currentRow++; // spacer
|
|
61
|
-
}
|
|
62
|
-
currentRow++; // "Untracked:" header
|
|
63
|
-
for (let i = 0; i < untrackedFiles.length; i++) {
|
|
64
|
-
if (listRow === currentRow) {
|
|
65
|
-
return currentFileIndex;
|
|
66
|
-
}
|
|
67
|
-
currentRow++;
|
|
68
|
-
currentFileIndex++;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// Staged section
|
|
72
|
-
if (stagedFiles.length > 0) {
|
|
73
|
-
if (modifiedFiles.length > 0 || untrackedFiles.length > 0) {
|
|
74
|
-
currentRow++; // spacer
|
|
75
|
-
}
|
|
76
|
-
currentRow++; // "Staged:" header
|
|
77
|
-
for (let i = 0; i < stagedFiles.length; i++) {
|
|
78
|
-
if (listRow === currentRow) {
|
|
79
|
-
return currentFileIndex;
|
|
80
|
-
}
|
|
81
|
-
currentRow++;
|
|
82
|
-
currentFileIndex++;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return -1;
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Calculate the x-coordinate boundaries for each tab in the footer.
|
|
89
|
-
* Tab layout (right-aligned): [1]Diff [2]Commit [3]History [4]Compare [5]Explorer (51 chars total)
|
|
90
|
-
*/
|
|
91
|
-
export function getTabBoundaries(terminalWidth) {
|
|
92
|
-
const tabsStart = terminalWidth - 51; // 1-indexed start of tabs section
|
|
93
|
-
return {
|
|
94
|
-
diffStart: tabsStart,
|
|
95
|
-
diffEnd: tabsStart + 6,
|
|
96
|
-
commitStart: tabsStart + 8,
|
|
97
|
-
commitEnd: tabsStart + 16,
|
|
98
|
-
historyStart: tabsStart + 18,
|
|
99
|
-
historyEnd: tabsStart + 27,
|
|
100
|
-
compareStart: tabsStart + 29,
|
|
101
|
-
compareEnd: tabsStart + 38,
|
|
102
|
-
explorerStart: tabsStart + 40,
|
|
103
|
-
explorerEnd: tabsStart + 50,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Given an x-coordinate in the footer row, determine which tab was clicked.
|
|
108
|
-
* Returns null if no tab was clicked.
|
|
109
|
-
*/
|
|
110
|
-
export function getClickedTab(x, terminalWidth) {
|
|
111
|
-
const bounds = getTabBoundaries(terminalWidth);
|
|
112
|
-
if (x >= bounds.diffStart && x <= bounds.diffEnd) {
|
|
113
|
-
return 'diff';
|
|
114
|
-
}
|
|
115
|
-
else if (x >= bounds.commitStart && x <= bounds.commitEnd) {
|
|
116
|
-
return 'commit';
|
|
117
|
-
}
|
|
118
|
-
else if (x >= bounds.historyStart && x <= bounds.historyEnd) {
|
|
119
|
-
return 'history';
|
|
120
|
-
}
|
|
121
|
-
else if (x >= bounds.compareStart && x <= bounds.compareEnd) {
|
|
122
|
-
return 'compare';
|
|
123
|
-
}
|
|
124
|
-
else if (x >= bounds.explorerStart && x <= bounds.explorerEnd) {
|
|
125
|
-
return 'explorer';
|
|
126
|
-
}
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Check if a click is in the file button area (first 6 columns for stage/unstage toggle).
|
|
131
|
-
*/
|
|
132
|
-
export function isButtonAreaClick(x) {
|
|
133
|
-
return x <= 6;
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Check if a y-coordinate is within a given pane.
|
|
137
|
-
*/
|
|
138
|
-
export function isInPane(y, paneStart, paneEnd) {
|
|
139
|
-
return y >= paneStart && y <= paneEnd;
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Given an x-coordinate in the footer row, determine which left indicator was clicked.
|
|
143
|
-
* Layout (scroll mode): "? [scroll] [auto] [wrap]"
|
|
144
|
-
* 1 3 10 12 17 19 24
|
|
145
|
-
* (In select mode, mouse tracking is disabled so clicks don't register)
|
|
146
|
-
*/
|
|
147
|
-
export function getFooterLeftClick(x) {
|
|
148
|
-
// "?" area: column 1
|
|
149
|
-
if (x === 1) {
|
|
150
|
-
return 'hotkeys';
|
|
151
|
-
}
|
|
152
|
-
// "[scroll]" or "[select]" area: columns 3-10
|
|
153
|
-
if (x >= 3 && x <= 10) {
|
|
154
|
-
return 'mouse-mode';
|
|
155
|
-
}
|
|
156
|
-
// "[auto]" area: columns 12-17
|
|
157
|
-
if (x >= 12 && x <= 17) {
|
|
158
|
-
return 'auto-tab';
|
|
159
|
-
}
|
|
160
|
-
// "[wrap]" area: columns 19-24
|
|
161
|
-
if (x >= 19 && x <= 24) {
|
|
162
|
-
return 'wrap';
|
|
163
|
-
}
|
|
164
|
-
return null;
|
|
165
|
-
}
|