diff --git a/docs/src/content/docs/docs/Advanced/CLI.md b/docs/src/content/docs/docs/Advanced/CLI.md index 8825fb40..7e3bfcc1 100644 --- a/docs/src/content/docs/docs/Advanced/CLI.md +++ b/docs/src/content/docs/docs/Advanced/CLI.md @@ -72,6 +72,19 @@ obsidian vault=dev quickadd:run-template \ - The picker (interactive command) only lists templates inside your configured template folder(s); `path=` here is explicit, so any vault file resolves. - Like `quickadd:run`, name collisions on the target note still prompt (the file-exists choice is not a pre-collected input). Under `quickadd:interactive` that prompt is forwarded to you like any other. +### Save a clipboard image: `quickadd:save-clipboard-image` {#quickaddsave-clipboard-image} + +Save a 1x1 PNG through the same path QuickAdd uses when you paste an image into a prompt or when `{{CLIPBOARD}}` falls back to an image. Useful for checking attachment naming without driving a modal. + +```bash +obsidian vault=dev quickadd:save-clipboard-image \ + sourcePath="Meetings/Meeting notes.md" \ + nameAfterNoteTitle=true +``` + +- `sourcePath=` is the note the attachment belongs to (the capture destination). Empty uses vault-root attachment placement and the timestamp name even when title-naming is on. +- `nameAfterNoteTitle=` overrides the **Name pasted images after the note title** setting for this save. Omit it to use the setting. + ## Pass variables to a choice {#passing-variables} QuickAdd's CLI accepts variables three ways: diff --git a/docs/src/content/docs/docs/FormatSyntax.md b/docs/src/content/docs/docs/FormatSyntax.md index 4ab19ba3..ec6d5d42 100644 --- a/docs/src/content/docs/docs/FormatSyntax.md +++ b/docs/src/content/docs/docs/FormatSyntax.md @@ -286,7 +286,9 @@ screenshot or copied image: QuickAdd saves it using Obsidian's attachment settings and inserts an embedded link at the cursor. You can mix typed text and images, and paste more than one. Clipboard text wins over an image when both are present (copying a file in a file manager usually pastes its path as -text). Prompts for file names, folders, capture targets, and +text). Turn on **Name pasted images after the note title** in QuickAdd +settings to name the file after the destination note when that path is known +(otherwise the file stays `Clipboard image YYYY-MM-DD HH.MM.SS`). Prompts for file names, folders, capture targets, and insert-after/before targets never accept image paste, since an embed link would break the path. Pasted attachments are ordinary vault files; cancelling the prompt afterwards does not delete them. @@ -905,6 +907,8 @@ In Capture content, if the clipboard has no text but holds a supported image, QuickAdd saves the image using Obsidian's attachment settings and inserts an embedded link. Text wins when both are present. You can also paste an image straight into a [value prompt](#value) while typing - no placeholder needed. +The **Name pasted images after the note title** setting names those files +after the destination note when QuickAdd already knows that path. ### A template file: `{{TEMPLATE:}}` {#template} diff --git a/docs/src/content/docs/docs/Settings.md b/docs/src/content/docs/docs/Settings.md index 023f69cb..853ab014 100644 --- a/docs/src/content/docs/docs/Settings.md +++ b/docs/src/content/docs/docs/Settings.md @@ -23,6 +23,7 @@ The choice picker is the list you see when you run **QuickAdd: Run**. - **Use multi-line input prompt** - get a large text box for text prompts instead of a single line, so you can write several lines at once. Multi-line prompts submit with Ctrl/Cmd+Enter, and plain Enter adds a newline. See [Controlling Prompts](/docs/ControllingPrompts/#submit-keys). - **Persist input prompt drafts** - don't lose what you typed if you close a prompt by accident. When on, a closed prompt keeps its draft and restores it when you reopen. Drafts last only for the current session. - **Use editor selection as default Capture value** - let a Capture reuse text you already have highlighted. When on, Capture uses the current editor selection as `{{VALUE}}` and may skip the prompt entirely. When off, Capture always asks for `{{VALUE}}`. Individual Capture choices can override this. +- **Name pasted images after the note title** - name clipboard images after the destination note instead of `Clipboard image YYYY-MM-DD HH.MM.SS`. Applies when you paste an image into a prompt whose answer lands in note content, and when Capture's `{{CLIPBOARD}}` falls back to an image. If the destination is not known yet, QuickAdd keeps the timestamp name. Duplicate names follow Obsidian's attachment folder setting. Off by default. - **One-page input for choices** - answer all of a choice's questions in one form up front, instead of one prompt after another. Works with Template and Capture choices, and with Macros whose scripts declare inputs. Template and Capture choices can [override this individually](/docs/Advanced/onePageInputs/#per-choice-override). See [One-page Inputs](/docs/Advanced/onePageInputs/) and [Controlling Prompts](/docs/ControllingPrompts/). - **Date aliases** - set your own shortcodes for natural-language dates, so typing `tm` in a date prompt means `tomorrow`. Write one per line as `alias = phrase`, for example `tm = tomorrow`. **Reset to defaults** restores the built-in aliases. diff --git a/src/cli/registerQuickAddCliHandlers.test.ts b/src/cli/registerQuickAddCliHandlers.test.ts index 03cc5896..d8b2ca35 100644 --- a/src/cli/registerQuickAddCliHandlers.test.ts +++ b/src/cli/registerQuickAddCliHandlers.test.ts @@ -197,6 +197,7 @@ describe("registerQuickAddCliHandlers", () => { "quickadd:check", "quickadd:package-preview", "quickadd:interactive", + "quickadd:save-clipboard-image", ]); }); diff --git a/src/cli/registerQuickAddCliHandlers.ts b/src/cli/registerQuickAddCliHandlers.ts index b1bfd4e4..def77c80 100644 --- a/src/cli/registerQuickAddCliHandlers.ts +++ b/src/cli/registerQuickAddCliHandlers.ts @@ -24,6 +24,11 @@ import type ITemplateChoice from "../types/choices/ITemplateChoice"; import type ICaptureChoice from "../types/choices/ICaptureChoice"; import type IMacroChoice from "../types/choices/IMacroChoice"; import { applyInvocationDate } from "../utils/resolveDateOrigin"; +import { + SAVE_CLIPBOARD_IMAGE_COMMAND, + SAVE_CLIPBOARD_IMAGE_FLAGS, + saveClipboardImageHandler, +} from "./saveClipboardImageCli"; import { analysePackagePreview, readQuickAddPackage, @@ -196,6 +201,7 @@ const CLI_COMMANDS = { check: "quickadd:check", preview: "quickadd:package-preview", interactive: "quickadd:interactive", + saveClipboardImage: SAVE_CLIPBOARD_IMAGE_COMMAND, } as const; const SUPPORTED_LIST_TYPES = new Set(["template", "capture", "macro", "multi"]); @@ -1005,6 +1011,12 @@ export function registerQuickAddCliHandlers(plugin: QuickAdd): boolean { INTERACTIVE_FLAGS, (params: CliData) => interactiveHandler(plugin, params), ); + register( + CLI_COMMANDS.saveClipboardImage, + "Save a 1x1 PNG as a vault attachment using QuickAdd clipboard-image naming", + SAVE_CLIPBOARD_IMAGE_FLAGS, + (params: CliData) => saveClipboardImageHandler(plugin, params), + ); log.logMessage("Registered QuickAdd CLI handlers."); return true; diff --git a/src/cli/saveClipboardImageCli.test.ts b/src/cli/saveClipboardImageCli.test.ts new file mode 100644 index 00000000..1a3d7387 --- /dev/null +++ b/src/cli/saveClipboardImageCli.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it, vi } from "vitest"; +import type { TFile } from "obsidian"; +import type QuickAdd from "../main"; +import { + SAVE_CLIPBOARD_IMAGE_COMMAND, + saveClipboardImageHandler, +} from "./saveClipboardImageCli"; + +vi.mock("../utils/clipboardImageAttachments", () => ({ + saveClipboardImageToVault: vi.fn(), +})); + +import { saveClipboardImageToVault } from "../utils/clipboardImageAttachments"; + +const saveMock = vi.mocked(saveClipboardImageToVault); + +function pluginWithSetting(nameAfterNoteTitle: boolean): QuickAdd { + return { + app: {}, + settings: { namePastedImagesAfterNoteTitle: nameAfterNoteTitle }, + } as unknown as QuickAdd; +} + +describe("saveClipboardImageHandler", () => { + it("saves a png named after the destination when the flag is true", async () => { + saveMock.mockResolvedValue({ + path: "attachments/Meeting notes.png", + name: "Meeting notes.png", + } as TFile); + + const payload = JSON.parse( + await saveClipboardImageHandler(pluginWithSetting(false), { + sourcePath: "Meetings/Meeting notes.md", + nameAfterNoteTitle: "true", + }), + ); + + expect(payload).toMatchObject({ + ok: true, + command: SAVE_CLIPBOARD_IMAGE_COMMAND, + path: "attachments/Meeting notes.png", + name: "Meeting notes.png", + sourcePath: "Meetings/Meeting notes.md", + nameAfterNoteTitle: true, + }); + expect(saveMock).toHaveBeenCalledWith( + expect.anything(), + expect.any(ArrayBuffer), + "image/png", + "Meetings/Meeting notes.md", + { nameAfterNoteTitle: true }, + ); + }); + + it("omits the override when the flag is absent so the setting applies", async () => { + saveMock.mockResolvedValue({ + path: "attachments/Clipboard image 2026-08-29 21.40.00.png", + name: "Clipboard image 2026-08-29 21.40.00.png", + } as TFile); + + const payload = JSON.parse( + await saveClipboardImageHandler(pluginWithSetting(false), { + sourcePath: "Meetings/Meeting notes.md", + }), + ); + + expect(payload.ok).toBe(true); + expect(payload.nameAfterNoteTitle).toBe(false); + expect(saveMock).toHaveBeenCalledWith( + expect.anything(), + expect.any(ArrayBuffer), + "image/png", + "Meetings/Meeting notes.md", + undefined, + ); + }); + + it("rejects an invalid nameAfterNoteTitle value", async () => { + const payload = JSON.parse( + await saveClipboardImageHandler(pluginWithSetting(false), { + nameAfterNoteTitle: "maybe", + }), + ); + + expect(payload.ok).toBe(false); + expect(payload.error).toMatch(/Invalid nameAfterNoteTitle/); + }); +}); diff --git a/src/cli/saveClipboardImageCli.ts b/src/cli/saveClipboardImageCli.ts new file mode 100644 index 00000000..13a839b9 --- /dev/null +++ b/src/cli/saveClipboardImageCli.ts @@ -0,0 +1,89 @@ +import type { CliData, CliFlags } from "obsidian"; +import type QuickAdd from "../main"; +import { saveClipboardImageToVault } from "../utils/clipboardImageAttachments"; + +export const SAVE_CLIPBOARD_IMAGE_COMMAND = "quickadd:save-clipboard-image"; + +export const SAVE_CLIPBOARD_IMAGE_FLAGS: CliFlags = { + sourcePath: { + value: "", + description: + "Note path the image will live in (capture destination). Empty keeps a vault-root attachment.", + }, + nameAfterNoteTitle: { + value: "", + description: + "Override the Name pasted images after the note title setting for this save", + }, +}; + +const ONE_PIXEL_PNG = Uint8Array.from( + atob( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", + ), + (char) => char.charCodeAt(0), +); + +function parseOptionalBoolean(value: string | undefined): boolean | undefined { + if (value === undefined || value === "") return undefined; + const normalized = value.toLowerCase(); + if ( + normalized === "true" || + normalized === "1" || + normalized === "yes" || + normalized === "on" + ) { + return true; + } + if ( + normalized === "false" || + normalized === "0" || + normalized === "no" || + normalized === "off" + ) { + return false; + } + throw new Error( + `Invalid nameAfterNoteTitle: ${value}. Use true or false.`, + ); +} + +export async function saveClipboardImageHandler( + plugin: QuickAdd, + params: CliData, +): Promise { + try { + const sourcePath = + typeof params.sourcePath === "string" ? params.sourcePath : ""; + const nameAfterNoteTitle = parseOptionalBoolean( + typeof params.nameAfterNoteTitle === "string" + ? params.nameAfterNoteTitle + : undefined, + ); + const file = await saveClipboardImageToVault( + plugin.app, + ONE_PIXEL_PNG.buffer, + "image/png", + sourcePath, + nameAfterNoteTitle === undefined + ? undefined + : { nameAfterNoteTitle }, + ); + return JSON.stringify({ + ok: true, + command: SAVE_CLIPBOARD_IMAGE_COMMAND, + path: file.path, + name: file.name, + sourcePath: sourcePath || "", + nameAfterNoteTitle: + nameAfterNoteTitle ?? + plugin.settings.namePastedImagesAfterNoteTitle, + }); + } catch (error) { + return JSON.stringify({ + ok: false, + command: SAVE_CLIPBOARD_IMAGE_COMMAND, + error: error instanceof Error ? error.message : String(error), + }); + } +} diff --git a/src/formatters/captureChoiceFormatter-clipboard.test.ts b/src/formatters/captureChoiceFormatter-clipboard.test.ts index bbe8040e..4c6f184a 100644 --- a/src/formatters/captureChoiceFormatter-clipboard.test.ts +++ b/src/formatters/captureChoiceFormatter-clipboard.test.ts @@ -103,6 +103,7 @@ vi.mock("../logger/logManager", () => ({ })); import { CaptureChoiceFormatter } from "./captureChoiceFormatter"; +import { settingsStore } from "../settingsStore"; function createTFile(path: string): TFile { const name = path.split("/").pop() ?? path; @@ -240,6 +241,26 @@ describe("CaptureChoiceFormatter clipboard image support", () => { ); }); + it("asks for a destination-title attachment name when the setting is on", async () => { + settingsStore.setState({ namePastedImagesAfterNoteTitle: true }); + try { + const { app, getAvailablePathForAttachment } = createMockApp(); + const item = createClipboardItem("image/png", [1, 2, 3]); + setClipboard({ items: [item] }); + const formatter = createFormatter(app); + formatter.setDestinationSourcePath("Notes/Clip.md"); + + await formatter.formatContentOnly("{{clipboard}}"); + + expect(getAvailablePathForAttachment).toHaveBeenCalledWith( + "Clip.png", + "Notes/Clip.md", + ); + } finally { + settingsStore.setState({ namePastedImagesAfterNoteTitle: false }); + } + }); + it("inserts clipboard text literally when it contains the clipboard token", async () => { const { app } = createMockApp(); setClipboard({ text: "{{clipboard}}" }); diff --git a/src/gui/imagePasteHandler.test.ts b/src/gui/imagePasteHandler.test.ts index 557de8d5..1cc48748 100644 --- a/src/gui/imagePasteHandler.test.ts +++ b/src/gui/imagePasteHandler.test.ts @@ -11,6 +11,7 @@ vi.mock("../logger/logManager", () => ({ })); import { Notice } from "obsidian"; +import { settingsStore } from "../settingsStore"; function makeApp() { const created: string[] = []; @@ -112,6 +113,28 @@ describe("attachImagePasteHandler", () => { ); }); + it("names a pasted image after the destination note when the setting is on", async () => { + settingsStore.setState({ namePastedImagesAfterNoteTitle: true }); + try { + const { app, createBinary } = makeApp(); + const input = makeInput(); + const handle = attachImagePasteHandler(app, input, { + sourcePath: "Meetings/Meeting notes.md", + }); + + dispatchPaste(input, makeClipboardData([makeImageFile()])); + await flushSaves(handle); + + expect(createBinary).toHaveBeenCalledWith( + "attachments/Meeting notes.png", + expect.any(ArrayBuffer), + ); + expect(input.value).toBe("![[attachments/Meeting notes.png]]"); + } finally { + settingsStore.setState({ namePastedImagesAfterNoteTitle: false }); + } + }); + it("fires an input event so component onChange observers update", async () => { const { app } = makeApp(); const input = makeInput(); diff --git a/src/quickAddSettingsTab.test.ts b/src/quickAddSettingsTab.test.ts index a6fdc8ba..f311fe12 100644 --- a/src/quickAddSettingsTab.test.ts +++ b/src/quickAddSettingsTab.test.ts @@ -263,6 +263,7 @@ describe("QuickAddSettingsTab declarative bridge", () => { "inputPrompt", "persistInputPromptDrafts", "useSelectionAsCaptureValue", + "namePastedImagesAfterNoteTitle", "onePageInputEnabled", "enableTemplatePropertyTypes", "announceUpdates", diff --git a/src/quickAddSettingsTab.ts b/src/quickAddSettingsTab.ts index 88d4f554..086d38ff 100644 --- a/src/quickAddSettingsTab.ts +++ b/src/quickAddSettingsTab.ts @@ -235,6 +235,11 @@ export class QuickAddSettingsTab extends PluginSettingTab { desc: "When enabled, Capture uses the current editor selection as {{VALUE}} and may skip the prompt. When disabled, Capture always prompts for {{VALUE}}.", control: { type: "toggle", key: "useSelectionAsCaptureValue" }, }, + { + name: "Name pasted images after the note title", + desc: "When on, clipboard images saved by QuickAdd (pasting into a prompt, or {{CLIPBOARD}} with an image and no text) are named after the destination note. When the destination is not yet known, QuickAdd keeps the timestamp name. Duplicate names are handled by Obsidian's attachment folder setting.", + control: { type: "toggle", key: "namePastedImagesAfterNoteTitle" }, + }, { name: "One-page input for choices", // The trailing sentence used to read "See One-page Inputs in the diff --git a/src/settings.ts b/src/settings.ts index d69a83bf..7e33ecf9 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -11,9 +11,15 @@ export interface QuickAddSettings { inputPrompt: "multi-line" | "single-line"; persistInputPromptDrafts: boolean; /** - * When enabled, Capture uses the current editor selection as the default {{VALUE}}. - */ + * When enabled, Capture uses the current editor selection as the default {{VALUE}}. + */ useSelectionAsCaptureValue: boolean; + /** + * Name clipboard images (prompt paste and {{CLIPBOARD}} image fallback) + * after the destination note when that path is known. Unknown destination + * keeps the timestamp name. Collisions use Obsidian's attachment-folder API. + */ + namePastedImagesAfterNoteTitle: boolean; /** * When enabled, typing in the choice picker also searches choices nested * inside Multi choices and shows matches with their folder path. @@ -111,6 +117,7 @@ export const DEFAULT_SETTINGS: QuickAddSettings = { inputPrompt: "single-line", persistInputPromptDrafts: true, useSelectionAsCaptureValue: true, + namePastedImagesAfterNoteTitle: false, searchNestedChoices: true, templateFolderLauncherRow: "bottom", devMode: false, diff --git a/src/utils/clipboardImageAttachments.test.ts b/src/utils/clipboardImageAttachments.test.ts index e8fa96da..01410815 100644 --- a/src/utils/clipboardImageAttachments.test.ts +++ b/src/utils/clipboardImageAttachments.test.ts @@ -3,7 +3,9 @@ import type { App, TFile } from "obsidian"; import { IMAGE_CLIPBOARD_MIME_EXTENSIONS, buildImageEmbedLink, + clipboardImageAttachmentFileName, formatClipboardAttachmentTimestamp, + sanitizeClipboardImageStem, saveClipboardImageToVault, } from "./clipboardImageAttachments"; @@ -56,6 +58,37 @@ describe("saveClipboardImageToVault", () => { expect(file.path).toMatch(/^attachments\/Clipboard image .*\.png$/); }); + it("names the file after the destination note when asked", async () => { + const { app, getAvailablePathForAttachment } = makeApp(); + + await saveClipboardImageToVault( + app, + data, + "image/png", + "Meetings/Meeting notes.md", + { nameAfterNoteTitle: true }, + ); + + expect(getAvailablePathForAttachment).toHaveBeenCalledWith( + "Meeting notes.png", + "Meetings/Meeting notes.md", + ); + }); + + it("keeps the timestamp name when destination-title naming is on but the path is empty", async () => { + const { app, getAvailablePathForAttachment } = makeApp(); + + await saveClipboardImageToVault(app, data, "image/png", "", { + nameAfterNoteTitle: true, + now: new Date(2026, 7, 29, 21, 40, 0), + }); + + expect(getAvailablePathForAttachment).toHaveBeenCalledWith( + "Clipboard image 2026-08-29 21.40.00.png", + undefined, + ); + }); + it("passes undefined source context when the destination is unknown", async () => { const { app, getAvailablePathForAttachment } = makeApp(); @@ -125,3 +158,46 @@ describe("formatClipboardAttachmentTimestamp", () => { expect(stamp).toBe("2026-07-06 09.05.03"); }); }); + +describe("clipboardImageAttachmentFileName", () => { + const now = new Date(2026, 7, 29, 21, 40, 0); + + it("uses the timestamp name when destination-title naming is off", () => { + expect( + clipboardImageAttachmentFileName({ + extension: "png", + sourcePath: "Meetings/Meeting notes.md", + now, + nameAfterNoteTitle: false, + }), + ).toBe("Clipboard image 2026-08-29 21.40.00.png"); + }); + + it("uses the destination basename when destination-title naming is on", () => { + expect( + clipboardImageAttachmentFileName({ + extension: "png", + sourcePath: "Meetings/Meeting notes.md", + now, + nameAfterNoteTitle: true, + }), + ).toBe("Meeting notes.png"); + }); + + it("falls back to the timestamp when the stem sanitizes to empty", () => { + expect( + clipboardImageAttachmentFileName({ + extension: "png", + sourcePath: "???.md", + now, + nameAfterNoteTitle: true, + }), + ).toBe("Clipboard image 2026-08-29 21.40.00.png"); + }); +}); + +describe("sanitizeClipboardImageStem", () => { + it("strips path separators and Windows-illegal characters", () => { + expect(sanitizeClipboardImageStem('a/b:c*d?e"fh|i')).toBe("abcdefghi"); + }); +}); diff --git a/src/utils/clipboardImageAttachments.ts b/src/utils/clipboardImageAttachments.ts index 4161b769..f7d6c969 100644 --- a/src/utils/clipboardImageAttachments.ts +++ b/src/utils/clipboardImageAttachments.ts @@ -1,4 +1,6 @@ import type { App, TFile } from "obsidian"; +import { settingsStore } from "../settingsStore"; +import { fileBasenameFromPath } from "./fileSyntax"; import { escapesVaultBoundary } from "./vaultPathBoundary"; /** @@ -28,6 +30,49 @@ export function formatClipboardAttachmentTimestamp(date: Date): string { )}`; } +const CLIPBOARD_IMAGE_STEM_FORBIDDEN = /[/\\:*?"<>|#^[\]]/g; + +export function sanitizeClipboardImageStem(stem: string): string { + return stem + .replace(CLIPBOARD_IMAGE_STEM_FORBIDDEN, "") + .replace(/\s+/g, " ") + .replace(/[. ]+$/g, "") + .trim(); +} + +export interface ClipboardImageFileNameInput { + extension: string; + sourcePath: string; + now: Date; + nameAfterNoteTitle: boolean; +} + +/** + * Basename (with extension) passed to `getAvailablePathForAttachment`. + * Destination-title naming only applies when the note path is known and the + * sanitized stem is non-empty; otherwise this keeps the timestamp name. + */ +export function clipboardImageAttachmentFileName( + input: ClipboardImageFileNameInput, +): string { + if (input.nameAfterNoteTitle) { + const stem = sanitizeClipboardImageStem( + fileBasenameFromPath(input.sourcePath), + ); + if (stem.length > 0) { + return `${stem}.${input.extension}`; + } + } + return `Clipboard image ${formatClipboardAttachmentTimestamp(input.now)}.${ + input.extension + }`; +} + +export interface SaveClipboardImageOptions { + nameAfterNoteTitle?: boolean; + now?: Date; +} + /** * Saves clipboard image bytes as a vault attachment and returns the created * file. Link generation is a separate step ({@link buildImageEmbedLink}) so a @@ -47,15 +92,22 @@ export async function saveClipboardImageToVault( data: ArrayBuffer, mimeType: string, sourcePath: string, + options?: SaveClipboardImageOptions, ): Promise { const extension = IMAGE_CLIPBOARD_MIME_EXTENSIONS[mimeType]; if (!extension) { throw new Error(`Unsupported clipboard image type: ${mimeType}`); } - const filename = `Clipboard image ${formatClipboardAttachmentTimestamp( - new Date(), - )}.${extension}`; + const nameAfterNoteTitle = + options?.nameAfterNoteTitle ?? + settingsStore.getState().namePastedImagesAfterNoteTitle; + const filename = clipboardImageAttachmentFileName({ + extension, + sourcePath, + now: options?.now ?? new Date(), + nameAfterNoteTitle, + }); const attachmentPath = await app.fileManager.getAvailablePathForAttachment( filename, sourcePath || undefined,