@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.
Files changed (2) hide show
  1. package/dist/cli.js +127 -35
  2. 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) => /* @__PURE__ */ jsxs(Text, { children: [
420
- i + 1,
421
- ". ",
422
- s
423
- ] }, i)),
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: [steps.length + 1, ". "]
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
- steps.length === 0 ? /* @__PURE__ */ jsx(Text, {
463
+ active ? /* @__PURE__ */ jsx(Text, {
436
464
  color: "gray",
437
- children: "⏎ adds a step · ⌫ on an empty line removes the last"
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, lockedApp }) {
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
- }), lockedApp ? /* @__PURE__ */ jsx(Text, {
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, lockedApp, title }) {
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(lockedApp ? 1 : 0);
733
- const [appIndex, setAppIndex] = useState(0);
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, lockedApp ? 1 : 0));
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
- if (key.return) {
803
- if (draft.stepLine.trim()) update({
804
- steps: [...draft.steps, draft.stepLine.trim()],
805
- stepLine: ""
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
- if (draft.stepLine) return update({ stepLine: draft.stepLine.slice(0, -1) });
811
- return update({ steps: draft.steps.slice(0, -1) });
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
- lockedApp
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
- lockedApp: editTarget.app,
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: (_app, entry) => editEntry(dataDir, editTarget.file, editTarget.index, entry, editTarget.action),
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.6.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
  }