Build out lib dir

This commit is contained in:
Endeavorance 2024-10-28 11:03:02 -04:00
parent b93e472f8d
commit dea972cfda
11 changed files with 242 additions and 112 deletions

2
.gitignore vendored
View file

@ -20,3 +20,5 @@ data.json
# Exclude macOS Finder (System Explorer) View States # Exclude macOS Finder (System Explorer) View States
.DS_Store .DS_Store
dist

View file

@ -6,9 +6,10 @@ This plugin manages a folder in your Obsidian Vault which contains "Scraps": fil
## Commands ## Commands
- `Scraps: Create new Scrap` - Creates a new file based on the plugin settings - `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: 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 as a scrap - `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 ## Status

View file

@ -1,20 +1,28 @@
import { moment } from "obsidian"; import { moment } from "obsidian";
import { adjectives } from "adjectives.js";
import { capitalize, sample } from "lodash-es"; 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 <moment format>}: 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 formatted = str.trim();
let extension = options?.ext ?? "";
if (extension.length > 0 && !extension.startsWith(".")) {
extension = `.${extension}`;
}
// Replace {N} with a random noun // Replace {N} with a random noun
formatted = formatted.replace(/{N}/g, () => { formatted = formatted.replace(/{N}/g, () => {
return capitalize(sample(nouns)); return capitalize(sample(nouns));
@ -47,5 +55,5 @@ export function format(str: string, options?: FormatOptions): string {
} }
); );
return formatted + extension; return formatted;
} }

6
lib/resource/icons.ts Normal file
View file

@ -0,0 +1,6 @@
export const ICON = {
New: "file-plus",
Rename: "pen-line",
Move: "replace",
Copy: "copy-plus",
} as const;

27
lib/util.ts Normal file
View file

@ -0,0 +1,27 @@
import { Vault } from "obsidian";
export async function mkdirp(vault: Vault, folderPath: string): Promise<void> {
// 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;
}
}
}

261
main.ts
View file

@ -1,90 +1,125 @@
import { import { App, Plugin, PluginSettingTab, Setting, Notice } from "obsidian";
App, import { format } from "./lib/format.js";
Plugin, import { mkdirp } from "./lib/util.js";
PluginSettingTab, import { ICON } from "./lib/resource/icons.js";
Setting,
Vault,
Notice,
} from "obsidian";
import { format } from "./format.js";
interface ScrapsPluginSettings { type NewScrapBehaviorOption = "default" | "tab" | "split" | "window";
export interface ScrapsPluginSettings {
scrapsRootDir: string; scrapsRootDir: string;
scrapsPathFormat: string; scrapsPathFormat: string;
scrapsFileName: string; scrapsFileName: string;
newScrapBehavior: NewScrapBehaviorOption;
} }
const DEFAULT_SETTINGS: ScrapsPluginSettings = { const DEFAULT_SETTINGS: ScrapsPluginSettings = {
scrapsRootDir: "_Scraps", scrapsRootDir: "_Scraps",
scrapsPathFormat: "{time YYYY} Week {time WW MMM}", scrapsPathFormat: "{time YYYY} Week {time WW MMM}",
scrapsFileName: "{time DDDD} {A} {N}", scrapsFileName: "{time DDDD} {A} {N}",
newScrapBehavior: "default",
}; };
const ICON = { type CommandId = "New" | "Move" | "Copy" | "Rename";
New: "file-plus", interface CommandDef {
Convert: "shuffle", id: string;
Move: "replace", icon: string;
Copy: "copy-plus", name: string;
} as const;
async function mkdirp(vault: Vault, folderPath: string): Promise<void> {
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;
}
}
} }
const COMMANDS: Record<CommandId, CommandDef> = {
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 { export default class ScrapsPlugin extends Plugin {
settings: ScrapsPluginSettings; settings: ScrapsPluginSettings;
async ensureScrapDir(): Promise<string> { /**
const pathname = `${this.getScrapDir()}`; * Ensures that the scrap directory exists by creating it if it does not
* already exist.
await mkdirp(this.app.vault, pathname); *
* @returns {Promise<void>} A promise that resolves when the directory
return pathname; * has been ensured.
*/
async ensureScrapDir(): Promise<void> {
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( return `${this.settings.scrapsRootDir}/${format(
this.settings.scrapsPathFormat 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 { get newScrapLeafType(): false | "tab" | "split" | "window" {
return `${this.getScrapDir()}/${this.getScrapFileName()}`; 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<void>} A promise that resolves when the scrap file is created and opened.
*/
async createNewMarkdownScrap(): Promise<void> {
await this.ensureScrapDir(); await this.ensureScrapDir();
const newScrap = await this.app.vault.create( 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<void>} A promise that resolves when the file has been converted to a scrap.
*/
async moveCurrentFileToScraps(): Promise<void> {
const currentFile = this.app.workspace.getActiveFile(); const currentFile = this.app.workspace.getActiveFile();
if (currentFile === null) { if (currentFile === null) {
@ -94,13 +129,38 @@ export default class ScrapsPlugin extends Plugin {
await this.ensureScrapDir(); await this.ensureScrapDir();
const filename = rename ? this.getScrapFileName() : currentFile.name; const renamePath = `${this.scrapsDirectory}/${currentFile.name}`;
const renamePath = `${this.getScrapDir()}/${filename}`;
await this.app.fileManager.renameFile(currentFile, renamePath); await this.app.fileManager.renameFile(currentFile, renamePath);
} }
async copyToScrap() { /**
* Renames the currently active file to use a generated Scrap title
*
* @returns {Promise<void>} A promise that resolves when the file has been renamed.
*/
async renameCurrentFileAsScrap(): Promise<void> {
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<void>} A promise that resolves when the file has been copied and opened.
*/
async copyCurrentFileToScraps(): Promise<void> {
const currentFile = this.app.workspace.getActiveFile(); const currentFile = this.app.workspace.getActiveFile();
if (currentFile === null) { if (currentFile === null) {
@ -112,61 +172,71 @@ export default class ScrapsPlugin extends Plugin {
const newScrap = await this.app.vault.copy( const newScrap = await this.app.vault.copy(
currentFile, 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() { async onload() {
await this.loadSettings(); await this.loadSettings();
this.addSettingTab(new ScrapsSettingTab(this.app, this)); this.addSettingTab(new ScrapsSettingTab(this.app, this));
///////////////////////////////
// Command: Create New Scrap //
///////////////////////////////
this.addCommand({ this.addCommand({
id: "scraps-new", ...COMMANDS.New,
name: "Scraps: Create new Scrap",
icon: ICON.New,
callback: async () => { callback: async () => {
this.createScrap(); this.createNewMarkdownScrap();
}, },
}); });
this.addRibbonIcon(ICON.New, "Create new Scrap", async () => { this.addRibbonIcon(COMMANDS.New.icon, COMMANDS.New.name, async () => {
this.createScrap(); this.createNewMarkdownScrap();
}); });
/////////////////////////////
// Command: Move to Scraps //
/////////////////////////////
this.addCommand({ this.addCommand({
id: "scraps-convert", ...COMMANDS.Move,
name: "Scraps: Convert current file to Scrap", editorCallback: async () => this.moveCurrentFileToScraps(),
icon: ICON.Convert,
editorCallback: async () => this.convertToScrap(),
}); });
this.addRibbonIcon(ICON.Convert, "Convert file to Scrap", async () => { this.addRibbonIcon(COMMANDS.Move.icon, COMMANDS.Move.name, async () => {
this.convertToScrap(); this.moveCurrentFileToScraps();
}); });
/////////////////////////////
// Command: Copy to Scraps //
/////////////////////////////
this.addCommand({ this.addCommand({
id: "scraps-move", ...COMMANDS.Copy,
name: "Scraps: Move current file to Scraps", editorCallback: () => this.copyCurrentFileToScraps(),
icon: ICON.Move,
editorCallback: async () => this.convertToScrap(false),
}); });
this.addRibbonIcon(ICON.Move, "Move file to Scraps", async () => { this.addRibbonIcon(COMMANDS.Copy.icon, COMMANDS.Copy.name, async () => {
this.convertToScrap(false); this.copyCurrentFileToScraps();
}); });
///////////////////////////////
// Command: Rename to Scraps //
///////////////////////////////
this.addCommand({ this.addCommand({
id: "scraps-copy", ...COMMANDS.Rename,
name: "Scraps: Copy current file to Scraps", editorCallback: () => this.copyCurrentFileToScraps(),
icon: ICON.Copy,
editorCallback: () => this.copyToScrap(),
}); });
this.addRibbonIcon(ICON.Copy, "Copy file to Scraps", async () => { this.addRibbonIcon(
this.copyToScrap(); COMMANDS.Rename.icon,
}); COMMANDS.Rename.name,
async () => {
this.renameCurrentFileAsScrap();
}
);
} }
onunload() {} 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) const pathFormatSetting = new Setting(containerEl)
.setName("Scraps Path Format") .setName("Scraps Path Format")
.setDesc( .setDesc(
@ -229,9 +318,9 @@ class ScrapsSettingTab extends PluginSettingTab {
const fileFormatSetting = new Setting(containerEl) const fileFormatSetting = new Setting(containerEl)
.setName("Scraps File Name Format") .setName("Scraps File Name Format")
.setDesc( .setDesc(
`Preview: ${format(this.plugin.settings.scrapsFileName, { `Preview: ${
ext: "md", format(this.plugin.settings.scrapsFileName) + ".md"
})}` }`
) )
.addText((text) => .addText((text) =>
text text
@ -240,7 +329,7 @@ class ScrapsSettingTab extends PluginSettingTab {
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.scrapsFileName = value; this.plugin.settings.scrapsFileName = value;
fileFormatSetting.setDesc( fileFormatSetting.setDesc(
`Preview: ${format(value, { ext: "md" })}` `Preview: ${format(value) + ".md"}`
); );
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })

5
pack.sh Executable file
View file

@ -0,0 +1,5 @@
#!/usr/bin/env bash
rm -rf ./dist
mkdir dist
cp main.js ./dist
cp manifest.json ./dist

View file

@ -1,7 +1,7 @@
{ {
"name": "obsidian-sample-plugin", "name": "obsidian-scraps",
"version": "1.0.0", "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", "main": "main.js",
"scripts": { "scripts": {
"dev": "node esbuild.config.mjs", "dev": "node esbuild.config.mjs",
@ -9,7 +9,7 @@
"version": "node version-bump.mjs && git add manifest.json versions.json" "version": "node version-bump.mjs && git add manifest.json versions.json"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "Endeavorance <hello@endeavorance.camp>",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",

View file

@ -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.
*/