From dea972cfda8c90a09e4dae347ae843e3f195122c Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Mon, 28 Oct 2024 11:03:02 -0400 Subject: [PATCH] Build out lib dir --- .gitignore | 2 + README.md | 7 +- format.ts => lib/format.ts | 32 ++- adjectives.ts => lib/resource/adjectives.ts | 0 lib/resource/icons.ts | 6 + nouns.ts => lib/resource/nouns.ts | 0 lib/util.ts | 27 ++ main.ts | 261 +++++++++++++------- pack.sh | 5 + package.json | 6 +- styles.css | 8 - 11 files changed, 242 insertions(+), 112 deletions(-) rename format.ts => lib/format.ts (57%) rename adjectives.ts => lib/resource/adjectives.ts (100%) create mode 100644 lib/resource/icons.ts rename nouns.ts => lib/resource/nouns.ts (100%) create mode 100644 lib/util.ts create mode 100755 pack.sh delete mode 100644 styles.css diff --git a/.gitignore b/.gitignore index e09a007..0c0b710 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ data.json # Exclude macOS Finder (System Explorer) View States .DS_Store + +dist \ No newline at end of file diff --git a/README.md b/README.md index e8b1b35..b6a0b15 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,10 @@ This plugin manages a folder in your Obsidian Vault which contains "Scraps": fil ## Commands -- `Scraps: Create new Scrap` - Creates a new file based on the plugin settings -- `Scraps: Convert current file to Scrap` - Moves and renames the current file to be a scrap -- `Scraps: Copy current file to Scraps` - Creates a copy of the current file as a scrap +- `Scraps: Create new Scrap` - Creates a new file based on the plugin settings. +- `Scraps: Move current file to Scraps` - Moves and the current file into the current Scrap directory. Does not rename the file. +- `Scraps: Copy current file to Scraps` - Creates a copy of the current file in the current Scrap directory. Does not rename the file. +- `Scraps: Rename current file as Scrap` - Renames the current file using the scrap file name settings and moves the file into the current Scrap directory. ## Status diff --git a/format.ts b/lib/format.ts similarity index 57% rename from format.ts rename to lib/format.ts index 0104094..2e6872d 100644 --- a/format.ts +++ b/lib/format.ts @@ -1,20 +1,28 @@ import { moment } from "obsidian"; -import { adjectives } from "adjectives.js"; import { capitalize, sample } from "lodash-es"; -import { nouns } from "nouns.js"; +import { adjectives } from "./resource/adjectives.js"; +import { nouns } from "./resource/nouns.js"; -interface FormatOptions { - ext?: string; -} +/* + * A simple formatter/templater which uses a {tag} format for tokens. + * + * Supported tokens: + * - {N}: Random noun + * - {n}: Random noun (lower case) + * - {A}: Random adjective + * - {a}: Random adjective (lower case) + * - {MW}: Month week (1-5) + * - {time }: Current date/time in the specified moment format + * + * Examples: + * format("Note {N} created at {time HH:mm}") -> "Note Apple created at 14:30" + * format("Note {n} created at {time HH:mm}") -> "Note apple created at 14:30" + * format("Scrap from {time YYYY-MM-DD}") -> "Scrap from 2024-06-01" + */ -export function format(str: string, options?: FormatOptions): string { +export function format(str: string): string { let formatted = str.trim(); - let extension = options?.ext ?? ""; - if (extension.length > 0 && !extension.startsWith(".")) { - extension = `.${extension}`; - } - // Replace {N} with a random noun formatted = formatted.replace(/{N}/g, () => { return capitalize(sample(nouns)); @@ -47,5 +55,5 @@ export function format(str: string, options?: FormatOptions): string { } ); - return formatted + extension; + return formatted; } diff --git a/adjectives.ts b/lib/resource/adjectives.ts similarity index 100% rename from adjectives.ts rename to lib/resource/adjectives.ts diff --git a/lib/resource/icons.ts b/lib/resource/icons.ts new file mode 100644 index 0000000..41d2b1a --- /dev/null +++ b/lib/resource/icons.ts @@ -0,0 +1,6 @@ +export const ICON = { + New: "file-plus", + Rename: "pen-line", + Move: "replace", + Copy: "copy-plus", +} as const; diff --git a/nouns.ts b/lib/resource/nouns.ts similarity index 100% rename from nouns.ts rename to lib/resource/nouns.ts diff --git a/lib/util.ts b/lib/util.ts new file mode 100644 index 0000000..83f6d46 --- /dev/null +++ b/lib/util.ts @@ -0,0 +1,27 @@ +import { Vault } from "obsidian"; + +export async function mkdirp(vault: Vault, folderPath: string): Promise { + // Noop if the path exists + if (vault.getAbstractFileByPath(folderPath)) { + return; + } + + // Start building the path incrementally + const pathParts = folderPath.split("/"); + let currentPath = ""; + + for (const part of pathParts) { + currentPath = currentPath ? `${currentPath}/${part}` : part; + + try { + // Check if folder exists + if (!vault.getAbstractFileByPath(currentPath)) { + // Create the folder if it doesn't exist + await vault.createFolder(currentPath); + } + } catch (error) { + console.error(`Error creating folder ${currentPath}:`, error); + throw error; + } + } +} diff --git a/main.ts b/main.ts index 3c85a75..697fd45 100644 --- a/main.ts +++ b/main.ts @@ -1,90 +1,125 @@ -import { - App, - Plugin, - PluginSettingTab, - Setting, - Vault, - Notice, -} from "obsidian"; -import { format } from "./format.js"; +import { App, Plugin, PluginSettingTab, Setting, Notice } from "obsidian"; +import { format } from "./lib/format.js"; +import { mkdirp } from "./lib/util.js"; +import { ICON } from "./lib/resource/icons.js"; -interface ScrapsPluginSettings { +type NewScrapBehaviorOption = "default" | "tab" | "split" | "window"; + +export interface ScrapsPluginSettings { scrapsRootDir: string; scrapsPathFormat: string; scrapsFileName: string; + newScrapBehavior: NewScrapBehaviorOption; } const DEFAULT_SETTINGS: ScrapsPluginSettings = { scrapsRootDir: "_Scraps", scrapsPathFormat: "{time YYYY} Week {time WW MMM}", scrapsFileName: "{time DDDD} {A} {N}", + newScrapBehavior: "default", }; -const ICON = { - New: "file-plus", - Convert: "shuffle", - Move: "replace", - Copy: "copy-plus", -} as const; - -async function mkdirp(vault: Vault, folderPath: string): Promise { - const pathParts = folderPath.split("/"); - - // Start building the path incrementally - let currentPath = ""; - - for (const part of pathParts) { - currentPath = currentPath ? `${currentPath}/${part}` : part; - - try { - // Check if folder exists - if (!vault.getAbstractFileByPath(currentPath)) { - // Create the folder if it doesn't exist - await vault.createFolder(currentPath); - } - } catch (error) { - console.error(`Error creating folder ${currentPath}:`, error); - throw error; - } - } +type CommandId = "New" | "Move" | "Copy" | "Rename"; +interface CommandDef { + id: string; + icon: string; + name: string; } +const COMMANDS: Record = { + New: { + id: "scraps-new", + icon: ICON.New, + name: "Scraps: Create new Scrap", + }, + Move: { + id: "scraps-move", + icon: ICON.Move, + name: "Scraps: Move current file to Scraps", + }, + Copy: { + id: "scraps-copy", + icon: ICON.Copy, + name: "Scraps: Copy current file to Scraps", + }, + Rename: { + id: "scraps-rename", + icon: ICON.Rename, + name: "Scraps: Rename current file as Scrap", + }, +}; export default class ScrapsPlugin extends Plugin { settings: ScrapsPluginSettings; - async ensureScrapDir(): Promise { - const pathname = `${this.getScrapDir()}`; - - await mkdirp(this.app.vault, pathname); - - return pathname; + /** + * Ensures that the scrap directory exists by creating it if it does not + * already exist. + * + * @returns {Promise} A promise that resolves when the directory + * has been ensured. + */ + async ensureScrapDir(): Promise { + await mkdirp(this.app.vault, this.scrapsDirectory); } - getScrapDir(): string { + /** + * The current scrap directory path based on the configured root + * directory and path format. + * + * @returns {string} The full path to the current scrap directory. + */ + get scrapsDirectory(): string { return `${this.settings.scrapsRootDir}/${format( this.settings.scrapsPathFormat )}`; } - getScrapFileName(): string { - return format(this.settings.scrapsFileName, { ext: "md" }); + /** + * A formatted scrap file name based on the current settings. + * + * @note This will generate a new name each time it is accessed. + * @returns {string} The formatted scrap file name. + */ + get nextScrapFileName(): string { + return format(this.settings.scrapsFileName); } - getScrapFilePath(): string { - return `${this.getScrapDir()}/${this.getScrapFileName()}`; + get newScrapLeafType(): false | "tab" | "split" | "window" { + switch (this.settings.newScrapBehavior) { + case "tab": + return "tab"; + case "split": + return "split"; + case "window": + return "window"; + default: + return false; + } } - async createScrap() { + /** + * Creates a new scrap file in the designated scrap directory. + * + * @returns {Promise} A promise that resolves when the scrap file is created and opened. + */ + async createNewMarkdownScrap(): Promise { await this.ensureScrapDir(); const newScrap = await this.app.vault.create( - this.getScrapFilePath(), + `${this.scrapsDirectory}/${this.nextScrapFileName}.md`, "" ); - await this.app.workspace.getLeaf(false).openFile(newScrap); + await this.app.workspace + .getLeaf(this.newScrapLeafType) + .openFile(newScrap); } - async convertToScrap(rename = true) { + /** + * Moves the currently active file to the Scraps directory. + * + * @returns {Promise} A promise that resolves when the file has been converted to a scrap. + */ + async moveCurrentFileToScraps(): Promise { const currentFile = this.app.workspace.getActiveFile(); if (currentFile === null) { @@ -94,13 +129,38 @@ export default class ScrapsPlugin extends Plugin { await this.ensureScrapDir(); - const filename = rename ? this.getScrapFileName() : currentFile.name; - const renamePath = `${this.getScrapDir()}/${filename}`; + const renamePath = `${this.scrapsDirectory}/${currentFile.name}`; await this.app.fileManager.renameFile(currentFile, renamePath); } - async copyToScrap() { + /** + * Renames the currently active file to use a generated Scrap title + * + * @returns {Promise} A promise that resolves when the file has been renamed. + */ + async renameCurrentFileAsScrap(): Promise { + const currentFile = this.app.workspace.getActiveFile(); + + if (currentFile === null) { + new Notice("No file is currently open"); + return; + } + + await this.ensureScrapDir(); + + const renamePath = `${this.scrapsDirectory}/${this.nextScrapFileName}.${currentFile.extension}`; + + await this.app.fileManager.renameFile(currentFile, renamePath); + } + + /** + * Copies the currently active file to a designated "scrap" directory and opens the copied file. + * If no file is currently open, a notice is displayed to the user. + * + * @returns {Promise} A promise that resolves when the file has been copied and opened. + */ + async copyCurrentFileToScraps(): Promise { const currentFile = this.app.workspace.getActiveFile(); if (currentFile === null) { @@ -112,61 +172,71 @@ export default class ScrapsPlugin extends Plugin { const newScrap = await this.app.vault.copy( currentFile, - this.getScrapFilePath() + `${this.scrapsDirectory}/${currentFile.name}` ); - await this.app.workspace.getLeaf(false).openFile(newScrap); + await this.app.workspace + .getLeaf(this.newScrapLeafType) + .openFile(newScrap); } async onload() { await this.loadSettings(); this.addSettingTab(new ScrapsSettingTab(this.app, this)); + /////////////////////////////// + // Command: Create New Scrap // + /////////////////////////////// this.addCommand({ - id: "scraps-new", - name: "Scraps: Create new Scrap", - icon: ICON.New, + ...COMMANDS.New, callback: async () => { - this.createScrap(); + this.createNewMarkdownScrap(); }, }); - this.addRibbonIcon(ICON.New, "Create new Scrap", async () => { - this.createScrap(); + this.addRibbonIcon(COMMANDS.New.icon, COMMANDS.New.name, async () => { + this.createNewMarkdownScrap(); }); + ///////////////////////////// + // Command: Move to Scraps // + ///////////////////////////// this.addCommand({ - id: "scraps-convert", - name: "Scraps: Convert current file to Scrap", - icon: ICON.Convert, - editorCallback: async () => this.convertToScrap(), + ...COMMANDS.Move, + editorCallback: async () => this.moveCurrentFileToScraps(), }); - this.addRibbonIcon(ICON.Convert, "Convert file to Scrap", async () => { - this.convertToScrap(); + this.addRibbonIcon(COMMANDS.Move.icon, COMMANDS.Move.name, async () => { + this.moveCurrentFileToScraps(); }); + ///////////////////////////// + // Command: Copy to Scraps // + ///////////////////////////// this.addCommand({ - id: "scraps-move", - name: "Scraps: Move current file to Scraps", - icon: ICON.Move, - editorCallback: async () => this.convertToScrap(false), + ...COMMANDS.Copy, + editorCallback: () => this.copyCurrentFileToScraps(), }); - this.addRibbonIcon(ICON.Move, "Move file to Scraps", async () => { - this.convertToScrap(false); + this.addRibbonIcon(COMMANDS.Copy.icon, COMMANDS.Copy.name, async () => { + this.copyCurrentFileToScraps(); }); + /////////////////////////////// + // Command: Rename to Scraps // + /////////////////////////////// this.addCommand({ - id: "scraps-copy", - name: "Scraps: Copy current file to Scraps", - icon: ICON.Copy, - editorCallback: () => this.copyToScrap(), + ...COMMANDS.Rename, + editorCallback: () => this.copyCurrentFileToScraps(), }); - this.addRibbonIcon(ICON.Copy, "Copy file to Scraps", async () => { - this.copyToScrap(); - }); + this.addRibbonIcon( + COMMANDS.Rename.icon, + COMMANDS.Rename.name, + async () => { + this.renameCurrentFileAsScrap(); + } + ); } onunload() {} @@ -210,6 +280,25 @@ class ScrapsSettingTab extends PluginSettingTab { }) ); + new Setting(containerEl) + .setName("New Scrap Behavior") + .setDesc("The behavior when creating a new scrap") + .addDropdown((dropdown) => + dropdown + .addOptions({ + default: "Default", + tab: "Open in new tab", + split: "Split the current pane", + window: "Open in new window", + }) + .setValue(this.plugin.settings.newScrapBehavior) + .onChange(async (value) => { + this.plugin.settings.newScrapBehavior = + value as NewScrapBehaviorOption; + await this.plugin.saveSettings(); + }) + ); + const pathFormatSetting = new Setting(containerEl) .setName("Scraps Path Format") .setDesc( @@ -229,9 +318,9 @@ class ScrapsSettingTab extends PluginSettingTab { const fileFormatSetting = new Setting(containerEl) .setName("Scraps File Name Format") .setDesc( - `Preview: ${format(this.plugin.settings.scrapsFileName, { - ext: "md", - })}` + `Preview: ${ + format(this.plugin.settings.scrapsFileName) + ".md" + }` ) .addText((text) => text @@ -240,7 +329,7 @@ class ScrapsSettingTab extends PluginSettingTab { .onChange(async (value) => { this.plugin.settings.scrapsFileName = value; fileFormatSetting.setDesc( - `Preview: ${format(value, { ext: "md" })}` + `Preview: ${format(value) + ".md"}` ); await this.plugin.saveSettings(); }) diff --git a/pack.sh b/pack.sh new file mode 100755 index 0000000..365b2f7 --- /dev/null +++ b/pack.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +rm -rf ./dist +mkdir dist +cp main.js ./dist +cp manifest.json ./dist \ No newline at end of file diff --git a/package.json b/package.json index 9ee4159..b6ccdf7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "obsidian-sample-plugin", + "name": "obsidian-scraps", "version": "1.0.0", - "description": "This is a sample plugin for Obsidian (https://obsidian.md)", + "description": "Create and manage scraps of information in your vault.", "main": "main.js", "scripts": { "dev": "node esbuild.config.mjs", @@ -9,7 +9,7 @@ "version": "node version-bump.mjs && git add manifest.json versions.json" }, "keywords": [], - "author": "", + "author": "Endeavorance ", "license": "MIT", "devDependencies": { "@types/lodash-es": "^4.17.12", diff --git a/styles.css b/styles.css deleted file mode 100644 index 71cc60f..0000000 --- a/styles.css +++ /dev/null @@ -1,8 +0,0 @@ -/* - -This CSS file will be included with your plugin, and -available in the app when your plugin is enabled. - -If your plugin does not need CSS, delete this file. - -*/