diffwatch 1.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +115 -47
- package/bin/diffwatch.js +30 -0
- package/dist/diffwatch.exe +0 -0
- package/package.json +67 -29
- package/AGENTS.md +0 -201
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -605
- package/dist/index.js.map +0 -1
- package/dist/utils/diff-formatter.d.ts +0 -1
- package/dist/utils/diff-formatter.js +0 -108
- package/dist/utils/diff-formatter.js.map +0 -1
- package/dist/utils/git.d.ts +0 -16
- package/dist/utils/git.d.ts.map +0 -1
- package/dist/utils/git.js +0 -196
- package/dist/utils/git.js.map +0 -1
- package/jest.config.js +0 -11
- package/screenshot.jpg +0 -0
- package/src/index.ts +0 -640
- package/src/utils/diff-formatter.ts +0 -81
- package/src/utils/git.ts +0 -185
- package/tests/git.test.d.ts +0 -2
- package/tests/git.test.d.ts.map +0 -1
- package/tests/git.test.js.map +0 -1
- package/tests/git.test.ts +0 -148
- package/tsconfig.json +0 -16
package/src/index.ts
DELETED
|
@@ -1,640 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
const blessed = require('neo-neo-blessed');
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import { spawn } from 'child_process';
|
|
5
|
-
import { GitHandler, FileStatus } from './utils/git';
|
|
6
|
-
import { formatDiffWithDiff2Html } from './utils/diff-formatter';
|
|
7
|
-
import { readFileSync } from 'fs';
|
|
8
|
-
import { dirname, join } from 'path';
|
|
9
|
-
|
|
10
|
-
const packageJson = JSON.parse(readFileSync(join(dirname(__filename), '..', 'package.json'), 'utf-8'));
|
|
11
|
-
|
|
12
|
-
async function main() {
|
|
13
|
-
const args = process.argv.slice(2);
|
|
14
|
-
let repoPath = process.cwd();
|
|
15
|
-
|
|
16
|
-
const showHelp = () => {
|
|
17
|
-
console.log(`
|
|
18
|
-
Usage: diffwatch [path]
|
|
19
|
-
|
|
20
|
-
Arguments:
|
|
21
|
-
path Path to the git repository (default: current directory)
|
|
22
|
-
|
|
23
|
-
Options:
|
|
24
|
-
-h, --help Show help information
|
|
25
|
-
-v, --version Show version information
|
|
26
|
-
`);
|
|
27
|
-
process.exit(0);
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const showVersion = () => {
|
|
31
|
-
console.log(`diffwatch v${packageJson.version}`);
|
|
32
|
-
process.exit(0);
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const positionalArgs = [];
|
|
36
|
-
for (let i = 0; i < args.length; i++) {
|
|
37
|
-
if (args[i] === '-h' || args[i] === '--help') {
|
|
38
|
-
showHelp();
|
|
39
|
-
} else if (args[i] === '-v' || args[i] === '--version') {
|
|
40
|
-
showVersion();
|
|
41
|
-
} else if (args[i].startsWith('-')) {
|
|
42
|
-
// Ignore unknown flags or handle them if needed
|
|
43
|
-
} else {
|
|
44
|
-
positionalArgs.push(args[i]);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (positionalArgs.length > 0) {
|
|
49
|
-
repoPath = positionalArgs[0];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
process.chdir(repoPath);
|
|
54
|
-
} catch (err) {
|
|
55
|
-
console.error(`Error: Could not change directory to ${repoPath}`);
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const gitHandler = new GitHandler(repoPath);
|
|
60
|
-
|
|
61
|
-
if (!(await gitHandler.isRepo())) {
|
|
62
|
-
console.log(chalk.red(`Error: ${repoPath} is not a git repository.`));
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Force xterm-256color to encourage better mouse support on Windows terminals
|
|
67
|
-
if (process.platform === 'win32' && !process.env.TERM) {
|
|
68
|
-
process.env.TERM = 'xterm-256color';
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const screen = blessed.screen({
|
|
72
|
-
smartCSR: true,
|
|
73
|
-
title: 'diffwatch',
|
|
74
|
-
mouse: true,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// Explicitly enable mouse tracking
|
|
78
|
-
screen.program.enableMouse();
|
|
79
|
-
|
|
80
|
-
let fileList: any = blessed.list({
|
|
81
|
-
top: 0,
|
|
82
|
-
left: 0,
|
|
83
|
-
width: '30%',
|
|
84
|
-
height: '99%',
|
|
85
|
-
label: 'Files (0)',
|
|
86
|
-
keys: true,
|
|
87
|
-
vi: true,
|
|
88
|
-
mouse: true,
|
|
89
|
-
tags: true,
|
|
90
|
-
scrollbar: {
|
|
91
|
-
ch: ' ',
|
|
92
|
-
track: { bg: 'white' },
|
|
93
|
-
style: { bg: 'blue' },
|
|
94
|
-
},
|
|
95
|
-
style: {
|
|
96
|
-
selected: { fg: 'black', bg: 'white' },
|
|
97
|
-
border: { fg: 'white' },
|
|
98
|
-
},
|
|
99
|
-
border: { type: 'line' },
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
const createFileList = () => {
|
|
103
|
-
const newList = blessed.list({
|
|
104
|
-
top: 0,
|
|
105
|
-
left: 0,
|
|
106
|
-
width: '30%',
|
|
107
|
-
height: '99%',
|
|
108
|
-
label: 'Files (0)',
|
|
109
|
-
keys: true,
|
|
110
|
-
vi: true,
|
|
111
|
-
mouse: true,
|
|
112
|
-
tags: true,
|
|
113
|
-
scrollbar: {
|
|
114
|
-
ch: ' ',
|
|
115
|
-
track: { bg: 'white' },
|
|
116
|
-
style: { bg: 'blue' },
|
|
117
|
-
},
|
|
118
|
-
style: {
|
|
119
|
-
selected: { fg: 'black', bg: 'white' },
|
|
120
|
-
border: { fg: 'white' },
|
|
121
|
-
},
|
|
122
|
-
border: { type: 'line' },
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
newList.on('select item', () => {
|
|
126
|
-
scheduleDiffUpdate();
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
newList.key(['up', 'down'], () => {
|
|
130
|
-
scheduleDiffUpdate();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
newList.on('wheeldown', () => handleScroll('down'));
|
|
134
|
-
newList.on('wheelup', () => handleScroll('up'));
|
|
135
|
-
|
|
136
|
-
return newList;
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const diffView = blessed.scrollabletext({
|
|
140
|
-
top: 0,
|
|
141
|
-
left: '30%',
|
|
142
|
-
width: '70%',
|
|
143
|
-
height: '99%',
|
|
144
|
-
label: ' Diff () ',
|
|
145
|
-
keys: true,
|
|
146
|
-
vi: true,
|
|
147
|
-
mouse: true,
|
|
148
|
-
scrollbar: {
|
|
149
|
-
ch: ' ',
|
|
150
|
-
track: { bg: 'white' },
|
|
151
|
-
style: { bg: 'blue' },
|
|
152
|
-
},
|
|
153
|
-
style: {
|
|
154
|
-
border: { fg: 'white' },
|
|
155
|
-
},
|
|
156
|
-
border: { type: 'line' },
|
|
157
|
-
tags: false,
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
const searchBox = blessed.box({
|
|
161
|
-
top: 'center',
|
|
162
|
-
left: '40%', // Center of right pane (30% + 70%/2 = 65%)
|
|
163
|
-
width: '50%',
|
|
164
|
-
height: 3,
|
|
165
|
-
label: ' Search ',
|
|
166
|
-
border: { type: 'line' },
|
|
167
|
-
style: { border: { fg: 'yellow' } },
|
|
168
|
-
hidden: true,
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
const searchInput = blessed.textbox({
|
|
172
|
-
parent: searchBox,
|
|
173
|
-
top: 0,
|
|
174
|
-
left: 0,
|
|
175
|
-
width: '100%-2',
|
|
176
|
-
height: 1,
|
|
177
|
-
keys: true,
|
|
178
|
-
inputOnFocus: true,
|
|
179
|
-
style: { fg: 'white', bg: 'black' },
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
// Confirmation dialog for revert
|
|
183
|
-
const confirmDialog = blessed.box({
|
|
184
|
-
top: 'center',
|
|
185
|
-
left: '45%', // Center of right pane (30% + 70%/2 = 65%)
|
|
186
|
-
width: '38%',
|
|
187
|
-
label: ' Confirm Revert ',
|
|
188
|
-
height: 3,
|
|
189
|
-
content: 'Press SPACE key to confirm revert or ESC to cancel.',
|
|
190
|
-
border: { type: 'line' },
|
|
191
|
-
style: {
|
|
192
|
-
fg: 'yellow',
|
|
193
|
-
bg: 'black',
|
|
194
|
-
border: { fg: 'yellow' }
|
|
195
|
-
},
|
|
196
|
-
hidden: true,
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
const notificationBox = blessed.box({
|
|
200
|
-
top: 'center',
|
|
201
|
-
left: '45%', // Center of right pane (30% + 70%/2 = 65%)
|
|
202
|
-
width: '38%',
|
|
203
|
-
height: 3,
|
|
204
|
-
label: ' Notification ',
|
|
205
|
-
border: { type: 'line' },
|
|
206
|
-
style: {
|
|
207
|
-
fg: 'green',
|
|
208
|
-
bg: 'black',
|
|
209
|
-
border: { fg: 'green' }
|
|
210
|
-
},
|
|
211
|
-
hidden: true,
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
const showNotification = (message: string, color: 'green' | 'red' = 'green') => {
|
|
215
|
-
const previouslyFocused = screen.focused;
|
|
216
|
-
notificationBox.setContent(message);
|
|
217
|
-
notificationBox.style = {
|
|
218
|
-
fg: color,
|
|
219
|
-
bg: 'black',
|
|
220
|
-
border: { fg: color }
|
|
221
|
-
};
|
|
222
|
-
notificationBox.show();
|
|
223
|
-
notificationBox.setFront();
|
|
224
|
-
screen.render();
|
|
225
|
-
|
|
226
|
-
setTimeout(() => {
|
|
227
|
-
notificationBox.hide();
|
|
228
|
-
if (previouslyFocused && previouslyFocused !== notificationBox) {
|
|
229
|
-
previouslyFocused.focus();
|
|
230
|
-
} else {
|
|
231
|
-
fileList.focus();
|
|
232
|
-
}
|
|
233
|
-
updateBorders();
|
|
234
|
-
screen.render();
|
|
235
|
-
}, 3000);
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
// Branch display in bottom right
|
|
239
|
-
const branchBox = blessed.box({
|
|
240
|
-
bottom: 0,
|
|
241
|
-
right: 0,
|
|
242
|
-
width: 'shrink',
|
|
243
|
-
height: 1,
|
|
244
|
-
align: 'left',
|
|
245
|
-
content: chalk.cyan('Branch: '),
|
|
246
|
-
style: {
|
|
247
|
-
fg: 'white',
|
|
248
|
-
},
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
// Footer box to show shortcuts - aligned with the panes
|
|
252
|
-
const footer = blessed.box({
|
|
253
|
-
bottom: 0,
|
|
254
|
-
left: 0,
|
|
255
|
-
width: '100%',
|
|
256
|
-
height: 1,
|
|
257
|
-
content: chalk.green('←→') + ' Switch |' + chalk.green(' ⏎') + ' Open | ' + chalk.green('S') + ' Search | ' + chalk.green('R') + ' Revert | ' + chalk.green('Q') + ' Quit ',
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
screen.append(fileList);
|
|
261
|
-
screen.append(diffView);
|
|
262
|
-
screen.append(searchBox);
|
|
263
|
-
screen.append(notificationBox);
|
|
264
|
-
screen.append(confirmDialog);
|
|
265
|
-
screen.append(footer);
|
|
266
|
-
screen.append(branchBox);
|
|
267
|
-
|
|
268
|
-
const updateBorders = () => {
|
|
269
|
-
fileList.style.border.fg = screen.focused === fileList ? 'yellow' : 'white';
|
|
270
|
-
diffView.style.border.fg = screen.focused === diffView ? 'yellow' : 'white';
|
|
271
|
-
screen.render();
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
let currentFiles: FileStatus[] = [];
|
|
275
|
-
let lastSelectedPath: string | null = null;
|
|
276
|
-
let diffUpdateTimeout: NodeJS.Timeout | null = null;
|
|
277
|
-
let currentSearchTerm: string = '';
|
|
278
|
-
let currentBranch: string = '';
|
|
279
|
-
let lastFilePaths: string[] = [];
|
|
280
|
-
let isUpdatingList = false;
|
|
281
|
-
|
|
282
|
-
const scheduleDiffUpdate = () => {
|
|
283
|
-
if (diffUpdateTimeout) clearTimeout(diffUpdateTimeout);
|
|
284
|
-
diffUpdateTimeout = setTimeout(async () => {
|
|
285
|
-
await updateDiff();
|
|
286
|
-
}, 150); // 150ms debounce
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
const handleScroll = (direction: 'up' | 'down') => {
|
|
290
|
-
const focused = screen.focused;
|
|
291
|
-
if (focused === fileList) {
|
|
292
|
-
if (direction === 'up') {
|
|
293
|
-
focused.up(1);
|
|
294
|
-
} else {
|
|
295
|
-
focused.down(1);
|
|
296
|
-
}
|
|
297
|
-
scheduleDiffUpdate();
|
|
298
|
-
screen.render();
|
|
299
|
-
} else if (focused === diffView) {
|
|
300
|
-
const scrollAmount = direction === 'up' ? -2 : 2;
|
|
301
|
-
diffView.scroll(scrollAmount);
|
|
302
|
-
screen.render();
|
|
303
|
-
}
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
// Remove default wheel listeners to enforce "scroll focused only" behavior
|
|
307
|
-
fileList.removeAllListeners('wheeldown');
|
|
308
|
-
fileList.removeAllListeners('wheelup');
|
|
309
|
-
diffView.removeAllListeners('wheeldown');
|
|
310
|
-
diffView.removeAllListeners('wheelup');
|
|
311
|
-
|
|
312
|
-
// Attach custom scroll handlers to widgets (captures wheel even if hovering specific widget)
|
|
313
|
-
// We use widget-level listeners now that screen.mouse is true.
|
|
314
|
-
// We attach to both to ensure the event is caught regardless of where the mouse is.
|
|
315
|
-
// We use widget-level listeners now that screen.mouse is true.
|
|
316
|
-
// We attach to both to ensure the event is caught regardless of where the mouse is.
|
|
317
|
-
// The handleScroll function will then decide WHAT to scroll based on focus.
|
|
318
|
-
|
|
319
|
-
diffView.on('wheeldown', () => handleScroll('down'));
|
|
320
|
-
diffView.on('wheelup', () => handleScroll('up'));
|
|
321
|
-
|
|
322
|
-
// Also listen on screen for events that might miss the widgets (margins, borders)
|
|
323
|
-
screen.on('wheeldown', () => handleScroll('down'));
|
|
324
|
-
screen.on('wheelup', () => handleScroll('up'));
|
|
325
|
-
|
|
326
|
-
const openInEditor = (filePath: string) => {
|
|
327
|
-
try {
|
|
328
|
-
if (process.platform === 'win32') {
|
|
329
|
-
// On Windows, use 'start' to open with default program
|
|
330
|
-
spawn('cmd', ['/c', 'start', '', filePath], { stdio: 'ignore', detached: true }).unref();
|
|
331
|
-
} else {
|
|
332
|
-
// On Unix-like systems, try EDITOR, fallback to xdg-open
|
|
333
|
-
const editor = process.env.EDITOR || process.env.VISUAL || 'xdg-open';
|
|
334
|
-
spawn(editor, [filePath], { stdio: 'ignore', detached: true }).unref();
|
|
335
|
-
}
|
|
336
|
-
} catch (error) {
|
|
337
|
-
console.error(`Failed to open ${filePath}: ${error}`);
|
|
338
|
-
}
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
const updateBranch = async () => {
|
|
342
|
-
const branch = await gitHandler.getBranch();
|
|
343
|
-
if (branch !== currentBranch) {
|
|
344
|
-
currentBranch = branch;
|
|
345
|
-
branchBox.setContent(chalk.cyan('Branch: ') + chalk.yellow(branch));
|
|
346
|
-
screen.render();
|
|
347
|
-
}
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
const updateDiff = async () => {
|
|
351
|
-
const selectedIndex = fileList.selected;
|
|
352
|
-
const selectedFile = currentFiles[selectedIndex];
|
|
353
|
-
if (!selectedFile) {
|
|
354
|
-
// No valid file selected, clear diff view
|
|
355
|
-
diffView.setContent('Select a file to view diff.');
|
|
356
|
-
diffView.setLabel(' Diff () ');
|
|
357
|
-
lastSelectedPath = null;
|
|
358
|
-
screen.render();
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
if (selectedFile) {
|
|
362
|
-
let content = '';
|
|
363
|
-
let label = ` Diff (${selectedFile.path}) `;
|
|
364
|
-
|
|
365
|
-
if (selectedFile.status !== 'unchanged' && selectedFile.status !== 'deleted') {
|
|
366
|
-
const diff = await gitHandler.getDiff(selectedFile.path);
|
|
367
|
-
content = formatDiffWithDiff2Html(diff, currentSearchTerm);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (!content && selectedFile.status !== 'deleted') {
|
|
371
|
-
content = await gitHandler.getFileContent(selectedFile.path);
|
|
372
|
-
// Highlight search term in full content
|
|
373
|
-
if (currentSearchTerm) {
|
|
374
|
-
const regex = new RegExp(`(${currentSearchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
|
375
|
-
content = content.replace(regex, `\x1b[43m\x1b[30m$1\x1b[0m`);
|
|
376
|
-
}
|
|
377
|
-
label = ` File (${selectedFile.path}) `;
|
|
378
|
-
} else if (!content && selectedFile.status === 'deleted') {
|
|
379
|
-
content = chalk.red('File was deleted.');
|
|
380
|
-
label = ` Diff (${selectedFile.path}) `;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const currentContent = diffView.content;
|
|
384
|
-
const currentLabel = diffView.label;
|
|
385
|
-
|
|
386
|
-
// Only update if content or label changed to reduce flickering
|
|
387
|
-
if (content !== currentContent || label !== currentLabel) {
|
|
388
|
-
const savedScroll = diffView.scrollTop;
|
|
389
|
-
const isNewFile = selectedFile.path !== lastSelectedPath;
|
|
390
|
-
|
|
391
|
-
diffView.setContent(content);
|
|
392
|
-
diffView.setLabel(label);
|
|
393
|
-
|
|
394
|
-
if (isNewFile) {
|
|
395
|
-
diffView.scrollTo(0);
|
|
396
|
-
} else {
|
|
397
|
-
diffView.scrollTop = savedScroll;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
lastSelectedPath = selectedFile.path;
|
|
401
|
-
} else {
|
|
402
|
-
const newContent = 'Select a file to view diff.';
|
|
403
|
-
const newLabel = ' Diff () ';
|
|
404
|
-
if (diffView.content !== newContent || diffView.label !== newLabel) {
|
|
405
|
-
diffView.setContent(newContent);
|
|
406
|
-
diffView.setLabel(newLabel);
|
|
407
|
-
diffView.scrollTo(0);
|
|
408
|
-
}
|
|
409
|
-
lastSelectedPath = null;
|
|
410
|
-
}
|
|
411
|
-
screen.render();
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
const updateFileList = async () => {
|
|
415
|
-
if (isUpdatingList) return;
|
|
416
|
-
isUpdatingList = true;
|
|
417
|
-
|
|
418
|
-
const selectedPath = currentFiles[fileList.selected]?.path;
|
|
419
|
-
|
|
420
|
-
let files: FileStatus[];
|
|
421
|
-
|
|
422
|
-
if (currentSearchTerm) {
|
|
423
|
-
files = await gitHandler.searchFiles(currentSearchTerm);
|
|
424
|
-
} else {
|
|
425
|
-
files = await gitHandler.getStatus();
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
currentFiles = files;
|
|
429
|
-
|
|
430
|
-
const items = files.map((f) => {
|
|
431
|
-
let color = '{white-fg}';
|
|
432
|
-
if (f.status === 'added') color = '{green-fg}';
|
|
433
|
-
else if (f.status === 'deleted') color = '{red-fg}';
|
|
434
|
-
else if (f.status === 'modified') color = '{blue-fg}';
|
|
435
|
-
else if (f.status === 'unstaged') color = '{green-fg}';
|
|
436
|
-
else if (f.status === 'unchanged') color = '{grey-fg}';
|
|
437
|
-
|
|
438
|
-
let statusSymbol = '';
|
|
439
|
-
if (f.status === 'added') statusSymbol = 'A ';
|
|
440
|
-
else if (f.status === 'deleted') statusSymbol = 'D ';
|
|
441
|
-
else if (f.status === 'modified') statusSymbol = 'M ';
|
|
442
|
-
else if (f.status === 'unstaged') statusSymbol = '+ ';
|
|
443
|
-
else if (f.status === 'unchanged') statusSymbol = ' ';
|
|
444
|
-
|
|
445
|
-
return `${color}${statusSymbol}${f.path}{/}`;
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
const labelTitle = currentSearchTerm ? `Files (${files.length}) - Searching: "${currentSearchTerm}"` : `Files (${files.length})`;
|
|
449
|
-
|
|
450
|
-
const newFilePaths = files.map(f => f.path);
|
|
451
|
-
const selectedFileStillExists = selectedPath ? newFilePaths.includes(selectedPath) : false;
|
|
452
|
-
const listChanged = lastFilePaths.length !== newFilePaths.length ||
|
|
453
|
-
lastFilePaths.some(path => !newFilePaths.includes(path)) ||
|
|
454
|
-
newFilePaths.some(path => !lastFilePaths.includes(path));
|
|
455
|
-
|
|
456
|
-
if (items.length > 0) {
|
|
457
|
-
const newSelectedIndex = selectedPath ? currentFiles.findIndex(f => f.path === selectedPath) : -1;
|
|
458
|
-
|
|
459
|
-
const oldFileList = fileList;
|
|
460
|
-
const newFileList = createFileList();
|
|
461
|
-
newFileList.setLabel(labelTitle);
|
|
462
|
-
newFileList.setItems(items);
|
|
463
|
-
newFileList.select(newSelectedIndex >= 0 ? newSelectedIndex : 0);
|
|
464
|
-
|
|
465
|
-
if (!selectedFileStillExists || listChanged) {
|
|
466
|
-
lastSelectedPath = null;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
lastFilePaths = newFilePaths;
|
|
470
|
-
|
|
471
|
-
if (diffUpdateTimeout) {
|
|
472
|
-
clearTimeout(diffUpdateTimeout);
|
|
473
|
-
diffUpdateTimeout = null;
|
|
474
|
-
}
|
|
475
|
-
await updateDiff();
|
|
476
|
-
|
|
477
|
-
oldFileList.destroy();
|
|
478
|
-
screen.remove(oldFileList);
|
|
479
|
-
fileList = newFileList;
|
|
480
|
-
screen.append(fileList);
|
|
481
|
-
} else {
|
|
482
|
-
const oldFileList = fileList;
|
|
483
|
-
const newFileList = createFileList();
|
|
484
|
-
newFileList.setLabel(labelTitle);
|
|
485
|
-
newFileList.setItems([]);
|
|
486
|
-
|
|
487
|
-
diffView.setContent(currentSearchTerm ? `No files match "${currentSearchTerm}".` : 'No changes detected.');
|
|
488
|
-
diffView.setLabel(' Diff () ');
|
|
489
|
-
lastSelectedPath = null;
|
|
490
|
-
lastFilePaths = [];
|
|
491
|
-
|
|
492
|
-
oldFileList.destroy();
|
|
493
|
-
screen.remove(oldFileList);
|
|
494
|
-
fileList = newFileList;
|
|
495
|
-
screen.append(fileList);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
screen.render();
|
|
499
|
-
isUpdatingList = false;
|
|
500
|
-
};
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
screen.key(['s'], () => {
|
|
505
|
-
searchBox.show();
|
|
506
|
-
searchBox.setFront();
|
|
507
|
-
searchInput.setValue(currentSearchTerm);
|
|
508
|
-
searchInput.focus();
|
|
509
|
-
screen.render();
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
searchInput.on('submit', async (value: string) => {
|
|
513
|
-
currentSearchTerm = (value || '').trim();
|
|
514
|
-
searchBox.hide();
|
|
515
|
-
fileList.focus();
|
|
516
|
-
// Force immediate update
|
|
517
|
-
await updateFileList();
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
searchInput.on('cancel', () => {
|
|
521
|
-
searchBox.hide();
|
|
522
|
-
fileList.focus();
|
|
523
|
-
screen.render();
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
screen.key(['tab'], () => {
|
|
527
|
-
if (screen.focused === fileList) {
|
|
528
|
-
diffView.focus();
|
|
529
|
-
} else {
|
|
530
|
-
fileList.focus();
|
|
531
|
-
}
|
|
532
|
-
updateBorders();
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
screen.key(['left'], () => {
|
|
536
|
-
if (screen.focused !== fileList) {
|
|
537
|
-
fileList.focus();
|
|
538
|
-
updateBorders();
|
|
539
|
-
}
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
screen.key(['right'], () => {
|
|
543
|
-
if (screen.focused !== diffView) {
|
|
544
|
-
diffView.focus();
|
|
545
|
-
updateBorders();
|
|
546
|
-
}
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
screen.key(['enter'], () => {
|
|
550
|
-
const selectedIndex = fileList.selected;
|
|
551
|
-
const selectedFile = currentFiles[selectedIndex];
|
|
552
|
-
if (selectedFile) {
|
|
553
|
-
openInEditor(selectedFile.path);
|
|
554
|
-
}
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
// Handle revert key press
|
|
558
|
-
screen.key(['r'], async () => {
|
|
559
|
-
const selectedIndex = fileList.selected;
|
|
560
|
-
const selectedFile = currentFiles[selectedIndex];
|
|
561
|
-
if (selectedFile) {
|
|
562
|
-
confirmDialog.show();
|
|
563
|
-
confirmDialog.setFront();
|
|
564
|
-
confirmDialog.focus();
|
|
565
|
-
screen.render();
|
|
566
|
-
}
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
// Handle dialog keys
|
|
570
|
-
confirmDialog.key(['space'], async () => {
|
|
571
|
-
const selectedIndex = fileList.selected;
|
|
572
|
-
const selectedFile = currentFiles[selectedIndex];
|
|
573
|
-
if (selectedFile) {
|
|
574
|
-
try {
|
|
575
|
-
await gitHandler.revertFile(selectedFile.path);
|
|
576
|
-
showNotification(`File ${selectedFile.path} reverted successfully.`, 'green');
|
|
577
|
-
await updateFileList();
|
|
578
|
-
} catch (error) {
|
|
579
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
580
|
-
showNotification(`Error reverting file: ${errorMessage}`, 'red');
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
confirmDialog.hide();
|
|
584
|
-
fileList.focus();
|
|
585
|
-
screen.render();
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
confirmDialog.key(['escape'], () => {
|
|
589
|
-
confirmDialog.hide();
|
|
590
|
-
fileList.focus();
|
|
591
|
-
screen.render();
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
await updateBranch();
|
|
595
|
-
await updateFileList();
|
|
596
|
-
fileList.focus();
|
|
597
|
-
updateBorders();
|
|
598
|
-
|
|
599
|
-
// Set up periodic updates to detect new git changes
|
|
600
|
-
const updateInterval = setInterval(async () => {
|
|
601
|
-
const currentlyFocused = screen.focused;
|
|
602
|
-
const wasFileListFocused = currentlyFocused === fileList;
|
|
603
|
-
|
|
604
|
-
await updateBranch();
|
|
605
|
-
await updateFileList();
|
|
606
|
-
|
|
607
|
-
// Restore focus to the same pane it was on before the update
|
|
608
|
-
if (wasFileListFocused) {
|
|
609
|
-
fileList.focus();
|
|
610
|
-
} else if (currentlyFocused === diffView) {
|
|
611
|
-
diffView.focus();
|
|
612
|
-
}
|
|
613
|
-
updateBorders();
|
|
614
|
-
}, 2000); // Check for changes every 2 seconds
|
|
615
|
-
|
|
616
|
-
// Clean up interval on exit
|
|
617
|
-
screen.key(['escape', 'q', 'C-c'], () => {
|
|
618
|
-
// Don't handle ESC if confirmation dialog has focus or is visible
|
|
619
|
-
if (screen.focused === confirmDialog || !confirmDialog.hidden) {
|
|
620
|
-
return false;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
if (!searchBox.hidden) {
|
|
624
|
-
searchBox.hide();
|
|
625
|
-
screen.render();
|
|
626
|
-
fileList.focus();
|
|
627
|
-
return false;
|
|
628
|
-
} else {
|
|
629
|
-
// Clear interval only when actually exiting
|
|
630
|
-
clearInterval(updateInterval);
|
|
631
|
-
screen.destroy();
|
|
632
|
-
process.exit(0);
|
|
633
|
-
}
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
main().catch(err => {
|
|
638
|
-
console.error(err);
|
|
639
|
-
process.exit(1);
|
|
640
|
-
});
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import * as cheerio from 'cheerio';
|
|
2
|
-
const Diff2Html = require('diff2html');
|
|
3
|
-
|
|
4
|
-
export function formatDiffWithDiff2Html(diffString: string, searchTerm?: string): string {
|
|
5
|
-
if (!diffString || diffString.trim() === '') {
|
|
6
|
-
return '';
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
try {
|
|
10
|
-
const html = Diff2Html.html(diffString, {
|
|
11
|
-
drawFileList: false,
|
|
12
|
-
matching: 'lines',
|
|
13
|
-
outputFormat: 'line-by-line',
|
|
14
|
-
colorScheme: 'dark',
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
const $ = cheerio.load(html);
|
|
18
|
-
|
|
19
|
-
let blessedText = '';
|
|
20
|
-
|
|
21
|
-
$('.d2h-diff-tbody tr').each((_, row) => {
|
|
22
|
-
const $row = $(row);
|
|
23
|
-
|
|
24
|
-
if ($row.find('.d2h-code-line').length > 0) {
|
|
25
|
-
const $lineNumberCell = $row.find('td.d2h-code-linenumber');
|
|
26
|
-
const isAdded = $row.find('td.d2h-ins').length > 0;
|
|
27
|
-
const isDeleted = $row.find('td.d2h-del').length > 0;
|
|
28
|
-
const $lineContent = $row.find('.d2h-code-line-ctn');
|
|
29
|
-
const $linePrefix = $row.find('.d2h-code-line-prefix');
|
|
30
|
-
const $lineWrapper = $row.find('.d2h-code-line');
|
|
31
|
-
|
|
32
|
-
let prefix = '';
|
|
33
|
-
let content = '';
|
|
34
|
-
let lineNumber = '';
|
|
35
|
-
|
|
36
|
-
if ($lineNumberCell.length > 0) {
|
|
37
|
-
lineNumber = $lineNumberCell.text().trim();
|
|
38
|
-
}
|
|
39
|
-
if ($linePrefix.length > 0) {
|
|
40
|
-
prefix = $linePrefix.text();
|
|
41
|
-
}
|
|
42
|
-
if ($lineContent.length > 0) {
|
|
43
|
-
content = $lineContent.text();
|
|
44
|
-
} else {
|
|
45
|
-
content = $lineWrapper.text().trim();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
let fullLine = prefix + content;
|
|
49
|
-
|
|
50
|
-
// Add line number with proper formatting
|
|
51
|
-
const formattedLineNumber = lineNumber ? `${lineNumber}: ` : ' ';
|
|
52
|
-
|
|
53
|
-
// Highlight search term if present
|
|
54
|
-
if (searchTerm && fullLine.toLowerCase().includes(searchTerm.toLowerCase())) {
|
|
55
|
-
const regex = new RegExp(`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
|
56
|
-
// We need to know the base color to restore it.
|
|
57
|
-
// Added: 32, Deleted: 31, Normal: 37
|
|
58
|
-
const baseColor = isAdded ? '32' : (isDeleted ? '31' : '37');
|
|
59
|
-
fullLine = fullLine.replace(regex, `\x1b[43m\x1b[30m$1\x1b[0m\x1b[${baseColor}m`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (isAdded) {
|
|
63
|
-
blessedText += `\x1b[90m${formattedLineNumber}\x1b[0m\x1b[32m${fullLine}\x1b[0m\n`;
|
|
64
|
-
} else if (isDeleted) {
|
|
65
|
-
blessedText += `\x1b[90m${formattedLineNumber}\x1b[0m\x1b[31m${fullLine}\x1b[0m\n`;
|
|
66
|
-
} else {
|
|
67
|
-
blessedText += `\x1b[90m${formattedLineNumber}\x1b[0m\x1b[37m${fullLine}\x1b[0m\n`;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if ($row.find('.d2h-info').length > 0) {
|
|
72
|
-
const hunkText = $row.find('.d2h-info').text().trim();
|
|
73
|
-
blessedText += `\x1b[36m${hunkText}\x1b[0m\n`;
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
return blessedText.trim();
|
|
78
|
-
} catch (error) {
|
|
79
|
-
return `Error formatting diff: ${error}`;
|
|
80
|
-
}
|
|
81
|
-
}
|