@arthony/keybook 0.8.1 → 0.9.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/README.md +3 -2
- package/dist/cli.js +83 -8
- package/package.json +33 -21
package/README.md
CHANGED
|
@@ -44,8 +44,9 @@ In the TUI: type to fuzzy-search, ↑/↓ to move, ⏎ to copy the shortcut (or
|
|
|
44
44
|
recipe's command) to the clipboard, ⎋ to quit.
|
|
45
45
|
|
|
46
46
|
Manage entries without leaving the search screen: `⌃O` to add, `⌃E` to edit the
|
|
47
|
-
selected entry (pre-filled form), `⌃X` to delete it (with a `y/n` confirm).
|
|
48
|
-
|
|
47
|
+
selected entry (pre-filled form), `⌃X` to delete it (with a `y/n` confirm). In
|
|
48
|
+
the form, the App picker jumps as you type (`vs` lands on VS Code; `↑`/`↓` still
|
|
49
|
+
step through it). Or script an add: `keybook add --app Fork --action 'Push' --keys 'shift cmd p' --tags push`
|
|
49
50
|
(`--keys` accepts glyphs or words; recipes use repeatable `--step`).
|
|
50
51
|
|
|
51
52
|
### Text editing
|
package/dist/cli.js
CHANGED
|
@@ -553,7 +553,7 @@ function Field({ label, value, focused, pos, width }) {
|
|
|
553
553
|
focused
|
|
554
554
|
})] });
|
|
555
555
|
}
|
|
556
|
-
function FormFields({ draft, apps, appIndex, focused, pos, width, existingTags, stepCursor, grabbed, editingStep, editBuffer }) {
|
|
556
|
+
function FormFields({ draft, apps, appIndex, appPrefix = "", focused, pos, width, existingTags, stepCursor, grabbed, editingStep, editBuffer }) {
|
|
557
557
|
const appChoices = [...apps, "Create new app…"];
|
|
558
558
|
const keysPreview = draft.keys.trim() ? ` → ${normalizeKeys(draft.keys)}` : "";
|
|
559
559
|
return /* @__PURE__ */ jsxs(Box, {
|
|
@@ -567,7 +567,7 @@ function FormFields({ draft, apps, appIndex, focused, pos, width, existingTags,
|
|
|
567
567
|
pos,
|
|
568
568
|
width,
|
|
569
569
|
focused: focused === 0
|
|
570
|
-
}) : /* @__PURE__ */ jsxs(Text, { children: [appChoices[appIndex] ?? "—", focused === 0 ? " (↑/↓)" : ""] })] }),
|
|
570
|
+
}) : /* @__PURE__ */ jsxs(Text, { children: [appChoices[appIndex] ?? "—", focused === 0 ? appPrefix ? ` (↑/↓ · "${appPrefix}")` : " (↑/↓ or type)" : ""] })] }),
|
|
571
571
|
/* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
|
|
572
572
|
color: focused === 1 ? "cyan" : "gray",
|
|
573
573
|
children: "Type".padEnd(8)
|
|
@@ -1079,6 +1079,7 @@ function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, reso
|
|
|
1079
1079
|
const i = apps.indexOf(initial?.app ?? apps[0] ?? "");
|
|
1080
1080
|
return i >= 0 ? i : 0;
|
|
1081
1081
|
});
|
|
1082
|
+
const [appPrefix, setAppPrefix] = useState("");
|
|
1082
1083
|
const [screen, setScreen] = useState("form");
|
|
1083
1084
|
const [hint, setHint] = useState("");
|
|
1084
1085
|
const [writeError, setWriteError] = useState("");
|
|
@@ -1088,6 +1089,8 @@ function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, reso
|
|
|
1088
1089
|
posRef.current = pos;
|
|
1089
1090
|
const focusedRef = useRef(focused);
|
|
1090
1091
|
focusedRef.current = focused;
|
|
1092
|
+
const appPrefixRef = useRef(appPrefix);
|
|
1093
|
+
appPrefixRef.current = appPrefix;
|
|
1091
1094
|
const editingStepRef = useRef(editingStep);
|
|
1092
1095
|
editingStepRef.current = editingStep;
|
|
1093
1096
|
const editBufferRef = useRef(editBuffer);
|
|
@@ -1109,9 +1112,15 @@ function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, reso
|
|
|
1109
1112
|
* after the batch is committed and rendered, i.e. too late for that second
|
|
1110
1113
|
* keystroke).
|
|
1111
1114
|
*/
|
|
1115
|
+
/** Set the type-to-jump prefix, keeping the ref in sync for same-batch reads. */
|
|
1116
|
+
const setPrefix = (p) => {
|
|
1117
|
+
appPrefixRef.current = p;
|
|
1118
|
+
setAppPrefix(p);
|
|
1119
|
+
};
|
|
1112
1120
|
const focusField = (f) => {
|
|
1113
1121
|
focusedRef.current = f;
|
|
1114
1122
|
setFocused(f);
|
|
1123
|
+
setPrefix("");
|
|
1115
1124
|
const next = [...fieldValue(draftRef.current, f)].length;
|
|
1116
1125
|
posRef.current = next;
|
|
1117
1126
|
setPos(next);
|
|
@@ -1176,6 +1185,15 @@ function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, reso
|
|
|
1176
1185
|
});
|
|
1177
1186
|
}
|
|
1178
1187
|
}
|
|
1188
|
+
/** Highlight the first app whose name starts with `prefix`; false if none does. */
|
|
1189
|
+
function jumpToApp(prefix) {
|
|
1190
|
+
const needle = prefix.toLowerCase();
|
|
1191
|
+
const i = apps.findIndex((a) => a.toLowerCase().startsWith(needle));
|
|
1192
|
+
if (i < 0) return false;
|
|
1193
|
+
setAppIndex(i);
|
|
1194
|
+
commitAppSelection(i);
|
|
1195
|
+
return true;
|
|
1196
|
+
}
|
|
1179
1197
|
function goReview() {
|
|
1180
1198
|
const d = flushStep(draftRef.current);
|
|
1181
1199
|
const v = validateDraft(d);
|
|
@@ -1217,11 +1235,13 @@ function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, reso
|
|
|
1217
1235
|
if (key.ctrl && input === "p") return focusField(Math.max(focusedRef.current - 1, 0));
|
|
1218
1236
|
if (focusedRef.current === 0) {
|
|
1219
1237
|
if (key.upArrow) {
|
|
1238
|
+
setPrefix("");
|
|
1220
1239
|
const next = Math.max(appIndex - 1, 0);
|
|
1221
1240
|
setAppIndex(next);
|
|
1222
1241
|
return commitAppSelection(next);
|
|
1223
1242
|
}
|
|
1224
1243
|
if (key.downArrow) {
|
|
1244
|
+
setPrefix("");
|
|
1225
1245
|
const next = Math.min(appIndex + 1, appChoices.length - 1);
|
|
1226
1246
|
setAppIndex(next);
|
|
1227
1247
|
return commitAppSelection(next);
|
|
@@ -1244,6 +1264,16 @@ function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, reso
|
|
|
1244
1264
|
return;
|
|
1245
1265
|
}
|
|
1246
1266
|
if (key.return) return focusField(1);
|
|
1267
|
+
if (key.backspace || key.delete) {
|
|
1268
|
+
const shorter = [...appPrefixRef.current].slice(0, -1).join("");
|
|
1269
|
+
setPrefix(shorter);
|
|
1270
|
+
if (shorter) jumpToApp(shorter);
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
if (input && !key.ctrl && !key.meta) {
|
|
1274
|
+
const longer = appPrefixRef.current + input;
|
|
1275
|
+
if (jumpToApp(longer)) setPrefix(longer);
|
|
1276
|
+
}
|
|
1247
1277
|
return;
|
|
1248
1278
|
}
|
|
1249
1279
|
if (focusedRef.current === 1) {
|
|
@@ -1428,6 +1458,7 @@ function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, reso
|
|
|
1428
1458
|
draft,
|
|
1429
1459
|
apps,
|
|
1430
1460
|
appIndex,
|
|
1461
|
+
appPrefix,
|
|
1431
1462
|
focused,
|
|
1432
1463
|
pos,
|
|
1433
1464
|
width: formFieldWidth(columns),
|
|
@@ -1629,12 +1660,55 @@ function FilterPicker({ apps, onSelect, onCancel, height = 16, width = 40 }) {
|
|
|
1629
1660
|
]
|
|
1630
1661
|
});
|
|
1631
1662
|
}
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1663
|
+
/**
|
|
1664
|
+
* Hints in display order. Terminals narrower than the budget cannot show them
|
|
1665
|
+
* all, so `hintLine` drops them by `priority` (highest number first) until
|
|
1666
|
+
* the row fits. The quit / clear-filter hint and the result count are never
|
|
1667
|
+
* dropped: the count is what a narrow terminal used to lose, and ⎋ is the only
|
|
1668
|
+
* way out.
|
|
1669
|
+
*/
|
|
1670
|
+
const HINTS = [
|
|
1671
|
+
{
|
|
1672
|
+
text: "↑↓ move",
|
|
1673
|
+
priority: 7
|
|
1674
|
+
},
|
|
1675
|
+
{
|
|
1676
|
+
text: "⏎ copy",
|
|
1677
|
+
priority: 1
|
|
1678
|
+
},
|
|
1679
|
+
{
|
|
1680
|
+
text: "⌃O add",
|
|
1681
|
+
priority: 2
|
|
1682
|
+
},
|
|
1683
|
+
{
|
|
1684
|
+
text: "⌃E edit",
|
|
1685
|
+
priority: 4
|
|
1686
|
+
},
|
|
1687
|
+
{
|
|
1688
|
+
text: "⌃X del",
|
|
1689
|
+
priority: 3
|
|
1690
|
+
},
|
|
1691
|
+
{
|
|
1692
|
+
text: "⌃F filter",
|
|
1693
|
+
priority: 5
|
|
1694
|
+
},
|
|
1695
|
+
{
|
|
1696
|
+
text: "⌃S star",
|
|
1697
|
+
priority: 6
|
|
1698
|
+
}
|
|
1699
|
+
];
|
|
1700
|
+
function hintLine(filterActive, resultCount, columns = 80) {
|
|
1701
|
+
const tail = `${filterActive ? "⎋ clear filter" : "⎋ quit"} (${resultCount})`;
|
|
1702
|
+
const fit = Math.min(columns, 80);
|
|
1703
|
+
const hints = [...HINTS];
|
|
1704
|
+
for (;;) {
|
|
1705
|
+
const line = [...hints.map((h) => h.text), tail].join(" ");
|
|
1706
|
+
if ([...line].length <= fit || hints.length === 0) return line;
|
|
1707
|
+
const drop = hints.reduce((a, b) => b.priority > a.priority ? b : a);
|
|
1708
|
+
hints.splice(hints.indexOf(drop), 1);
|
|
1709
|
+
}
|
|
1636
1710
|
}
|
|
1637
|
-
function Footer({ flash, errorCount, resultCount, confirm, filterActive = false }) {
|
|
1711
|
+
function Footer({ flash, errorCount, resultCount, confirm, filterActive = false, columns }) {
|
|
1638
1712
|
if (confirm) return /* @__PURE__ */ jsxs(Box, {
|
|
1639
1713
|
marginTop: 1,
|
|
1640
1714
|
justifyContent: "space-between",
|
|
@@ -1654,7 +1728,7 @@ function Footer({ flash, errorCount, resultCount, confirm, filterActive = false
|
|
|
1654
1728
|
children: [/* @__PURE__ */ jsx(Text, {
|
|
1655
1729
|
color: "gray",
|
|
1656
1730
|
wrap: "truncate-end",
|
|
1657
|
-
children: hintLine(filterActive, resultCount)
|
|
1731
|
+
children: hintLine(filterActive, resultCount, columns)
|
|
1658
1732
|
}), flash ? /* @__PURE__ */ jsx(Text, {
|
|
1659
1733
|
color: "green",
|
|
1660
1734
|
wrap: "truncate-end",
|
|
@@ -1968,6 +2042,7 @@ function App({ entries: initial, errorCount = 0, dataDir, onCopy = copyToClipboa
|
|
|
1968
2042
|
errorCount,
|
|
1969
2043
|
resultCount: results.length,
|
|
1970
2044
|
filterActive: filter.type !== "all",
|
|
2045
|
+
columns,
|
|
1971
2046
|
confirm: pendingDelete ? `Delete '${pendingDelete.app}: ${pendingDelete.action}'?` : void 0
|
|
1972
2047
|
})
|
|
1973
2048
|
]
|
package/package.json
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arthony/keybook",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"publishConfig": {
|
|
3
|
+
"version": "0.9.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,23 +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
|
-
"
|
|
34
|
-
"bump-tap": "./scripts/bump-tap.sh"
|
|
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"
|
|
35
41
|
},
|
|
36
42
|
"dependencies": {
|
|
37
43
|
"commander": "^12.1.0",
|
|
@@ -52,8 +58,14 @@
|
|
|
52
58
|
"vitest": "^2.1.0"
|
|
53
59
|
},
|
|
54
60
|
"license": "MIT",
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
"
|
|
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"
|
|
58
70
|
}
|
|
59
|
-
}
|
|
71
|
+
}
|