Skip to content

Plugin Helpers

@mnaoumov/obsidian-lib provides some helpers to simplify your own Obsidian plugin.

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 with addChild(); the universal components, such as pluginNoticeComponent and consoleDebugComponent, 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 (default false) — 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 (default true) — the notice occupies the single per-plugin reusable slot, so the next reusable notice hides it. Pass false for a standalone notice that no later notice hides (multiple standalone notices coexist); standalone notices are still hidden together on unload.
  • requiresCloseConfirmation (default false) — 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 (implies isReusable: 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: true together with isReusable: false throws.
  • A hard-to-close notice is standalone, so requiresCloseConfirmation: true together with isReusable: true throws.
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 setting a render callback receives is a full SettingEx, so every custom component adder (addNumber(), addMultipleText(), addCodeHighlighter(), …) and bind() 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 a render callback, which runs only when the row is actually rendered.
  • disabled and visible accept a function, re-evaluated on every render and on every refreshDomState(). Prefer refreshDomState() when only those predicates change: it toggles the rendered DOM in place. Use refresh() only when the structure changes, i.e. rows are added or removed.
  • Native control definitions are supported too — getControlValue() / setControlValue() are wired to the settings component — but render + 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 override displayLegacy(), which Obsidian falls back to when the definitions are empty.

The most important methods in the execution order:

  • getSettingDefinitionItems() (or displayLegacy() 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 to fs.promises.rm(path, { recursive }), which throws ERR_FS_EISDIR for any directory when recursive is false - 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 the SettingEx.addCodeHighlighter() field.
  • registerCodeBlockLanguage({ editorMode, language, prismGrammar }) - additionally registers a CodeMirror 5 mode, which is what highlights a fenced code block inside the editor. editorMode is the mode name or MIME type the fence is highlighted as, e.g. text/typescript. Omit prismGrammar when the fence is replaced in reading view, e.g. rendered as a button.

grammar / prismGrammar accepts three forms:

  • a Grammar object - 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 call loadPrism() yourself. Use requirePrismLanguage('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'
});
}
}

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.