Skip to content

API Reference

Electric edited this page Nov 6, 2026 · 5 revisions

๐Ÿ“ฅ How to Install a Plugin

If you downloaded a plugin manually (e.g., from GitHub), here is how to install it:

  1. Extract the plugin folder if it was downloaded as a .zip (it should contain a manifest.json and an index.js).
  2. Open Selah and go to Settings > Plugins.
  3. Click the Import Plugin button at the top.
  4. Select the plugin folder you extracted.
  5. Selah will automatically install it and reload the plugin list.

(Note: The easiest way to install is via the Community Plugins tab in Selah, which handles downloading and verification automatically.)


Selah Plugin API Documentation

Welcome to the Selah Plugin API! Selah supports community plugins that can extend the app in powerful waysโ€”from adding entirely new views to your sidebar, to injecting custom widgets, shortcuts, and even interactive blocks directly into your notes.

โš ๏ธ Security & Trust Warning

Plugins are executable JavaScript code that run inside Selah with access to the app environment and Tauri APIs. Only install plugins from authors you trust.

While Selah provides a sandboxed file system (ctx.fs) for plugins to store their own data, this is an advisory boundary, not a hard security sandbox. Always review the code of a plugin before installing it.


Getting Started

Plugins live in the .plugins/ folder inside your vault.

Folder Structure

<vault>/.plugins/my-plugin/
โ”œโ”€โ”€ manifest.json
โ”œโ”€โ”€ index.js
โ””โ”€โ”€ data/           <-- Automatically created by Selah for storing plugin files

manifest.json

Every plugin must have a manifest.json at the root of its folder:

{
  "id": "my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "description": "What this plugin does",
  "author": "Your Name",
  "icon": "โœจ",
  "apiVersion": 1,
  "permissions": ["filesystem"]
}

Note: The id must be alphanumeric with dashes or underscores (no spaces). apiVersion must be 1. The permissions array is optional and is only used to warn the user on the plugin card.

index.js

The index.js file is executed when the app loads or when the plugin is reloaded. Selah passes an API object called selah into this file. Top-level await is supported.


๐Ÿงฉ Custom Blocks (The Powerhouse)

Plugins can create fully interactive React components that live directly inside .selah notes (like a Kanban board, a timer, or a stat tracker).

selah.registerBlockSpec({
    type: "my-custom-block",
    propSchema: { data: { default: "{}" } },
// 1. How to save the block to the note file
toExternalHTML: (props) =&gt; {
    const div = document.createElement("div");
    div.setAttribute("data-my-block", props.block.props.data);
    return div;
},

// 2. How to load the block when the note is opened
parse: (element) =&gt; {
    if (element.hasAttribute("data-my-block")) {
        return { data: element.getAttribute("data-my-block") };
    }
    return undefined;
},

// 3. How to render it (Receives React and createPortal!)
render: (props, React, createPortal) =&gt; {
    const { block, editor } = props;
    
    return React.createElement("div", {
        className: "bg-[var(--c-surface)] p-4 rounded-xl border border-[var(--c-border)]"
    }, [
        React.createElement("h3", { key: "title" }, "My Block"),
        React.createElement("button", { 
            key: "btn",
            onClick: () =&gt; {
                editor.updateBlock(block, { props: { data: JSON.stringify({ clicked: true }) } });
            }
        }, "Click me")
    ]);
}

});


๐Ÿ“‚ Category Types (Sidebar Views)

Category Types allow you to create entirely new views in Selah's sidebar. When rendering, you receive a ctx object with:

  • ctx.dir: Absolute path to this category's folder.
  • ctx.vaultPath: Absolute path to the vault.
  • ctx.fs: Sandboxed file storage (scoped to this category's folder).
  • ctx.sidebarContainer: An empty DOM element in the sidebar you can render into.
  • ctx.theme: An object containing Tailwind CSS classes (bg, surface, border, text, accent).
selah.registerCategoryType({
    id: "my-custom-view",
    label: "My Custom View",
    icon: "๐Ÿš€",
    layout: "default", // "default" shows sidebar. "full" currently behaves the same.
    render(container, ctx) {
        container.innerHTML = `<h1 style="color: var(--c-text)">Welcome to ${ctx.name}</h1>`;
        return () => { container.innerHTML = ""; }; // Cleanup
    },
    // Optional: Actions users can pin as Home Screen shortcuts
    shortcutActions: [
        {
            id: "do-something",
            label: "Do Something Cool",
            icon: "โœจ",
            run() {
                console.log("Shortcut clicked!");
            }
        }
    ]
});

๐Ÿ  Home Screen Integrations

Plugins have three distinct ways to interact with the Home Screen.

1. Home Widgets

Widgets are injected into specific slots on the default Home Screen. Users toggle these in Settings > Home > Widgets.

selah.registerHomeWidget({
    id: "my-widget",
    label: "My Widget",
    icon: "๐Ÿ“–",
    // Options: "header", "above-shortcuts", "below-shortcuts", "above-verse"
    slot: "above-verse", 
    render(container, ctx) {
        container.textContent = "Hello";
        return () => { /* optional cleanup */ };
    }
});

2. Home Shortcuts

Single buttons that appear on the Home Screen. Users toggle these in Settings > Home > Widgets.

selah.registerShortcut({
    id: "open-timer",
    label: "Start Timer",
    icon: "โฑ๏ธ",
    onClick() { console.log("Timer started!"); }
});

3. Custom Home Screens

Completely replaces the default Home layout (greeting, search bar, shortcuts). Note: The Psalm 19:14 verse is always pinned at the bottom and cannot be removed.

selah.registerHomeScreen({
    id: "sveltab-clone",
    label: "Sveltab Layout",
    description: "A completely custom home screen",
    render(container, ctx) {
        container.innerHTML = `<div style="color: var(--c-text); padding: 20px;">My Home Screen</div>`;
    }
});

๐Ÿ› ๏ธ Utilities & APIs

The Selah Rich Text Editor (selah.Editor)

Plugins have access to the exact same Rich Text Editor used in Selah's Daily Journal and Abba views. This means users can use [[ wiki-links and the / slash menu inside your plugin!

// Requires React and ReactDOM (passed as the 2nd and 3rd arguments to index.js)
render(container, ctx, React, ReactDOM) {
    const root = ReactDOM.createRoot(container);
    root.render(React.createElement(selah.Editor, { 
        path: ctx.dir + "/my-note.selah", 
        vaultPath: ctx.vaultPath 
    }));
}

Picking Notes from the Vault (selah.pickNote())

Opens the native file picker so the user can select a note from anywhere in their vault.

const notePath = await selah.pickNote();
if (notePath) {
    console.log("Selected:", notePath);
}

Sandboxed File System (selah.fs)

Store data safely in <plugin>/data/. Path traversal (..) is blocked.

// Reading and writing text
await selah.fs.writeText("config.json", JSON.stringify({ theme: "dark" }));
const text = await selah.fs.readText("config.json");

// Reading and writing binary files (images, audio) const bytes = await selah.fs.readFile("image.png"); await selah.fs.writeFile("audio.mp3", new Uint8Array([...])); await selah.fs.copyFile("a.png", "b.png");

// Other file operations const exists = await selah.fs.exists("config.json"); const files = await selah.fs.list(); // Returns an array of filenames await selah.fs.mkdir("subfolder"); // Creates a folder await selah.fs.remove("old_file.txt"); // Moves to system trash

// Open a native save dialog const savePath = await selah.fs.saveDialog({ defaultPath: "export.txt" });

Vault Access (Read-Only)

Plugins can read notes across the entire vault (perfect for dashboards). They cannot write to vault notes without user interaction.

// List files in a directory
const files = await selah.listVaultDir(ctx.vaultPath);

// Read the text of a note const rawText = await selah.readVaultText("/path/to/note.selah");

Persistent Settings (selah.settings)

Easily save plugin-wide settings without dealing with JSON files directly. Data is stored in <plugin>/data/settings.json.

selah.settings.set("userPreference", "dark");
const pref = selah.settings.get("userPreference", "light");

The Native Emoji Picker

You can use the exact same emoji picker that Selah uses.

// In your render function:
const [showPicker, setShowPicker] = React.useState(false);

React.createElement("button", { onClick: () => setShowPicker(true) }, "Pick Emoji"), showPicker && createPortal( React.createElement(selah.EmojiPicker, { theme: selah.currentEmojiTheme(), onEmojiClick: (emojiData) => { console.log("Picked:", emojiData.emoji); setShowPicker(false); } }), document.body )

Timers & Cleanup

Use selah.setInterval and selah.setTimeout instead of the native browser functions. Selah will automatically clean these up when the plugin is disabled, preventing memory leaks.

selah.setInterval(() => {
    console.log("This stops when the plugin unloads!");
}, 5000);

selah.onUnload(() => { console.log("Plugin is unloading..."); });

Slash Menu Items

Add custom templates to the / menu inside the editor.

selah.registerSlashItem({
    title: "My Template",
    icon: "๐Ÿ“",
    aliases: ["tpl"],
    markdown: "## Heading\n\n- [ ] Task",
});

Configuration UI

Provide a custom settings UI for your plugin that users access via the "Configure" button in the Plugins menu.

selah.registerConfiguration((container) => {
    container.innerHTML = "<h3>Plugin Settings</h3>";
    // Return a cleanup function (optional)
    return () => { container.innerHTML = ""; };
});

๐ŸŽจ Styling (CSS Variables)

To make your plugin match the user's selected theme, use Selah's CSS variables instead of hardcoding colors:

  • var(--c-bg) - Main background
  • var(--c-surface) - Panel background
  • var(--c-border) - Borders
  • var(--c-text) - Primary text
  • var(--c-text-muted) - Dimmer text
  • var(--c-accent) - Accent color (blue by default)
<div style="background: var(--c-surface); color: var(--c-text); border: 1px solid var(--c-border);">
  Hello World
</div>

โš™๏ธ Advanced Notes

Using React

Selah passes React and ReactDOM into the execution context as the second and third arguments. You do not need to bundle React with your plugin.

Note: The plugin engine uses new Function(), so JSX is not supported out of the box. You must use React.createElement() directly, or compile your plugin using a bundler like Vite or esbuild.


๐Ÿ’ก Full "Hello World" Example

Here is a complete index.js that registers a custom view, a home screen shortcut, and a setting. You can copy this into a new plugin folder to see how it all connects.

// index.js
selah.registerCategoryType({
    id: "my-tool",
    label: "My Tool",
    icon: "๐Ÿ› ๏ธ",
    render(container, ctx) {
        const currentColor = selah.settings.get("color", "var(--c-accent)");
    container.innerHTML = `
        &lt;div style="padding: 20px; text-align: center;"&gt;
            &lt;h1 style="color: ${currentColor};"&gt;Welcome to My Tool!&lt;/h1&gt;
            &lt;p style="color: var(--c-text-muted)"&gt;Your data folder is at: ${ctx.dir}&lt;/p&gt;
        &lt;/div&gt;
    `;
},
shortcutActions: [
    {
        id: "go-to-tool",
        label: "Open My Tool",
        icon: "๐Ÿ› ๏ธ",
        run() {
            alert("Go to the sidebar and click 'My Tool'!");
        }
    }
]

});

// Add a custom configuration button to the Plugins menu selah.registerConfiguration((container) => { const currentColor = selah.settings.get("color", "#3b82f6");

container.innerHTML = `
    &lt;div style="padding: 10px; color: var(--c-text);"&gt;
        &lt;h3&gt;My Tool Settings&lt;/h3&gt;
        &lt;label&gt;Theme Color: &lt;/label&gt;
        &lt;input type="color" id="colorPicker" value="${currentColor}"&gt;
    &lt;/div&gt;
`;

const picker = container.querySelector("#colorPicker");
picker.addEventListener("input", (e) =&gt; {
    selah.settings.set("color", e.target.value);
});

});