From ce963d9d39b58eeb7fbff5bb6ab671e5150e168d Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Tue, 6 Oct 2026 00:37:35 +0200 Subject: [PATCH] fix: resolve capture choices by id, use generated preference types, space after a picked tag A capture preference stores a name, and two choices in different Multi folders can share one, so the capture commands now look the name up in quickadd:list and run the unique id (a lone Capture wins over a same-named Template; two Captures are refused). Hand-written Preferences interfaces are replaced by the types Raycast generates in raycast-env.d.ts, so tsc runs after the build in CI. A picked tag is followed by a space unless one is already there, so the next word cannot join it. --- .github/workflows/ci.yml | 2 +- src/lib/capture-choice.test.ts | 48 ++++++++++++++++++++++++++++++++++ src/lib/capture-choice.ts | 22 ++++++++++++++++ src/lib/capture.ts | 16 +++++------- src/lib/completion.test.ts | 5 ++++ src/lib/completion.ts | 4 ++- src/lib/obsidianCli.ts | 15 ----------- src/run-choice.tsx | 4 --- tsconfig.json | 2 +- 9 files changed, 87 insertions(+), 31 deletions(-) create mode 100644 src/lib/capture-choice.test.ts create mode 100644 src/lib/capture-choice.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65c0d2f..479933a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,5 +17,5 @@ jobs: - run: npm ci - run: npm run lint - run: npm test - - run: npx tsc --noEmit - run: npm run build + - run: npx tsc --noEmit diff --git a/src/lib/capture-choice.test.ts b/src/lib/capture-choice.test.ts new file mode 100644 index 0000000..9e90bbf --- /dev/null +++ b/src/lib/capture-choice.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest"; +import { resolveCaptureChoice } from "./capture-choice"; +import type { ChoiceSummary } from "./types"; + +const choice = ( + id: string, + name: string, + type: ChoiceSummary["type"] = "Capture", +): ChoiceSummary => ({ + id, + name, + type, + command: false, + path: name, + runnable: type !== "Multi", +}); + +describe("resolveCaptureChoice", () => { + it("returns the one choice with that name", () => { + expect( + resolveCaptureChoice( + [choice("a", "Inbox"), choice("b", "Journal")], + "Inbox", + ).id, + ).toBe("a"); + }); + + it("prefers the Capture when a Template shares the name", () => { + const choices = [choice("t", "Inbox", "Template"), choice("c", "Inbox")]; + expect(resolveCaptureChoice(choices, "Inbox").id).toBe("c"); + }); + + it("refuses two Captures with the same name instead of guessing", () => { + const choices = [choice("x", "Inbox"), choice("y", "Inbox")]; + expect(() => resolveCaptureChoice(choices, "Inbox")).toThrow(/Several/); + }); + + it("names the missing choice", () => { + expect(() => resolveCaptureChoice([choice("a", "Inbox")], "Nope")).toThrow( + /"Nope"/, + ); + }); + + it("ignores a Multi folder that shares the name", () => { + const choices = [choice("m", "Inbox", "Multi"), choice("c", "Inbox")]; + expect(resolveCaptureChoice(choices, "Inbox").id).toBe("c"); + }); +}); diff --git a/src/lib/capture-choice.ts b/src/lib/capture-choice.ts new file mode 100644 index 0000000..6a0e36f --- /dev/null +++ b/src/lib/capture-choice.ts @@ -0,0 +1,22 @@ +import type { ChoiceSummary } from "./types"; + +/** + * The capture preference stores a name, but QuickAdd runs choices by id and + * two choices in different Multi folders can share a name. + */ +export function resolveCaptureChoice( + choices: ChoiceSummary[], + name: string, +): ChoiceSummary { + const wanted = name.trim(); + const named = choices.filter((c) => c.runnable && c.name === wanted); + if (named.length === 1) return named[0]; + if (named.length === 0) { + throw new Error(`No QuickAdd choice is named "${wanted}".`); + } + const captures = named.filter((c) => c.type === "Capture"); + if (captures.length === 1) return captures[0]; + throw new Error( + `Several QuickAdd choices are named "${wanted}". Rename one of them.`, + ); +} diff --git a/src/lib/capture.ts b/src/lib/capture.ts index 0d07c55..6f97fe2 100644 --- a/src/lib/capture.ts +++ b/src/lib/capture.ts @@ -1,14 +1,10 @@ import { Toast, getPreferenceValues, showToast } from "@raycast/api"; import { showFailureToast } from "@raycast/utils"; import { doneMessage } from "./interactive"; -import { prepareVault, runChoiceByName } from "./obsidianCli"; +import { listChoices, prepareVault, runChoice } from "./obsidianCli"; +import { resolveCaptureChoice } from "./capture-choice"; import { chooseVault, readRegistry } from "./vaults"; -interface CapturePreferences { - captureChoice: string; - vaultPath?: string; -} - export interface CaptureContext { vaultPath?: string; } @@ -18,7 +14,7 @@ export async function capture( contextVaultPath: string | undefined, ): Promise { const { captureChoice, vaultPath } = - getPreferenceValues(); + getPreferenceValues(); const toast = await showToast({ style: Toast.Style.Animated, @@ -36,14 +32,16 @@ export async function capture( } const ready = await prepareVault(chosen.vault, undefined, registry); if (!ready.ok) throw new Error(ready.message); - const result = await runChoiceByName(chosen.vault, captureChoice, { + const listed = await listChoices(chosen.vault); + const choice = resolveCaptureChoice(listed.choices ?? [], captureChoice); + const result = await runChoice(chosen.vault, choice.id, { vars: { value: text }, }); if (!result.ok) { throw new Error(result.error ?? "Capture failed"); } toast.style = Toast.Style.Success; - toast.title = doneMessage(captureChoice, result); + toast.title = doneMessage(choice.name, result); } catch (error) { await toast.hide(); await showFailureToast(error, { title: "Could not capture" }); diff --git a/src/lib/completion.test.ts b/src/lib/completion.test.ts index c068833..f308006 100644 --- a/src/lib/completion.test.ts +++ b/src/lib/completion.test.ts @@ -86,4 +86,9 @@ describe("insertTag", () => { it("replaces the # with the tag and keeps the text after it", () => { expect(insertTag("a # c", 2, "work")).toBe("a #work c"); }); + + it("adds a space so the next word does not join the tag", () => { + expect(insertTag("a #", 2, "work")).toBe("a #work "); + expect(insertTag("a #later", 2, "work")).toBe("a #work later"); + }); }); diff --git a/src/lib/completion.ts b/src/lib/completion.ts index d15ee6b..ba96e36 100644 --- a/src/lib/completion.ts +++ b/src/lib/completion.ts @@ -43,5 +43,7 @@ export function insertLink(value: string, at: number, text: string): string { } export function insertTag(value: string, at: number, tag: string): string { - return `${value.slice(0, at)}#${tag}${value.slice(at + 1)}`; + const rest = value.slice(at + 1); + const separator = /^\s/.test(rest) ? "" : " "; + return `${value.slice(0, at)}#${tag}${separator}${rest}`; } diff --git a/src/lib/obsidianCli.ts b/src/lib/obsidianCli.ts index 9ee6fe0..2e5ff40 100644 --- a/src/lib/obsidianCli.ts +++ b/src/lib/obsidianCli.ts @@ -26,10 +26,6 @@ const CLI_CANDIDATES = [ `${homedir()}/Applications/Obsidian.app/Contents/MacOS/obsidian-cli`, ]; -interface Preferences { - cliPath?: string; -} - /** Raised for transport-level failures (CLI missing, Obsidian/vault unreachable). */ export class ObsidianCliError extends Error {} @@ -196,17 +192,6 @@ export async function runChoice( }); } -export async function runChoiceByName( - vault: Vault, - name: string, - options: RunOptions = {}, -): Promise { - return invoke(vault, "quickadd:run", { - choice: name, - ...runParams(options), - }); -} - export function obsidianOpenUrl( vault: Vault, vaultRelativePath?: string, diff --git a/src/run-choice.tsx b/src/run-choice.tsx index 3308346..d0286de 100644 --- a/src/run-choice.tsx +++ b/src/run-choice.tsx @@ -72,10 +72,6 @@ export default function RunChoiceCommand( return ; } -interface Preferences { - vaultPath?: string; -} - function runChoiceDeeplink(context: RunChoiceContext): string { return createDeeplink({ command: "run-choice", context }); } diff --git a/tsconfig.json b/tsconfig.json index 28d63a2..be2a909 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/tsconfig", - "include": ["src/**/*"], + "include": ["src/**/*", "raycast-env.d.ts"], "compilerOptions": { "lib": ["ES2023"], "module": "commonjs",