Skip to main content
Inkdown features a powerful plugin system that allows developers to extend functionality, customize behavior, and integrate with external services. Plugins are first-class citizens in the architecture, with access to the same APIs used by built-in features.

Plugin Architecture

Plugins in Inkdown are built on a foundation of platform-agnostic APIs, ensuring that plugins work seamlessly across Desktop, Web, and Mobile platforms without modification.

The Plugin Base Class

All plugins extend the Plugin base class from @inkdown/core:
LivePreviewPlugin.ts
import { Plugin } from '@inkdown/core';

export default class LivePreviewPlugin extends Plugin {
    async onload(): Promise<void> {
        console.log('LivePreviewPlugin loaded');
        // Initialize plugin functionality
    }

    async onunload(): Promise<void> {
        console.log('LivePreviewPlugin unloaded');
        // Cleanup resources
    }
}

Plugin Manifest

Every plugin requires a manifest that defines metadata:
manifest.ts
export const manifest: PluginManifest = {
    id: 'live-preview',
    name: 'Live Preview',
    version: '1.0.0',
    description: 'Renders markdown elements in real-time',
    author: 'Inkdown',
    minAppVersion: '0.1.0',
};
id
string
required
Unique identifier for the plugin (kebab-case recommended)
name
string
required
Display name shown in the UI
version
string
required
Semantic version (e.g., “1.0.0”)
description
string
Brief description of plugin functionality
author
string
Plugin author name or organization
minAppVersion
string
Minimum Inkdown version required

Plugin Capabilities

Plugins have access to a rich set of APIs to extend Inkdown’s functionality:

1. Commands

Register commands that users can execute via command palette or hotkeys:
this.addCommand({
    id: 'insert-timestamp',
    name: 'Insert Current Timestamp',
    hotkeys: [{ modifiers: ['Ctrl'], key: 't' }],
    callback: () => {
        const editor = this.app.editorRegistry.getActive();
        if (editor) {
            const timestamp = new Date().toISOString();
            editor.view.dispatch({
                changes: { from: editor.view.state.selection.main.from, insert: timestamp }
            });
        }
    }
});

2. Settings Tab

Create custom settings interfaces for your plugin:
import { PluginSettingTab } from '@inkdown/core';

class MyPluginSettingTab extends PluginSettingTab {
    constructor(app: App, plugin: MyPlugin) {
        super(app, plugin);
    }

    display(): void {
        const { containerEl } = this;
        
        containerEl.createEl('h2', { text: 'My Plugin Settings' });
        
        // Add settings controls
        new Setting(containerEl)
            .setName('Enable feature')
            .setDesc('Toggle this feature on or off')
            .addToggle(toggle => toggle
                .setValue(this.plugin.settings.enabled)
                .onChange(async (value) => {
                    this.plugin.settings.enabled = value;
                    await this.plugin.saveSettings();
                })
            );
    }
}

// In plugin onload:
this.addSettingTab(new MyPluginSettingTab(this.app, this));

3. Editor Extensions

Register CodeMirror extensions to customize editor behavior:
import { keymap } from '@codemirror/view';

this.registerEditorExtension(
    keymap.of([{
        key: 'Ctrl-k',
        run: (view) => {
            console.log('Custom key pressed');
            return true;
        }
    }])
);

4. Markdown Processing

Process markdown content with code block processors:
this.registerMarkdownCodeBlockProcessor('mermaid', (source, el, ctx) => {
    // Render mermaid diagram
    el.innerHTML = renderMermaidDiagram(source);
});
Or post-process rendered markdown:
this.registerMarkdownPostProcessor((el, ctx) => {
    // Add click handlers to all links
    const links = el.findAll('a');
    links.forEach(link => {
        link.addEventListener('click', (e) => {
            e.preventDefault();
            // Custom link handling
        });
    });
});

5. Custom Views

Register custom view types:
this.registerView('calendar-view', (container) => {
    return new CalendarView(container, this.app);
});

6. Status Bar Items

Add items to the status bar:
const statusBarItem = this.addStatusBarItem();
statusBarItem.setText('Word count: 0');

// Update on editor changes
this.registerEvent(
    this.app.workspace.on('editor-change', () => {
        const count = this.countWords();
        statusBarItem.setText(`Word count: ${count}`);
    })
);

7. Event Listeners

Listen to application events:
this.registerEvent(
    this.app.workspace.on('file-open', (file) => {
        console.log('File opened:', file.path);
    })
);

this.registerEvent(
    this.app.workspace.on('file-modify', (file) => {
        console.log('File modified:', file.path);
    })
);

Plugin Lifecycle

Plugins follow a well-defined lifecycle with automatic resource management:

Lifecycle Methods

Called when the plugin is loaded and enabled. This is where you should:
  • Load plugin settings
  • Register commands
  • Add setting tabs
  • Register event listeners
  • Initialize plugin state
async onload(): Promise<void> {
    await this.loadSettings();
    
    this.addCommand({
        id: 'my-command',
        name: 'My Command',
        callback: () => this.doSomething()
    });
    
    this.addSettingTab(new MySettingTab(this.app, this));
}
Called when the plugin is being disabled or unloaded. Handle custom cleanup here:
  • Close open dialogs
  • Cancel pending operations
  • Save final state
async onunload(): Promise<void> {
    // Save any pending data
    await this.savePendingChanges();
    
    // Close custom UI
    this.closeCustomDialogs();
}
Commands, event listeners, status bar items, and other resources registered through plugin methods are automatically cleaned up. You only need to handle custom cleanup.
Automatically called by the plugin manager to clean up registered resources:
  • Unregister commands
  • Remove status bar items
  • Clear event listeners
  • Remove setting tabs
  • Clear editor extensions
You should never call this method directly.

Plugin Settings

Plugins can persist settings using the built-in data storage API:
interface MyPluginSettings {
    apiKey: string;
    enableFeature: boolean;
    maxResults: number;
}

const DEFAULT_SETTINGS: MyPluginSettings = {
    apiKey: '',
    enableFeature: true,
    maxResults: 10
};

export default class MyPlugin extends Plugin {
    settings: MyPluginSettings;

    async onload() {
        await this.loadSettings();
        // Use this.settings...
    }

    async loadSettings() {
        this.settings = Object.assign(
            {},
            DEFAULT_SETTINGS,
            await this.loadData()
        );
    }

    async saveSettings() {
        await this.saveData(this.settings);
    }
}
Settings are automatically stored in the platform-appropriate location:
  • Desktop (Tauri): JSON files in the app config directory
  • Web: LocalStorage
  • Mobile: AsyncStorage

Built-in Plugins

Inkdown includes several built-in plugins that demonstrate best practices:

Live Preview

Renders markdown syntax in real-time, hiding markers and showing formatted text

Word Count

Displays word and character count in the status bar

Quick Finder

Fast file search with fuzzy matching

Slash Commands

Quick insertion commands triggered by /

Emoji

Emoji picker and autocomplete

Plugin Manager API

The PluginManager handles plugin registration, loading, and lifecycle:
// Check if a plugin is enabled
if (app.pluginManager.isPluginEnabled('live-preview')) {
    // Do something
}

// Enable a plugin
await app.pluginManager.enablePlugin('word-count');

// Disable a plugin
await app.pluginManager.disablePlugin('word-count');

// Get all plugins
const allPlugins = app.pluginManager.getAllPlugins();

// Get plugin instance
const plugin = app.pluginManager.getPlugin('live-preview');

// Listen for plugin changes
app.pluginManager.onPluginChange((pluginId, changeType) => {
    console.log(`Plugin ${pluginId} was ${changeType}`);
});

Cross-Platform Compatibility

Plugins remain platform-agnostic by using bridge patterns:
import { native } from '@inkdown/core';

// Access file system (works on Desktop, Web, Mobile)
const content = await native.fs.readFile('/path/to/file.md');

// Show dialog (if platform supports it)
if (native.dialog) {
    const result = await native.dialog.showSaveDialog({
        defaultPath: 'export.md'
    });
}

// Check platform capabilities
if (native.supports('nativeDialog')) {
    // Use native dialog
} else {
    // Use fallback dialog
}
Avoid using platform-specific APIs directly (like window, document, or Tauri APIs) in plugin code. Always use the provided bridge APIs for maximum compatibility.

Best Practices

Always use plugin registration methods for resources that need cleanup:
// Good - automatically cleaned up
this.registerDomEvent(document, 'click', handler);
this.registerInterval(() => doSomething(), 1000);

// Bad - you must manually clean up
document.addEventListener('click', handler);
setInterval(() => doSomething(), 1000);
Handle errors gracefully and show user-friendly messages:
try {
    await this.performOperation();
    this.showNotice('Operation successful!');
} catch (error) {
    console.error('Operation failed:', error);
    this.showNotice('Operation failed. Please try again.');
}
  • Debounce expensive operations
  • Use viewport-based processing for editor decorations
  • Cache computed values when possible
  • Lazy-load heavy dependencies
// Debounce expensive processing
const debouncedUpdate = debounce(() => {
    this.updateExpensiveView();
}, 300);

this.registerEvent(
    this.app.workspace.on('editor-change', debouncedUpdate)
);
  • Provide clear command names and descriptions
  • Use intuitive keyboard shortcuts
  • Show feedback for long operations
  • Validate user input
  • Handle edge cases gracefully

Community Plugins

Community plugins are installed from GitHub repositories:
// Install a community plugin
await app.communityPluginManager.installPlugin(
    'username/repo-name'
);

// Update a plugin
await app.communityPluginManager.updatePlugin('plugin-id');

// Uninstall a plugin
await app.communityPluginManager.uninstallPlugin('plugin-id');
Community plugins are loaded dynamically and have the same capabilities as built-in plugins.

Build Your First Plugin

Step-by-step guide to creating an Inkdown plugin

Plugin API Reference

Complete API documentation for plugin development

Architecture Overview

Understand Inkdown’s overall architecture

Cross-Platform

Learn about platform compatibility