@arthony/keybook 0.5.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 +355 -70
- package/package.json +33 -20
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
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, {
|
|
@@ -868,6 +959,52 @@ function copyToClipboard(text) {
|
|
|
868
959
|
}
|
|
869
960
|
}
|
|
870
961
|
//#endregion
|
|
962
|
+
//#region src/data/favorites.ts
|
|
963
|
+
const SEP = "\0";
|
|
964
|
+
function favKey(app, action) {
|
|
965
|
+
return `${app}${SEP}${action}`;
|
|
966
|
+
}
|
|
967
|
+
function favoritesPath(dir) {
|
|
968
|
+
return join(dir, "favorites.json");
|
|
969
|
+
}
|
|
970
|
+
/** Read favorites.json -> Set of favKeys. Missing/malformed -> empty set (never throws). */
|
|
971
|
+
function loadFavorites(dir) {
|
|
972
|
+
const set = /* @__PURE__ */ new Set();
|
|
973
|
+
try {
|
|
974
|
+
const data = JSON.parse(readFileSync(favoritesPath(dir), "utf8"));
|
|
975
|
+
const list = Array.isArray(data?.favorites) ? data.favorites : [];
|
|
976
|
+
for (const f of list) if (f && typeof f.app === "string" && typeof f.action === "string") set.add(favKey(f.app, f.action));
|
|
977
|
+
} catch {}
|
|
978
|
+
return set;
|
|
979
|
+
}
|
|
980
|
+
/** Atomic write (temp + rename) of the favKey set back to favorites.json. */
|
|
981
|
+
function saveFavorites(dir, keys) {
|
|
982
|
+
const data = {
|
|
983
|
+
version: 1,
|
|
984
|
+
favorites: [...keys].map((k) => {
|
|
985
|
+
const i = k.indexOf(SEP);
|
|
986
|
+
return {
|
|
987
|
+
app: k.slice(0, i),
|
|
988
|
+
action: k.slice(i + 1)
|
|
989
|
+
};
|
|
990
|
+
})
|
|
991
|
+
};
|
|
992
|
+
const path = favoritesPath(dir);
|
|
993
|
+
const tmp = `${path}.tmp`;
|
|
994
|
+
writeFileSync(tmp, `${JSON.stringify(data, null, 2)}
|
|
995
|
+
`, "utf8");
|
|
996
|
+
renameSync(tmp, path);
|
|
997
|
+
}
|
|
998
|
+
/** Toggle one favorite, persist, and return the new set. */
|
|
999
|
+
function toggleFavorite(dir, app, action) {
|
|
1000
|
+
const keys = loadFavorites(dir);
|
|
1001
|
+
const k = favKey(app, action);
|
|
1002
|
+
if (keys.has(k)) keys.delete(k);
|
|
1003
|
+
else keys.add(k);
|
|
1004
|
+
saveFavorites(dir, keys);
|
|
1005
|
+
return keys;
|
|
1006
|
+
}
|
|
1007
|
+
//#endregion
|
|
871
1008
|
//#region src/search.ts
|
|
872
1009
|
function haystack(e) {
|
|
873
1010
|
return [
|
|
@@ -888,8 +1025,109 @@ function search(entries, query) {
|
|
|
888
1025
|
}).find(q).map((r) => r.item);
|
|
889
1026
|
}
|
|
890
1027
|
//#endregion
|
|
1028
|
+
//#region src/tui/FilterPicker.tsx
|
|
1029
|
+
function FilterPicker({ apps, onSelect, onCancel, height = 16, width = 40 }) {
|
|
1030
|
+
const [query, setQuery] = useState("");
|
|
1031
|
+
const [selected, setSelected] = useState(0);
|
|
1032
|
+
const matchedApps = useMemo(() => {
|
|
1033
|
+
const q = query.trim().toLowerCase();
|
|
1034
|
+
return q ? apps.filter((a) => a.toLowerCase().includes(q)) : apps;
|
|
1035
|
+
}, [apps, query]);
|
|
1036
|
+
const total = 2 + matchedApps.length;
|
|
1037
|
+
const sel = Math.min(selected, Math.max(0, total - 1));
|
|
1038
|
+
useInput((input, key) => {
|
|
1039
|
+
if (key.escape || key.ctrl && input === "f") return onCancel();
|
|
1040
|
+
if (key.downArrow || key.ctrl && input === "n") {
|
|
1041
|
+
setSelected((s) => Math.min(s + 1, total - 1));
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
if (key.upArrow || key.ctrl && input === "p") {
|
|
1045
|
+
setSelected((s) => Math.max(s - 1, 0));
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (key.return) {
|
|
1049
|
+
if (sel === 0) return onSelect({ type: "favorites" });
|
|
1050
|
+
if (sel === 1) return onSelect({ type: "all" });
|
|
1051
|
+
const app = matchedApps[sel - 2];
|
|
1052
|
+
if (app) return onSelect({
|
|
1053
|
+
type: "app",
|
|
1054
|
+
app
|
|
1055
|
+
});
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
if (key.backspace || key.delete) {
|
|
1059
|
+
setQuery((q) => q.slice(0, -1));
|
|
1060
|
+
setSelected(0);
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
if (input && !key.ctrl && !key.meta) {
|
|
1064
|
+
setQuery((q) => q + input);
|
|
1065
|
+
setSelected(2);
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
const bodyHeight = Math.max(1, height - 2);
|
|
1069
|
+
const appSel = Math.max(0, sel - 2);
|
|
1070
|
+
const startApp = matchedApps.length > bodyHeight ? Math.max(0, Math.min(appSel - Math.floor(bodyHeight / 2), matchedApps.length - bodyHeight)) : 0;
|
|
1071
|
+
const visibleApps = matchedApps.slice(startApp, startApp + bodyHeight);
|
|
1072
|
+
const row = (text, idx) => {
|
|
1073
|
+
const isSel = idx === sel;
|
|
1074
|
+
return /* @__PURE__ */ jsxs(Text, {
|
|
1075
|
+
wrap: "truncate-end",
|
|
1076
|
+
color: isSel ? "cyan" : void 0,
|
|
1077
|
+
inverse: isSel,
|
|
1078
|
+
children: [isSel ? "▸ " : " ", text]
|
|
1079
|
+
}, `${idx}:${text}`);
|
|
1080
|
+
};
|
|
1081
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
1082
|
+
flexDirection: "column",
|
|
1083
|
+
borderStyle: "round",
|
|
1084
|
+
paddingX: 1,
|
|
1085
|
+
width,
|
|
1086
|
+
children: [
|
|
1087
|
+
/* @__PURE__ */ jsx(Text, {
|
|
1088
|
+
wrap: "truncate-end",
|
|
1089
|
+
color: "cyan",
|
|
1090
|
+
children: "Filter by app"
|
|
1091
|
+
}),
|
|
1092
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
1093
|
+
wrap: "truncate-end",
|
|
1094
|
+
children: [query ? query : /* @__PURE__ */ jsx(Text, {
|
|
1095
|
+
color: "gray",
|
|
1096
|
+
children: "type to filter…"
|
|
1097
|
+
}), /* @__PURE__ */ jsx(Text, {
|
|
1098
|
+
inverse: true,
|
|
1099
|
+
children: " "
|
|
1100
|
+
})]
|
|
1101
|
+
}),
|
|
1102
|
+
row("★ Favorites", 0),
|
|
1103
|
+
row("All apps", 1),
|
|
1104
|
+
/* @__PURE__ */ jsx(Text, {
|
|
1105
|
+
wrap: "truncate-end",
|
|
1106
|
+
color: "gray",
|
|
1107
|
+
children: "──────────────"
|
|
1108
|
+
}),
|
|
1109
|
+
startApp > 0 ? /* @__PURE__ */ jsxs(Text, {
|
|
1110
|
+
wrap: "truncate-end",
|
|
1111
|
+
color: "gray",
|
|
1112
|
+
children: [" ", "↑ more"]
|
|
1113
|
+
}) : null,
|
|
1114
|
+
visibleApps.map((app, i) => row(app, startApp + i + 2)),
|
|
1115
|
+
startApp + bodyHeight < matchedApps.length ? /* @__PURE__ */ jsxs(Text, {
|
|
1116
|
+
wrap: "truncate-end",
|
|
1117
|
+
color: "gray",
|
|
1118
|
+
children: [" ", "↓ more"]
|
|
1119
|
+
}) : null,
|
|
1120
|
+
/* @__PURE__ */ jsx(Text, {
|
|
1121
|
+
wrap: "truncate-end",
|
|
1122
|
+
color: "gray",
|
|
1123
|
+
children: "↑↓ move ⏎ select ⎋ cancel"
|
|
1124
|
+
})
|
|
1125
|
+
]
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
//#endregion
|
|
891
1129
|
//#region src/tui/Footer.tsx
|
|
892
|
-
function Footer({ flash, errorCount, resultCount, confirm }) {
|
|
1130
|
+
function Footer({ flash, errorCount, resultCount, confirm, filterActive = false }) {
|
|
893
1131
|
if (confirm) return /* @__PURE__ */ jsxs(Box, {
|
|
894
1132
|
marginTop: 1,
|
|
895
1133
|
justifyContent: "space-between",
|
|
@@ -906,19 +1144,17 @@ function Footer({ flash, errorCount, resultCount, confirm }) {
|
|
|
906
1144
|
return /* @__PURE__ */ jsxs(Box, {
|
|
907
1145
|
marginTop: 1,
|
|
908
1146
|
justifyContent: "space-between",
|
|
909
|
-
children: [/* @__PURE__ */
|
|
1147
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
910
1148
|
color: "gray",
|
|
911
1149
|
wrap: "truncate-end",
|
|
912
|
-
children:
|
|
913
|
-
"↑↓ move ⏎ copy ⌃O add ⌃E edit ⌃X del ⎋ quit ⌃U clear (",
|
|
914
|
-
resultCount,
|
|
915
|
-
")"
|
|
916
|
-
]
|
|
1150
|
+
children: `↑↓ move ⏎ copy ⌃O add ⌃E edit ⌃X del ⌃F filter ⌃S star ${filterActive ? "⎋ clear filter" : "⎋ quit"} ⌃U clear (${resultCount})`
|
|
917
1151
|
}), flash ? /* @__PURE__ */ jsx(Text, {
|
|
918
1152
|
color: "green",
|
|
1153
|
+
wrap: "truncate-end",
|
|
919
1154
|
children: flash
|
|
920
1155
|
}) : errorCount > 0 ? /* @__PURE__ */ jsxs(Text, {
|
|
921
1156
|
color: "yellow",
|
|
1157
|
+
wrap: "truncate-end",
|
|
922
1158
|
children: [
|
|
923
1159
|
"⚠ ",
|
|
924
1160
|
errorCount,
|
|
@@ -973,7 +1209,7 @@ function PreviewPane({ entry, width = "50%" }) {
|
|
|
973
1209
|
}
|
|
974
1210
|
//#endregion
|
|
975
1211
|
//#region src/tui/ResultRow.tsx
|
|
976
|
-
function ResultRow({ entry, selected }) {
|
|
1212
|
+
function ResultRow({ entry, selected, favorite = false }) {
|
|
977
1213
|
const right = entry.keys ?? "recipe";
|
|
978
1214
|
return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, {
|
|
979
1215
|
wrap: "truncate-end",
|
|
@@ -982,6 +1218,7 @@ function ResultRow({ entry, selected }) {
|
|
|
982
1218
|
inverse: selected,
|
|
983
1219
|
children: [
|
|
984
1220
|
selected ? "▸ " : " ",
|
|
1221
|
+
favorite ? "★ " : " ",
|
|
985
1222
|
entry.app,
|
|
986
1223
|
" · ",
|
|
987
1224
|
entry.action
|
|
@@ -994,17 +1231,13 @@ function ResultRow({ entry, selected }) {
|
|
|
994
1231
|
}
|
|
995
1232
|
//#endregion
|
|
996
1233
|
//#region src/tui/ResultList.tsx
|
|
997
|
-
function ResultList({ results, selected, query, height = 12, width = "50%" }) {
|
|
1234
|
+
function ResultList({ results, selected, query, height = 12, width = "50%", favorites, emptyMessage }) {
|
|
998
1235
|
if (results.length === 0) return /* @__PURE__ */ jsx(Box, {
|
|
999
1236
|
width,
|
|
1000
1237
|
justifyContent: "center",
|
|
1001
|
-
children: /* @__PURE__ */
|
|
1238
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
1002
1239
|
color: "gray",
|
|
1003
|
-
children:
|
|
1004
|
-
"No matches for \"",
|
|
1005
|
-
query,
|
|
1006
|
-
"\""
|
|
1007
|
-
]
|
|
1240
|
+
children: emptyMessage ?? `No matches for "${query}"`
|
|
1008
1241
|
})
|
|
1009
1242
|
});
|
|
1010
1243
|
const start = Math.max(0, Math.min(selected - Math.floor(height / 2), results.length - height));
|
|
@@ -1013,27 +1246,35 @@ function ResultList({ results, selected, query, height = 12, width = "50%" }) {
|
|
|
1013
1246
|
width,
|
|
1014
1247
|
children: results.slice(Math.max(0, start), Math.max(0, start) + height).map((e, i) => /* @__PURE__ */ jsx(ResultRow, {
|
|
1015
1248
|
entry: e,
|
|
1016
|
-
selected: Math.max(0, start) + i === selected
|
|
1249
|
+
selected: Math.max(0, start) + i === selected,
|
|
1250
|
+
favorite: favorites?.has(favKey(e.app, e.action)) ?? false
|
|
1017
1251
|
}, `${e.app}:${e.action}`))
|
|
1018
1252
|
});
|
|
1019
1253
|
}
|
|
1020
1254
|
//#endregion
|
|
1021
1255
|
//#region src/tui/SearchInput.tsx
|
|
1022
|
-
function SearchInput({ query }) {
|
|
1023
|
-
return /* @__PURE__ */
|
|
1024
|
-
|
|
1025
|
-
children: [
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1256
|
+
function SearchInput({ query, filterLabel }) {
|
|
1257
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
1258
|
+
justifyContent: "space-between",
|
|
1259
|
+
children: [/* @__PURE__ */ jsxs(Text, {
|
|
1260
|
+
wrap: "truncate-end",
|
|
1261
|
+
children: [
|
|
1262
|
+
/* @__PURE__ */ jsx(Text, {
|
|
1263
|
+
color: "cyan",
|
|
1264
|
+
children: "search: "
|
|
1265
|
+
}),
|
|
1266
|
+
/* @__PURE__ */ jsx(Text, { children: query }),
|
|
1267
|
+
/* @__PURE__ */ jsx(Text, {
|
|
1268
|
+
inverse: true,
|
|
1269
|
+
children: " "
|
|
1270
|
+
})
|
|
1271
|
+
]
|
|
1272
|
+
}), filterLabel ? /* @__PURE__ */ jsx(Text, {
|
|
1273
|
+
color: "gray",
|
|
1274
|
+
wrap: "truncate-end",
|
|
1275
|
+
children: filterLabel
|
|
1276
|
+
}) : null]
|
|
1277
|
+
});
|
|
1037
1278
|
}
|
|
1038
1279
|
//#endregion
|
|
1039
1280
|
//#region src/tui/input.ts
|
|
@@ -1073,11 +1314,14 @@ function columnWidths(terminalCols) {
|
|
|
1073
1314
|
}
|
|
1074
1315
|
//#endregion
|
|
1075
1316
|
//#region src/tui/App.tsx
|
|
1317
|
+
const sameApp = (a, b) => a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
1076
1318
|
function App({ entries: initial, errorCount = 0, dataDir, onCopy = copyToClipboard }) {
|
|
1077
1319
|
const { exit } = useApp();
|
|
1078
1320
|
const { stdout } = useStdout();
|
|
1079
1321
|
const [entries, setEntries] = useState(initial);
|
|
1080
1322
|
const [mode, setMode] = useState("search");
|
|
1323
|
+
const [filter, setFilter] = useState({ type: "all" });
|
|
1324
|
+
const [favorites, setFavorites] = useState(() => dataDir ? loadFavorites(dataDir) : /* @__PURE__ */ new Set());
|
|
1081
1325
|
const [editTarget, setEditTarget] = useState(null);
|
|
1082
1326
|
const [pendingDelete, setPendingDelete] = useState(null);
|
|
1083
1327
|
const [query, setQuery] = useState("");
|
|
@@ -1086,7 +1330,16 @@ function App({ entries: initial, errorCount = 0, dataDir, onCopy = copyToClipboa
|
|
|
1086
1330
|
const reload = useCallback(() => {
|
|
1087
1331
|
if (dataDir) setEntries(loadEntries(dataDir).entries);
|
|
1088
1332
|
}, [dataDir]);
|
|
1089
|
-
const
|
|
1333
|
+
const scoped = useMemo(() => {
|
|
1334
|
+
if (filter.type === "app") return entries.filter((e) => e.app === filter.app);
|
|
1335
|
+
if (filter.type === "favorites") return entries.filter((e) => favorites.has(favKey(e.app, e.action)));
|
|
1336
|
+
return entries;
|
|
1337
|
+
}, [
|
|
1338
|
+
entries,
|
|
1339
|
+
filter,
|
|
1340
|
+
favorites
|
|
1341
|
+
]);
|
|
1342
|
+
const results = useMemo(() => search(scoped, query), [scoped, query]);
|
|
1090
1343
|
const sel = results.length ? Math.min(selected, results.length - 1) : 0;
|
|
1091
1344
|
const current = results[sel];
|
|
1092
1345
|
const listHeight = visibleListHeight(stdout?.rows);
|
|
@@ -1110,6 +1363,11 @@ function App({ entries: initial, errorCount = 0, dataDir, onCopy = copyToClipboa
|
|
|
1110
1363
|
}
|
|
1111
1364
|
if (!key.return && flash) setFlash("");
|
|
1112
1365
|
if (key.escape) {
|
|
1366
|
+
if (filter.type !== "all") {
|
|
1367
|
+
setFilter({ type: "all" });
|
|
1368
|
+
setSelected(0);
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1113
1371
|
exit();
|
|
1114
1372
|
return;
|
|
1115
1373
|
}
|
|
@@ -1126,6 +1384,16 @@ function App({ entries: initial, errorCount = 0, dataDir, onCopy = copyToClipboa
|
|
|
1126
1384
|
setPendingDelete(current);
|
|
1127
1385
|
return;
|
|
1128
1386
|
}
|
|
1387
|
+
if (key.ctrl && input === "f") {
|
|
1388
|
+
setMode("filter");
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
if (dataDir && current && key.ctrl && input === "s") {
|
|
1392
|
+
const next = toggleFavorite(dataDir, current.app, current.action);
|
|
1393
|
+
setFavorites(next);
|
|
1394
|
+
setFlash(next.has(favKey(current.app, current.action)) ? "★ starred" : "☆ unstarred");
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1129
1397
|
if (key.downArrow || key.ctrl && input === "n") {
|
|
1130
1398
|
setSelected((s) => Math.min(s + 1, results.length - 1));
|
|
1131
1399
|
return;
|
|
@@ -1159,6 +1427,17 @@ function App({ entries: initial, errorCount = 0, dataDir, onCopy = copyToClipboa
|
|
|
1159
1427
|
}
|
|
1160
1428
|
}, { isActive: mode === "search" });
|
|
1161
1429
|
const existingTags = [...new Set(entries.flatMap((e) => e.tags ?? []))].sort();
|
|
1430
|
+
if (mode === "filter") return /* @__PURE__ */ jsx(FilterPicker, {
|
|
1431
|
+
apps: [...new Set(entries.map((e) => e.app))].sort(),
|
|
1432
|
+
height: listHeight,
|
|
1433
|
+
width: left,
|
|
1434
|
+
onSelect: (f) => {
|
|
1435
|
+
setFilter(f);
|
|
1436
|
+
setSelected(0);
|
|
1437
|
+
setMode("search");
|
|
1438
|
+
},
|
|
1439
|
+
onCancel: () => setMode("search")
|
|
1440
|
+
});
|
|
1162
1441
|
if (mode === "add" && dataDir) return /* @__PURE__ */ jsx(AddEntryForm, {
|
|
1163
1442
|
apps: listApps(dataDir),
|
|
1164
1443
|
existingTags,
|
|
@@ -1177,14 +1456,14 @@ function App({ entries: initial, errorCount = 0, dataDir, onCopy = copyToClipboa
|
|
|
1177
1456
|
if (mode === "edit" && dataDir && editTarget) return /* @__PURE__ */ jsx(AddEntryForm, {
|
|
1178
1457
|
apps: listApps(dataDir),
|
|
1179
1458
|
existingTags,
|
|
1180
|
-
|
|
1459
|
+
initialFocus: 1,
|
|
1181
1460
|
initial: entryToDraft(editTarget.app, editTarget),
|
|
1182
1461
|
title: `Edit entry — ${editTarget.app}`,
|
|
1183
|
-
resolveTarget: () => ({
|
|
1462
|
+
resolveTarget: (app) => sameApp(app, editTarget.app) ? {
|
|
1184
1463
|
file: editTarget.file,
|
|
1185
1464
|
created: false
|
|
1186
|
-
}),
|
|
1187
|
-
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),
|
|
1188
1467
|
onComplete: (result) => {
|
|
1189
1468
|
if (result.ok) {
|
|
1190
1469
|
reload();
|
|
@@ -1202,13 +1481,18 @@ function App({ entries: initial, errorCount = 0, dataDir, onCopy = copyToClipboa
|
|
|
1202
1481
|
return /* @__PURE__ */ jsxs(Box, {
|
|
1203
1482
|
flexDirection: "column",
|
|
1204
1483
|
children: [
|
|
1205
|
-
/* @__PURE__ */ jsx(SearchInput, {
|
|
1484
|
+
/* @__PURE__ */ jsx(SearchInput, {
|
|
1485
|
+
query,
|
|
1486
|
+
filterLabel: filter.type === "app" ? `(filter: ${filter.app})` : filter.type === "favorites" ? "(★ Favorites)" : void 0
|
|
1487
|
+
}),
|
|
1206
1488
|
/* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(ResultList, {
|
|
1207
1489
|
results,
|
|
1208
1490
|
selected: sel,
|
|
1209
1491
|
query,
|
|
1210
1492
|
height: listHeight,
|
|
1211
|
-
width: left
|
|
1493
|
+
width: left,
|
|
1494
|
+
favorites,
|
|
1495
|
+
emptyMessage: filter.type === "favorites" ? "No favorites yet — ⌃S stars an entry." : void 0
|
|
1212
1496
|
}), /* @__PURE__ */ jsx(PreviewPane, {
|
|
1213
1497
|
entry: current,
|
|
1214
1498
|
width: right
|
|
@@ -1217,6 +1501,7 @@ function App({ entries: initial, errorCount = 0, dataDir, onCopy = copyToClipboa
|
|
|
1217
1501
|
flash,
|
|
1218
1502
|
errorCount,
|
|
1219
1503
|
resultCount: results.length,
|
|
1504
|
+
filterActive: filter.type !== "all",
|
|
1220
1505
|
confirm: pendingDelete ? `Delete '${pendingDelete.app}: ${pendingDelete.action}'?` : void 0
|
|
1221
1506
|
})
|
|
1222
1507
|
]
|
package/package.json
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arthony/keybook",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"publishConfig": {
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
5
7
|
"description": "A macOS TUI for searching keyboard shortcuts and recipes of your favorite apps",
|
|
6
8
|
"keywords": [
|
|
7
9
|
"cli",
|
|
@@ -15,22 +17,27 @@
|
|
|
15
17
|
"productivity"
|
|
16
18
|
],
|
|
17
19
|
"homepage": "https://github.com/Ariyapong/keybook#readme",
|
|
18
|
-
"repository": {
|
|
19
|
-
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/Ariyapong/keybook.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/Ariyapong/keybook/issues"
|
|
26
|
+
},
|
|
20
27
|
"author": "Ariyapong Wimolnoch",
|
|
21
28
|
"type": "module",
|
|
22
|
-
"bin": {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
29
|
+
"bin": {
|
|
30
|
+
"keybook": "dist/cli.js",
|
|
31
|
+
"kb": "dist/cli.js"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"seed",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=22"
|
|
34
41
|
},
|
|
35
42
|
"dependencies": {
|
|
36
43
|
"commander": "^12.1.0",
|
|
@@ -51,8 +58,14 @@
|
|
|
51
58
|
"vitest": "^2.1.0"
|
|
52
59
|
},
|
|
53
60
|
"license": "MIT",
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
"
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "tsdown",
|
|
63
|
+
"dev": "tsx src/cli.ts",
|
|
64
|
+
"test": "vitest run",
|
|
65
|
+
"test:watch": "vitest",
|
|
66
|
+
"typecheck": "tsc --noEmit",
|
|
67
|
+
"lint": "biome check .",
|
|
68
|
+
"format": "biome format --write .",
|
|
69
|
+
"bump-tap": "./scripts/bump-tap.sh"
|
|
57
70
|
}
|
|
58
|
-
}
|
|
71
|
+
}
|