aethel 0.1.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/.env.example +2 -0
- package/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +190 -0
- package/docs/ARCHITECTURE.md +237 -0
- package/package.json +60 -0
- package/src/cli.js +1063 -0
- package/src/core/auth.js +288 -0
- package/src/core/config.js +117 -0
- package/src/core/diff.js +254 -0
- package/src/core/drive-api.js +1442 -0
- package/src/core/ignore.js +146 -0
- package/src/core/local-fs.js +109 -0
- package/src/core/remote-cache.js +65 -0
- package/src/core/snapshot.js +159 -0
- package/src/core/staging.js +125 -0
- package/src/core/sync.js +227 -0
- package/src/tui/app.js +1025 -0
- package/src/tui/index.js +10 -0
package/src/tui/app.js
ADDED
|
@@ -0,0 +1,1025 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
4
|
+
import Spinner from "ink-spinner";
|
|
5
|
+
import TextInput from "ink-text-input";
|
|
6
|
+
import {
|
|
7
|
+
batchOperateFiles,
|
|
8
|
+
getAccountInfo,
|
|
9
|
+
humanSize,
|
|
10
|
+
iconForMime,
|
|
11
|
+
listAccessibleFiles,
|
|
12
|
+
sourceBadgeForItem,
|
|
13
|
+
syncLocalDirectoryToParent,
|
|
14
|
+
uploadLocalEntry,
|
|
15
|
+
} from "../core/drive-api.js";
|
|
16
|
+
import {
|
|
17
|
+
defaultLocalRoot,
|
|
18
|
+
deleteLocalEntry,
|
|
19
|
+
ensureLocalDirectory,
|
|
20
|
+
listLocalEntries,
|
|
21
|
+
renameLocalEntry,
|
|
22
|
+
} from "../core/local-fs.js";
|
|
23
|
+
|
|
24
|
+
const h = React.createElement;
|
|
25
|
+
|
|
26
|
+
function truncate(value, width) {
|
|
27
|
+
if (value.length <= width) {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (width <= 1) {
|
|
32
|
+
return value.slice(0, width);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return `${value.slice(0, width - 1)}…`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function scrollWindow(length, cursor, height) {
|
|
39
|
+
return Math.min(
|
|
40
|
+
Math.max(cursor - Math.floor(height / 2), 0),
|
|
41
|
+
Math.max(length - height, 0)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function renderRemoteRow(file, isCursor, isSelected, width) {
|
|
46
|
+
const mark = isSelected ? "[x]" : "[ ]";
|
|
47
|
+
const sourceBadge = sourceBadgeForItem(file);
|
|
48
|
+
const icon = iconForMime(file.mimeType || "");
|
|
49
|
+
const size = humanSize(file.size);
|
|
50
|
+
const line = truncate(
|
|
51
|
+
`${mark} ${sourceBadge} ${icon} ${size} ${file.name}`,
|
|
52
|
+
width
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return h(
|
|
56
|
+
Text,
|
|
57
|
+
{
|
|
58
|
+
key: file.id,
|
|
59
|
+
inverse: isCursor,
|
|
60
|
+
color: isSelected ? "cyan" : undefined,
|
|
61
|
+
bold: isSelected,
|
|
62
|
+
wrap: "truncate-end",
|
|
63
|
+
},
|
|
64
|
+
line
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function renderLocalRow(entry, isCursor, width) {
|
|
69
|
+
const icon = entry.isDirectory ? "[DIR]" : "[FIL]";
|
|
70
|
+
const line = truncate(`[LOC] ${icon} ${entry.sizeLabel} ${entry.name}`, width);
|
|
71
|
+
|
|
72
|
+
return h(
|
|
73
|
+
Text,
|
|
74
|
+
{
|
|
75
|
+
key: entry.id,
|
|
76
|
+
inverse: isCursor,
|
|
77
|
+
color: entry.isDirectory ? "green" : undefined,
|
|
78
|
+
bold: entry.isDirectory,
|
|
79
|
+
wrap: "truncate-end",
|
|
80
|
+
},
|
|
81
|
+
line
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderPane({
|
|
86
|
+
title,
|
|
87
|
+
breadcrumb,
|
|
88
|
+
focused,
|
|
89
|
+
entries,
|
|
90
|
+
cursor,
|
|
91
|
+
renderer,
|
|
92
|
+
width,
|
|
93
|
+
height,
|
|
94
|
+
emptyMessage,
|
|
95
|
+
}) {
|
|
96
|
+
const bodyHeight = Math.max(height - 3, 1);
|
|
97
|
+
const start = scrollWindow(entries.length, cursor, bodyHeight);
|
|
98
|
+
const visibleEntries = entries.slice(start, start + bodyHeight);
|
|
99
|
+
|
|
100
|
+
return h(
|
|
101
|
+
Box,
|
|
102
|
+
{
|
|
103
|
+
width,
|
|
104
|
+
flexDirection: "column",
|
|
105
|
+
paddingRight: 1,
|
|
106
|
+
},
|
|
107
|
+
h(
|
|
108
|
+
Text,
|
|
109
|
+
{
|
|
110
|
+
bold: true,
|
|
111
|
+
color: focused ? "cyan" : "white",
|
|
112
|
+
},
|
|
113
|
+
truncate(`${focused ? ">" : " "} ${title}`, width)
|
|
114
|
+
),
|
|
115
|
+
h(Text, { dimColor: true }, truncate(breadcrumb, width)),
|
|
116
|
+
...(visibleEntries.length
|
|
117
|
+
? visibleEntries.map((entry, index) =>
|
|
118
|
+
renderer(entry, start + index === cursor, width)
|
|
119
|
+
)
|
|
120
|
+
: [
|
|
121
|
+
h(
|
|
122
|
+
Text,
|
|
123
|
+
{ key: `${title}-empty`, dimColor: true },
|
|
124
|
+
truncate(emptyMessage, width)
|
|
125
|
+
),
|
|
126
|
+
])
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function renderHelp() {
|
|
131
|
+
return h(
|
|
132
|
+
Box,
|
|
133
|
+
{
|
|
134
|
+
borderStyle: "round",
|
|
135
|
+
borderColor: "cyan",
|
|
136
|
+
paddingX: 1,
|
|
137
|
+
marginTop: 1,
|
|
138
|
+
flexDirection: "column",
|
|
139
|
+
},
|
|
140
|
+
h(Text, { bold: true }, "Aethel TUI - Keyboard Shortcuts"),
|
|
141
|
+
h(Text, null, "Tab Switch focus between Drive and Local panes"),
|
|
142
|
+
h(Text, null, "Up/Down, j/k Navigate the focused pane"),
|
|
143
|
+
h(Text, null, "Left/Right Move to parent or enter directory in focused pane"),
|
|
144
|
+
h(Text, null, "u Upload selected local entry to current Drive directory"),
|
|
145
|
+
h(Text, null, "s Sync selected local directory contents to current Drive directory"),
|
|
146
|
+
h(Text, null, "n Rename selected local file or directory"),
|
|
147
|
+
h(Text, null, "x Delete selected local file or directory"),
|
|
148
|
+
h(Text, null, "Space Toggle Drive selection in Drive pane"),
|
|
149
|
+
h(Text, null, "t / d Trash or permanently delete selected Drive items"),
|
|
150
|
+
h(Text, null, "/ Filter the focused pane"),
|
|
151
|
+
h(Text, null, "U Manually enter a local path and upload"),
|
|
152
|
+
h(Text, null, "r Reload Drive and Local panes"),
|
|
153
|
+
h(Text, null, "q Quit"),
|
|
154
|
+
h(Text, { dimColor: true }, "Press any key to close this help.")
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function AethelTui({ drive, includeSharedDrives = false }) {
|
|
159
|
+
const { exit } = useApp();
|
|
160
|
+
const [mode, setMode] = useState("loading");
|
|
161
|
+
const [remoteLoading, setRemoteLoading] = useState(true);
|
|
162
|
+
const [focusPane, setFocusPane] = useState("local");
|
|
163
|
+
const [account, setAccount] = useState(null);
|
|
164
|
+
const [remoteFiles, setRemoteFiles] = useState([]);
|
|
165
|
+
const [remoteFolderStack, setRemoteFolderStack] = useState([]);
|
|
166
|
+
const [localRoot] = useState(defaultLocalRoot);
|
|
167
|
+
const [localDirectory, setLocalDirectory] = useState(defaultLocalRoot);
|
|
168
|
+
const [localEntries, setLocalEntries] = useState([]);
|
|
169
|
+
const [remoteFilter, setRemoteFilter] = useState("");
|
|
170
|
+
const [localFilter, setLocalFilter] = useState("");
|
|
171
|
+
const [remoteCursor, setRemoteCursor] = useState(0);
|
|
172
|
+
const [localCursor, setLocalCursor] = useState(0);
|
|
173
|
+
const [selectedRemoteIds, setSelectedRemoteIds] = useState(() => new Set());
|
|
174
|
+
const [status, setStatus] = useState("Loading account info...");
|
|
175
|
+
const [errorMessage, setErrorMessage] = useState("");
|
|
176
|
+
const [pendingAction, setPendingAction] = useState(null);
|
|
177
|
+
const [inputValue, setInputValue] = useState("");
|
|
178
|
+
const width = process.stdout.columns || 80;
|
|
179
|
+
const height = process.stdout.rows || 24;
|
|
180
|
+
const currentRemoteFolderId = remoteFolderStack.length
|
|
181
|
+
? remoteFolderStack[remoteFolderStack.length - 1].id
|
|
182
|
+
: null;
|
|
183
|
+
const currentUploadParentId = currentRemoteFolderId || "root";
|
|
184
|
+
|
|
185
|
+
const remoteFolderIds = useMemo(
|
|
186
|
+
() =>
|
|
187
|
+
new Set(
|
|
188
|
+
remoteFiles.filter((file) => file.isFolder).map((file) => file.id)
|
|
189
|
+
),
|
|
190
|
+
[remoteFiles]
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const currentRemoteDirectoryEntries = useMemo(() => {
|
|
194
|
+
const entries = remoteFiles.filter((file) => {
|
|
195
|
+
if (!currentRemoteFolderId) {
|
|
196
|
+
return (
|
|
197
|
+
file.isRootLevel ||
|
|
198
|
+
!file.parentId ||
|
|
199
|
+
!remoteFolderIds.has(file.parentId)
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return file.parentId === currentRemoteFolderId;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return entries.sort((left, right) => {
|
|
207
|
+
if (left.isFolder !== right.isFolder) {
|
|
208
|
+
return left.isFolder ? -1 : 1;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return left.name.localeCompare(right.name);
|
|
212
|
+
});
|
|
213
|
+
}, [currentRemoteFolderId, remoteFiles, remoteFolderIds]);
|
|
214
|
+
|
|
215
|
+
const filteredRemoteEntries = useMemo(() => {
|
|
216
|
+
const query = remoteFilter.trim().toLowerCase();
|
|
217
|
+
if (!query) {
|
|
218
|
+
return currentRemoteDirectoryEntries;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return currentRemoteDirectoryEntries.filter((file) =>
|
|
222
|
+
file.name.toLowerCase().includes(query)
|
|
223
|
+
);
|
|
224
|
+
}, [currentRemoteDirectoryEntries, remoteFilter]);
|
|
225
|
+
|
|
226
|
+
const filteredLocalEntries = useMemo(() => {
|
|
227
|
+
const query = localFilter.trim().toLowerCase();
|
|
228
|
+
if (!query) {
|
|
229
|
+
return localEntries;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return localEntries.filter((entry) =>
|
|
233
|
+
entry.name.toLowerCase().includes(query)
|
|
234
|
+
);
|
|
235
|
+
}, [localEntries, localFilter]);
|
|
236
|
+
|
|
237
|
+
const currentLocalEntry = filteredLocalEntries[localCursor] || null;
|
|
238
|
+
const currentRemoteEntry = filteredRemoteEntries[remoteCursor] || null;
|
|
239
|
+
const currentRemoteFolderMeta = currentRemoteFolderId
|
|
240
|
+
? remoteFiles.find((file) => file.id === currentRemoteFolderId) || null
|
|
241
|
+
: null;
|
|
242
|
+
const currentRemoteFolderWritable =
|
|
243
|
+
!currentRemoteFolderMeta ||
|
|
244
|
+
currentRemoteFolderMeta.capabilities?.canAddChildren !== false;
|
|
245
|
+
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
setRemoteCursor((current) =>
|
|
248
|
+
Math.min(current, Math.max(filteredRemoteEntries.length - 1, 0))
|
|
249
|
+
);
|
|
250
|
+
}, [filteredRemoteEntries.length]);
|
|
251
|
+
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
setLocalCursor((current) =>
|
|
254
|
+
Math.min(current, Math.max(filteredLocalEntries.length - 1, 0))
|
|
255
|
+
);
|
|
256
|
+
}, [filteredLocalEntries.length]);
|
|
257
|
+
|
|
258
|
+
async function loadLocalPane(nextDirectory = localDirectory) {
|
|
259
|
+
const resolvedDirectory = await ensureLocalDirectory(nextDirectory);
|
|
260
|
+
const items = await listLocalEntries(resolvedDirectory);
|
|
261
|
+
setLocalDirectory(resolvedDirectory);
|
|
262
|
+
setLocalEntries(items);
|
|
263
|
+
setLocalCursor(0);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function loadAllData(nextStatus = "", preserveRemoteFolder = false) {
|
|
267
|
+
const nextRemoteFolderStack = preserveRemoteFolder ? [...remoteFolderStack] : [];
|
|
268
|
+
setRemoteLoading(true);
|
|
269
|
+
|
|
270
|
+
// Load local pane immediately — it's instant (disk I/O only)
|
|
271
|
+
try {
|
|
272
|
+
await loadLocalPane(localDirectory);
|
|
273
|
+
// Switch to normal mode so the user can browse local files while remote loads
|
|
274
|
+
setMode("normal");
|
|
275
|
+
setStatus("Loading Drive files...");
|
|
276
|
+
} catch (error) {
|
|
277
|
+
setErrorMessage(error.message);
|
|
278
|
+
setMode("error");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Fetch remote in background
|
|
283
|
+
try {
|
|
284
|
+
const [nextAccount, nextRemoteFiles] = await Promise.all([
|
|
285
|
+
getAccountInfo(drive),
|
|
286
|
+
listAccessibleFiles(drive, includeSharedDrives),
|
|
287
|
+
]);
|
|
288
|
+
|
|
289
|
+
setAccount(nextAccount);
|
|
290
|
+
setRemoteFiles(nextRemoteFiles);
|
|
291
|
+
setRemoteFolderStack(nextRemoteFolderStack);
|
|
292
|
+
setSelectedRemoteIds(new Set());
|
|
293
|
+
setRemoteLoading(false);
|
|
294
|
+
setStatus(
|
|
295
|
+
nextStatus || `Loaded ${nextRemoteFiles.length} Drive item(s). Press ? for help.`
|
|
296
|
+
);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
setRemoteLoading(false);
|
|
299
|
+
setStatus(`Drive load failed: ${error.message}. Local pane is still usable.`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
void loadAllData();
|
|
305
|
+
}, []);
|
|
306
|
+
|
|
307
|
+
async function executeRemoteDelete(permanent) {
|
|
308
|
+
const targets = remoteFiles.filter((file) => selectedRemoteIds.has(file.id));
|
|
309
|
+
if (targets.length === 0) {
|
|
310
|
+
setStatus("No Drive items selected.");
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
setMode("busy");
|
|
315
|
+
try {
|
|
316
|
+
const result = await batchOperateFiles(drive, targets, {
|
|
317
|
+
permanent,
|
|
318
|
+
includeSharedDrives,
|
|
319
|
+
onProgress: (done, total, verb, name) => {
|
|
320
|
+
setStatus(`[${done}/${total}] ${verb}: ${name}`);
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
await loadAllData(
|
|
325
|
+
`Done: ${result.success} succeeded, ${result.errors} failed.`,
|
|
326
|
+
true
|
|
327
|
+
);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
setErrorMessage(error.message);
|
|
330
|
+
setMode("error");
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function executeUpload(targetPath) {
|
|
335
|
+
const trimmedPath = targetPath.trim();
|
|
336
|
+
if (!trimmedPath) {
|
|
337
|
+
setMode("normal");
|
|
338
|
+
setStatus("Upload cancelled.");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
setMode("busy");
|
|
343
|
+
try {
|
|
344
|
+
const result = await uploadLocalEntry(
|
|
345
|
+
drive,
|
|
346
|
+
trimmedPath,
|
|
347
|
+
currentUploadParentId,
|
|
348
|
+
(verb, localPath, name) => {
|
|
349
|
+
if (verb === "mkdir") {
|
|
350
|
+
setStatus(`Creating remote directory: ${name}`);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
setStatus(`Uploading: ${name} (${localPath})`);
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
setInputValue("");
|
|
358
|
+
await loadAllData(
|
|
359
|
+
`Uploaded ${result.uploadedFiles} file(s) and ${result.uploadedDirectories} director${result.uploadedDirectories === 1 ? "y" : "ies"}.`,
|
|
360
|
+
true
|
|
361
|
+
);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
setMode("normal");
|
|
364
|
+
setStatus(`Upload failed: ${error.message}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function executeSyncDirectory(targetPath) {
|
|
369
|
+
setMode("busy");
|
|
370
|
+
try {
|
|
371
|
+
const result = await syncLocalDirectoryToParent(
|
|
372
|
+
drive,
|
|
373
|
+
targetPath,
|
|
374
|
+
currentUploadParentId,
|
|
375
|
+
(verb, localPath, name) => {
|
|
376
|
+
if (verb === "mkdir") {
|
|
377
|
+
setStatus(`Sync mkdir: ${name}`);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
setStatus(`Sync upload: ${name} (${localPath})`);
|
|
381
|
+
}
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
await loadAllData(
|
|
385
|
+
`Synced ${result.uploadedFiles} file(s) and ${result.uploadedDirectories} director${result.uploadedDirectories === 1 ? "y" : "ies"} into current Drive directory.`,
|
|
386
|
+
true
|
|
387
|
+
);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
setMode("normal");
|
|
390
|
+
setStatus(`Sync failed: ${error.message}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function executeLocalDelete(targetPath) {
|
|
395
|
+
setMode("busy");
|
|
396
|
+
try {
|
|
397
|
+
await deleteLocalEntry(targetPath);
|
|
398
|
+
await loadLocalPane(path.dirname(targetPath) === targetPath ? localDirectory : localDirectory);
|
|
399
|
+
setMode("normal");
|
|
400
|
+
setStatus(`Deleted local entry: ${path.basename(targetPath)}`);
|
|
401
|
+
} catch (error) {
|
|
402
|
+
setMode("normal");
|
|
403
|
+
setStatus(`Local delete failed: ${error.message}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function executeLocalRename(targetPath, nextName) {
|
|
408
|
+
setMode("busy");
|
|
409
|
+
try {
|
|
410
|
+
const renamedPath = await renameLocalEntry(targetPath, nextName);
|
|
411
|
+
await loadLocalPane(path.dirname(renamedPath));
|
|
412
|
+
setMode("normal");
|
|
413
|
+
setStatus(`Renamed local entry to: ${path.basename(renamedPath)}`);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
setMode("normal");
|
|
416
|
+
setStatus(`Local rename failed: ${error.message}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function openLocalDirectory(nextPath) {
|
|
421
|
+
try {
|
|
422
|
+
await loadLocalPane(nextPath);
|
|
423
|
+
setLocalFilter("");
|
|
424
|
+
setStatus(`Opened local directory: ${path.basename(nextPath) || nextPath}`);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
setStatus(`Local directory error: ${error.message}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function switchFocus() {
|
|
431
|
+
setFocusPane((current) => (current === "local" ? "remote" : "local"));
|
|
432
|
+
setStatus(
|
|
433
|
+
focusPane === "local"
|
|
434
|
+
? "Switched focus to Drive pane."
|
|
435
|
+
: "Switched focus to Local pane."
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
useInput((input, key) => {
|
|
440
|
+
if ((key.ctrl && input === "c") || (mode === "error" && input === "q")) {
|
|
441
|
+
exit();
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (mode === "loading" || mode === "busy") {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (mode === "error") {
|
|
450
|
+
if (key.escape || input === "q" || input === "Q") {
|
|
451
|
+
exit();
|
|
452
|
+
}
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (mode === "help") {
|
|
457
|
+
setMode("normal");
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (mode === "confirm") {
|
|
462
|
+
if (input === "y" || input === "Y") {
|
|
463
|
+
const action = pendingAction;
|
|
464
|
+
setPendingAction(null);
|
|
465
|
+
if (!action) {
|
|
466
|
+
setMode("normal");
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (action.type === "remote-trash") {
|
|
471
|
+
void executeRemoteDelete(false);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (action.type === "remote-delete") {
|
|
476
|
+
void executeRemoteDelete(true);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (action.type === "local-delete") {
|
|
481
|
+
void executeLocalDelete(action.targetPath);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (action.type === "local-sync") {
|
|
486
|
+
void executeSyncDirectory(action.targetPath);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (input === "n" || input === "N" || key.escape) {
|
|
492
|
+
setPendingAction(null);
|
|
493
|
+
setMode("normal");
|
|
494
|
+
setStatus("Cancelled.");
|
|
495
|
+
}
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (mode === "filter") {
|
|
500
|
+
if (key.escape) {
|
|
501
|
+
if (focusPane === "local") {
|
|
502
|
+
setLocalFilter("");
|
|
503
|
+
} else {
|
|
504
|
+
setRemoteFilter("");
|
|
505
|
+
}
|
|
506
|
+
setMode("normal");
|
|
507
|
+
setStatus("Filter cleared.");
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (key.return) {
|
|
512
|
+
setMode("normal");
|
|
513
|
+
setStatus("Filter applied.");
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (key.backspace || input === "\u007f") {
|
|
518
|
+
if (focusPane === "local") {
|
|
519
|
+
setLocalFilter((current) => current.slice(0, -1));
|
|
520
|
+
} else {
|
|
521
|
+
setRemoteFilter((current) => current.slice(0, -1));
|
|
522
|
+
}
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (input && !key.ctrl && !key.meta) {
|
|
527
|
+
if (focusPane === "local") {
|
|
528
|
+
setLocalFilter((current) => current + input);
|
|
529
|
+
} else {
|
|
530
|
+
setRemoteFilter((current) => current + input);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (mode === "upload") {
|
|
537
|
+
if (key.escape) {
|
|
538
|
+
setInputValue("");
|
|
539
|
+
setMode("normal");
|
|
540
|
+
setStatus("Manual upload cancelled.");
|
|
541
|
+
}
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (mode === "rename") {
|
|
546
|
+
if (key.escape) {
|
|
547
|
+
setInputValue("");
|
|
548
|
+
setPendingAction(null);
|
|
549
|
+
setMode("normal");
|
|
550
|
+
setStatus("Rename cancelled.");
|
|
551
|
+
}
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (input === "q" || input === "Q") {
|
|
556
|
+
exit();
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (key.tab || input === "\t") {
|
|
561
|
+
switchFocus();
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (input === "?") {
|
|
566
|
+
setMode("help");
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (input === "/") {
|
|
571
|
+
setMode("filter");
|
|
572
|
+
setStatus(`Type to filter the ${focusPane} pane.`);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (input === "r" || input === "R") {
|
|
577
|
+
void loadAllData("Reloaded Drive and Local panes.", true);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (key.upArrow || input === "k") {
|
|
582
|
+
if (focusPane === "local") {
|
|
583
|
+
setLocalCursor((current) => Math.max(current - 1, 0));
|
|
584
|
+
} else {
|
|
585
|
+
setRemoteCursor((current) => Math.max(current - 1, 0));
|
|
586
|
+
}
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (key.downArrow || input === "j") {
|
|
591
|
+
if (focusPane === "local") {
|
|
592
|
+
setLocalCursor((current) =>
|
|
593
|
+
Math.min(current + 1, Math.max(filteredLocalEntries.length - 1, 0))
|
|
594
|
+
);
|
|
595
|
+
} else {
|
|
596
|
+
setRemoteCursor((current) =>
|
|
597
|
+
Math.min(current + 1, Math.max(filteredRemoteEntries.length - 1, 0))
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (key.pageUp) {
|
|
604
|
+
const delta = Math.max(height - 13, 1);
|
|
605
|
+
if (focusPane === "local") {
|
|
606
|
+
setLocalCursor((current) => Math.max(current - delta, 0));
|
|
607
|
+
} else {
|
|
608
|
+
setRemoteCursor((current) => Math.max(current - delta, 0));
|
|
609
|
+
}
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (key.pageDown) {
|
|
614
|
+
const delta = Math.max(height - 13, 1);
|
|
615
|
+
if (focusPane === "local") {
|
|
616
|
+
setLocalCursor((current) =>
|
|
617
|
+
Math.min(current + delta, Math.max(filteredLocalEntries.length - 1, 0))
|
|
618
|
+
);
|
|
619
|
+
} else {
|
|
620
|
+
setRemoteCursor((current) =>
|
|
621
|
+
Math.min(current + delta, Math.max(filteredRemoteEntries.length - 1, 0))
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (key.home) {
|
|
628
|
+
if (focusPane === "local") {
|
|
629
|
+
setLocalCursor(0);
|
|
630
|
+
} else {
|
|
631
|
+
setRemoteCursor(0);
|
|
632
|
+
}
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (key.end) {
|
|
637
|
+
if (focusPane === "local") {
|
|
638
|
+
setLocalCursor(Math.max(filteredLocalEntries.length - 1, 0));
|
|
639
|
+
} else {
|
|
640
|
+
setRemoteCursor(Math.max(filteredRemoteEntries.length - 1, 0));
|
|
641
|
+
}
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (key.leftArrow) {
|
|
646
|
+
if (focusPane === "local") {
|
|
647
|
+
if (
|
|
648
|
+
localDirectory === localRoot ||
|
|
649
|
+
localDirectory === path.dirname(localDirectory)
|
|
650
|
+
) {
|
|
651
|
+
setStatus("Already at the local root directory.");
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
void openLocalDirectory(path.dirname(localDirectory));
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (remoteFolderStack.length === 0) {
|
|
659
|
+
setStatus("Already at the Drive root directory.");
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
setRemoteFolderStack((current) => current.slice(0, -1));
|
|
664
|
+
setRemoteCursor(0);
|
|
665
|
+
setRemoteFilter("");
|
|
666
|
+
setStatus("Moved to the parent Drive directory.");
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (key.rightArrow) {
|
|
671
|
+
if (focusPane === "local") {
|
|
672
|
+
if (!currentLocalEntry) {
|
|
673
|
+
setStatus("No local item is selected.");
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (!currentLocalEntry.isDirectory) {
|
|
678
|
+
setStatus("The selected local item is not a directory.");
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
void openLocalDirectory(currentLocalEntry.absolutePath);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (!currentRemoteEntry) {
|
|
687
|
+
setStatus("No Drive item is selected.");
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (!currentRemoteEntry.isFolder) {
|
|
692
|
+
setStatus("The selected Drive item is not a directory.");
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
setRemoteFolderStack((current) => [
|
|
697
|
+
...current,
|
|
698
|
+
{ id: currentRemoteEntry.id, name: currentRemoteEntry.name },
|
|
699
|
+
]);
|
|
700
|
+
setRemoteCursor(0);
|
|
701
|
+
setRemoteFilter("");
|
|
702
|
+
setStatus(`Entered Drive directory: ${currentRemoteEntry.name}`);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (focusPane === "local") {
|
|
707
|
+
if (input === "u" || input === "U") {
|
|
708
|
+
if (!currentLocalEntry) {
|
|
709
|
+
setStatus("No local item is selected.");
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (!currentRemoteFolderWritable) {
|
|
713
|
+
setStatus(
|
|
714
|
+
`Cannot upload into current Drive directory${
|
|
715
|
+
currentRemoteFolderMeta?.name ? `: ${currentRemoteFolderMeta.name}` : ""
|
|
716
|
+
}. This folder does not allow adding children.`
|
|
717
|
+
);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
void executeUpload(currentLocalEntry.absolutePath);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (input === "s" || input === "S") {
|
|
725
|
+
if (!currentLocalEntry) {
|
|
726
|
+
setStatus("No local directory is selected.");
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
if (!currentLocalEntry.isDirectory) {
|
|
730
|
+
setStatus("Batch sync requires a local directory.");
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
if (!currentRemoteFolderWritable) {
|
|
734
|
+
setStatus(
|
|
735
|
+
`Cannot sync into current Drive directory${
|
|
736
|
+
currentRemoteFolderMeta?.name ? `: ${currentRemoteFolderMeta.name}` : ""
|
|
737
|
+
}. This folder does not allow adding children.`
|
|
738
|
+
);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
setPendingAction({
|
|
743
|
+
type: "local-sync",
|
|
744
|
+
targetPath: currentLocalEntry.absolutePath,
|
|
745
|
+
});
|
|
746
|
+
setMode("confirm");
|
|
747
|
+
setStatus(
|
|
748
|
+
`Press y to sync contents of ${currentLocalEntry.name} into current Drive directory.`
|
|
749
|
+
);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (input === "n" || input === "N") {
|
|
754
|
+
if (!currentLocalEntry) {
|
|
755
|
+
setStatus("No local item is selected.");
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
setPendingAction({
|
|
760
|
+
type: "local-rename",
|
|
761
|
+
targetPath: currentLocalEntry.absolutePath,
|
|
762
|
+
});
|
|
763
|
+
setInputValue(currentLocalEntry.name);
|
|
764
|
+
setMode("rename");
|
|
765
|
+
setStatus(`Enter a new name for ${currentLocalEntry.name}.`);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (input === "x" || input === "X") {
|
|
770
|
+
if (!currentLocalEntry) {
|
|
771
|
+
setStatus("No local item is selected.");
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
setPendingAction({
|
|
776
|
+
type: "local-delete",
|
|
777
|
+
targetPath: currentLocalEntry.absolutePath,
|
|
778
|
+
});
|
|
779
|
+
setMode("confirm");
|
|
780
|
+
setStatus(
|
|
781
|
+
`Press y to delete local ${currentLocalEntry.isDirectory ? "directory" : "file"} ${currentLocalEntry.name}.`
|
|
782
|
+
);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (input === " ") {
|
|
790
|
+
if (!currentRemoteEntry) {
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
setSelectedRemoteIds((current) => {
|
|
795
|
+
const next = new Set(current);
|
|
796
|
+
if (next.has(currentRemoteEntry.id)) {
|
|
797
|
+
next.delete(currentRemoteEntry.id);
|
|
798
|
+
} else {
|
|
799
|
+
next.add(currentRemoteEntry.id);
|
|
800
|
+
}
|
|
801
|
+
return next;
|
|
802
|
+
});
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (input === "a" || input === "A") {
|
|
807
|
+
const visibleIds = filteredRemoteEntries.map((file) => file.id);
|
|
808
|
+
const allVisibleSelected =
|
|
809
|
+
visibleIds.length > 0 && visibleIds.every((id) => selectedRemoteIds.has(id));
|
|
810
|
+
|
|
811
|
+
setSelectedRemoteIds((current) => {
|
|
812
|
+
const next = new Set(current);
|
|
813
|
+
if (allVisibleSelected) {
|
|
814
|
+
for (const id of visibleIds) {
|
|
815
|
+
next.delete(id);
|
|
816
|
+
}
|
|
817
|
+
} else {
|
|
818
|
+
for (const id of visibleIds) {
|
|
819
|
+
next.add(id);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return next;
|
|
823
|
+
});
|
|
824
|
+
setStatus(
|
|
825
|
+
allVisibleSelected
|
|
826
|
+
? "Deselected all visible Drive items."
|
|
827
|
+
: `Selected ${visibleIds.length} visible Drive item(s).`
|
|
828
|
+
);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (input === "t" || input === "T") {
|
|
833
|
+
if (selectedRemoteIds.size === 0) {
|
|
834
|
+
setStatus("No Drive items selected.");
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
setPendingAction({ type: "remote-trash" });
|
|
839
|
+
setMode("confirm");
|
|
840
|
+
setStatus(`Press y to trash ${selectedRemoteIds.size} Drive item(s).`);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (input === "d" || input === "D") {
|
|
845
|
+
if (selectedRemoteIds.size === 0) {
|
|
846
|
+
setStatus("No Drive items selected.");
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
setPendingAction({ type: "remote-delete" });
|
|
851
|
+
setMode("confirm");
|
|
852
|
+
setStatus(`Press y to permanently delete ${selectedRemoteIds.size} Drive item(s).`);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (input === "U") {
|
|
857
|
+
if (!currentRemoteFolderWritable) {
|
|
858
|
+
setStatus(
|
|
859
|
+
`Cannot upload into current Drive directory${
|
|
860
|
+
currentRemoteFolderMeta?.name ? `: ${currentRemoteFolderMeta.name}` : ""
|
|
861
|
+
}. This folder does not allow adding children.`
|
|
862
|
+
);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
setInputValue("");
|
|
866
|
+
setMode("upload");
|
|
867
|
+
setStatus("Enter a local file or directory path to upload.");
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
const driveBreadcrumb = remoteFolderStack.length
|
|
872
|
+
? `Drive: /${remoteFolderStack.map((folder) => folder.name).join("/")}`
|
|
873
|
+
: "Drive: /";
|
|
874
|
+
const localBreadcrumb = `Local: ${localDirectory}`;
|
|
875
|
+
const selectedCount = selectedRemoteIds.size;
|
|
876
|
+
const contentHeight = Math.max(height - 10, 6);
|
|
877
|
+
const paneWidth = Math.max(Math.floor((width - 3) / 2), 24);
|
|
878
|
+
|
|
879
|
+
if (mode === "error") {
|
|
880
|
+
return h(
|
|
881
|
+
Box,
|
|
882
|
+
{ flexDirection: "column" },
|
|
883
|
+
h(Text, { color: "red", bold: true }, "Aethel failed to load."),
|
|
884
|
+
h(Text, null, errorMessage || "Unknown error."),
|
|
885
|
+
h(Text, { dimColor: true }, "Press q or Esc to quit.")
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (mode === "loading") {
|
|
890
|
+
return h(
|
|
891
|
+
Box,
|
|
892
|
+
{ flexDirection: "column" },
|
|
893
|
+
h(
|
|
894
|
+
Text,
|
|
895
|
+
{ color: "cyan" },
|
|
896
|
+
h(Spinner, { type: "dots" }),
|
|
897
|
+
` ${status}`
|
|
898
|
+
)
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
let hints =
|
|
903
|
+
"Tab:focus Left/Right:navigate u:upload local s:sync dir n:rename local x:delete local Space:select drive t/d:delete drive /:filter U:manual upload r:reload q:quit ?:help";
|
|
904
|
+
if (mode === "filter") {
|
|
905
|
+
hints = "Enter: apply filter Esc: cancel";
|
|
906
|
+
} else if (mode === "confirm") {
|
|
907
|
+
hints = "y: confirm n: cancel";
|
|
908
|
+
} else if (mode === "upload") {
|
|
909
|
+
hints = "Enter: upload path Esc: cancel";
|
|
910
|
+
} else if (mode === "rename") {
|
|
911
|
+
hints = "Enter: rename Esc: cancel";
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return h(
|
|
915
|
+
Box,
|
|
916
|
+
{ flexDirection: "column" },
|
|
917
|
+
h(Text, { color: "cyan", bold: true }, truncate("Aethel", width)),
|
|
918
|
+
account
|
|
919
|
+
? h(
|
|
920
|
+
Text,
|
|
921
|
+
null,
|
|
922
|
+
truncate(
|
|
923
|
+
`${account.name} <${account.email}> | ${account.usage} / ${account.limit}`,
|
|
924
|
+
width
|
|
925
|
+
)
|
|
926
|
+
)
|
|
927
|
+
: null,
|
|
928
|
+
h(
|
|
929
|
+
Text,
|
|
930
|
+
{ dimColor: true },
|
|
931
|
+
truncate(
|
|
932
|
+
`${driveBreadcrumb} | ${localBreadcrumb}`,
|
|
933
|
+
width
|
|
934
|
+
)
|
|
935
|
+
),
|
|
936
|
+
h(
|
|
937
|
+
Text,
|
|
938
|
+
{ dimColor: true },
|
|
939
|
+
truncate(
|
|
940
|
+
`Legend: [MY ] owned by me [SHR] shared with me [DRV] shared drive [LOC] local item | ${selectedCount} Drive item(s) selected | Upload ${currentRemoteFolderWritable ? "enabled" : "blocked"}`,
|
|
941
|
+
width
|
|
942
|
+
)
|
|
943
|
+
),
|
|
944
|
+
h(Text, { dimColor: true }, "─".repeat(Math.max(Math.min(width, 80), 10))),
|
|
945
|
+
h(
|
|
946
|
+
Box,
|
|
947
|
+
{ flexDirection: "row" },
|
|
948
|
+
renderPane({
|
|
949
|
+
title: "Local",
|
|
950
|
+
breadcrumb: truncate(localDirectory, paneWidth),
|
|
951
|
+
focused: focusPane === "local",
|
|
952
|
+
entries: filteredLocalEntries,
|
|
953
|
+
cursor: localCursor,
|
|
954
|
+
renderer: renderLocalRow,
|
|
955
|
+
width: paneWidth,
|
|
956
|
+
height: contentHeight,
|
|
957
|
+
emptyMessage: localFilter
|
|
958
|
+
? "No local items match the filter."
|
|
959
|
+
: "No local items found.",
|
|
960
|
+
}),
|
|
961
|
+
h(Text, { dimColor: true }, " | "),
|
|
962
|
+
renderPane({
|
|
963
|
+
title: remoteLoading ? "Drive (loading...)" : "Drive",
|
|
964
|
+
breadcrumb: truncate(
|
|
965
|
+
remoteFolderStack.length
|
|
966
|
+
? `/${remoteFolderStack.map((folder) => folder.name).join("/")}`
|
|
967
|
+
: "/",
|
|
968
|
+
paneWidth
|
|
969
|
+
),
|
|
970
|
+
focused: focusPane === "remote",
|
|
971
|
+
entries: filteredRemoteEntries,
|
|
972
|
+
cursor: remoteCursor,
|
|
973
|
+
renderer: (entry, isCursor, itemWidth) =>
|
|
974
|
+
renderRemoteRow(
|
|
975
|
+
entry,
|
|
976
|
+
isCursor,
|
|
977
|
+
selectedRemoteIds.has(entry.id),
|
|
978
|
+
itemWidth
|
|
979
|
+
),
|
|
980
|
+
width: paneWidth,
|
|
981
|
+
height: contentHeight,
|
|
982
|
+
emptyMessage: remoteFilter
|
|
983
|
+
? "No Drive items match the filter."
|
|
984
|
+
: "No Drive items found.",
|
|
985
|
+
})
|
|
986
|
+
),
|
|
987
|
+
h(Text, { color: mode === "busy" ? "yellow" : "green" }, truncate(status, width)),
|
|
988
|
+
mode === "upload"
|
|
989
|
+
? h(
|
|
990
|
+
Box,
|
|
991
|
+
{ flexDirection: "column" },
|
|
992
|
+
h(Text, { color: "cyan" }, "Local path to upload into current Drive directory:"),
|
|
993
|
+
h(TextInput, {
|
|
994
|
+
value: inputValue,
|
|
995
|
+
onChange: setInputValue,
|
|
996
|
+
onSubmit: (value) => {
|
|
997
|
+
void executeUpload(value);
|
|
998
|
+
},
|
|
999
|
+
})
|
|
1000
|
+
)
|
|
1001
|
+
: null,
|
|
1002
|
+
mode === "rename"
|
|
1003
|
+
? h(
|
|
1004
|
+
Box,
|
|
1005
|
+
{ flexDirection: "column" },
|
|
1006
|
+
h(Text, { color: "cyan" }, "New local name:"),
|
|
1007
|
+
h(TextInput, {
|
|
1008
|
+
value: inputValue,
|
|
1009
|
+
onChange: setInputValue,
|
|
1010
|
+
onSubmit: (value) => {
|
|
1011
|
+
const action = pendingAction;
|
|
1012
|
+
setPendingAction(null);
|
|
1013
|
+
if (!action || action.type !== "local-rename") {
|
|
1014
|
+
setMode("normal");
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
void executeLocalRename(action.targetPath, value);
|
|
1018
|
+
},
|
|
1019
|
+
})
|
|
1020
|
+
)
|
|
1021
|
+
: null,
|
|
1022
|
+
h(Text, { dimColor: true }, truncate(hints, width)),
|
|
1023
|
+
mode === "help" ? renderHelp() : null
|
|
1024
|
+
);
|
|
1025
|
+
}
|