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.
@@ -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.0",
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
- }