@arthony/keybook 0.6.0 → 0.7.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/dist/cli.js +127 -35
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { basename, dirname, join } from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import { Box, Text, render, useApp, useInput, useStdout } from "ink";
|
|
8
|
-
import { createElement, useCallback, useMemo, useState } from "react";
|
|
8
|
+
import { createElement, useCallback, useEffect, useMemo, useState } from "react";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { Document, isMap, isSeq, parse, parseDocument } from "yaml";
|
|
11
11
|
import { z } from "zod";
|
|
@@ -379,6 +379,23 @@ function editEntry(dir, file, index, entry, expectedAction) {
|
|
|
379
379
|
lines: [`✓ updated '${clean.action}'`]
|
|
380
380
|
};
|
|
381
381
|
}
|
|
382
|
+
function moveEntry(dir, sourceFile, index, expectedAction, targetApp, entry) {
|
|
383
|
+
if (resolveTargetFile(dir, targetApp).file === join(dir, sourceFile)) return editEntry(dir, sourceFile, index, entry, expectedAction);
|
|
384
|
+
const addRes = addEntry(dir, targetApp, entry);
|
|
385
|
+
if (!addRes.ok) return addRes;
|
|
386
|
+
if (!deleteEntry(dir, sourceFile, index, expectedAction).ok) return {
|
|
387
|
+
ok: true,
|
|
388
|
+
file: addRes.file,
|
|
389
|
+
created: addRes.created,
|
|
390
|
+
lines: [`⚠ moved to ${targetApp}; original still in ${sourceFile} — remove it manually`]
|
|
391
|
+
};
|
|
392
|
+
return {
|
|
393
|
+
ok: true,
|
|
394
|
+
file: addRes.file,
|
|
395
|
+
created: addRes.created,
|
|
396
|
+
lines: [`✓ moved '${entry.action}' → ${targetApp}`]
|
|
397
|
+
};
|
|
398
|
+
}
|
|
382
399
|
//#endregion
|
|
383
400
|
//#region src/commands.ts
|
|
384
401
|
function runPath(env = process.env) {
|
|
@@ -412,29 +429,40 @@ function runAdd(dir, draft) {
|
|
|
412
429
|
}
|
|
413
430
|
//#endregion
|
|
414
431
|
//#region src/tui/StepsBuilder.tsx
|
|
415
|
-
function StepsBuilder({ steps, line, active }) {
|
|
432
|
+
function StepsBuilder({ steps, line, active, cursor, grabbed = false }) {
|
|
433
|
+
const cur = cursor ?? steps.length;
|
|
434
|
+
const onAppendLine = cur >= steps.length;
|
|
435
|
+
const hint = grabbed ? "↑↓ move · Space/⏎ drop" : steps.length === 0 ? "⏎ adds a step · ⌫ on an empty line removes the last" : onAppendLine ? "⏎ adds a step · ↑ select a step to reorder" : "↑↓ select · Space grab · ⌫ delete";
|
|
416
436
|
return /* @__PURE__ */ jsxs(Box, {
|
|
417
437
|
flexDirection: "column",
|
|
418
438
|
children: [
|
|
419
|
-
steps.map((s, i) =>
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
439
|
+
steps.map((s, i) => {
|
|
440
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
441
|
+
active && cur === i ? grabbed ? "⇅ " : "> " : " ",
|
|
442
|
+
i + 1,
|
|
443
|
+
". ",
|
|
444
|
+
s
|
|
445
|
+
] }, i);
|
|
446
|
+
}),
|
|
424
447
|
/* @__PURE__ */ jsxs(Box, { children: [
|
|
425
448
|
/* @__PURE__ */ jsxs(Text, {
|
|
426
|
-
color: active ? "cyan" : "gray",
|
|
427
|
-
children: [
|
|
449
|
+
color: active && onAppendLine ? "cyan" : "gray",
|
|
450
|
+
children: [
|
|
451
|
+
" ",
|
|
452
|
+
steps.length + 1,
|
|
453
|
+
".",
|
|
454
|
+
" "
|
|
455
|
+
]
|
|
428
456
|
}),
|
|
429
457
|
/* @__PURE__ */ jsx(Text, { children: line }),
|
|
430
|
-
active ? /* @__PURE__ */ jsx(Text, {
|
|
458
|
+
active && onAppendLine ? /* @__PURE__ */ jsx(Text, {
|
|
431
459
|
inverse: true,
|
|
432
460
|
children: " "
|
|
433
461
|
}) : null
|
|
434
462
|
] }),
|
|
435
|
-
|
|
463
|
+
active ? /* @__PURE__ */ jsx(Text, {
|
|
436
464
|
color: "gray",
|
|
437
|
-
children:
|
|
465
|
+
children: hint
|
|
438
466
|
}) : null
|
|
439
467
|
]
|
|
440
468
|
});
|
|
@@ -459,7 +487,7 @@ function Field({ label, value, focused }) {
|
|
|
459
487
|
}) : null
|
|
460
488
|
] });
|
|
461
489
|
}
|
|
462
|
-
function FormFields({ draft, apps, appIndex, focused, existingTags,
|
|
490
|
+
function FormFields({ draft, apps, appIndex, focused, existingTags, stepCursor, grabbed }) {
|
|
463
491
|
const appChoices = [...apps, "Create new app…"];
|
|
464
492
|
return /* @__PURE__ */ jsxs(Box, {
|
|
465
493
|
flexDirection: "column",
|
|
@@ -467,10 +495,7 @@ function FormFields({ draft, apps, appIndex, focused, existingTags, lockedApp })
|
|
|
467
495
|
/* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
|
|
468
496
|
color: focused === 0 ? "cyan" : "gray",
|
|
469
497
|
children: "App".padEnd(8)
|
|
470
|
-
}),
|
|
471
|
-
color: "gray",
|
|
472
|
-
children: `${lockedApp} (locked)`
|
|
473
|
-
}) : draft.creatingApp ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Text, { children: draft.newApp }), focused === 0 ? /* @__PURE__ */ jsx(Text, {
|
|
498
|
+
}), draft.creatingApp ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Text, { children: draft.newApp }), focused === 0 ? /* @__PURE__ */ jsx(Text, {
|
|
474
499
|
inverse: true,
|
|
475
500
|
children: " "
|
|
476
501
|
}) : null] }) : /* @__PURE__ */ jsxs(Text, { children: [appChoices[appIndex] ?? "—", focused === 0 ? " (↑/↓)" : ""] })] }),
|
|
@@ -509,7 +534,9 @@ function FormFields({ draft, apps, appIndex, focused, existingTags, lockedApp })
|
|
|
509
534
|
}), /* @__PURE__ */ jsx(StepsBuilder, {
|
|
510
535
|
steps: draft.steps,
|
|
511
536
|
line: draft.stepLine,
|
|
512
|
-
active: focused === 3
|
|
537
|
+
active: focused === 3,
|
|
538
|
+
cursor: stepCursor,
|
|
539
|
+
grabbed
|
|
513
540
|
})]
|
|
514
541
|
}),
|
|
515
542
|
/* @__PURE__ */ jsx(Field, {
|
|
@@ -656,6 +683,20 @@ const emptyDraft = {
|
|
|
656
683
|
function parseTags(raw) {
|
|
657
684
|
return raw.split(",").map((t) => t.trim()).filter(Boolean);
|
|
658
685
|
}
|
|
686
|
+
/** Move the step at `from` to position `to`, returning a NEW array. No-op if out of range or from===to. */
|
|
687
|
+
function moveStep(steps, from, to) {
|
|
688
|
+
if (from < 0 || from >= steps.length || to < 0 || to >= steps.length || from === to) return steps;
|
|
689
|
+
const next = [...steps];
|
|
690
|
+
const [moved] = next.splice(from, 1);
|
|
691
|
+
if (moved === void 0) return steps;
|
|
692
|
+
next.splice(to, 0, moved);
|
|
693
|
+
return next;
|
|
694
|
+
}
|
|
695
|
+
/** Remove the step at `i`, returning a NEW array. No-op if out of range. */
|
|
696
|
+
function deleteStep(steps, i) {
|
|
697
|
+
if (i < 0 || i >= steps.length) return steps;
|
|
698
|
+
return steps.filter((_, idx) => idx !== i);
|
|
699
|
+
}
|
|
659
700
|
function resolvedApp(d) {
|
|
660
701
|
return (d.creatingApp ? d.newApp : d.app).trim();
|
|
661
702
|
}
|
|
@@ -727,13 +768,28 @@ const TYPES = [
|
|
|
727
768
|
"recipe"
|
|
728
769
|
];
|
|
729
770
|
const LAST_FIELD = 5;
|
|
730
|
-
function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, resolveTarget, initial,
|
|
771
|
+
function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, resolveTarget, initial, initialFocus, title }) {
|
|
731
772
|
const { draft, update, setDraft } = useAddForm(initial ?? { app: apps[0] ?? "" });
|
|
732
|
-
const [focused, setFocused] = useState(
|
|
733
|
-
const [
|
|
773
|
+
const [focused, setFocused] = useState(initialFocus ?? 0);
|
|
774
|
+
const [stepCursor, setStepCursor] = useState(initial?.steps?.length ?? 0);
|
|
775
|
+
const [grabbed, setGrabbed] = useState(false);
|
|
776
|
+
const [appIndex, setAppIndex] = useState(() => {
|
|
777
|
+
const i = apps.indexOf(initial?.app ?? apps[0] ?? "");
|
|
778
|
+
return i >= 0 ? i : 0;
|
|
779
|
+
});
|
|
734
780
|
const [screen, setScreen] = useState("form");
|
|
735
781
|
const [hint, setHint] = useState("");
|
|
736
782
|
const [writeError, setWriteError] = useState("");
|
|
783
|
+
useEffect(() => {
|
|
784
|
+
if (!(focused === 3 && draft.type === "recipe")) {
|
|
785
|
+
setGrabbed(false);
|
|
786
|
+
setStepCursor(draft.steps.length);
|
|
787
|
+
}
|
|
788
|
+
}, [
|
|
789
|
+
focused,
|
|
790
|
+
draft.type,
|
|
791
|
+
draft.steps.length
|
|
792
|
+
]);
|
|
737
793
|
const appChoices = [...apps, "Create new app…"];
|
|
738
794
|
function commitAppSelection(index) {
|
|
739
795
|
if (index === apps.length) update({ creatingApp: true });
|
|
@@ -767,9 +823,10 @@ function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, reso
|
|
|
767
823
|
}
|
|
768
824
|
return;
|
|
769
825
|
}
|
|
826
|
+
if (grabbed && key.escape) return setGrabbed(false);
|
|
770
827
|
if (key.escape) return onCancel();
|
|
771
828
|
if (key.ctrl && input === "n") return setFocused((f) => Math.min(f + 1, LAST_FIELD));
|
|
772
|
-
if (key.ctrl && input === "p") return setFocused((f) => Math.max(f - 1,
|
|
829
|
+
if (key.ctrl && input === "p") return setFocused((f) => Math.max(f - 1, 0));
|
|
773
830
|
if (focused === 0) {
|
|
774
831
|
if (key.upArrow) {
|
|
775
832
|
const next = Math.max(appIndex - 1, 0);
|
|
@@ -799,18 +856,51 @@ function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, reso
|
|
|
799
856
|
return;
|
|
800
857
|
}
|
|
801
858
|
if (focused === 3 && draft.type === "recipe") {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
859
|
+
const steps = draft.steps;
|
|
860
|
+
const onAppendLine = stepCursor >= steps.length;
|
|
861
|
+
if (grabbed) {
|
|
862
|
+
if (key.upArrow && stepCursor > 0) {
|
|
863
|
+
update({ steps: moveStep(steps, stepCursor, stepCursor - 1) });
|
|
864
|
+
return setStepCursor(stepCursor - 1);
|
|
865
|
+
}
|
|
866
|
+
if (key.downArrow && stepCursor < steps.length - 1) {
|
|
867
|
+
update({ steps: moveStep(steps, stepCursor, stepCursor + 1) });
|
|
868
|
+
return setStepCursor(stepCursor + 1);
|
|
869
|
+
}
|
|
870
|
+
if (key.return || input === " " || key.escape) return setGrabbed(false);
|
|
807
871
|
return;
|
|
808
872
|
}
|
|
873
|
+
if (onAppendLine) {
|
|
874
|
+
if (key.return) {
|
|
875
|
+
if (draft.stepLine.trim()) {
|
|
876
|
+
update({
|
|
877
|
+
steps: [...steps, draft.stepLine.trim()],
|
|
878
|
+
stepLine: ""
|
|
879
|
+
});
|
|
880
|
+
setStepCursor(steps.length + 1);
|
|
881
|
+
}
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
if (key.backspace || key.delete) {
|
|
885
|
+
if (draft.stepLine) return update({ stepLine: draft.stepLine.slice(0, -1) });
|
|
886
|
+
if (steps.length) return setStepCursor(steps.length - 1);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (key.upArrow) {
|
|
890
|
+
if (steps.length) return setStepCursor(steps.length - 1);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
if (input && !key.ctrl && !key.meta) return update({ stepLine: draft.stepLine + input });
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (key.upArrow) return setStepCursor(Math.max(0, stepCursor - 1));
|
|
897
|
+
if (key.downArrow) return setStepCursor(stepCursor + 1);
|
|
898
|
+
if (key.return || input === " ") return setGrabbed(true);
|
|
809
899
|
if (key.backspace || key.delete) {
|
|
810
|
-
|
|
811
|
-
|
|
900
|
+
const next = deleteStep(steps, stepCursor);
|
|
901
|
+
update({ steps: next });
|
|
902
|
+
return setStepCursor(Math.min(stepCursor, next.length));
|
|
812
903
|
}
|
|
813
|
-
if (input && !key.ctrl && !key.meta) return update({ stepLine: draft.stepLine + input });
|
|
814
904
|
return;
|
|
815
905
|
}
|
|
816
906
|
if (key.return) return goReview();
|
|
@@ -840,7 +930,8 @@ function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, reso
|
|
|
840
930
|
appIndex,
|
|
841
931
|
focused,
|
|
842
932
|
existingTags,
|
|
843
|
-
|
|
933
|
+
stepCursor,
|
|
934
|
+
grabbed
|
|
844
935
|
})
|
|
845
936
|
}),
|
|
846
937
|
/* @__PURE__ */ jsxs(Box, {
|
|
@@ -1223,6 +1314,7 @@ function columnWidths(terminalCols) {
|
|
|
1223
1314
|
}
|
|
1224
1315
|
//#endregion
|
|
1225
1316
|
//#region src/tui/App.tsx
|
|
1317
|
+
const sameApp = (a, b) => a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
1226
1318
|
function App({ entries: initial, errorCount = 0, dataDir, onCopy = copyToClipboard }) {
|
|
1227
1319
|
const { exit } = useApp();
|
|
1228
1320
|
const { stdout } = useStdout();
|
|
@@ -1364,14 +1456,14 @@ function App({ entries: initial, errorCount = 0, dataDir, onCopy = copyToClipboa
|
|
|
1364
1456
|
if (mode === "edit" && dataDir && editTarget) return /* @__PURE__ */ jsx(AddEntryForm, {
|
|
1365
1457
|
apps: listApps(dataDir),
|
|
1366
1458
|
existingTags,
|
|
1367
|
-
|
|
1459
|
+
initialFocus: 1,
|
|
1368
1460
|
initial: entryToDraft(editTarget.app, editTarget),
|
|
1369
1461
|
title: `Edit entry — ${editTarget.app}`,
|
|
1370
|
-
resolveTarget: () => ({
|
|
1462
|
+
resolveTarget: (app) => sameApp(app, editTarget.app) ? {
|
|
1371
1463
|
file: editTarget.file,
|
|
1372
1464
|
created: false
|
|
1373
|
-
}),
|
|
1374
|
-
onSubmit: (
|
|
1465
|
+
} : resolveTargetFile(dataDir, app),
|
|
1466
|
+
onSubmit: (app, entry) => sameApp(app, editTarget.app) ? editEntry(dataDir, editTarget.file, editTarget.index, entry, editTarget.action) : moveEntry(dataDir, editTarget.file, editTarget.index, editTarget.action, app, entry),
|
|
1375
1467
|
onComplete: (result) => {
|
|
1376
1468
|
if (result.ok) {
|
|
1377
1469
|
reload();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arthony/keybook",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"test:watch": "vitest",
|
|
66
66
|
"typecheck": "tsc --noEmit",
|
|
67
67
|
"lint": "biome check .",
|
|
68
|
-
"format": "biome format --write ."
|
|
68
|
+
"format": "biome format --write .",
|
|
69
|
+
"bump-tap": "./scripts/bump-tap.sh"
|
|
69
70
|
}
|
|
70
71
|
}
|