diffstalker 0.2.2 → 0.2.4

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.
Files changed (47) hide show
  1. package/.dependency-cruiser.cjs +2 -2
  2. package/dist/App.js +299 -664
  3. package/dist/KeyBindings.js +125 -39
  4. package/dist/ModalController.js +166 -0
  5. package/dist/MouseHandlers.js +43 -25
  6. package/dist/NavigationController.js +290 -0
  7. package/dist/StagingOperations.js +199 -0
  8. package/dist/config.js +39 -0
  9. package/dist/core/CompareManager.js +134 -0
  10. package/dist/core/ExplorerStateManager.js +27 -40
  11. package/dist/core/GitStateManager.js +28 -630
  12. package/dist/core/HistoryManager.js +72 -0
  13. package/dist/core/RemoteOperationManager.js +109 -0
  14. package/dist/core/WorkingTreeManager.js +412 -0
  15. package/dist/git/status.js +95 -0
  16. package/dist/index.js +59 -54
  17. package/dist/state/FocusRing.js +40 -0
  18. package/dist/state/UIState.js +82 -48
  19. package/dist/types/remote.js +5 -0
  20. package/dist/ui/PaneRenderers.js +11 -4
  21. package/dist/ui/modals/BaseBranchPicker.js +4 -7
  22. package/dist/ui/modals/CommitActionConfirm.js +66 -0
  23. package/dist/ui/modals/DiscardConfirm.js +4 -7
  24. package/dist/ui/modals/FileFinder.js +33 -27
  25. package/dist/ui/modals/HotkeysModal.js +32 -13
  26. package/dist/ui/modals/Modal.js +1 -0
  27. package/dist/ui/modals/RepoPicker.js +109 -0
  28. package/dist/ui/modals/ThemePicker.js +4 -7
  29. package/dist/ui/widgets/CommitPanel.js +52 -14
  30. package/dist/ui/widgets/CompareListView.js +1 -11
  31. package/dist/ui/widgets/DiffView.js +2 -27
  32. package/dist/ui/widgets/ExplorerContent.js +1 -4
  33. package/dist/ui/widgets/ExplorerView.js +1 -11
  34. package/dist/ui/widgets/FileList.js +2 -8
  35. package/dist/ui/widgets/Footer.js +1 -0
  36. package/dist/ui/widgets/Header.js +37 -3
  37. package/dist/utils/ansi.js +38 -0
  38. package/dist/utils/ansiTruncate.js +1 -5
  39. package/dist/utils/displayRows.js +72 -59
  40. package/dist/utils/fileCategories.js +7 -0
  41. package/dist/utils/fileResolution.js +23 -0
  42. package/dist/utils/languageDetection.js +3 -2
  43. package/dist/utils/logger.js +32 -0
  44. package/metrics/v0.2.3.json +243 -0
  45. package/metrics/v0.2.4.json +236 -0
  46. package/package.json +5 -2
  47. package/dist/utils/layoutCalculations.js +0 -100
@@ -0,0 +1,23 @@
1
+ import { getFileAtIndex } from './fileCategories.js';
2
+ import { getFlatFileAtIndex } from './flatFileList.js';
3
+ /**
4
+ * Resolve a FileEntry from an index, abstracting over flat vs categorized mode.
5
+ * In flat mode, returns the unstaged entry (preferred) or staged entry.
6
+ * In categorized mode, returns the file at the categorized index.
7
+ */
8
+ export function resolveFileAtIndex(index, flatViewMode, flatFiles, files) {
9
+ if (flatViewMode) {
10
+ const flatEntry = getFlatFileAtIndex(flatFiles, index);
11
+ return flatEntry?.unstagedEntry ?? flatEntry?.stagedEntry ?? null;
12
+ }
13
+ return getFileAtIndex(files, index);
14
+ }
15
+ /**
16
+ * Get the maximum valid file index for the current view mode.
17
+ */
18
+ export function getFileListMaxIndex(flatViewMode, flatFiles, files) {
19
+ if (flatViewMode) {
20
+ return flatFiles.length - 1;
21
+ }
22
+ return files.length - 1;
23
+ }
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { createEmphasize } from 'emphasize';
6
6
  import { common } from 'lowlight';
7
+ import { ANSI_FG_RESET } from './ansi.js';
7
8
  // Create emphasize instance with common languages
8
9
  const emphasize = createEmphasize(common);
9
10
  // Map file extensions to highlight.js language names
@@ -182,7 +183,7 @@ export function highlightLinePreserveBg(content, language) {
182
183
  const result = emphasize.highlight(language, content);
183
184
  // Replace full reset (\x1b[0m) with foreground-only reset (\x1b[39m)
184
185
  // This preserves any background color set by the caller
185
- return result.value.replace(/\x1b\[0m/g, '\x1b[39m');
186
+ return result.value.replace(/\x1b\[0m/g, ANSI_FG_RESET);
186
187
  }
187
188
  catch {
188
189
  return content;
@@ -217,7 +218,7 @@ export function highlightBlockPreserveBg(lines, language) {
217
218
  const block = lines.join('\n');
218
219
  const result = emphasize.highlight(language, block);
219
220
  // Replace full resets with foreground-only resets
220
- const highlighted = result.value.replace(/\x1b\[0m/g, '\x1b[39m');
221
+ const highlighted = result.value.replace(/\x1b\[0m/g, ANSI_FG_RESET);
221
222
  return highlighted.split('\n');
222
223
  }
223
224
  catch {
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Lightweight structured logger writing to stderr.
3
+ *
4
+ * - `debug()` is gated by `setDebug(true)` (set from the --debug flag)
5
+ * - `warn()` and `error()` always write to stderr
6
+ */
7
+ let debugEnabled = false;
8
+ export function setDebug(enabled) {
9
+ debugEnabled = enabled;
10
+ }
11
+ function timestamp() {
12
+ return new Date().toISOString();
13
+ }
14
+ export function debug(message) {
15
+ if (debugEnabled) {
16
+ process.stderr.write(`[diffstalker ${timestamp()}] ${message}\n`);
17
+ }
18
+ }
19
+ export function warn(message) {
20
+ process.stderr.write(`[diffstalker warn] ${message}\n`);
21
+ }
22
+ function formatError(err) {
23
+ if (err instanceof Error)
24
+ return err.message;
25
+ if (err)
26
+ return String(err);
27
+ return '';
28
+ }
29
+ export function error(message, err) {
30
+ const detail = err ? `: ${formatError(err)}` : '';
31
+ process.stderr.write(`[diffstalker error] ${message}${detail}\n`);
32
+ }
@@ -0,0 +1,243 @@
1
+ {
2
+ "timestamp": "2026-02-06T18:38:55.083Z",
3
+ "gitRef": "v0.2.3",
4
+ "gitSha": "11ba9fc",
5
+ "summary": {
6
+ "files": 82,
7
+ "lines": 16165,
8
+ "functions": 1146,
9
+ "avgCyclomaticComplexity": 5.1,
10
+ "maxCyclomaticComplexity": {
11
+ "value": 42,
12
+ "function": "buildDiffDisplayRows",
13
+ "file": "src/utils/displayRows.ts:112"
14
+ },
15
+ "avgCognitiveComplexity": 6.5,
16
+ "maxCognitiveComplexity": {
17
+ "value": 68,
18
+ "function": "buildDiffDisplayRows",
19
+ "file": "src/utils/displayRows.ts:112"
20
+ },
21
+ "smells": 0
22
+ },
23
+ "hotspots": [
24
+ {
25
+ "file": "src/utils/displayRows.ts",
26
+ "lines": 567,
27
+ "cyclomaticMax": 42,
28
+ "cognitiveMax": 68,
29
+ "smells": 0
30
+ },
31
+ {
32
+ "file": "src/ui/widgets/CommitPanel.ts",
33
+ "lines": 217,
34
+ "cyclomaticMax": 34,
35
+ "cognitiveMax": 42,
36
+ "smells": 0
37
+ },
38
+ {
39
+ "file": "src/App.ts",
40
+ "lines": 1548,
41
+ "cyclomaticMax": 31,
42
+ "cognitiveMax": 13,
43
+ "smells": 0
44
+ },
45
+ {
46
+ "file": "src/utils/flatFileList.ts",
47
+ "lines": 100,
48
+ "cyclomaticMax": 29,
49
+ "cognitiveMax": 14,
50
+ "smells": 0
51
+ },
52
+ {
53
+ "file": "src/git/diff.ts",
54
+ "lines": 656,
55
+ "cyclomaticMax": 26,
56
+ "cognitiveMax": 41,
57
+ "smells": 0
58
+ },
59
+ {
60
+ "file": "src/git/status.ts",
61
+ "lines": 392,
62
+ "cyclomaticMax": 22,
63
+ "cognitiveMax": 25,
64
+ "smells": 0
65
+ },
66
+ {
67
+ "file": "src/ui/PaneRenderers.ts",
68
+ "lines": 249,
69
+ "cyclomaticMax": 20,
70
+ "cognitiveMax": 8,
71
+ "smells": 0
72
+ },
73
+ {
74
+ "file": "src/ui/widgets/ExplorerContent.ts",
75
+ "lines": 130,
76
+ "cyclomaticMax": 20,
77
+ "cognitiveMax": 24,
78
+ "smells": 0
79
+ },
80
+ {
81
+ "file": "src/ui/widgets/Header.ts",
82
+ "lines": 134,
83
+ "cyclomaticMax": 18,
84
+ "cognitiveMax": 22,
85
+ "smells": 0
86
+ },
87
+ {
88
+ "file": "src/MouseHandlers.ts",
89
+ "lines": 268,
90
+ "cyclomaticMax": 17,
91
+ "cognitiveMax": 15,
92
+ "smells": 0
93
+ },
94
+ {
95
+ "file": "src/ipc/CommandServer.ts",
96
+ "lines": 266,
97
+ "cyclomaticMax": 17,
98
+ "cognitiveMax": 6,
99
+ "smells": 0
100
+ },
101
+ {
102
+ "file": "src/index.ts",
103
+ "lines": 178,
104
+ "cyclomaticMax": 16,
105
+ "cognitiveMax": 17,
106
+ "smells": 0
107
+ },
108
+ {
109
+ "file": "src/ui/widgets/ExplorerView.ts",
110
+ "lines": 245,
111
+ "cyclomaticMax": 16,
112
+ "cognitiveMax": 21,
113
+ "smells": 0
114
+ },
115
+ {
116
+ "file": "src/utils/ansiTruncate.ts",
117
+ "lines": 123,
118
+ "cyclomaticMax": 16,
119
+ "cognitiveMax": 24,
120
+ "smells": 0
121
+ },
122
+ {
123
+ "file": "src/ui/widgets/DiffView.ts",
124
+ "lines": 511,
125
+ "cyclomaticMax": 15,
126
+ "cognitiveMax": 12,
127
+ "smells": 0
128
+ },
129
+ {
130
+ "file": "src/ui/widgets/CompareListView.ts",
131
+ "lines": 383,
132
+ "cyclomaticMax": 14,
133
+ "cognitiveMax": 16,
134
+ "smells": 0
135
+ },
136
+ {
137
+ "file": "src/utils/diffRowCalculations.ts",
138
+ "lines": 136,
139
+ "cyclomaticMax": 13,
140
+ "cognitiveMax": 24,
141
+ "smells": 0
142
+ },
143
+ {
144
+ "file": "src/core/ExplorerStateManager.ts",
145
+ "lines": 727,
146
+ "cyclomaticMax": 12,
147
+ "cognitiveMax": 15,
148
+ "smells": 0
149
+ },
150
+ {
151
+ "file": "src/ui/widgets/FileList.ts",
152
+ "lines": 215,
153
+ "cyclomaticMax": 12,
154
+ "cognitiveMax": 12,
155
+ "smells": 0
156
+ },
157
+ {
158
+ "file": "src/utils/lineBreaking.ts",
159
+ "lines": 114,
160
+ "cyclomaticMax": 12,
161
+ "cognitiveMax": 17,
162
+ "smells": 0
163
+ },
164
+ {
165
+ "file": "src/ui/modals/BranchPicker.ts",
166
+ "lines": 185,
167
+ "cyclomaticMax": 11,
168
+ "cognitiveMax": 17,
169
+ "smells": 0
170
+ },
171
+ {
172
+ "file": "src/config.ts",
173
+ "lines": 107,
174
+ "cyclomaticMax": 10,
175
+ "cognitiveMax": 13,
176
+ "smells": 0
177
+ },
178
+ {
179
+ "file": "src/state/UIState.ts",
180
+ "lines": 307,
181
+ "cyclomaticMax": 10,
182
+ "cognitiveMax": 12,
183
+ "smells": 0
184
+ },
185
+ {
186
+ "file": "src/KeyBindings.ts",
187
+ "lines": 373,
188
+ "cyclomaticMax": 9,
189
+ "cognitiveMax": 15,
190
+ "smells": 0
191
+ },
192
+ {
193
+ "file": "src/core/GitStateManager.ts",
194
+ "lines": 989,
195
+ "cyclomaticMax": 9,
196
+ "cognitiveMax": 12,
197
+ "smells": 0
198
+ },
199
+ {
200
+ "file": "src/ui/widgets/HistoryView.ts",
201
+ "lines": 97,
202
+ "cyclomaticMax": 9,
203
+ "cognitiveMax": 12,
204
+ "smells": 0
205
+ },
206
+ {
207
+ "file": "src/utils/formatPath.ts",
208
+ "lines": 72,
209
+ "cyclomaticMax": 9,
210
+ "cognitiveMax": 11,
211
+ "smells": 0
212
+ },
213
+ {
214
+ "file": "src/utils/explorerDisplayRows.ts",
215
+ "lines": 206,
216
+ "cyclomaticMax": 8,
217
+ "cognitiveMax": 15,
218
+ "smells": 0
219
+ },
220
+ {
221
+ "file": "src/ui/modals/FileFinder.ts",
222
+ "lines": 238,
223
+ "cyclomaticMax": 7,
224
+ "cognitiveMax": 13,
225
+ "smells": 0
226
+ },
227
+ {
228
+ "file": "src/ui/modals/BaseBranchPicker.ts",
229
+ "lines": 134,
230
+ "cyclomaticMax": 6,
231
+ "cognitiveMax": 13,
232
+ "smells": 0
233
+ },
234
+ {
235
+ "file": "src/ui/modals/StashListModal.ts",
236
+ "lines": 122,
237
+ "cyclomaticMax": 5,
238
+ "cognitiveMax": 11,
239
+ "smells": 0
240
+ }
241
+ ],
242
+ "smellsByRule": {}
243
+ }
@@ -0,0 +1,236 @@
1
+ {
2
+ "timestamp": "2026-03-06T05:12:44.045Z",
3
+ "gitRef": "v0.2.4",
4
+ "gitSha": "0677167",
5
+ "summary": {
6
+ "files": 91,
7
+ "lines": 16342,
8
+ "functions": 1163,
9
+ "avgCyclomaticComplexity": 4.9,
10
+ "maxCyclomaticComplexity": {
11
+ "value": 34,
12
+ "function": "reconcileSelectionAfterStateChange",
13
+ "file": "src/App.ts:538"
14
+ },
15
+ "avgCognitiveComplexity": 6.1,
16
+ "maxCognitiveComplexity": {
17
+ "value": 41,
18
+ "function": "(anonymous)",
19
+ "file": "src/git/diff.ts:517"
20
+ },
21
+ "smells": 0
22
+ },
23
+ "hotspots": [
24
+ {
25
+ "file": "src/App.ts",
26
+ "lines": 1004,
27
+ "cyclomaticMax": 34,
28
+ "cognitiveMax": 19,
29
+ "smells": 0
30
+ },
31
+ {
32
+ "file": "src/utils/flatFileList.ts",
33
+ "lines": 100,
34
+ "cyclomaticMax": 29,
35
+ "cognitiveMax": 14,
36
+ "smells": 0
37
+ },
38
+ {
39
+ "file": "src/git/diff.ts",
40
+ "lines": 656,
41
+ "cyclomaticMax": 26,
42
+ "cognitiveMax": 41,
43
+ "smells": 0
44
+ },
45
+ {
46
+ "file": "src/utils/displayRows.ts",
47
+ "lines": 614,
48
+ "cyclomaticMax": 25,
49
+ "cognitiveMax": 39,
50
+ "smells": 0
51
+ },
52
+ {
53
+ "file": "src/git/status.ts",
54
+ "lines": 392,
55
+ "cyclomaticMax": 22,
56
+ "cognitiveMax": 25,
57
+ "smells": 0
58
+ },
59
+ {
60
+ "file": "src/ui/PaneRenderers.ts",
61
+ "lines": 240,
62
+ "cyclomaticMax": 20,
63
+ "cognitiveMax": 8,
64
+ "smells": 0
65
+ },
66
+ {
67
+ "file": "src/ui/widgets/ExplorerContent.ts",
68
+ "lines": 126,
69
+ "cyclomaticMax": 20,
70
+ "cognitiveMax": 24,
71
+ "smells": 0
72
+ },
73
+ {
74
+ "file": "src/NavigationController.ts",
75
+ "lines": 352,
76
+ "cyclomaticMax": 19,
77
+ "cognitiveMax": 13,
78
+ "smells": 0
79
+ },
80
+ {
81
+ "file": "src/config.ts",
82
+ "lines": 168,
83
+ "cyclomaticMax": 18,
84
+ "cognitiveMax": 25,
85
+ "smells": 0
86
+ },
87
+ {
88
+ "file": "src/ui/widgets/CommitPanel.ts",
89
+ "lines": 133,
90
+ "cyclomaticMax": 18,
91
+ "cognitiveMax": 16,
92
+ "smells": 0
93
+ },
94
+ {
95
+ "file": "src/ui/widgets/Header.ts",
96
+ "lines": 134,
97
+ "cyclomaticMax": 18,
98
+ "cognitiveMax": 22,
99
+ "smells": 0
100
+ },
101
+ {
102
+ "file": "src/MouseHandlers.ts",
103
+ "lines": 276,
104
+ "cyclomaticMax": 17,
105
+ "cognitiveMax": 15,
106
+ "smells": 0
107
+ },
108
+ {
109
+ "file": "src/ipc/CommandServer.ts",
110
+ "lines": 266,
111
+ "cyclomaticMax": 17,
112
+ "cognitiveMax": 6,
113
+ "smells": 0
114
+ },
115
+ {
116
+ "file": "src/index.ts",
117
+ "lines": 180,
118
+ "cyclomaticMax": 16,
119
+ "cognitiveMax": 17,
120
+ "smells": 0
121
+ },
122
+ {
123
+ "file": "src/ui/widgets/ExplorerView.ts",
124
+ "lines": 245,
125
+ "cyclomaticMax": 16,
126
+ "cognitiveMax": 21,
127
+ "smells": 0
128
+ },
129
+ {
130
+ "file": "src/utils/ansiTruncate.ts",
131
+ "lines": 118,
132
+ "cyclomaticMax": 16,
133
+ "cognitiveMax": 24,
134
+ "smells": 0
135
+ },
136
+ {
137
+ "file": "src/ui/widgets/DiffView.ts",
138
+ "lines": 493,
139
+ "cyclomaticMax": 15,
140
+ "cognitiveMax": 12,
141
+ "smells": 0
142
+ },
143
+ {
144
+ "file": "src/ui/widgets/CompareListView.ts",
145
+ "lines": 383,
146
+ "cyclomaticMax": 14,
147
+ "cognitiveMax": 16,
148
+ "smells": 0
149
+ },
150
+ {
151
+ "file": "src/core/ExplorerStateManager.ts",
152
+ "lines": 735,
153
+ "cyclomaticMax": 13,
154
+ "cognitiveMax": 17,
155
+ "smells": 0
156
+ },
157
+ {
158
+ "file": "src/utils/diffRowCalculations.ts",
159
+ "lines": 136,
160
+ "cyclomaticMax": 13,
161
+ "cognitiveMax": 24,
162
+ "smells": 0
163
+ },
164
+ {
165
+ "file": "src/StagingOperations.ts",
166
+ "lines": 224,
167
+ "cyclomaticMax": 12,
168
+ "cognitiveMax": 10,
169
+ "smells": 0
170
+ },
171
+ {
172
+ "file": "src/ui/widgets/FileList.ts",
173
+ "lines": 209,
174
+ "cyclomaticMax": 12,
175
+ "cognitiveMax": 12,
176
+ "smells": 0
177
+ },
178
+ {
179
+ "file": "src/utils/lineBreaking.ts",
180
+ "lines": 114,
181
+ "cyclomaticMax": 12,
182
+ "cognitiveMax": 17,
183
+ "smells": 0
184
+ },
185
+ {
186
+ "file": "src/core/WorkingTreeManager.ts",
187
+ "lines": 515,
188
+ "cyclomaticMax": 10,
189
+ "cognitiveMax": 13,
190
+ "smells": 0
191
+ },
192
+ {
193
+ "file": "src/ui/widgets/HistoryView.ts",
194
+ "lines": 97,
195
+ "cyclomaticMax": 9,
196
+ "cognitiveMax": 12,
197
+ "smells": 0
198
+ },
199
+ {
200
+ "file": "src/utils/formatPath.ts",
201
+ "lines": 72,
202
+ "cyclomaticMax": 9,
203
+ "cognitiveMax": 11,
204
+ "smells": 0
205
+ },
206
+ {
207
+ "file": "src/utils/explorerDisplayRows.ts",
208
+ "lines": 206,
209
+ "cyclomaticMax": 8,
210
+ "cognitiveMax": 15,
211
+ "smells": 0
212
+ },
213
+ {
214
+ "file": "src/ui/modals/FileFinder.ts",
215
+ "lines": 236,
216
+ "cyclomaticMax": 7,
217
+ "cognitiveMax": 13,
218
+ "smells": 0
219
+ },
220
+ {
221
+ "file": "src/ui/modals/BaseBranchPicker.ts",
222
+ "lines": 132,
223
+ "cyclomaticMax": 6,
224
+ "cognitiveMax": 13,
225
+ "smells": 0
226
+ },
227
+ {
228
+ "file": "src/ui/modals/RepoPicker.ts",
229
+ "lines": 134,
230
+ "cyclomaticMax": 6,
231
+ "cognitiveMax": 13,
232
+ "smells": 0
233
+ }
234
+ ],
235
+ "smellsByRule": {}
236
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffstalker",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Terminal application that displays git diff/status for directories",
5
5
  "author": "yogh-io",
6
6
  "license": "MIT",
@@ -35,7 +35,10 @@
35
35
  "deps": "depcruise src/ --config .dependency-cruiser.cjs",
36
36
  "metrics": "bun scripts/collect-metrics.ts",
37
37
  "metrics:snapshot": "bun scripts/collect-metrics.ts --save",
38
- "prepublishOnly": "bun run build:prod"
38
+ "prepublishOnly": "bun run build:prod",
39
+ "release": "scripts/release.sh",
40
+ "release:minor": "scripts/release.sh minor",
41
+ "release:major": "scripts/release.sh major"
39
42
  },
40
43
  "keywords": [
41
44
  "git",
@@ -1,100 +0,0 @@
1
- import { getFileListSectionCounts } from './fileCategories.js';
2
- // Re-export for backwards compatibility
3
- export { getFileListSectionCounts } from './fileCategories.js';
4
- /**
5
- * Calculate total rows for the FileList component.
6
- * Accounts for headers and spacers between sections.
7
- */
8
- export function getFileListTotalRows(files) {
9
- const { modifiedCount, untrackedCount, stagedCount } = getFileListSectionCounts(files);
10
- let rows = 0;
11
- // Modified section
12
- if (modifiedCount > 0) {
13
- rows += 1 + modifiedCount; // header + files
14
- }
15
- // Untracked section
16
- if (untrackedCount > 0) {
17
- if (modifiedCount > 0)
18
- rows += 1; // spacer
19
- rows += 1 + untrackedCount; // header + files
20
- }
21
- // Staged section
22
- if (stagedCount > 0) {
23
- if (modifiedCount > 0 || untrackedCount > 0)
24
- rows += 1; // spacer
25
- rows += 1 + stagedCount; // header + files
26
- }
27
- return rows;
28
- }
29
- /**
30
- * Calculate the heights of the top (file list) and bottom (diff/commit/etc) panes
31
- * based on the number of files and available content area.
32
- *
33
- * The top pane grows to fit files up to 40% of content height.
34
- * The bottom pane gets the remaining space.
35
- *
36
- * When flatRowCount is provided (flat view mode), uses that directly instead
37
- * of computing row count from categorized file list.
38
- */
39
- export function calculatePaneHeights(files, contentHeight, maxTopRatio = 0.4, flatRowCount) {
40
- // Calculate content rows needed for staging area
41
- const neededRows = flatRowCount !== undefined ? flatRowCount : getFileListTotalRows(files);
42
- // Minimum height of 3 (header + 2 lines for empty state)
43
- const minHeight = 3;
44
- // Maximum is maxTopRatio of content area
45
- const maxHeight = Math.floor(contentHeight * maxTopRatio);
46
- // Use the smaller of needed or max, but at least min
47
- const topHeight = Math.max(minHeight, Math.min(neededRows, maxHeight));
48
- const bottomHeight = contentHeight - topHeight;
49
- return { topPaneHeight: topHeight, bottomPaneHeight: bottomHeight };
50
- }
51
- /**
52
- * Calculate which row in the file list a file at a given index occupies.
53
- * This accounts for headers and spacers in the list.
54
- * File order: Modified → Untracked → Staged (matches FileList.tsx)
55
- */
56
- export function getRowForFileIndex(selectedIndex, modifiedCount, untrackedCount, _stagedCount) {
57
- let row = 0;
58
- // Modified section
59
- if (selectedIndex < modifiedCount) {
60
- // In modified section: header + file rows
61
- return 1 + selectedIndex;
62
- }
63
- if (modifiedCount > 0) {
64
- row += 1 + modifiedCount; // header + files
65
- }
66
- // Untracked section
67
- const untrackedStart = modifiedCount;
68
- if (selectedIndex < untrackedStart + untrackedCount) {
69
- // In untracked section
70
- const untrackedIdx = selectedIndex - untrackedStart;
71
- if (modifiedCount > 0)
72
- row += 1; // spacer
73
- return row + 1 + untrackedIdx; // header + file position
74
- }
75
- if (untrackedCount > 0) {
76
- if (modifiedCount > 0)
77
- row += 1; // spacer
78
- row += 1 + untrackedCount; // header + files
79
- }
80
- // Staged section
81
- const stagedStart = modifiedCount + untrackedCount;
82
- const stagedIdx = selectedIndex - stagedStart;
83
- if (modifiedCount > 0 || untrackedCount > 0)
84
- row += 1; // spacer
85
- return row + 1 + stagedIdx; // header + file position
86
- }
87
- /**
88
- * Calculate the scroll offset needed to keep a selected row visible.
89
- */
90
- export function calculateScrollOffset(selectedRow, currentScrollOffset, visibleHeight) {
91
- // Scroll up if selected is above visible area
92
- if (selectedRow < currentScrollOffset) {
93
- return Math.max(0, selectedRow - 1);
94
- }
95
- // Scroll down if selected is below visible area
96
- else if (selectedRow >= currentScrollOffset + visibleHeight) {
97
- return selectedRow - visibleHeight + 1;
98
- }
99
- return currentScrollOffset;
100
- }