Repository navigation
API Reference
If you downloaded a plugin manually (e.g., from GitHub), here is how to install it:
- Extract the plugin folder if it was downloaded as a
.zip(it should contain amanifest.jsonand anindex.js). - Open Selah and go to Settings > Plugins.
- Click the Import Plugin button at the top.
- Select the plugin folder you extracted.
- 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.)
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.
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.
Plugins live in the .plugins/ folder inside your vault.
<vault>/.plugins/my-plugin/
โโโ manifest.json
โโโ index.js
โโโ data/ <-- Automatically created by Selah for storing plugin filesEvery 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.
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.
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) => {
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) => {
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) => {
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: () => {
editor.updateBlock(block, { props: { data: JSON.stringify({ clicked: true }) } });
}
}, "Click me")
]);
}
});
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!");
}
}
]
});Plugins have three distinct ways to interact with the Home Screen.
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 */ };
}
});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!"); }
});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>`;
}
});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
}));
}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);
}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" });
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");
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");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
)
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...");
});
Add custom templates to the / menu inside the editor.
selah.registerSlashItem({
title: "My Template",
icon: "๐",
aliases: ["tpl"],
markdown: "## Heading\n\n- [ ] Task",
});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 = ""; };
});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>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.
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 = `
<div style="padding: 20px; text-align: center;">
<h1 style="color: ${currentColor};">Welcome to My Tool!</h1>
<p style="color: var(--c-text-muted)">Your data folder is at: ${ctx.dir}</p>
</div>
`;
},
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 = `
<div style="padding: 10px; color: var(--c-text);">
<h3>My Tool Settings</h3>
<label>Theme Color: </label>
<input type="color" id="colorPicker" value="${currentColor}">
</div>
`;
const picker = container.querySelector("#colorPicker");
picker.addEventListener("input", (e) => {
selah.settings.set("color", e.target.value);
});
});