diffwatch 2.0.3 → 2.0.5
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/README.md +16 -0
- package/bin/diffwatch.js +36 -8
- package/package.json +11 -6
- package/src/App.tsx +420 -0
- package/src/components/DiffViewer.tsx +258 -0
- package/src/components/FileList.tsx +95 -0
- package/src/components/HistoryViewer.tsx +170 -0
- package/src/components/StatusBar.tsx +27 -0
- package/src/constants.ts +1 -0
- package/src/index.tsx +106 -0
- package/src/utils/git.ts +402 -0
- package/src/version.ts +1 -0
- package/dist/diffwatch.exe +0 -0
package/README.md
CHANGED
|
@@ -20,6 +20,22 @@ A TUI app for watching git repository file changes with diffs.
|
|
|
20
20
|
|
|
21
21
|
## Installation
|
|
22
22
|
|
|
23
|
+
### Prerequisites
|
|
24
|
+
|
|
25
|
+
**Bun runtime is required** to run DiffWatch. If you don't have it installed, install it first:
|
|
26
|
+
|
|
27
|
+
**macOS/Linux:**
|
|
28
|
+
```bash
|
|
29
|
+
curl -fsSL https://bun.sh/install | bash
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Windows (PowerShell):**
|
|
33
|
+
```powershell
|
|
34
|
+
irm bun.sh/install.ps1 | iex
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or visit [bun.sh](https://bun.sh) for other installation methods.
|
|
38
|
+
|
|
23
39
|
### Option 1: Install from npm (recommended)
|
|
24
40
|
|
|
25
41
|
```bash
|
package/bin/diffwatch.js
CHANGED
|
@@ -1,22 +1,50 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { spawn } from 'child_process';
|
|
3
|
+
import { spawn, execSync } from 'child_process';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { dirname, join } from 'path';
|
|
6
|
-
import {
|
|
6
|
+
import { existsSync } from 'fs';
|
|
7
7
|
|
|
8
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
9
|
const __dirname = dirname(__filename);
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
// Check if bun is installed
|
|
12
|
+
let bunPath;
|
|
13
|
+
try {
|
|
14
|
+
const result = execSync('where bun', { encoding: 'utf-8' }).trim();
|
|
15
|
+
bunPath = result.split('\n')[0]; // Take first result
|
|
16
|
+
} catch (error) {
|
|
17
|
+
// Check if bun is in common locations on Windows
|
|
18
|
+
if (process.platform === 'win32') {
|
|
19
|
+
const possiblePaths = [
|
|
20
|
+
join(process.env.LOCALAPPDATA, 'Bun', 'bun.exe'),
|
|
21
|
+
join(process.env.ProgramFiles, 'bun', 'bun.exe'),
|
|
22
|
+
];
|
|
23
|
+
for (const p of possiblePaths) {
|
|
24
|
+
if (existsSync(p)) {
|
|
25
|
+
bunPath = p;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
14
30
|
}
|
|
15
31
|
|
|
16
|
-
|
|
32
|
+
if (!bunPath) {
|
|
33
|
+
console.error('Error: Bun is not installed.');
|
|
34
|
+
console.error('');
|
|
35
|
+
console.error('DiffWatch requires Bun to run. Please install Bun:');
|
|
36
|
+
console.error('');
|
|
37
|
+
console.error(' curl -fsSL https://bun.sh/install | bash');
|
|
38
|
+
console.error('');
|
|
39
|
+
console.error('Or visit: https://bun.sh');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const srcPath = join(__dirname, '..', 'src', 'index.tsx');
|
|
17
44
|
|
|
18
|
-
const child = spawn(
|
|
45
|
+
const child = spawn(bunPath, ['run', srcPath, ...process.argv.slice(2)], {
|
|
19
46
|
stdio: 'inherit',
|
|
47
|
+
shell: true, // Required for Windows
|
|
20
48
|
cwd: process.cwd()
|
|
21
49
|
});
|
|
22
50
|
|
|
@@ -26,5 +54,5 @@ child.on('error', (err) => {
|
|
|
26
54
|
});
|
|
27
55
|
|
|
28
56
|
child.on('exit', (code) => {
|
|
29
|
-
process.exit(code);
|
|
57
|
+
process.exit(code || 0);
|
|
30
58
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "diffwatch",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"description": "A TUI app for watching git repository file changes with diffs.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"git",
|
|
@@ -26,15 +26,20 @@
|
|
|
26
26
|
"engines": {
|
|
27
27
|
"node": ">=18.0.0"
|
|
28
28
|
},
|
|
29
|
+
"os": [
|
|
30
|
+
"win32",
|
|
31
|
+
"darwin",
|
|
32
|
+
"linux"
|
|
33
|
+
],
|
|
29
34
|
"bin": {
|
|
30
|
-
"diffwatch": "
|
|
35
|
+
"diffwatch": "bin/diffwatch.js"
|
|
31
36
|
},
|
|
32
37
|
"files": [
|
|
33
|
-
"dist/**/*",
|
|
34
38
|
"bin/**/*",
|
|
39
|
+
"src/**/*",
|
|
40
|
+
"package.json",
|
|
35
41
|
"README.md",
|
|
36
|
-
"LICENSE"
|
|
37
|
-
"package.json"
|
|
42
|
+
"LICENSE"
|
|
38
43
|
],
|
|
39
44
|
"type": "module",
|
|
40
45
|
"devDependencies": {
|
|
@@ -59,7 +64,7 @@
|
|
|
59
64
|
"trash": "^9.0.0"
|
|
60
65
|
},
|
|
61
66
|
"scripts": {
|
|
62
|
-
"build": "bun run build:version
|
|
67
|
+
"build": "bun run build:version",
|
|
63
68
|
"build:version": "node -e \"console.log('export const BUILD_VERSION = \\\"' + require('./package.json').version + '\\\";')\" > src/version.ts",
|
|
64
69
|
"dev": "bun run src/index.tsx",
|
|
65
70
|
"start": "bun run src/index.tsx",
|
package/src/App.tsx
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { useRenderer, useKeyboard } from '@opentui/react';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { getChangedFiles, getCurrentBranch, getBranchCount, getCommitHistory, type FileStatus, type CommitInfo, type ChangedFile, revertFile, deleteFileSafely, runGit, searchFiles } from './utils/git';
|
|
6
|
+
import { FileList } from './components/FileList';
|
|
7
|
+
import { DiffViewer } from './components/DiffViewer';
|
|
8
|
+
import { HistoryViewer } from './components/HistoryViewer';
|
|
9
|
+
import { StatusBar } from './components/StatusBar';
|
|
10
|
+
import { POLLING_INTERVAL } from './constants';
|
|
11
|
+
|
|
12
|
+
export default function App() {
|
|
13
|
+
const renderer = useRenderer();
|
|
14
|
+
const [allFiles, setAllFiles] = useState<FileStatus[]>([]);
|
|
15
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
16
|
+
const [focused, setFocused] = useState<'fileList' | 'diffView'>('fileList');
|
|
17
|
+
const [repoPath] = useState(process.cwd());
|
|
18
|
+
const [branch, setBranch] = useState('...');
|
|
19
|
+
const [branchCount, setBranchCount] = useState(0);
|
|
20
|
+
const [confirmRevert, setConfirmRevert] = useState(false);
|
|
21
|
+
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
22
|
+
const [searchMode, setSearchMode] = useState(false);
|
|
23
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
24
|
+
const [searchActive, setSearchActive] = useState(false);
|
|
25
|
+
const [searchResults, setSearchResults] = useState<FileStatus[] | null>(null);
|
|
26
|
+
const [notification, setNotification] = useState<{ message: string; type: 'success' | 'error' } | null>(null);
|
|
27
|
+
const [errorNotification, setErrorNotification] = useState<string | null>(null);
|
|
28
|
+
const [historyMode, setHistoryMode] = useState(false);
|
|
29
|
+
const [commits, setCommits] = useState<CommitInfo[]>([]);
|
|
30
|
+
|
|
31
|
+
const refresh = async (skipIfSearching = false) => {
|
|
32
|
+
// Skip refresh if search is active and skipIfSearching is true
|
|
33
|
+
if (skipIfSearching && searchActive) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const newFiles = await getChangedFiles(repoPath);
|
|
38
|
+
setAllFiles(newFiles);
|
|
39
|
+
if (selectedIndex >= newFiles.length) {
|
|
40
|
+
setSelectedIndex(Math.max(0, newFiles.length - 1));
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Filter files based on search
|
|
45
|
+
const filteredFiles = useMemo(() => {
|
|
46
|
+
if (!searchActive || !searchQuery.trim()) {
|
|
47
|
+
return allFiles;
|
|
48
|
+
}
|
|
49
|
+
// Return search results if available, otherwise empty array
|
|
50
|
+
return searchResults ?? [];
|
|
51
|
+
}, [searchActive, searchQuery, allFiles, searchResults]);
|
|
52
|
+
|
|
53
|
+
// Execute search and update files when search is submitted
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const doSearch = async () => {
|
|
56
|
+
if (!searchActive || !searchQuery.trim()) {
|
|
57
|
+
setSearchResults(null);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// Search within the current allFiles only (by content, not filename)
|
|
64
|
+
const matchedFiles = await searchFiles(searchQuery, allFiles, repoPath);
|
|
65
|
+
setSearchResults(matchedFiles);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
68
|
+
setErrorNotification(`Search failed: ${errorMessage}`);
|
|
69
|
+
setSearchResults(null);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Debounce search
|
|
74
|
+
const timeout = setTimeout(doSearch, 300);
|
|
75
|
+
return () => clearTimeout(timeout);
|
|
76
|
+
}, [searchActive, searchQuery, repoPath, allFiles]);
|
|
77
|
+
|
|
78
|
+
// Auto-hide notifications after 3 seconds
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (notification) {
|
|
81
|
+
const timeout = setTimeout(() => setNotification(null), 3000);
|
|
82
|
+
return () => clearTimeout(timeout);
|
|
83
|
+
}
|
|
84
|
+
}, [notification]);
|
|
85
|
+
|
|
86
|
+
// Load initial data
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
refresh();
|
|
89
|
+
getCurrentBranch(repoPath).then(setBranch);
|
|
90
|
+
getBranchCount(repoPath).then(setBranchCount);
|
|
91
|
+
}, [repoPath]);
|
|
92
|
+
|
|
93
|
+
// Refs to store interval IDs to prevent memory leaks
|
|
94
|
+
const gitPollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
95
|
+
const branchPollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
96
|
+
|
|
97
|
+
// Poll for git changes periodically
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
// Clear any existing interval to prevent duplicates
|
|
100
|
+
if (gitPollingIntervalRef.current) {
|
|
101
|
+
clearInterval(gitPollingIntervalRef.current);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
gitPollingIntervalRef.current = setInterval(async () => {
|
|
105
|
+
await refresh(true);
|
|
106
|
+
}, POLLING_INTERVAL);
|
|
107
|
+
|
|
108
|
+
return () => {
|
|
109
|
+
if (gitPollingIntervalRef.current) {
|
|
110
|
+
clearInterval(gitPollingIntervalRef.current);
|
|
111
|
+
gitPollingIntervalRef.current = null;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}, [repoPath, searchActive]);
|
|
115
|
+
|
|
116
|
+
// Poll for branch changes
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
// Clear any existing interval to prevent duplicates
|
|
119
|
+
if (branchPollingIntervalRef.current) {
|
|
120
|
+
clearInterval(branchPollingIntervalRef.current);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
branchPollingIntervalRef.current = setInterval(async () => {
|
|
124
|
+
const currentBranch = await getCurrentBranch(repoPath);
|
|
125
|
+
const count = await getBranchCount(repoPath);
|
|
126
|
+
setBranch(currentBranch);
|
|
127
|
+
setBranchCount(count);
|
|
128
|
+
}, POLLING_INTERVAL);
|
|
129
|
+
|
|
130
|
+
return () => {
|
|
131
|
+
if (branchPollingIntervalRef.current) {
|
|
132
|
+
clearInterval(branchPollingIntervalRef.current);
|
|
133
|
+
branchPollingIntervalRef.current = null;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}, [repoPath]);
|
|
137
|
+
|
|
138
|
+
// Load commit history when history mode is opened
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (historyMode) {
|
|
141
|
+
getCommitHistory(repoPath).then(setCommits);
|
|
142
|
+
}
|
|
143
|
+
}, [historyMode, repoPath]);
|
|
144
|
+
|
|
145
|
+
// Open file in default editor
|
|
146
|
+
const openFile = (filePath: string) => {
|
|
147
|
+
try {
|
|
148
|
+
const absPath = filePath.startsWith('/') || filePath.match(/^[A-Za-z]:\\/)
|
|
149
|
+
? path.normalize(filePath)
|
|
150
|
+
: path.normalize(path.join(repoPath, filePath));
|
|
151
|
+
const isWindows = process.platform === 'win32';
|
|
152
|
+
const isMac = process.platform === 'darwin';
|
|
153
|
+
|
|
154
|
+
if (isWindows) {
|
|
155
|
+
// On Windows, use cmd to execute start command to properly handle paths with spaces
|
|
156
|
+
// Pass the path directly without additional quotes since shell: false passes args directly
|
|
157
|
+
spawn('cmd', ['/c', 'start', '""', absPath], {
|
|
158
|
+
detached: true,
|
|
159
|
+
stdio: 'ignore',
|
|
160
|
+
cwd: repoPath,
|
|
161
|
+
shell: false // Don't use shell to avoid double-quoting issues
|
|
162
|
+
}).unref();
|
|
163
|
+
} else {
|
|
164
|
+
// On macOS and Linux, use the open command with shell: true so it handles spaces properly
|
|
165
|
+
const command = isMac ? 'open' : 'xdg-open';
|
|
166
|
+
spawn(command, [absPath], {
|
|
167
|
+
detached: true,
|
|
168
|
+
stdio: 'ignore',
|
|
169
|
+
cwd: repoPath,
|
|
170
|
+
shell: true // Use shell to handle spaces in path
|
|
171
|
+
}).unref();
|
|
172
|
+
}
|
|
173
|
+
return; // Exit early after handling the file opening
|
|
174
|
+
} catch (e) {
|
|
175
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
176
|
+
setErrorNotification(`Failed to open file: ${errorMessage}`);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Keyboard handling
|
|
181
|
+
useKeyboard(async (key) => {
|
|
182
|
+
// Dismiss error notification if present
|
|
183
|
+
if (errorNotification) {
|
|
184
|
+
setErrorNotification(null);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// If history mode is open, skip file-related shortcuts
|
|
189
|
+
if (historyMode) {
|
|
190
|
+
if (key.name === 'q') {
|
|
191
|
+
renderer.destroy();
|
|
192
|
+
process.exit(0);
|
|
193
|
+
}
|
|
194
|
+
// Allow other keys but don't process file-related shortcuts
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Search mode
|
|
199
|
+
if (searchMode) {
|
|
200
|
+
if (key.name === 'escape') {
|
|
201
|
+
setSearchMode(false);
|
|
202
|
+
setSearchQuery('');
|
|
203
|
+
setSearchActive(false);
|
|
204
|
+
setSelectedIndex(0);
|
|
205
|
+
} else if (key.name === 'backspace') {
|
|
206
|
+
setSearchQuery(q => q.slice(0, -1));
|
|
207
|
+
} else if (key.name === 'enter' || key.name === 'return') {
|
|
208
|
+
setSearchActive(searchQuery.trim().length > 0);
|
|
209
|
+
setSearchMode(false);
|
|
210
|
+
setSelectedIndex(0);
|
|
211
|
+
} else if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
|
|
212
|
+
// Only add ASCII printable characters (space to tilde) and extended text
|
|
213
|
+
if (/^[\x20\x21-\x7E\u00A0-\uFFFF]$/.test(key.sequence)) {
|
|
214
|
+
setSearchQuery(q => q + key.sequence);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Confirm revert mode
|
|
221
|
+
if (confirmRevert) {
|
|
222
|
+
if (key.name === 'y') {
|
|
223
|
+
const file = filteredFiles[selectedIndex];
|
|
224
|
+
if (file) {
|
|
225
|
+
try {
|
|
226
|
+
if (file.status === 'new') {
|
|
227
|
+
await deleteFileSafely(file.path);
|
|
228
|
+
setNotification({ message: `File ${file.path} deleted.`, type: 'success' });
|
|
229
|
+
} else {
|
|
230
|
+
await revertFile(file.path);
|
|
231
|
+
setNotification({ message: `File ${file.path} reverted.`, type: 'success' });
|
|
232
|
+
}
|
|
233
|
+
await refresh();
|
|
234
|
+
} catch (error) {
|
|
235
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
236
|
+
setErrorNotification(`Revert failed: ${errorMessage}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
setConfirmRevert(false);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Confirm delete mode
|
|
245
|
+
if (confirmDelete) {
|
|
246
|
+
if (key.name === 'y') {
|
|
247
|
+
const file = filteredFiles[selectedIndex];
|
|
248
|
+
if (file) {
|
|
249
|
+
try {
|
|
250
|
+
const deleted = await deleteFileSafely(file.path);
|
|
251
|
+
if (deleted) {
|
|
252
|
+
setNotification({ message: `File ${file.path} deleted.`, type: 'success' });
|
|
253
|
+
await refresh();
|
|
254
|
+
} else {
|
|
255
|
+
setErrorNotification(`Failed to delete ${file.path}.`);
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
259
|
+
setErrorNotification(`Delete failed: ${errorMessage}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
setConfirmDelete(false);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Global shortcuts
|
|
268
|
+
if (key.name === 'q') {
|
|
269
|
+
renderer.destroy();
|
|
270
|
+
process.exit(0);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (key.name === 's' && (filteredFiles.length > 0 || searchActive)) {
|
|
274
|
+
setSearchMode(true);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (key.name === 'h') {
|
|
278
|
+
setHistoryMode(true);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (key.name === 'tab' || key.name === 'left' || key.name === 'right') {
|
|
282
|
+
setFocused(f => f === 'fileList' ? 'diffView' : 'fileList');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (focused === 'fileList') {
|
|
286
|
+
if (key.name === 'up') {
|
|
287
|
+
setSelectedIndex(i => Math.max(0, i - 1));
|
|
288
|
+
}
|
|
289
|
+
if (key.name === 'down') {
|
|
290
|
+
setSelectedIndex(i => Math.min(filteredFiles.length - 1, i + 1));
|
|
291
|
+
}
|
|
292
|
+
if (key.name === 'enter' || key.name === 'return') {
|
|
293
|
+
const file = filteredFiles[selectedIndex];
|
|
294
|
+
if (file) {
|
|
295
|
+
openFile(file.path);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (key.name === 'd' && (filteredFiles.length > 0 || searchActive)) {
|
|
299
|
+
setConfirmDelete(true);
|
|
300
|
+
}
|
|
301
|
+
if (key.name === 'r' && (filteredFiles.length > 0 || searchActive)) {
|
|
302
|
+
setConfirmRevert(true);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<box flexDirection="column" height="100%">
|
|
309
|
+
<box flexDirection="row" flexGrow={1}>
|
|
310
|
+
<FileList
|
|
311
|
+
files={filteredFiles}
|
|
312
|
+
selectedIndex={selectedIndex}
|
|
313
|
+
focused={focused === 'fileList'}
|
|
314
|
+
searchQuery={searchActive ? searchQuery : undefined}
|
|
315
|
+
onSelect={(index) => {
|
|
316
|
+
setSelectedIndex(index);
|
|
317
|
+
setFocused('fileList');
|
|
318
|
+
}}
|
|
319
|
+
onScroll={(delta) => {
|
|
320
|
+
setSelectedIndex(i => {
|
|
321
|
+
const next = i + (delta > 0 ? 1 : -1);
|
|
322
|
+
return Math.max(0, Math.min(filteredFiles.length - 1, next));
|
|
323
|
+
});
|
|
324
|
+
}}
|
|
325
|
+
/>
|
|
326
|
+
<DiffViewer
|
|
327
|
+
filename={filteredFiles[selectedIndex]?.path}
|
|
328
|
+
focused={focused === 'diffView'}
|
|
329
|
+
searchQuery={searchActive ? searchQuery : undefined}
|
|
330
|
+
status={filteredFiles[selectedIndex]?.status || 'modified'}
|
|
331
|
+
repoPath={repoPath}
|
|
332
|
+
/>
|
|
333
|
+
</box>
|
|
334
|
+
<StatusBar branch={branch} branchCount={branchCount} fileCount={filteredFiles.length} searchActive={searchActive} />
|
|
335
|
+
{searchMode && (
|
|
336
|
+
<box
|
|
337
|
+
position="absolute"
|
|
338
|
+
top="50%"
|
|
339
|
+
left="50%"
|
|
340
|
+
width={50}
|
|
341
|
+
height={3}
|
|
342
|
+
backgroundColor="black"
|
|
343
|
+
border
|
|
344
|
+
title=" Search "
|
|
345
|
+
flexDirection="column"
|
|
346
|
+
>
|
|
347
|
+
<text>{searchQuery}</text>
|
|
348
|
+
</box>
|
|
349
|
+
)}
|
|
350
|
+
{notification && (
|
|
351
|
+
<box
|
|
352
|
+
position="absolute"
|
|
353
|
+
top="50%"
|
|
354
|
+
left="50%"
|
|
355
|
+
width={50}
|
|
356
|
+
height={3}
|
|
357
|
+
backgroundColor="black"
|
|
358
|
+
border
|
|
359
|
+
title={notification.type === 'success' ? " Success " : " Error "}
|
|
360
|
+
>
|
|
361
|
+
<text fg={notification.type === 'success' ? 'green' : 'brightRed'}>
|
|
362
|
+
{notification.message}
|
|
363
|
+
</text>
|
|
364
|
+
</box>
|
|
365
|
+
)}
|
|
366
|
+
{errorNotification && (
|
|
367
|
+
<box
|
|
368
|
+
position="absolute"
|
|
369
|
+
top="50%"
|
|
370
|
+
left="50%"
|
|
371
|
+
width={60}
|
|
372
|
+
height={5}
|
|
373
|
+
backgroundColor="red"
|
|
374
|
+
border
|
|
375
|
+
flexDirection="column"
|
|
376
|
+
title=" Error "
|
|
377
|
+
>
|
|
378
|
+
<text fg="white">{errorNotification}</text>
|
|
379
|
+
<text fg="white">Press any key to dismiss</text>
|
|
380
|
+
</box>
|
|
381
|
+
)}
|
|
382
|
+
{confirmRevert && (
|
|
383
|
+
<box
|
|
384
|
+
position="absolute"
|
|
385
|
+
top="50%"
|
|
386
|
+
left="50%"
|
|
387
|
+
width={50}
|
|
388
|
+
height={3}
|
|
389
|
+
backgroundColor="yellow"
|
|
390
|
+
border
|
|
391
|
+
flexDirection="column"
|
|
392
|
+
title=" Confirm Revert "
|
|
393
|
+
>
|
|
394
|
+
<text fg="red">Press Y to revert or any other key to cancel</text>
|
|
395
|
+
</box>
|
|
396
|
+
)}
|
|
397
|
+
{confirmDelete && (
|
|
398
|
+
<box
|
|
399
|
+
position="absolute"
|
|
400
|
+
top="50%"
|
|
401
|
+
left="50%"
|
|
402
|
+
width={50}
|
|
403
|
+
height={3}
|
|
404
|
+
backgroundColor="red"
|
|
405
|
+
border
|
|
406
|
+
flexDirection="column"
|
|
407
|
+
title=" Confirm Delete "
|
|
408
|
+
>
|
|
409
|
+
<text fg="white">Press Y to delete or any other key to cancel</text>
|
|
410
|
+
</box>
|
|
411
|
+
)}
|
|
412
|
+
{historyMode && (
|
|
413
|
+
<HistoryViewer
|
|
414
|
+
commits={commits}
|
|
415
|
+
onClose={() => setHistoryMode(false)}
|
|
416
|
+
/>
|
|
417
|
+
)}
|
|
418
|
+
</box>
|
|
419
|
+
);
|
|
420
|
+
}
|