From a31fdaf936bc0fa765889a8ca18b3507b8330818 Mon Sep 17 00:00:00 2001 From: Al0cam Date: Mon, 18 May 2026 09:45:31 +0200 Subject: [PATCH 1/3] feat: counting moves passed | failed --- IO/SettingsIO.ts | 40 ++++++----- Settings/Settings.ts | 8 +++ Utils/LoggerUtil.ts | 122 ++++++++++++++++++++++++++++++++++ Utils/MovingUtil.ts | 74 +++++++++++++++------ Utils/RuleMatcherUtil.ts | 28 ++++++-- main.ts | 140 +++++++++++++++++++++++++-------------- 6 files changed, 318 insertions(+), 94 deletions(-) create mode 100644 Utils/LoggerUtil.ts diff --git a/IO/SettingsIO.ts b/IO/SettingsIO.ts index e9c31d1..c0de89d 100644 --- a/IO/SettingsIO.ts +++ b/IO/SettingsIO.ts @@ -1,5 +1,6 @@ import type { AutoMoverSettings } from "Settings/Settings"; -import { Notice, TFile } from "obsidian"; +import loggerUtil from "Utils/LoggerUtil"; +import { TFile } from "obsidian"; import type { App } from "obsidian"; class SettingsIO { @@ -8,6 +9,11 @@ class SettingsIO { private constructor() {} + /** + * Returns the singleton instance of SettingsIO, creating it on first access. + * + * @returns SettingsIO + */ public static getInstance(): SettingsIO { if (!SettingsIO.instance) { SettingsIO.instance = new SettingsIO(); @@ -15,6 +21,12 @@ class SettingsIO { return SettingsIO.instance; } + /** + * Stores the Obsidian app reference used for vault and dialog access. + * + * @param app - The Obsidian App instance + * @returns void + */ public init(app: App): void { this.app = app; } @@ -57,11 +69,10 @@ class SettingsIO { const fs = require("node:fs"); fs.writeFileSync(filePath, settingsData); - new Notice("Settings exported successfully"); + loggerUtil.infoNotice("Settings exported successfully"); return true; } catch (error) { - console.error("Failed to export settings:", error); - new Notice("Failed to export settings"); + loggerUtil.errorNotice("Failed to export settings", error); return false; } } @@ -77,11 +88,10 @@ class SettingsIO { try { const filename = "AutoMover_settings.json"; await this.app.vault.create(filename, settingsData); - new Notice(`Settings exported to vault: ${filename}`); + loggerUtil.infoNotice(`Settings exported to vault: ${filename}`); return true; } catch (error) { - console.error("Failed to export to vault:", error); - new Notice("Failed to export settings to vault"); + loggerUtil.errorNotice("Failed to export settings to vault", error); return false; } } @@ -123,15 +133,14 @@ class SettingsIO { const importedSettings = JSON.parse(fileContent); if (!this.validateSettings(importedSettings)) { - new Notice("Invalid settings file format"); + loggerUtil.warnNotice("Invalid settings file format"); return null; } - new Notice("Settings imported successfully"); + loggerUtil.infoNotice("Settings imported successfully"); return importedSettings; } catch (error) { - console.error("Failed to import settings:", error); - new Notice("Failed to import settings"); + loggerUtil.errorNotice("Failed to import settings", error); return null; } } @@ -148,7 +157,7 @@ class SettingsIO { const file = this.app.vault.getAbstractFileByPath(filename); if (!file || !(file instanceof TFile)) { - new Notice(`Could not find ${filename} in vault`); + loggerUtil.warnNotice(`Could not find ${filename} in vault`); return null; } @@ -156,15 +165,14 @@ class SettingsIO { const importedSettings = JSON.parse(fileContent); if (!this.validateSettings(importedSettings)) { - new Notice("Invalid settings file format"); + loggerUtil.warnNotice("Invalid settings file format"); return null; } - new Notice("Settings imported from vault successfully"); + loggerUtil.infoNotice("Settings imported from vault successfully"); return importedSettings; } catch (error) { - console.error("Failed to import from vault:", error); - new Notice("Failed to import settings from vault"); + loggerUtil.errorNotice("Failed to import settings from vault", error); return null; } } diff --git a/Settings/Settings.ts b/Settings/Settings.ts index 07f3915..3ac4fd7 100644 --- a/Settings/Settings.ts +++ b/Settings/Settings.ts @@ -12,6 +12,7 @@ export interface AutoMoverSettings { projectRules: ProjectRule[]; automaticMoving: boolean; timer: number | null; // in miliseconds + debugLogging: boolean; collapseSections: { tutorial: boolean; movingRules: boolean; @@ -30,6 +31,7 @@ export const DEFAULT_SETTINGS: Partial = { projectRules: [], automaticMoving: false, timer: null, + debugLogging: false, collapseSections: { tutorial: false, movingRules: false, @@ -39,6 +41,12 @@ export const DEFAULT_SETTINGS: Partial = { }, }; +/** + * Loads the plugin settings by merging persisted data on top of DEFAULT_SETTINGS. + * + * @param AutoMoverPlugin - The plugin instance used to read persisted data + * @returns Partial + */ function loadSettings(AutoMoverPlugin: AutoMoverPlugin): Partial { return Object.assign({}, DEFAULT_SETTINGS, AutoMoverPlugin.loadData()); } diff --git a/Utils/LoggerUtil.ts b/Utils/LoggerUtil.ts new file mode 100644 index 0000000..8c08490 --- /dev/null +++ b/Utils/LoggerUtil.ts @@ -0,0 +1,122 @@ +import type AutoMoverPlugin from "main"; +import { Notice } from "obsidian"; + +class LoggerUtil { + private static instance: LoggerUtil; + private static readonly PREFIX = "[AutoMover]"; + private static readonly NOTICE_DURATION_MS = 5000; + private plugin: AutoMoverPlugin | null = null; + + private constructor() {} + + /** + * Returns the singleton instance of LoggerUtil, creating it on first access. + * + * @returns LoggerUtil + */ + public static getInstance(): LoggerUtil { + if (!LoggerUtil.instance) { + LoggerUtil.instance = new LoggerUtil(); + } + return LoggerUtil.instance; + } + + /** + * Wires the logger to the plugin so it can read the current debug toggle + * from settings on every call (settings may be reassigned on reload). + */ + public init(plugin: AutoMoverPlugin): void { + this.plugin = plugin; + } + + /** + * Reads the current debug toggle from plugin settings. + * + * @returns true if debug logging is enabled, false otherwise + */ + private isDebugEnabled(): boolean { + return this.plugin?.settings?.debugLogging === true; + } + + /** + * Logs a debug message to the console, prefixed with the plugin tag. + * No-op when debug logging is disabled in settings. + * + * @param args - Values to log + * @returns void + */ + public debug(...args: unknown[]): void { + if (!this.isDebugEnabled()) return; + console.debug(LoggerUtil.PREFIX, ...args); + } + + /** + * Logs an informational message to the console, prefixed with the plugin tag. + * + * @param args - Values to log + * @returns void + */ + public info(...args: unknown[]): void { + console.info(LoggerUtil.PREFIX, ...args); + } + + /** + * Logs a warning message to the console, prefixed with the plugin tag. + * + * @param args - Values to log + * @returns void + */ + public warn(...args: unknown[]): void { + console.warn(LoggerUtil.PREFIX, ...args); + } + + /** + * Logs an error message to the console, prefixed with the plugin tag. + * + * @param args - Values to log + * @returns void + */ + public error(...args: unknown[]): void { + console.error(LoggerUtil.PREFIX, ...args); + } + + /** + * Console.info + Obsidian Notice. `message` shows in both places; any + * additional args (e.g. an Error or rule object) are appended to the + * console line only. + */ + public infoNotice(message: string, ...extra: unknown[]): void { + console.info(LoggerUtil.PREFIX, message, ...extra); + new Notice(message, LoggerUtil.NOTICE_DURATION_MS); + } + + /** + * Console.warn + Obsidian Notice. `message` shows in both places; any + * additional args are appended to the console line only. + * + * @param message - The user-facing warning message + * @param extra - Optional values appended to the console line only + * @returns void + */ + public warnNotice(message: string, ...extra: unknown[]): void { + console.warn(LoggerUtil.PREFIX, message, ...extra); + new Notice(message, LoggerUtil.NOTICE_DURATION_MS); + } + + /** + * Console.error + Obsidian Notice. `message` shows in both places; any + * additional args (e.g. an Error or rule object) are appended to the + * console line only. + * + * @param message - The user-facing error message + * @param extra - Optional values appended to the console line only + * @returns void + */ + public errorNotice(message: string, ...extra: unknown[]): void { + console.error(LoggerUtil.PREFIX, message, ...extra); + new Notice(message, LoggerUtil.NOTICE_DURATION_MS); + } +} + +const loggerUtil = LoggerUtil.getInstance(); +export default loggerUtil; diff --git a/Utils/MovingUtil.ts b/Utils/MovingUtil.ts index 1e3c218..9e4ef5d 100644 --- a/Utils/MovingUtil.ts +++ b/Utils/MovingUtil.ts @@ -1,6 +1,18 @@ -import { Notice, TFile, TFolder } from "obsidian"; +import loggerUtil from "Utils/LoggerUtil"; +import { TFile, TFolder } from "obsidian"; import type { App } from "obsidian"; +/** + * Result of a single `MovingUtil.moveFile` call. + * + * - `moved` — file was renamed successfully + * - `no_op` — file is already at the destination, nothing to do + * - `duplicate` — a different file already exists at the destination path + * - `invalid_path` — destination folder didn't exist and could not be created + * - `error` — `vault.rename` rejected for any other reason (logged to console) + */ +export type MoveOutcome = "moved" | "no_op" | "duplicate" | "invalid_path" | "error"; + class MovingUtil { private static instance: MovingUtil; private app: App; @@ -40,26 +52,46 @@ class MovingUtil { } /** - * Moves the file to the destination path + * Moves a file into the destination folder, preserving its name. + * + * Behavior: + * - If the file is already at the destination, returns `"no_op"` without touching the vault. + * - If another file already occupies the destination path, returns `"duplicate"` + * (checked up-front via `getAbstractFileByPath` rather than by parsing rename errors). + * - If the destination folder doesn't exist, intermediate folders are created; failure + * to create them returns `"invalid_path"`. + * - Any other rename rejection is caught, logged, and returned as `"error"`. * * @param file - File to be moved - * @param newPath - Destination path - * @returns void + * @param newPath - Destination folder (the file's own name is appended automatically) + * @returns a {@link MoveOutcome} describing what happened */ - public moveFile(file: TFile, newPath: string): void { - if (this.isFolder(newPath)) { - this.app.vault.rename(file, `${newPath}/${file.name}`); - } else { - new Notice( - `Invalid destination path\n${newPath} is not a folder!\nCreating requested folder.`, - 5000, - ); - console.error( - `Invalid destination path\n${newPath} is not a folder!\nCreating requested folder.`, - ); - this.createMissingFolders(newPath).then(() => { - this.app.vault.rename(file, `${newPath}/${file.name}`); - }); + public async moveFile(file: TFile, newPath: string): Promise { + const targetPath = `${newPath}/${file.name}`; + + if (file.path === targetPath) { + return "no_op"; + } + + if (this.app.vault.getAbstractFileByPath(targetPath)) { + loggerUtil.debug(`Duplicate at "${targetPath}", skipping "${file.path}"`); + return "duplicate"; + } + + if (!this.isFolder(newPath)) { + const created = await this.createMissingFolders(newPath); + if (!created) { + loggerUtil.error(`Could not create destination folder "${newPath}"`); + return "invalid_path"; + } + } + + try { + await this.app.vault.rename(file, targetPath); + return "moved"; + } catch (e) { + loggerUtil.error(`Failed to move "${file.path}" -> "${targetPath}": ${e}`); + return "error"; } } @@ -74,8 +106,7 @@ class MovingUtil { if (this.isFolder(newPath)) { this.app.vault.rename(folder, `${newPath}/${folder.name}`); } else { - new Notice(`Invalid destination path\n${newPath} is not a folder!`, 5000); - console.error(`Invalid destination path\n${newPath} is not a folder!`); + loggerUtil.errorNotice(`Invalid destination path\n${newPath} is not a folder!`); } } @@ -91,8 +122,7 @@ class MovingUtil { return folder; }); } else { - new Notice("Folder already exists", 5000); - console.error("Folder already exists"); + loggerUtil.errorNotice("Folder already exists"); } return null; } diff --git a/Utils/RuleMatcherUtil.ts b/Utils/RuleMatcherUtil.ts index b5cf075..badf751 100644 --- a/Utils/RuleMatcherUtil.ts +++ b/Utils/RuleMatcherUtil.ts @@ -1,5 +1,6 @@ import type { MovingRule } from "Models/MovingRule"; import { ProjectRule } from "Models/ProjectRule"; +import loggerUtil from "Utils/LoggerUtil"; import type { TFile } from "obsidian"; class RuleMatcherUtil { @@ -8,6 +9,11 @@ class RuleMatcherUtil { private constructor() {} + /** + * Returns the singleton instance of RuleMatcherUtil, creating it on first access. + * + * @returns RuleMatcherUtil + */ public static getInstance(): RuleMatcherUtil { if (!RuleMatcherUtil.instance) { RuleMatcherUtil.instance = new RuleMatcherUtil(); @@ -38,15 +44,15 @@ class RuleMatcherUtil { public getMatchingRuleByName(file: TFile, rules: MovingRule[]): MovingRule | null { for (const rule of rules) { if (rule.regex == null || rule.regex === "") { - console.error("Rule does not have a regex: ", rule); + loggerUtil.error("Rule does not have a regex: ", rule); continue; } if (rule.folder == null || rule.folder === "") { - console.error("Rule does not have a destination folder: ", rule); + loggerUtil.error("Rule does not have a destination folder: ", rule); continue; } if (!this.isValidRegex(rule.regex)) { - console.error("Rule has an invalid regex: ", rule); + loggerUtil.error("Rule has an invalid regex: ", rule); continue; } const regex = this.getCompiledRegex(rule.regex); @@ -67,15 +73,15 @@ class RuleMatcherUtil { public getMatchingRuleByTag(tags: string[], rules: MovingRule[]): MovingRule | null { for (const rule of rules) { if (rule.regex == null || rule.regex === "") { - console.error("Tag Rule does not have a regex: ", rule); + loggerUtil.error("Tag Rule does not have a regex: ", rule); continue; } if (rule.folder == null || rule.folder === "") { - console.error("Tag Rule does not have a destination folder: ", rule); + loggerUtil.error("Tag Rule does not have a destination folder: ", rule); continue; } if (!this.isValidRegex(rule.regex)) { - console.error("Tag Rule has an invalid regex: ", rule); + loggerUtil.error("Tag Rule has an invalid regex: ", rule); continue; } @@ -104,6 +110,14 @@ class RuleMatcherUtil { return matches; } + /** + * Returns the regex groups from the first tag that matches the rule + * If no tag matches, returns null + * + * @param tags - The list of tags to test + * @param rule - The rule whose regex is applied to each tag + * @returns RegExpMatchArray | null + */ public getGroupMatchesForTags(tags: string[], rule: MovingRule): RegExpMatchArray | null { const regex = this.getCompiledRegex(rule.regex); // console.log("Compiled regex: ", regex); @@ -130,7 +144,7 @@ class RuleMatcherUtil { return true; } catch (e) { if (e instanceof SyntaxError) { - console.error(`Invalid regex pattern: ${e.message}`); + loggerUtil.error(`Invalid regex pattern: ${e.message}`); } return false; } diff --git a/main.ts b/main.ts index 03d3409..3eecf74 100644 --- a/main.ts +++ b/main.ts @@ -2,7 +2,8 @@ import settingsIO from "IO/SettingsIO"; import * as Settings from "Settings/Settings"; import { SettingsTab } from "Settings/SettingsTab"; import exclusionMatcherUtil from "Utils/ExclusionMatcherUtil"; -import movingUtil from "Utils/MovingUtil"; +import loggerUtil from "Utils/LoggerUtil"; +import movingUtil, { type MoveOutcome } from "Utils/MovingUtil"; import ruleMatcherUtil from "Utils/RuleMatcherUtil"; import timerUtil from "Utils/TimerUtil"; import * as obsidian from "obsidian"; @@ -11,6 +12,13 @@ import projectMatcherUtil from "Utils/ProjectMatcherUtil"; export default class AutoMoverPlugin extends obsidian.Plugin { settings: Settings.AutoMoverSettings; + /** + * Obsidian lifecycle hook called when the plugin is enabled. + * Initializes utilities, loads settings, registers the settings tab, + * file-open and automatic-moving events, the command, and the ribbon icon. + * + * @returns void + */ async onload() { // console.log("loading plugin"); movingUtil.init(this.app); @@ -19,6 +27,7 @@ export default class AutoMoverPlugin extends obsidian.Plugin { * Loading settings and creating the settings tab */ this.settings = Object.assign({}, Settings.DEFAULT_SETTINGS, await this.loadData()); + loggerUtil.init(this); this.addSettingTab(new SettingsTab(this.app, this)); this.automaticMoving(); @@ -39,7 +48,7 @@ export default class AutoMoverPlugin extends obsidian.Plugin { this.registerEvent( // since i am defining my own event, ts-lint is crying about it but it works in the end this.app.workspace.on("AutoMover:automatic-moving-update", () => { - console.log("Automatic moving update"); + loggerUtil.debug("Automatic moving update"); this.automaticMoving(); }), ); @@ -75,15 +84,34 @@ export default class AutoMoverPlugin extends obsidian.Plugin { * * @returns void */ - goThroughAllFiles() { + async goThroughAllFiles() { // console.log("Going through all files"); - const files = this.app.vault.getFiles(); - for (const file of files) { + const candidates: obsidian.TFile[] = []; + for (const file of this.app.vault.getFiles()) { if (file == null || file.path == null) continue; if (this.isFileExcluded(file)) continue; - this.matchAndMoveFile(file); + candidates.push(file); } - new obsidian.Notice("All files moved!", 5000); + + const results = await Promise.all(candidates.map((file) => this.matchAndMoveFile(file))); + + let moved = 0; + let duplicate = 0; + let failed = 0; + for (const result of results) { + if (result === "moved") moved++; + else if (result === "duplicate") duplicate++; + else if (result === "invalid_path" || result === "error") failed++; + } + + const lines: string[] = []; + if (moved > 0) lines.push(`${moved} ${moved === 1 ? "file" : "files"} moved.`); + if (duplicate > 0) + lines.push(`${duplicate} ${duplicate === 1 ? "file" : "files"} not moved - duplicate name.`); + if (failed > 0) + lines.push(`${failed} ${failed === 1 ? "file" : "files"} failed to move.`); + + loggerUtil.infoNotice(lines.length === 0 ? "No files needed to be moved." : lines.join("\n")); } /** @@ -100,53 +128,56 @@ export default class AutoMoverPlugin extends obsidian.Plugin { } /** - * Matches the file to the rule and moves it to the destination folder + * Matches the file to the first applicable rule (project, then name, then tag) + * and moves it. * * @param file - File to be matched and moved - * @returns void + * @returns the MoveOutcome from the matched rule, or null if no rule matched */ - matchAndMoveFile(file: obsidian.TFile): void { + async matchAndMoveFile(file: obsidian.TFile): Promise { // console.log("Moving file: ", file.path); - if (this.matchAndMoveByProject(file)) return; - else if (this.matchAndMoveByName(file)) return; - else this.matchAndMoveByTag(file); + const projectResult = await this.matchAndMoveByProject(file); + if (projectResult !== null) return projectResult; + + const nameResult = await this.matchAndMoveByName(file); + if (nameResult !== null) return nameResult; + + return this.matchAndMoveByTag(file); } /** - * Matches the file by name and moves it to the destination folder + * Matches the file by name and moves it to the destination folder. * * @param file - File to be matched and moved - * @returns true if the file was moved, false otherwise + * @returns the MoveOutcome, or null if no rule matched */ - matchAndMoveByName(file: obsidian.TFile): boolean { + async matchAndMoveByName(file: obsidian.TFile): Promise { const rule = ruleMatcherUtil.getMatchingRuleByName(file, this.settings.movingRules); - if (rule == null || rule.folder == null) return false; + if (rule == null || rule.folder == null) return null; if (ruleMatcherUtil.isRegexGrouped(rule)) { const matches = ruleMatcherUtil.getGroupMatches(file, rule); const finalDestinationPath = ruleMatcherUtil.constructFinalDesinationPath(rule, matches!); - movingUtil.moveFile(file, finalDestinationPath); - } else { - movingUtil.moveFile(file, rule.folder); + return movingUtil.moveFile(file, finalDestinationPath); } - return true; + return movingUtil.moveFile(file, rule.folder); } /** - * Matches the file by tags it contains and moves it to the destination folder + * Matches the file by tags it contains and moves it to the destination folder. * * @param file - File to be matched and moved - * @returns true if the file was moved, false otherwise + * @returns the MoveOutcome, or null if no rule matched */ - matchAndMoveByTag(file: obsidian.TFile): boolean { + async matchAndMoveByTag(file: obsidian.TFile): Promise { const cache = this.app.metadataCache.getFileCache(file); - if (cache == null) return false; + if (cache == null) return null; const tags = obsidian.getAllTags(cache); - if (tags == null || tags.length === 0) return false; + if (tags == null || tags.length === 0) return null; const tagRule = ruleMatcherUtil.getMatchingRuleByTag(tags, this.settings.tagRules); - if (tagRule == null || tagRule.folder == null) return false; + if (tagRule == null || tagRule.folder == null) return null; if (ruleMatcherUtil.isRegexGrouped(tagRule)) { const matches = ruleMatcherUtil.getGroupMatchesForTags(tags, tagRule); @@ -154,38 +185,43 @@ export default class AutoMoverPlugin extends obsidian.Plugin { // console.log("Tag rule: ", tagRule); // console.log("Tag matches: ", matches); const finalDestinationPath = ruleMatcherUtil.constructFinalDesinationPath(tagRule, matches!); - movingUtil.moveFile(file, finalDestinationPath); - } else { - movingUtil.moveFile(file, tagRule.folder); + return movingUtil.moveFile(file, finalDestinationPath); } - return true; + return movingUtil.moveFile(file, tagRule.folder); } - matchAndMoveByProject(file: obsidian.TFile): boolean { + /** + * Matches the file by its `Project` (or `project`) frontmatter value and + * moves it according to the matched project rule. If the project rule has + * inner rules, the first matching inner rule's destination is appended; + * otherwise (or on a "./" destination) the file is moved to the project root. + * + * @param file - File to be matched and moved + * @returns the MoveOutcome, or null if no project rule matched + */ + async matchAndMoveByProject(file: obsidian.TFile): Promise { const cache = this.app.metadataCache.getFileCache(file); - if (cache == null) return false; - if (cache.frontmatter == null) return false; - if (cache.frontmatter.Project == null && cache.frontmatter.project == null) return false; + if (cache == null) return null; + if (cache.frontmatter == null) return null; + if (cache.frontmatter.Project == null && cache.frontmatter.project == null) return null; const projectName: string = cache.frontmatter.Project || cache.frontmatter.project; const projectRule = projectMatcherUtil.getMatchingProjectRule(projectName, this.settings.projectRules); - if (projectRule == null || projectRule.folder == null) return false; + if (projectRule == null || projectRule.folder == null) return null; // If no rules defined, move to project root if (projectRule.rules == null || projectRule.rules.length === 0) { - console.log("No rules defined, moving to project root"); - movingUtil.moveFile(file, projectRule.folder); - return true; + loggerUtil.debug("No rules defined, moving to project root"); + return movingUtil.moveFile(file, projectRule.folder); } const rule = ruleMatcherUtil.getMatchingRuleByName(file, projectRule.rules); // If no rule matches or folder is "./", move to project root if (rule == null || rule.folder === "./") { - console.log("No matching rule or './' destination, moving to project root"); - movingUtil.moveFile(file, projectRule.folder); - return true; + loggerUtil.debug("No matching rule or './' destination, moving to project root"); + return movingUtil.moveFile(file, projectRule.folder); } // console.log("Project rule's moving rule found: ", rule); @@ -194,20 +230,26 @@ export default class AutoMoverPlugin extends obsidian.Plugin { const matches = ruleMatcherUtil.getGroupMatches(file, rule); const ruleDesinationPath = ruleMatcherUtil.constructFinalDesinationPath(rule, matches!); const finalDestinationPath = projectMatcherUtil.constructProjectDestinationPath(projectRule, ruleDesinationPath); - - movingUtil.moveFile(file, finalDestinationPath); - } else { - const finalDestinationPath = projectMatcherUtil.constructProjectDestinationPath(projectRule, rule.folder); - - movingUtil.moveFile(file, finalDestinationPath); + return movingUtil.moveFile(file, finalDestinationPath); } - return true; + const finalDestinationPath = projectMatcherUtil.constructProjectDestinationPath(projectRule, rule.folder); + return movingUtil.moveFile(file, finalDestinationPath); } + /** + * Reloads the plugin settings from disk, merged on top of DEFAULT_SETTINGS. + * + * @returns void + */ async asyncloadSettings() { this.settings = Object.assign({}, Settings.DEFAULT_SETTINGS, await this.loadData()); } + /** + * Obsidian lifecycle hook called when the plugin is disabled or reloaded. + * + * @returns void + */ async onunload() { // console.log("unloading plugin"); } From c3d9e388a03ee87740d7097d4f6748905ea8cace Mon Sep 17 00:00:00 2001 From: Al0cam Date: Mon, 18 May 2026 09:49:07 +0200 Subject: [PATCH 2/3] fix: add missing debug toggle button --- Settings/SettingsTab.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Settings/SettingsTab.ts b/Settings/SettingsTab.ts index a2f6439..de70463 100644 --- a/Settings/SettingsTab.ts +++ b/Settings/SettingsTab.ts @@ -161,5 +161,18 @@ export class SettingsTab extends PluginSettingTab { tagSection(containerEl, this.plugin, this.display); exclusionSection(containerEl, this.plugin, this.display); projectSection(containerEl, this.plugin, this.display); + + new Setting(containerEl).setName("Advanced").setHeading(); + new Setting(containerEl) + .setName("Debug logging") + .setDesc( + "Log verbose debug messages to the developer console (Ctrl+Shift+I). Leave off unless diagnosing an issue.", + ) + .addToggle((cb) => + cb.setValue(this.plugin.settings.debugLogging).onChange(async (value) => { + this.plugin.settings.debugLogging = value; + await this.plugin.saveData(this.plugin.settings); + }), + ); }; } From 2689d5357305f6d47090964731347d2b9c33b3b2 Mon Sep 17 00:00:00 2001 From: Al0cam Date: Mon, 18 May 2026 14:30:53 +0200 Subject: [PATCH 3/3] feat: move count with version bump --- Settings/SettingsTab.ts | 24 ++++++++++++------------ docs/ui-guide.md | 19 +++++++++++++++++++ main.ts | 15 +++++++++++---- manifest.json | 2 +- package.json | 2 +- versions.json | 3 ++- 6 files changed, 46 insertions(+), 19 deletions(-) diff --git a/Settings/SettingsTab.ts b/Settings/SettingsTab.ts index de70463..88b45c9 100644 --- a/Settings/SettingsTab.ts +++ b/Settings/SettingsTab.ts @@ -96,6 +96,18 @@ export class SettingsTab extends PluginSettingTab { }), ); + new Setting(containerEl) + .setName("Debug logging") + .setDesc( + "Log verbose debug messages to the developer console (Ctrl+Shift+I). Leave off unless diagnosing an issue.", + ) + .addToggle((cb) => + cb.setValue(this.plugin.settings.debugLogging).onChange(async (value) => { + this.plugin.settings.debugLogging = value; + await this.plugin.saveData(this.plugin.settings); + }), + ); + // TUTORIAL START /** * Instead of using the default .setting-item class I created a custom class to add other styling, @@ -162,17 +174,5 @@ export class SettingsTab extends PluginSettingTab { exclusionSection(containerEl, this.plugin, this.display); projectSection(containerEl, this.plugin, this.display); - new Setting(containerEl).setName("Advanced").setHeading(); - new Setting(containerEl) - .setName("Debug logging") - .setDesc( - "Log verbose debug messages to the developer console (Ctrl+Shift+I). Leave off unless diagnosing an issue.", - ) - .addToggle((cb) => - cb.setValue(this.plugin.settings.debugLogging).onChange(async (value) => { - this.plugin.settings.debugLogging = value; - await this.plugin.saveData(this.plugin.settings); - }), - ); }; } diff --git a/docs/ui-guide.md b/docs/ui-guide.md index b9487a9..cef22e7 100644 --- a/docs/ui-guide.md +++ b/docs/ui-guide.md @@ -20,6 +20,25 @@ The numbers are elaborate below the image. 11. **Duplicate rule button**: This button will duplicate the selected rule from the list of rules. +## Notifications for the Moving Toggles + +Each of the three moving triggers above (on-open, manual run, automatic timer) reports back differently: + +- **On-open**: when opening a file actually triggers a move, a short notice is shown - `1 file moved.`, `1 file not moved - duplicate name.`, or `1 file failed to move.`. Opening a file that doesn't match any rule stays silent so normal navigation isn't interrupted. +- **Manual run** (ribbon icon or `AutoMover: Move files` command): always reports a summary, including `No files needed to be moved.` when nothing matched. This is the path to use when you want explicit confirmation of what happened. +- **Automatic timer**: only shows a notice when at least one file was actually moved on that tick. Duplicate-name skips and move failures are intentionally suppressed here, because those conditions persist across ticks - a filename that collides today will still collide on the next tick, and a rule with a bad destination will keep failing - so reporting them every interval would spam notices indefinitely. If you need the full breakdown of duplicates and failures, trigger a manual run. + + +## Debug Logging Toggle + +Directly below the moving toggles there is a **Debug logging** toggle. It is placed there on purpose - the rule lists further down can grow long, and burying a developer-facing switch at the very bottom of the settings tab makes it awkward to reach. It is independent of the moving toggles above and controls verbose console output, not Obsidian notices. + +- When **off** (the default), only error-level messages and the user-facing notices described above are produced. This is the right setting for normal use. +- When **on**, the plugin writes extra `debug` and `info` messages to the developer console (open it with `Ctrl+Shift+I` in Obsidian). These include things like which rule matched a given file, why a move was skipped as a duplicate, and the path a file was renamed to. None of this appears as a notice in the editor - you have to open the dev tools to see it. + +Leave this off unless you are diagnosing an issue or filing a bug report. The verbose output is meant to make problems reproducible, not to be read during regular use. + + ## Export and Import By default, if you are using some way of syncing your obsidian vaults, this doesn't provide anyting new for you. diff --git a/main.ts b/main.ts index 3eecf74..58b1fae 100644 --- a/main.ts +++ b/main.ts @@ -37,11 +37,16 @@ export default class AutoMoverPlugin extends obsidian.Plugin { if (!this.areThereRulesToApply()) return; this.registerEvent( - this.app.workspace.on("file-open", (file: obsidian.TFile) => { + this.app.workspace.on("file-open", async (file: obsidian.TFile) => { if (!this.settings.moveOnOpen) return; if (file == null || file.path == null) return; if (this.isFileExcluded(file)) return; - this.matchAndMoveFile(file); + const result = await this.matchAndMoveFile(file); + if (result === "moved") loggerUtil.infoNotice("1 file moved."); + else if (result === "duplicate") + loggerUtil.infoNotice("1 file not moved - duplicate name."); + else if (result === "invalid_path" || result === "error") + loggerUtil.infoNotice("1 file failed to move."); }), ); @@ -75,7 +80,7 @@ export default class AutoMoverPlugin extends obsidian.Plugin { // console.log("Automatic moving run"); if (!this.settings.automaticMoving) return; if (this.settings.timer == null || this.settings.timer <= 0) return; - this.goThroughAllFiles(); + this.goThroughAllFiles({ silentIfUnchanged: true }); timerUtil.startTimer(this.automaticMoving.bind(this), this.settings.timer); } @@ -84,7 +89,7 @@ export default class AutoMoverPlugin extends obsidian.Plugin { * * @returns void */ - async goThroughAllFiles() { + async goThroughAllFiles(options: { silentIfUnchanged?: boolean } = {}) { // console.log("Going through all files"); const candidates: obsidian.TFile[] = []; for (const file of this.app.vault.getFiles()) { @@ -104,6 +109,8 @@ export default class AutoMoverPlugin extends obsidian.Plugin { else if (result === "invalid_path" || result === "error") failed++; } + if (options.silentIfUnchanged && moved === 0) return; + const lines: string[] = []; if (moved > 0) lines.push(`${moved} ${moved === 1 ? "file" : "files"} moved.`); if (duplicate > 0) diff --git a/manifest.json b/manifest.json index ed42339..026d064 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "auto-mover", "name": "AutoMover", - "version": "1.0.8", + "version": "1.0.9", "minAppVersion": "0.15.0", "description": "Move files and notes with specified names into their designated folders according to rules you define.", "author": "Al0cam", diff --git a/package.json b/package.json index 99cb5a4..4f3e6a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "AutoMover", - "version": "1.0.8", + "version": "1.0.9", "description": "AutoMover: Move files and notes with specified names into their designated folders according to rules you define.", "main": "main.js", "scripts": { diff --git a/versions.json b/versions.json index ab70f02..8dec088 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,4 @@ { - "1.0.8": "0.15.0" + "1.0.8": "0.15.0", + "1.0.9": "0.15.0" }