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/AGENTS.md +201 -0
- package/dist/index.js +207 -70
- package/dist/index.js.map +1 -1
- package/dist/utils/git.d.ts +1 -0
- package/dist/utils/git.js +11 -0
- package/dist/utils/git.js.map +1 -1
- package/package.json +1 -1
- package/screenshot.jpg +0 -0
- package/src/index.ts +443 -290
- package/src/utils/git.ts +9 -0
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]
|
|
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
|
-
|
|
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
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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: '
|
|
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
|
-
|
|
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: '
|
|
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
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
screen.
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
290
|
+
const focused = screen.focused;
|
|
291
|
+
if (focused === fileList) {
|
|
188
292
|
if (direction === 'up') {
|
|
189
|
-
|
|
293
|
+
focused.up(1);
|
|
190
294
|
} else {
|
|
191
|
-
|
|
295
|
+
focused.down(1);
|
|
192
296
|
}
|
|
193
297
|
scheduleDiffUpdate();
|
|
194
298
|
screen.render();
|
|
195
|
-
} else if (
|
|
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
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (content
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (
|
|
285
|
-
diffView.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
329
|
-
|
|
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
|
-
|
|
337
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
screen.
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
|
418
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
+
});
|