@arthony/keybook 0.8.0 → 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 +118 -14
- package/package.json +1 -1
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)
|
|
@@ -1030,6 +1030,35 @@ function fieldValue(d, focused) {
|
|
|
1030
1030
|
return "";
|
|
1031
1031
|
}
|
|
1032
1032
|
//#endregion
|
|
1033
|
+
//#region src/tui/useTerminalSize.ts
|
|
1034
|
+
/**
|
|
1035
|
+
* The terminal's current size, re-read whenever stdout fires `resize`.
|
|
1036
|
+
*
|
|
1037
|
+
* Ink reacts to a resize by re-running Yoga layout and repainting, but it
|
|
1038
|
+
* never re-renders React, so any width or height computed from
|
|
1039
|
+
* `stdout.columns`/`stdout.rows` during render would stay frozen at the last
|
|
1040
|
+
* keystroke. Holding the size in state turns a resize into a render.
|
|
1041
|
+
*/
|
|
1042
|
+
function useTerminalSize() {
|
|
1043
|
+
const { stdout } = useStdout();
|
|
1044
|
+
const [size, setSize] = useState(() => ({
|
|
1045
|
+
columns: stdout?.columns,
|
|
1046
|
+
rows: stdout?.rows
|
|
1047
|
+
}));
|
|
1048
|
+
useEffect(() => {
|
|
1049
|
+
if (!stdout) return;
|
|
1050
|
+
const onResize = () => setSize({
|
|
1051
|
+
columns: stdout.columns,
|
|
1052
|
+
rows: stdout.rows
|
|
1053
|
+
});
|
|
1054
|
+
stdout.on("resize", onResize);
|
|
1055
|
+
return () => {
|
|
1056
|
+
stdout.off("resize", onResize);
|
|
1057
|
+
};
|
|
1058
|
+
}, [stdout]);
|
|
1059
|
+
return size;
|
|
1060
|
+
}
|
|
1061
|
+
//#endregion
|
|
1033
1062
|
//#region src/tui/AddEntryForm.tsx
|
|
1034
1063
|
const TYPES = [
|
|
1035
1064
|
"shortcut",
|
|
@@ -1039,7 +1068,7 @@ const TYPES = [
|
|
|
1039
1068
|
const LAST_FIELD = 5;
|
|
1040
1069
|
function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, resolveTarget, initial, initialFocus, title }) {
|
|
1041
1070
|
const { draft, update, setDraft } = useAddForm(initial ?? { app: apps[0] ?? "" });
|
|
1042
|
-
const {
|
|
1071
|
+
const { columns } = useTerminalSize();
|
|
1043
1072
|
const [focused, setFocused] = useState(initialFocus ?? 0);
|
|
1044
1073
|
const [pos, setPos] = useState(() => [...fieldValue(draft, initialFocus ?? 0)].length);
|
|
1045
1074
|
const [stepCursor, setStepCursor] = useState(initial?.steps?.length ?? 0);
|
|
@@ -1050,6 +1079,7 @@ function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, reso
|
|
|
1050
1079
|
const i = apps.indexOf(initial?.app ?? apps[0] ?? "");
|
|
1051
1080
|
return i >= 0 ? i : 0;
|
|
1052
1081
|
});
|
|
1082
|
+
const [appPrefix, setAppPrefix] = useState("");
|
|
1053
1083
|
const [screen, setScreen] = useState("form");
|
|
1054
1084
|
const [hint, setHint] = useState("");
|
|
1055
1085
|
const [writeError, setWriteError] = useState("");
|
|
@@ -1059,6 +1089,8 @@ function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, reso
|
|
|
1059
1089
|
posRef.current = pos;
|
|
1060
1090
|
const focusedRef = useRef(focused);
|
|
1061
1091
|
focusedRef.current = focused;
|
|
1092
|
+
const appPrefixRef = useRef(appPrefix);
|
|
1093
|
+
appPrefixRef.current = appPrefix;
|
|
1062
1094
|
const editingStepRef = useRef(editingStep);
|
|
1063
1095
|
editingStepRef.current = editingStep;
|
|
1064
1096
|
const editBufferRef = useRef(editBuffer);
|
|
@@ -1080,9 +1112,15 @@ function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, reso
|
|
|
1080
1112
|
* after the batch is committed and rendered, i.e. too late for that second
|
|
1081
1113
|
* keystroke).
|
|
1082
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
|
+
};
|
|
1083
1120
|
const focusField = (f) => {
|
|
1084
1121
|
focusedRef.current = f;
|
|
1085
1122
|
setFocused(f);
|
|
1123
|
+
setPrefix("");
|
|
1086
1124
|
const next = [...fieldValue(draftRef.current, f)].length;
|
|
1087
1125
|
posRef.current = next;
|
|
1088
1126
|
setPos(next);
|
|
@@ -1147,6 +1185,15 @@ function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, reso
|
|
|
1147
1185
|
});
|
|
1148
1186
|
}
|
|
1149
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
|
+
}
|
|
1150
1197
|
function goReview() {
|
|
1151
1198
|
const d = flushStep(draftRef.current);
|
|
1152
1199
|
const v = validateDraft(d);
|
|
@@ -1188,11 +1235,13 @@ function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, reso
|
|
|
1188
1235
|
if (key.ctrl && input === "p") return focusField(Math.max(focusedRef.current - 1, 0));
|
|
1189
1236
|
if (focusedRef.current === 0) {
|
|
1190
1237
|
if (key.upArrow) {
|
|
1238
|
+
setPrefix("");
|
|
1191
1239
|
const next = Math.max(appIndex - 1, 0);
|
|
1192
1240
|
setAppIndex(next);
|
|
1193
1241
|
return commitAppSelection(next);
|
|
1194
1242
|
}
|
|
1195
1243
|
if (key.downArrow) {
|
|
1244
|
+
setPrefix("");
|
|
1196
1245
|
const next = Math.min(appIndex + 1, appChoices.length - 1);
|
|
1197
1246
|
setAppIndex(next);
|
|
1198
1247
|
return commitAppSelection(next);
|
|
@@ -1215,6 +1264,16 @@ function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, reso
|
|
|
1215
1264
|
return;
|
|
1216
1265
|
}
|
|
1217
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
|
+
}
|
|
1218
1277
|
return;
|
|
1219
1278
|
}
|
|
1220
1279
|
if (focusedRef.current === 1) {
|
|
@@ -1399,9 +1458,10 @@ function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, reso
|
|
|
1399
1458
|
draft,
|
|
1400
1459
|
apps,
|
|
1401
1460
|
appIndex,
|
|
1461
|
+
appPrefix,
|
|
1402
1462
|
focused,
|
|
1403
1463
|
pos,
|
|
1404
|
-
width: formFieldWidth(
|
|
1464
|
+
width: formFieldWidth(columns),
|
|
1405
1465
|
existingTags,
|
|
1406
1466
|
stepCursor,
|
|
1407
1467
|
grabbed,
|
|
@@ -1600,12 +1660,55 @@ function FilterPicker({ apps, onSelect, onCancel, height = 16, width = 40 }) {
|
|
|
1600
1660
|
]
|
|
1601
1661
|
});
|
|
1602
1662
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
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
|
+
}
|
|
1607
1710
|
}
|
|
1608
|
-
function Footer({ flash, errorCount, resultCount, confirm, filterActive = false }) {
|
|
1711
|
+
function Footer({ flash, errorCount, resultCount, confirm, filterActive = false, columns }) {
|
|
1609
1712
|
if (confirm) return /* @__PURE__ */ jsxs(Box, {
|
|
1610
1713
|
marginTop: 1,
|
|
1611
1714
|
justifyContent: "space-between",
|
|
@@ -1625,7 +1728,7 @@ function Footer({ flash, errorCount, resultCount, confirm, filterActive = false
|
|
|
1625
1728
|
children: [/* @__PURE__ */ jsx(Text, {
|
|
1626
1729
|
color: "gray",
|
|
1627
1730
|
wrap: "truncate-end",
|
|
1628
|
-
children: hintLine(filterActive, resultCount)
|
|
1731
|
+
children: hintLine(filterActive, resultCount, columns)
|
|
1629
1732
|
}), flash ? /* @__PURE__ */ jsx(Text, {
|
|
1630
1733
|
color: "green",
|
|
1631
1734
|
wrap: "truncate-end",
|
|
@@ -1757,7 +1860,7 @@ function SearchInput({ query, pos, width, filterLabel }) {
|
|
|
1757
1860
|
const sameApp = (a, b) => a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
1758
1861
|
function App({ entries: initial, errorCount = 0, dataDir, onCopy = copyToClipboard }) {
|
|
1759
1862
|
const { exit } = useApp();
|
|
1760
|
-
const {
|
|
1863
|
+
const { columns, rows } = useTerminalSize();
|
|
1761
1864
|
const [entries, setEntries] = useState(initial);
|
|
1762
1865
|
const [mode, setMode] = useState("search");
|
|
1763
1866
|
const [filter, setFilter] = useState({ type: "all" });
|
|
@@ -1786,8 +1889,8 @@ function App({ entries: initial, errorCount = 0, dataDir, onCopy = copyToClipboa
|
|
|
1786
1889
|
const sel = results.length ? Math.min(selected, results.length - 1) : 0;
|
|
1787
1890
|
const current = results[sel];
|
|
1788
1891
|
currentRef.current = current;
|
|
1789
|
-
const listHeight = visibleListHeight(
|
|
1790
|
-
const { left, right } = columnWidths(
|
|
1892
|
+
const listHeight = visibleListHeight(rows);
|
|
1893
|
+
const { left, right } = columnWidths(columns);
|
|
1791
1894
|
useInput((input, key) => {
|
|
1792
1895
|
if (key.ctrl && input === "c") {
|
|
1793
1896
|
exit();
|
|
@@ -1919,7 +2022,7 @@ function App({ entries: initial, errorCount = 0, dataDir, onCopy = copyToClipboa
|
|
|
1919
2022
|
/* @__PURE__ */ jsx(SearchInput, {
|
|
1920
2023
|
query: query.text,
|
|
1921
2024
|
pos: query.pos,
|
|
1922
|
-
width: searchFieldWidth(
|
|
2025
|
+
width: searchFieldWidth(columns, filterLabelText?.length ?? 0),
|
|
1923
2026
|
filterLabel: filterLabelText
|
|
1924
2027
|
}),
|
|
1925
2028
|
/* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(ResultList, {
|
|
@@ -1939,6 +2042,7 @@ function App({ entries: initial, errorCount = 0, dataDir, onCopy = copyToClipboa
|
|
|
1939
2042
|
errorCount,
|
|
1940
2043
|
resultCount: results.length,
|
|
1941
2044
|
filterActive: filter.type !== "all",
|
|
2045
|
+
columns,
|
|
1942
2046
|
confirm: pendingDelete ? `Delete '${pendingDelete.app}: ${pendingDelete.action}'?` : void 0
|
|
1943
2047
|
})
|
|
1944
2048
|
]
|