aethel 0.2.5 → 0.3.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/CHANGELOG.md +8 -0
- package/README.md +86 -102
- package/package.json +1 -1
- package/src/cli.js +92 -181
- package/src/core/diff.js +7 -9
- package/src/core/drive-api.js +56 -13
- package/src/core/repository.js +309 -0
- package/src/core/snapshot.js +14 -3
- package/src/tui/app.js +530 -70
- package/src/tui/command-catalog.js +140 -0
- package/src/tui/commands.js +74 -0
- package/src/tui/index.js +12 -2
package/src/tui/app.js
CHANGED
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
2
3
|
import React, { useEffect, useMemo, useState } from "react";
|
|
3
4
|
import { Box, Text, useApp, useInput } from "ink";
|
|
4
5
|
import Spinner from "ink-spinner";
|
|
5
6
|
import TextInput from "ink-text-input";
|
|
6
7
|
import {
|
|
7
|
-
batchOperateFiles,
|
|
8
|
-
getAccountInfo,
|
|
9
8
|
humanSize,
|
|
10
9
|
iconForMime,
|
|
11
|
-
listAccessibleFiles,
|
|
12
10
|
sourceBadgeForItem,
|
|
13
|
-
syncLocalDirectoryToParent,
|
|
14
|
-
uploadLocalEntry,
|
|
15
11
|
} from "../core/drive-api.js";
|
|
16
12
|
import {
|
|
17
13
|
defaultLocalRoot,
|
|
18
|
-
deleteLocalEntry,
|
|
19
14
|
ensureLocalDirectory,
|
|
20
|
-
listLocalEntries,
|
|
21
|
-
renameLocalEntry,
|
|
22
15
|
} from "../core/local-fs.js";
|
|
16
|
+
import { COMMAND_CATALOG } from "./command-catalog.js";
|
|
17
|
+
import { parseCommandInput } from "./commands.js";
|
|
23
18
|
|
|
24
19
|
const h = React.createElement;
|
|
25
20
|
|
|
@@ -137,25 +132,148 @@ function renderHelp() {
|
|
|
137
132
|
marginTop: 1,
|
|
138
133
|
flexDirection: "column",
|
|
139
134
|
},
|
|
140
|
-
h(Text, { bold: true }, "
|
|
141
|
-
h(Text, null, "
|
|
142
|
-
h(Text,
|
|
143
|
-
h(Text, null, "Left/Right
|
|
144
|
-
h(Text, null, "
|
|
145
|
-
h(Text, null, "
|
|
146
|
-
h(Text,
|
|
147
|
-
h(Text, null, "
|
|
148
|
-
h(Text, null, "
|
|
149
|
-
h(Text, null, "
|
|
150
|
-
h(Text,
|
|
151
|
-
h(Text, null, "
|
|
152
|
-
h(Text, null, "
|
|
153
|
-
h(Text,
|
|
154
|
-
h(Text,
|
|
135
|
+
h(Text, { bold: true }, "Keyboard Shortcuts"),
|
|
136
|
+
h(Text, null, ""),
|
|
137
|
+
h(Text, { color: "cyan" }, "Navigation"),
|
|
138
|
+
h(Text, null, " Tab Switch panes Left/Right Parent / Enter dir"),
|
|
139
|
+
h(Text, null, " j/k Move cursor / Filter by name"),
|
|
140
|
+
h(Text, null, ""),
|
|
141
|
+
h(Text, { color: "cyan" }, "Local Pane"),
|
|
142
|
+
h(Text, null, " u Upload to Drive s Sync dir to Drive U Upload by path"),
|
|
143
|
+
h(Text, null, " n Rename x Delete"),
|
|
144
|
+
h(Text, null, ""),
|
|
145
|
+
h(Text, { color: "cyan" }, "Drive Pane"),
|
|
146
|
+
h(Text, null, " Space Toggle select a Select all t Trash d Delete"),
|
|
147
|
+
h(Text, null, ""),
|
|
148
|
+
h(Text, { color: "cyan" }, "Commands"),
|
|
149
|
+
h(Text, null, " f Open command panel : Run CLI command directly"),
|
|
150
|
+
h(Text, null, " r Reload panes q Quit"),
|
|
151
|
+
h(Text, { dimColor: true }, "Press any key to close.")
|
|
155
152
|
);
|
|
156
153
|
}
|
|
157
154
|
|
|
158
|
-
|
|
155
|
+
function renderCommandCatalog(width, height, cursor) {
|
|
156
|
+
const listHeight = Math.max(height - 7, 6);
|
|
157
|
+
const start = scrollWindow(COMMAND_CATALOG.length, cursor, listHeight);
|
|
158
|
+
const visibleEntries = COMMAND_CATALOG.slice(start, start + listHeight);
|
|
159
|
+
const currentEntry = COMMAND_CATALOG[cursor] || null;
|
|
160
|
+
|
|
161
|
+
return h(
|
|
162
|
+
Box,
|
|
163
|
+
{
|
|
164
|
+
borderStyle: "round",
|
|
165
|
+
borderColor: "cyan",
|
|
166
|
+
paddingX: 1,
|
|
167
|
+
flexDirection: "column",
|
|
168
|
+
},
|
|
169
|
+
...visibleEntries.map((entry, index) =>
|
|
170
|
+
h(
|
|
171
|
+
Text,
|
|
172
|
+
{
|
|
173
|
+
key: entry.name,
|
|
174
|
+
inverse: start + index === cursor,
|
|
175
|
+
color: start + index === cursor ? "cyan" : undefined,
|
|
176
|
+
wrap: "truncate-end",
|
|
177
|
+
},
|
|
178
|
+
truncate(`${entry.name.padEnd(16, " ")} ${entry.description}`, width - 4)
|
|
179
|
+
)
|
|
180
|
+
),
|
|
181
|
+
currentEntry
|
|
182
|
+
? h(
|
|
183
|
+
Text,
|
|
184
|
+
{ dimColor: true },
|
|
185
|
+
truncate(`> aethel ${currentEntry.template}`, width - 4)
|
|
186
|
+
)
|
|
187
|
+
: null
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function renderCommandActions(width, height, command, cursor) {
|
|
192
|
+
const actions = [
|
|
193
|
+
...command.actions,
|
|
194
|
+
{ label: "Custom Command", command: command.template },
|
|
195
|
+
];
|
|
196
|
+
const listHeight = Math.max(height - 8, 5);
|
|
197
|
+
const start = scrollWindow(actions.length, cursor, listHeight);
|
|
198
|
+
const visibleEntries = actions.slice(start, start + listHeight);
|
|
199
|
+
const currentAction = actions[cursor] || null;
|
|
200
|
+
|
|
201
|
+
return h(
|
|
202
|
+
Box,
|
|
203
|
+
{
|
|
204
|
+
borderStyle: "round",
|
|
205
|
+
borderColor: "cyan",
|
|
206
|
+
paddingX: 1,
|
|
207
|
+
flexDirection: "column",
|
|
208
|
+
},
|
|
209
|
+
h(Text, { bold: true }, truncate(`${command.name}`, width - 4)),
|
|
210
|
+
...visibleEntries.map((entry, index) =>
|
|
211
|
+
h(
|
|
212
|
+
Text,
|
|
213
|
+
{
|
|
214
|
+
key: `${command.name}-${entry.label}`,
|
|
215
|
+
inverse: start + index === cursor,
|
|
216
|
+
color: start + index === cursor ? "cyan" : undefined,
|
|
217
|
+
wrap: "truncate-end",
|
|
218
|
+
},
|
|
219
|
+
truncate(entry.label, width - 4)
|
|
220
|
+
)
|
|
221
|
+
),
|
|
222
|
+
currentAction
|
|
223
|
+
? h(
|
|
224
|
+
Text,
|
|
225
|
+
{ dimColor: true },
|
|
226
|
+
truncate(`> aethel ${currentAction.command}`, width - 4)
|
|
227
|
+
)
|
|
228
|
+
: null
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function renderCommandOutput(commandResult, width, height, scroll) {
|
|
233
|
+
const outputLines = commandResult.output
|
|
234
|
+
? commandResult.output.split(/\r?\n/)
|
|
235
|
+
: ["(no output)"];
|
|
236
|
+
const bodyHeight = Math.max(height - 8, 4);
|
|
237
|
+
const maxScroll = Math.max(outputLines.length - bodyHeight, 0);
|
|
238
|
+
const start = Math.min(scroll, maxScroll);
|
|
239
|
+
const visibleLines = outputLines.slice(start, start + bodyHeight);
|
|
240
|
+
|
|
241
|
+
return h(
|
|
242
|
+
Box,
|
|
243
|
+
{
|
|
244
|
+
borderStyle: "round",
|
|
245
|
+
borderColor: commandResult.exitCode === 0 ? "cyan" : "red",
|
|
246
|
+
paddingX: 1,
|
|
247
|
+
marginTop: 1,
|
|
248
|
+
flexDirection: "column",
|
|
249
|
+
},
|
|
250
|
+
h(Text, { bold: true }, truncate(`Command: aethel ${commandResult.command}`, width - 4)),
|
|
251
|
+
h(
|
|
252
|
+
Text,
|
|
253
|
+
{ color: commandResult.exitCode === 0 ? "green" : "red" },
|
|
254
|
+
`Exit code: ${commandResult.exitCode}`
|
|
255
|
+
),
|
|
256
|
+
...visibleLines.map((line, index) =>
|
|
257
|
+
h(
|
|
258
|
+
Text,
|
|
259
|
+
{ key: `${start + index}`, wrap: "truncate-end" },
|
|
260
|
+
truncate(line || " ", width - 4)
|
|
261
|
+
)
|
|
262
|
+
),
|
|
263
|
+
h(
|
|
264
|
+
Text,
|
|
265
|
+
{ dimColor: true },
|
|
266
|
+
truncate("Up/Down/PageUp/PageDown/Home/End: scroll Enter/Esc: close", width - 4)
|
|
267
|
+
)
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function AethelTui({
|
|
272
|
+
repo,
|
|
273
|
+
includeSharedDrives = false,
|
|
274
|
+
cliPath = null,
|
|
275
|
+
cliArgs = [],
|
|
276
|
+
}) {
|
|
159
277
|
const { exit } = useApp();
|
|
160
278
|
const [mode, setMode] = useState("loading");
|
|
161
279
|
const [remoteLoading, setRemoteLoading] = useState(true);
|
|
@@ -175,6 +293,11 @@ export function AethelTui({ drive, includeSharedDrives = false }) {
|
|
|
175
293
|
const [errorMessage, setErrorMessage] = useState("");
|
|
176
294
|
const [pendingAction, setPendingAction] = useState(null);
|
|
177
295
|
const [inputValue, setInputValue] = useState("");
|
|
296
|
+
const [commandResult, setCommandResult] = useState(null);
|
|
297
|
+
const [commandScroll, setCommandScroll] = useState(0);
|
|
298
|
+
const [commandCursor, setCommandCursor] = useState(0);
|
|
299
|
+
const [commandActionCursor, setCommandActionCursor] = useState(0);
|
|
300
|
+
const [commandReturnMode, setCommandReturnMode] = useState("normal");
|
|
178
301
|
const width = process.stdout.columns || 80;
|
|
179
302
|
const height = process.stdout.rows || 24;
|
|
180
303
|
const currentRemoteFolderId = remoteFolderStack.length
|
|
@@ -236,6 +359,7 @@ export function AethelTui({ drive, includeSharedDrives = false }) {
|
|
|
236
359
|
|
|
237
360
|
const currentLocalEntry = filteredLocalEntries[localCursor] || null;
|
|
238
361
|
const currentRemoteEntry = filteredRemoteEntries[remoteCursor] || null;
|
|
362
|
+
const currentCatalogCommand = COMMAND_CATALOG[commandCursor] || null;
|
|
239
363
|
const currentRemoteFolderMeta = currentRemoteFolderId
|
|
240
364
|
? remoteFiles.find((file) => file.id === currentRemoteFolderId) || null
|
|
241
365
|
: null;
|
|
@@ -255,9 +379,15 @@ export function AethelTui({ drive, includeSharedDrives = false }) {
|
|
|
255
379
|
);
|
|
256
380
|
}, [filteredLocalEntries.length]);
|
|
257
381
|
|
|
382
|
+
useEffect(() => {
|
|
383
|
+
setCommandCursor((current) =>
|
|
384
|
+
Math.min(current, Math.max(COMMAND_CATALOG.length - 1, 0))
|
|
385
|
+
);
|
|
386
|
+
}, []);
|
|
387
|
+
|
|
258
388
|
async function loadLocalPane(nextDirectory = localDirectory) {
|
|
259
389
|
const resolvedDirectory = await ensureLocalDirectory(nextDirectory);
|
|
260
|
-
const items = await listLocalEntries(resolvedDirectory);
|
|
390
|
+
const items = await repo.listLocalEntries(resolvedDirectory);
|
|
261
391
|
setLocalDirectory(resolvedDirectory);
|
|
262
392
|
setLocalEntries(items);
|
|
263
393
|
setLocalCursor(0);
|
|
@@ -282,13 +412,17 @@ export function AethelTui({ drive, includeSharedDrives = false }) {
|
|
|
282
412
|
// Fetch remote in background
|
|
283
413
|
try {
|
|
284
414
|
const [nextAccount, nextRemoteFiles] = await Promise.all([
|
|
285
|
-
getAccountInfo(
|
|
286
|
-
|
|
415
|
+
repo.getAccountInfo(),
|
|
416
|
+
repo.listRemoteFiles({ includeSharedDrives }),
|
|
287
417
|
]);
|
|
418
|
+
const remoteIds = new Set(nextRemoteFiles.map((file) => file.id));
|
|
419
|
+
const resolvedRemoteFolderStack = nextRemoteFolderStack.filter((folder) =>
|
|
420
|
+
remoteIds.has(folder.id)
|
|
421
|
+
);
|
|
288
422
|
|
|
289
423
|
setAccount(nextAccount);
|
|
290
424
|
setRemoteFiles(nextRemoteFiles);
|
|
291
|
-
setRemoteFolderStack(
|
|
425
|
+
setRemoteFolderStack(resolvedRemoteFolderStack);
|
|
292
426
|
setSelectedRemoteIds(new Set());
|
|
293
427
|
setRemoteLoading(false);
|
|
294
428
|
setStatus(
|
|
@@ -313,7 +447,7 @@ export function AethelTui({ drive, includeSharedDrives = false }) {
|
|
|
313
447
|
|
|
314
448
|
setMode("busy");
|
|
315
449
|
try {
|
|
316
|
-
const result = await batchOperateFiles(
|
|
450
|
+
const result = await repo.batchOperateFiles(targets, {
|
|
317
451
|
permanent,
|
|
318
452
|
includeSharedDrives,
|
|
319
453
|
onProgress: (done, total, verb, name) => {
|
|
@@ -341,8 +475,7 @@ export function AethelTui({ drive, includeSharedDrives = false }) {
|
|
|
341
475
|
|
|
342
476
|
setMode("busy");
|
|
343
477
|
try {
|
|
344
|
-
const result = await uploadLocalEntry(
|
|
345
|
-
drive,
|
|
478
|
+
const result = await repo.uploadLocalEntry(
|
|
346
479
|
trimmedPath,
|
|
347
480
|
currentUploadParentId,
|
|
348
481
|
(verb, localPath, name) => {
|
|
@@ -368,8 +501,7 @@ export function AethelTui({ drive, includeSharedDrives = false }) {
|
|
|
368
501
|
async function executeSyncDirectory(targetPath) {
|
|
369
502
|
setMode("busy");
|
|
370
503
|
try {
|
|
371
|
-
const result = await
|
|
372
|
-
drive,
|
|
504
|
+
const result = await repo.syncLocalDirectory(
|
|
373
505
|
targetPath,
|
|
374
506
|
currentUploadParentId,
|
|
375
507
|
(verb, localPath, name) => {
|
|
@@ -394,7 +526,7 @@ export function AethelTui({ drive, includeSharedDrives = false }) {
|
|
|
394
526
|
async function executeLocalDelete(targetPath) {
|
|
395
527
|
setMode("busy");
|
|
396
528
|
try {
|
|
397
|
-
await deleteLocalEntry(targetPath);
|
|
529
|
+
await repo.deleteLocalEntry(targetPath);
|
|
398
530
|
await loadLocalPane(path.dirname(targetPath) === targetPath ? localDirectory : localDirectory);
|
|
399
531
|
setMode("normal");
|
|
400
532
|
setStatus(`Deleted local entry: ${path.basename(targetPath)}`);
|
|
@@ -407,7 +539,7 @@ export function AethelTui({ drive, includeSharedDrives = false }) {
|
|
|
407
539
|
async function executeLocalRename(targetPath, nextName) {
|
|
408
540
|
setMode("busy");
|
|
409
541
|
try {
|
|
410
|
-
const renamedPath = await renameLocalEntry(targetPath, nextName);
|
|
542
|
+
const renamedPath = await repo.renameLocalEntry(targetPath, nextName);
|
|
411
543
|
await loadLocalPane(path.dirname(renamedPath));
|
|
412
544
|
setMode("normal");
|
|
413
545
|
setStatus(`Renamed local entry to: ${path.basename(renamedPath)}`);
|
|
@@ -436,6 +568,91 @@ export function AethelTui({ drive, includeSharedDrives = false }) {
|
|
|
436
568
|
);
|
|
437
569
|
}
|
|
438
570
|
|
|
571
|
+
function openCommandEditor(nextValue, returnMode = "normal") {
|
|
572
|
+
setInputValue(nextValue);
|
|
573
|
+
setCommandReturnMode(returnMode);
|
|
574
|
+
setMode("command");
|
|
575
|
+
setStatus("Edit the command and press Enter to run.");
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function openCommandActions(index = commandCursor) {
|
|
579
|
+
setCommandCursor(index);
|
|
580
|
+
setCommandActionCursor(0);
|
|
581
|
+
setMode("command-actions");
|
|
582
|
+
setStatus("Choose a TUI action or edit the command.");
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function executeCliCommand(rawCommand, returnMode = commandReturnMode) {
|
|
586
|
+
if (!cliPath) {
|
|
587
|
+
setMode("normal");
|
|
588
|
+
setStatus("CLI command runner is unavailable.");
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
setCommandReturnMode(returnMode);
|
|
593
|
+
|
|
594
|
+
let args;
|
|
595
|
+
try {
|
|
596
|
+
args = parseCommandInput(rawCommand);
|
|
597
|
+
} catch (error) {
|
|
598
|
+
setMode(returnMode);
|
|
599
|
+
setStatus(error.message);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (args.length === 0) {
|
|
604
|
+
setMode(returnMode);
|
|
605
|
+
setStatus("Command cancelled.");
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
setMode("busy");
|
|
610
|
+
setStatus(`Running: aethel ${args.join(" ")}`);
|
|
611
|
+
|
|
612
|
+
const output = await new Promise((resolve) => {
|
|
613
|
+
const child = spawn(process.execPath, [cliPath, ...cliArgs, ...args], {
|
|
614
|
+
cwd: process.cwd(),
|
|
615
|
+
env: process.env,
|
|
616
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
let stdout = "";
|
|
620
|
+
let stderr = "";
|
|
621
|
+
|
|
622
|
+
child.stdout.on("data", (chunk) => {
|
|
623
|
+
stdout += chunk.toString();
|
|
624
|
+
});
|
|
625
|
+
child.stderr.on("data", (chunk) => {
|
|
626
|
+
stderr += chunk.toString();
|
|
627
|
+
});
|
|
628
|
+
child.on("error", (error) => {
|
|
629
|
+
stderr += `${error.message}\n`;
|
|
630
|
+
});
|
|
631
|
+
child.on("close", (exitCode) => {
|
|
632
|
+
resolve({
|
|
633
|
+
command: args.join(" "),
|
|
634
|
+
exitCode: exitCode ?? 1,
|
|
635
|
+
output: [stdout.trimEnd(), stderr.trimEnd()].filter(Boolean).join("\n"),
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
setInputValue("");
|
|
641
|
+
setCommandResult(output);
|
|
642
|
+
setCommandScroll(
|
|
643
|
+
Math.max(output.output.split(/\r?\n/).length - Math.max(height - 8, 4), 0)
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
if (output.exitCode === 0) {
|
|
647
|
+
await loadAllData(`Command finished: aethel ${output.command}`, true);
|
|
648
|
+
} else {
|
|
649
|
+
setStatus(`Command failed: aethel ${output.command}`);
|
|
650
|
+
setMode("normal");
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
setMode("command-output");
|
|
654
|
+
}
|
|
655
|
+
|
|
439
656
|
useInput((input, key) => {
|
|
440
657
|
if ((key.ctrl && input === "c") || (mode === "error" && input === "q")) {
|
|
441
658
|
exit();
|
|
@@ -458,6 +675,183 @@ export function AethelTui({ drive, includeSharedDrives = false }) {
|
|
|
458
675
|
return;
|
|
459
676
|
}
|
|
460
677
|
|
|
678
|
+
if (mode === "commands-page") {
|
|
679
|
+
if (key.escape || input === "f" || input === "F") {
|
|
680
|
+
setMode("normal");
|
|
681
|
+
setStatus("Closed commands page.");
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (input === ":") {
|
|
686
|
+
openCommandEditor("", "commands-page");
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (key.return || key.rightArrow) {
|
|
691
|
+
if (currentCatalogCommand) {
|
|
692
|
+
openCommandActions(commandCursor);
|
|
693
|
+
}
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (key.upArrow || input === "k") {
|
|
698
|
+
setCommandCursor((current) => Math.max(current - 1, 0));
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (key.downArrow || input === "j") {
|
|
703
|
+
setCommandCursor((current) =>
|
|
704
|
+
Math.min(current + 1, Math.max(COMMAND_CATALOG.length - 1, 0))
|
|
705
|
+
);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (key.pageUp) {
|
|
710
|
+
setCommandCursor((current) =>
|
|
711
|
+
Math.max(current - Math.max(height - 11, 6), 0)
|
|
712
|
+
);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (key.pageDown) {
|
|
717
|
+
setCommandCursor((current) =>
|
|
718
|
+
Math.min(
|
|
719
|
+
current + Math.max(height - 11, 6),
|
|
720
|
+
Math.max(COMMAND_CATALOG.length - 1, 0)
|
|
721
|
+
)
|
|
722
|
+
);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (key.home) {
|
|
727
|
+
setCommandCursor(0);
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (key.end) {
|
|
732
|
+
setCommandCursor(Math.max(COMMAND_CATALOG.length - 1, 0));
|
|
733
|
+
}
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (mode === "command-actions") {
|
|
738
|
+
if (key.escape || key.leftArrow) {
|
|
739
|
+
setMode("commands-page");
|
|
740
|
+
setStatus("Back to commands list.");
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (!currentCatalogCommand) {
|
|
745
|
+
setMode("commands-page");
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const availableActions = [
|
|
750
|
+
...currentCatalogCommand.actions,
|
|
751
|
+
{ label: "Custom Command", command: currentCatalogCommand.template },
|
|
752
|
+
];
|
|
753
|
+
const currentAction = availableActions[commandActionCursor] || availableActions[0];
|
|
754
|
+
|
|
755
|
+
if (input === "e" || input === "E") {
|
|
756
|
+
openCommandEditor(currentAction.command, "command-actions");
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (key.return) {
|
|
761
|
+
if (currentAction.label === "Custom Command") {
|
|
762
|
+
openCommandEditor(currentAction.command, "command-actions");
|
|
763
|
+
} else {
|
|
764
|
+
void executeCliCommand(currentAction.command, "command-actions");
|
|
765
|
+
}
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (key.upArrow || input === "k") {
|
|
770
|
+
setCommandActionCursor((current) => Math.max(current - 1, 0));
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (key.downArrow || input === "j") {
|
|
775
|
+
setCommandActionCursor((current) =>
|
|
776
|
+
Math.min(current + 1, Math.max(availableActions.length - 1, 0))
|
|
777
|
+
);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (key.pageUp) {
|
|
782
|
+
setCommandActionCursor((current) =>
|
|
783
|
+
Math.max(current - Math.max(height - 12, 5), 0)
|
|
784
|
+
);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (key.pageDown) {
|
|
789
|
+
setCommandActionCursor((current) =>
|
|
790
|
+
Math.min(
|
|
791
|
+
current + Math.max(height - 12, 5),
|
|
792
|
+
Math.max(availableActions.length - 1, 0)
|
|
793
|
+
)
|
|
794
|
+
);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (key.home) {
|
|
799
|
+
setCommandActionCursor(0);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (key.end) {
|
|
804
|
+
setCommandActionCursor(Math.max(availableActions.length - 1, 0));
|
|
805
|
+
}
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (mode === "command-output") {
|
|
810
|
+
if (key.escape || key.return || input === "q" || input === "Q") {
|
|
811
|
+
setCommandResult(null);
|
|
812
|
+
setCommandScroll(0);
|
|
813
|
+
setMode(commandReturnMode);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (key.upArrow || input === "k") {
|
|
818
|
+
setCommandScroll((current) => Math.max(current - 1, 0));
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (key.downArrow || input === "j") {
|
|
823
|
+
const lines = commandResult?.output?.split(/\r?\n/).length || 1;
|
|
824
|
+
const maxScroll = Math.max(lines - Math.max(height - 8, 4), 0);
|
|
825
|
+
setCommandScroll((current) => Math.min(current + 1, maxScroll));
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
if (key.pageUp) {
|
|
830
|
+
setCommandScroll((current) => Math.max(current - Math.max(height - 8, 4), 0));
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (key.pageDown) {
|
|
835
|
+
const lines = commandResult?.output?.split(/\r?\n/).length || 1;
|
|
836
|
+
const maxScroll = Math.max(lines - Math.max(height - 8, 4), 0);
|
|
837
|
+
setCommandScroll((current) =>
|
|
838
|
+
Math.min(current + Math.max(height - 8, 4), maxScroll)
|
|
839
|
+
);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (key.home) {
|
|
844
|
+
setCommandScroll(0);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (key.end) {
|
|
849
|
+
const lines = commandResult?.output?.split(/\r?\n/).length || 1;
|
|
850
|
+
setCommandScroll(Math.max(lines - Math.max(height - 8, 4), 0));
|
|
851
|
+
}
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
461
855
|
if (mode === "confirm") {
|
|
462
856
|
if (input === "y" || input === "Y") {
|
|
463
857
|
const action = pendingAction;
|
|
@@ -542,6 +936,15 @@ export function AethelTui({ drive, includeSharedDrives = false }) {
|
|
|
542
936
|
return;
|
|
543
937
|
}
|
|
544
938
|
|
|
939
|
+
if (mode === "command") {
|
|
940
|
+
if (key.escape) {
|
|
941
|
+
setInputValue("");
|
|
942
|
+
setMode(commandReturnMode);
|
|
943
|
+
setStatus("Command cancelled.");
|
|
944
|
+
}
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
|
|
545
948
|
if (mode === "rename") {
|
|
546
949
|
if (key.escape) {
|
|
547
950
|
setInputValue("");
|
|
@@ -573,6 +976,18 @@ export function AethelTui({ drive, includeSharedDrives = false }) {
|
|
|
573
976
|
return;
|
|
574
977
|
}
|
|
575
978
|
|
|
979
|
+
if (input === ":") {
|
|
980
|
+
openCommandEditor("", "normal");
|
|
981
|
+
setStatus("Enter an Aethel command without `aethel`.");
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (input === "f" || input === "F") {
|
|
986
|
+
setMode("commands-page");
|
|
987
|
+
setStatus("Browse commands and press Enter to edit one.");
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
|
|
576
991
|
if (input === "r" || input === "R") {
|
|
577
992
|
void loadAllData("Reloaded Drive and Local panes.", true);
|
|
578
993
|
return;
|
|
@@ -704,7 +1119,7 @@ export function AethelTui({ drive, includeSharedDrives = false }) {
|
|
|
704
1119
|
}
|
|
705
1120
|
|
|
706
1121
|
if (focusPane === "local") {
|
|
707
|
-
if (input === "u"
|
|
1122
|
+
if (input === "u") {
|
|
708
1123
|
if (!currentLocalEntry) {
|
|
709
1124
|
setStatus("No local item is selected.");
|
|
710
1125
|
return;
|
|
@@ -873,7 +1288,7 @@ export function AethelTui({ drive, includeSharedDrives = false }) {
|
|
|
873
1288
|
: "Drive: /";
|
|
874
1289
|
const localBreadcrumb = `Local: ${localDirectory}`;
|
|
875
1290
|
const selectedCount = selectedRemoteIds.size;
|
|
876
|
-
const contentHeight = Math.max(height -
|
|
1291
|
+
const contentHeight = Math.max(height - 7, 6);
|
|
877
1292
|
const paneWidth = Math.max(Math.floor((width - 3) / 2), 24);
|
|
878
1293
|
|
|
879
1294
|
if (mode === "error") {
|
|
@@ -899,49 +1314,77 @@ export function AethelTui({ drive, includeSharedDrives = false }) {
|
|
|
899
1314
|
);
|
|
900
1315
|
}
|
|
901
1316
|
|
|
902
|
-
|
|
903
|
-
|
|
1317
|
+
if (mode === "commands-page") {
|
|
1318
|
+
return h(
|
|
1319
|
+
Box,
|
|
1320
|
+
{ flexDirection: "column" },
|
|
1321
|
+
h(Text, { color: "cyan", bold: true }, truncate("Aethel Commands", width)),
|
|
1322
|
+
h(Text, { dimColor: true }, truncate(status, width)),
|
|
1323
|
+
renderCommandCatalog(width, height, commandCursor),
|
|
1324
|
+
h(
|
|
1325
|
+
Text,
|
|
1326
|
+
{ dimColor: true },
|
|
1327
|
+
truncate("j/k:move Enter:open ::custom command Esc:close", width)
|
|
1328
|
+
)
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
if (mode === "command-actions" && currentCatalogCommand) {
|
|
1333
|
+
return h(
|
|
1334
|
+
Box,
|
|
1335
|
+
{ flexDirection: "column" },
|
|
1336
|
+
h(Text, { color: "cyan", bold: true }, truncate("Aethel Commands", width)),
|
|
1337
|
+
h(Text, { dimColor: true }, truncate(status, width)),
|
|
1338
|
+
renderCommandActions(width, height, currentCatalogCommand, commandActionCursor),
|
|
1339
|
+
h(
|
|
1340
|
+
Text,
|
|
1341
|
+
{ dimColor: true },
|
|
1342
|
+
truncate("j/k:move Enter:run e:edit Esc:back", width)
|
|
1343
|
+
)
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
let hints;
|
|
904
1348
|
if (mode === "filter") {
|
|
905
|
-
hints = "Enter: apply
|
|
1349
|
+
hints = "Enter: apply Esc: cancel";
|
|
906
1350
|
} else if (mode === "confirm") {
|
|
907
1351
|
hints = "y: confirm n: cancel";
|
|
908
1352
|
} else if (mode === "upload") {
|
|
909
|
-
hints = "Enter: upload
|
|
1353
|
+
hints = "Enter: upload Esc: cancel";
|
|
1354
|
+
} else if (mode === "command") {
|
|
1355
|
+
hints = "Enter: run Esc: cancel";
|
|
910
1356
|
} else if (mode === "rename") {
|
|
911
1357
|
hints = "Enter: rename Esc: cancel";
|
|
1358
|
+
} else if (mode === "command-output") {
|
|
1359
|
+
hints = "j/k: scroll Enter/Esc: close";
|
|
1360
|
+
} else {
|
|
1361
|
+
hints = focusPane === "local"
|
|
1362
|
+
? "u:upload s:sync n:rename x:delete /:filter Tab:switch f:Commands ?:help q:quit"
|
|
1363
|
+
: "Space:select a:all t:trash d:delete /:filter Tab:switch f:Commands ?:help q:quit";
|
|
912
1364
|
}
|
|
913
1365
|
|
|
1366
|
+
const headerRight = selectedCount > 0
|
|
1367
|
+
? `${selectedCount} selected`
|
|
1368
|
+
: "";
|
|
1369
|
+
|
|
914
1370
|
return h(
|
|
915
1371
|
Box,
|
|
916
1372
|
{ 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
1373
|
h(
|
|
929
|
-
|
|
930
|
-
{
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
)
|
|
1374
|
+
Box,
|
|
1375
|
+
{ justifyContent: "space-between" },
|
|
1376
|
+
h(
|
|
1377
|
+
Text,
|
|
1378
|
+
{ color: "cyan", bold: true },
|
|
1379
|
+
truncate(
|
|
1380
|
+
account ? `Aethel ${account.email} ${account.usage}/${account.limit}` : "Aethel",
|
|
1381
|
+
width - headerRight.length - 2
|
|
1382
|
+
)
|
|
1383
|
+
),
|
|
1384
|
+
headerRight
|
|
1385
|
+
? h(Text, { color: "yellow" }, headerRight)
|
|
1386
|
+
: null
|
|
943
1387
|
),
|
|
944
|
-
h(Text, { dimColor: true }, "─".repeat(Math.max(Math.min(width, 80), 10))),
|
|
945
1388
|
h(
|
|
946
1389
|
Box,
|
|
947
1390
|
{ flexDirection: "row" },
|
|
@@ -989,7 +1432,7 @@ export function AethelTui({ drive, includeSharedDrives = false }) {
|
|
|
989
1432
|
? h(
|
|
990
1433
|
Box,
|
|
991
1434
|
{ flexDirection: "column" },
|
|
992
|
-
h(Text, { color: "cyan" }, "Local path to upload
|
|
1435
|
+
h(Text, { color: "cyan" }, "Local path to upload:"),
|
|
993
1436
|
h(TextInput, {
|
|
994
1437
|
value: inputValue,
|
|
995
1438
|
onChange: setInputValue,
|
|
@@ -1003,7 +1446,7 @@ export function AethelTui({ drive, includeSharedDrives = false }) {
|
|
|
1003
1446
|
? h(
|
|
1004
1447
|
Box,
|
|
1005
1448
|
{ flexDirection: "column" },
|
|
1006
|
-
h(Text, { color: "cyan" }, "New
|
|
1449
|
+
h(Text, { color: "cyan" }, "New name:"),
|
|
1007
1450
|
h(TextInput, {
|
|
1008
1451
|
value: inputValue,
|
|
1009
1452
|
onChange: setInputValue,
|
|
@@ -1019,7 +1462,24 @@ export function AethelTui({ drive, includeSharedDrives = false }) {
|
|
|
1019
1462
|
})
|
|
1020
1463
|
)
|
|
1021
1464
|
: null,
|
|
1465
|
+
mode === "command"
|
|
1466
|
+
? h(
|
|
1467
|
+
Box,
|
|
1468
|
+
{ flexDirection: "column" },
|
|
1469
|
+
h(Text, { color: "cyan" }, "aethel "),
|
|
1470
|
+
h(TextInput, {
|
|
1471
|
+
value: inputValue,
|
|
1472
|
+
onChange: setInputValue,
|
|
1473
|
+
onSubmit: (value) => {
|
|
1474
|
+
void executeCliCommand(value);
|
|
1475
|
+
},
|
|
1476
|
+
})
|
|
1477
|
+
)
|
|
1478
|
+
: null,
|
|
1022
1479
|
h(Text, { dimColor: true }, truncate(hints, width)),
|
|
1023
|
-
mode === "help" ? renderHelp() : null
|
|
1480
|
+
mode === "help" ? renderHelp() : null,
|
|
1481
|
+
mode === "command-output" && commandResult
|
|
1482
|
+
? renderCommandOutput(commandResult, width, height, commandScroll)
|
|
1483
|
+
: null
|
|
1024
1484
|
);
|
|
1025
1485
|
}
|