@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.
Files changed (3) hide show
  1. package/README.md +3 -2
  2. package/dist/cli.js +83 -8
  3. 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). Or
48
- script an add: `keybook add --app Fork --action 'Push' --keys 'shift cmd p' --tags push`
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
- //#endregion
1633
- //#region src/tui/Footer.tsx
1634
- function hintLine(filterActive, resultCount) {
1635
- return `↑↓ move ⏎ copy ⌃O add ⌃E edit ⌃X del ⌃F filter ⌃S star ${filterActive ? "⎋ clear filter" : "⎋ quit"} (${resultCount})`;
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.8.1",
4
- "publishConfig": { "access": "public" },
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": { "type": "git", "url": "git+https://github.com/Ariyapong/keybook.git" },
19
- "bugs": { "url": "https://github.com/Ariyapong/keybook/issues" },
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": { "keybook": "dist/cli.js", "kb": "dist/cli.js" },
23
- "files": ["dist", "seed", "README.md", "LICENSE"],
24
- "engines": { "node": ">=22" },
25
- "scripts": {
26
- "build": "tsdown",
27
- "dev": "tsx src/cli.ts",
28
- "test": "vitest run",
29
- "test:watch": "vitest",
30
- "typecheck": "tsc --noEmit",
31
- "lint": "biome check .",
32
- "format": "biome format --write .",
33
- "prepublishOnly": "pnpm build",
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
- "packageManager": "pnpm@10.25.0",
56
- "pnpm": {
57
- "onlyBuiltDependencies": ["@biomejs/biome", "esbuild"]
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
+ }