Plugin Helpers
@mnaoumov/obsidian-lib provides some helpers to simplify your own Obsidian plugin.
- Sample Plugin Extended - sample with different technologies included.
- create-obsidian-plugin - creates a plugin from scratch.
The links below contains the full documentation. Here in the docs we mention only the most important ones.
PluginBase is a base class for plugins, that has some additional useful features to standard Obsidian Plugin class.
import { PluginBase } from '@mnaoumov/obsidian-lib/plugin/plugin';
export class FooPlugin extends PluginBase {}The most important methods to override:
onloadImpl()- your load logic. Add child components here withaddChild(); the universal components, such aspluginNoticeComponentandconsoleDebugComponent, are already available.getPluginApis(),getPluginDependencies(),getPluginConflicts()- declare the APIs the plugin publishes and the plugins it needs or conflicts with. See Cross-plugin APIs.createTranslationsMap()- supply the plugin’s own translations.
Three are public Obsidian methods that PluginBase already implements, so you rarely override them: onload(), onExternalSettingsChange() and onunload(). Settings are loaded and saved by the plugin’s settings component, whose onLoadSettings() and onSaveSettings() hooks you override there; see the PluginSettingsComponentBase section below.
this.pluginNoticeComponent shows notices prefixed with the plugin name and tied to the plugin lifecycle. Prefer it over constructing an Obsidian Notice directly. It is a protected member of PluginBase, so reach it from inside your plugin class, or pass it to the components that need it.
import { PluginBase } from '@mnaoumov/obsidian-lib/plugin/plugin';
export class FooPlugin extends PluginBase { protected override onloadImpl(): void { this.pluginNoticeComponent.showNotice('Something happened'); }}showNotice(message, options?) accepts these options:
isPermanent(defaultfalse) — the notice stays until it is replaced, the plugin reloads, or the user dismisses it. There is at most one permanent notice per plugin.isReusable(defaulttrue) — the notice occupies the single per-plugin reusable slot, so the next reusable notice hides it. Passfalsefor a standalone notice that no later notice hides (multiple standalone notices coexist); standalone notices are still hidden together on unload.requiresCloseConfirmation(defaultfalse) — a hard-to-close notice: it never dismisses on a stray click and instead shows a close (X) button whose click opens a confirmation modal, dismissing the notice only if confirmed. It is shown with an infinite duration and is standalone (impliesisReusable: false).onHide— a callback invoked the first time the notice is hidden, whether by the user closing it, by a later reusable notice replacing it, by its duration elapsing, or on unload.
The implication rules are enforced — a contradictory combination throws:
- A permanent notice must be reusable, so
isPermanent: truetogether withisReusable: falsethrows. - A hard-to-close notice is standalone, so
requiresCloseConfirmation: truetogether withisReusable: truethrows.
import { PluginSettingsComponentBase } from '@mnaoumov/obsidian-lib/components/plugin-settings-component';
export class FooPluginSettings { public invalidCharacterAction: 'Remove' | 'Replace' = 'Remove'; public replacementCharacter = '_';}
export class FooPluginSettingsComponent extends PluginSettingsComponentBase<FooPluginSettings> {}The default settings are a default-constructed instance of the settings class, passed to the component’s constructor as pluginSettingsClass.
The most important methods in the execution order:
registerValidators()onLoadRecord()onSavingRecord()
import { PluginSettingsTabBase } from '@mnaoumov/obsidian-lib/plugin/plugin-settings-tab';
export class FooPluginSettingsTab extends PluginSettingsTabBase<FooPluginSettings> {}Obsidian 1.13 renders a settings tab from the definitions it returns, so a tab describes its rows instead of building them into a container. Override getSettingDefinitionItems() and build each row with settingEx(), grouping them with settingGroupEx():
import type { SettingDefinitionItem } from 'obsidian';
import { PluginSettingsTabBase } from '@mnaoumov/obsidian-lib/plugin/plugin-settings-tab';
export class FooPluginSettingsTab extends PluginSettingsTabBase<FooPluginSettings> { protected override getSettingDefinitionItems(): SettingDefinitionItem[] { return [ this.settingGroupEx({ heading: 'Invalid characters', items: [ this.settingEx({ desc: 'How to process invalid characters in the new title.', name: 'Invalid characters action', render: (setting) => { setting.addDropdown((dropdown) => { dropdown.addOptions({ Remove: 'Remove', Replace: 'Replace' }); this.bind({ onChanged: () => { this.refreshDomState(); }, propertyName: 'invalidCharacterAction', valueComponent: dropdown }); }); } }), this.settingEx({ disabled: () => this.pluginSettingsComponent.settings.invalidCharacterAction !== 'Replace', name: 'Replacement character', render: (setting) => { setting.addText((text) => { this.bind({ propertyName: 'replacementCharacter', valueComponent: text }); }); } }) ] }) ]; }}Notes:
- The
settingarendercallback receives is a fullSettingEx, so every custom component adder (addNumber(),addMultipleText(),addCodeHighlighter(), …) andbind()work exactly as they do in an imperative tab. getSettingDefinitionItems()must be a pure builder. Obsidian calls it when the tab is registered — at plugin load, long before the tab is opened — to index the settings for search. Put anything with a side effect inside arendercallback, which runs only when the row is actually rendered.disabledandvisibleaccept a function, re-evaluated on every render and on everyrefreshDomState(). PreferrefreshDomState()when only those predicates change: it toggles the rendered DOM in place. Userefresh()only when the structure changes, i.e. rows are added or removed.- Native
controldefinitions are supported too —getControlValue()/setControlValue()are wired to the settings component — butrender+bind()is the richer path: it carries validation messages, the reset-when-empty behavior, placeholders for default values, and the value converters. - A tab that has not migrated keeps working: leave
getSettingDefinitionItems()alone and overridedisplayLegacy(), which Obsidian falls back to when the definitions are empty.
The most important methods in the execution order:
getSettingDefinitionItems()(ordisplayLegacy()for the imperative fallback)settingGroupEx()/settingEx()bind()
app.vault.adapter.rmdir(path, recursive) does not honor recursive: false, and the two adapters get it wrong in opposite directions:
CapacitorAdapter(mobile) does not accept the argument at all - it always removes recursively, so a non-recursive call silently deletes the folder and everything under it.FileSystemAdapter(desktop) forwards it tofs.promises.rm(path, { recursive }), which throwsERR_FS_EISDIRfor any directory whenrecursiveisfalse- so the non-recursive call never succeeds, not even on an empty folder.
RmdirGuardComponent gives both platforms the same, correct behavior while it is loaded: a folder that still has children is refused with an error carrying code: 'ENOTEMPTY' and nothing is deleted, and an empty folder is removed. Calls that already pass recursive: true, and calls whose target is not a folder, are passed through untouched.
Emptiness is decided from the adapter, not from the vault file tree - the tree omits dot-prefixed and otherwise hidden entries, so a folder holding only hidden files reads as empty there and the guard would wave through exactly the deletion it exists to prevent.
It is deliberately opt-in. app.vault.adapter is shared by the whole app, so a library that installed this patch on its own would change rmdir semantics for every other plugin, including the ones that never asked for it. A plugin that deletes folders opts in for itself:
import { RmdirGuardComponent } from '@mnaoumov/obsidian-lib/components/rmdir-guard-component';import { PluginBase } from '@mnaoumov/obsidian-lib/plugin/plugin';
export class FooPlugin extends PluginBase { protected override onloadImpl(): void { this.addChild(new RmdirGuardComponent(this.app)); }}Check error.code === 'ENOTEMPTY' rather than instanceof - every plugin bundles its own copy of the library, so the error may cross a copy boundary.
SyntaxHighlightingComponent registers your own syntax highlighting languages and unregisters them when it is unloaded. Add it as a child of your plugin: const syntaxHighlightingComponent = this.addChild(new SyntaxHighlightingComponent());, as in the example below.
Obsidian highlights code through two independent registries, so there are two methods:
registerPrismLanguage({ grammar, language })- registers a Prism language, which highlights code in reading view and in every<pre><code>the library renders, including theSettingEx.addCodeHighlighter()field.registerCodeBlockLanguage({ editorMode, language, prismGrammar })- additionally registers a CodeMirror 5 mode, which is what highlights a fenced code block inside the editor.editorModeis the mode name or MIME type the fence is highlighted as, e.g.text/typescript. OmitprismGrammarwhen the fence is replaced in reading view, e.g. rendered as a button.
grammar / prismGrammar accepts three forms:
- a
Grammarobject - the grammar itself. - a
string- the name of an already registered language to alias, e.g.'typescript'. - a factory
({ prism, requirePrismLanguage }) => Grammar- builds the grammar from the loaded Prism module, so you never need to callloadPrism()yourself. UserequirePrismLanguage('javascript')to nest an existing language’s grammar into your own.
A missing language always throws - both when aliasing a language that is not registered and from requirePrismLanguage() - so a typo fails loudly instead of silently leaving your code without highlighting.
import { SyntaxHighlightingComponent } from '@mnaoumov/obsidian-lib/components/syntax-highlighting-component';import { PluginBase } from '@mnaoumov/obsidian-lib/plugin/plugin';
export class FooPlugin extends PluginBase { protected override async onloadImpl(): Promise<void> { const syntaxHighlightingComponent = this.addChild(new SyntaxHighlightingComponent()); await syntaxHighlightingComponent.registerCodeBlockLanguage({ editorMode: 'text/typescript', language: 'my-language', prismGrammar: 'typescript' }); }}Working with plugin settings
Section titled “Working with plugin settings”Most of the times, it’s enough to use this.pluginSettingsComponent.settings (pluginSettingsComponent is a protected member of PluginBase) which is just an alias to this.pluginSettingsComponent.settingsState.effectiveValues.
For more advanced scenarios, you can use this.pluginSettingsComponent.settingsState with the following properties:
inputValues- values as been set, even if they don’t pass validation.effectiveValues- set values, if they pass validation, or default value, otherwise.validationMessages- contains validation messages for each setting properties that don’t pass validation.