diffwatch 1.0.6 → 1.0.9

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