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
.DS_Store
dist

View file

@ -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

View file

@ -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 <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 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;
}

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 {
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<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;
}
}
type CommandId = "New" | "Move" | "Copy" | "Rename";
interface CommandDef {
id: string;
icon: string;
name: string;
}
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 {
settings: ScrapsPluginSettings;
async ensureScrapDir(): Promise<string> {
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<void>} A promise that resolves when the directory
* 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(
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<void>} A promise that resolves when the scrap file is created and opened.
*/
async createNewMarkdownScrap(): Promise<void> {
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<void>} A promise that resolves when the file has been converted to a scrap.
*/
async moveCurrentFileToScraps(): Promise<void> {
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<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();
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();
})

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",
"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 <hello@endeavorance.camp>",
"license": "MIT",
"devDependencies": {
"@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.
*/