# Kasane Plugin Development Guide
Kasane plugins are WASM components packaged as single `.kpk` artifacts.
Build one with `kasane plugin build`, install it with `kasane plugin install`,
and Kasane loads it from the plugins directory at startup.
Plugins describe *what* to display. The framework handles rendering,
layout, and cache invalidation.
For API details, see [plugin-api.md](./plugin-api.md). For composition semantics, see [semantics.md](./semantics.md).
## What Plugins Can Do
| Mechanism | Examples |
|---|---|
| `contribute_to()` | Line numbers, git markers, status bar widgets |
| `annotate_line()` | Cursor line highlight, indent guides |
| `contribute_overlay_v2()` | Color picker, tooltips, diagnostic popups |
| `transform()` | Status bar customization, menu layout changes, overlay repositioning |
| `display_directives()` | Code folding, line hiding, virtual text insertion |
| `define_projection()` | Named structural/additive display strategies (e.g. Semantic Zoom) |
| `handle_key()` + `handle_mouse()` | Interactive pickers, dialogs |
| `handle_default_scroll()` | Wheel policy, smooth scrolling |
| `Command::EditBuffer` | Structured buffer edits (insert, replace, delete) |
| `Command::InjectInput` | Programmatic key injection |
> Native plugins can also use `Surface` for sidebars and dedicated panels. See [Appendix A](#appendix-a-alternative-native-plugin).
## Quick Start
### Hello World (3 lines)
```bash
kasane plugin new my-hello --template hello
cd my-hello && kasane plugin build
```
This creates a minimal plugin:
```rust
kasane_plugin_sdk::define_plugin! {
manifest: "kasane-plugin.toml",
slots {
STATUS_RIGHT => plain(" Hello from my_hello! "),
},
}
```
The simple slot form `SLOT => expr` auto-wraps the expression in `auto_contribution()`. For full control, use the closure form `SLOT(deps) => |ctx| { ... }`.
### Progressive Learning Path
| Level | Template | What you learn | Key concepts |
|---|---|---|---|
| 1 | `hello` | Minimal plugin, slot contribution | `define_plugin!`, `plain()`, simple slot form |
| 2 | `contribution` | State, dirty flags, state caching | `state {}`, `#[bind]`, `dirty::BUFFER` |
| 3 | `annotation` | Per-line decoration | `annotate()` |
| 4 | `overlay` | Interactive UI, key handling | `handle_key()`, `overlay()`, `redraw()` |
| 5 | `process` | External processes, I/O events | `capabilities`, `on_io_event_effects()`, `is_ctrl_shift()` |
### Project Setup
```bash
kasane plugin new my-plugin # Default (contribution template)
kasane plugin new my-plugin --template hello # Minimal hello world
kasane plugin new my-highlighter --template annotation # Line annotation template
kasane plugin new my-transform --template transform # Element transform template
kasane plugin new my-overlay --template overlay # Interactive overlay template
kasane plugin new my-runner --template process # Process launcher template
```
This generates a ready-to-build project with `Cargo.toml`, `kasane-plugin.toml`, `src/lib.rs`, and `.gitignore`. You also need the `wasm32-wasip2` target:
```bash
rustup target add wasm32-wasip2
# or: kasane plugin doctor --fix
```
Manual setup (without kasane plugin new)
```toml
# Cargo.toml
[package]
name = "my-plugin"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
kasane-plugin-sdk = "0.5.0"
wit-bindgen = "0.53"
```
### Full Example: cursor-line
This plugin highlights the active cursor line via the `display` directive
stream. It is the canonical small bundled example.
```rust
kasane_plugin_sdk::define_plugin! {
manifest: "kasane-plugin.toml",
state {
#[bind(host_state::get_cursor_line(), on: dirty::BUFFER)]
active_line: i32 = -1,
},
display() {
if state.active_line < 0 {
return vec![];
}
let bg = theme_style_or(
"cursor.line.bg",
if is_dark_background() {
style_bg(rgb(40, 40, 50))
} else {
style_bg(rgb(220, 220, 235))
},
);
vec![style_line(state.active_line as u32, bg)]
},
}
```
**Key points:**
- **`define_plugin!`** combines `generate!()`, state declaration, `#[plugin]`, and `export!()` into a single macro. All sections are optional except `id` (or `manifest:` which supplies it).
- **`#[bind(expr, on: flags)]`** on state fields auto-generates sync code in `on_state_changed_effects()`. The expression is evaluated when the specified dirty flags are set.
- **`display() { … }`** declares display directives — the simplest extension surface for line styling, folds, virtual text, etc.
- **`theme_style_or(token, fallback)`** resolves a theme token if defined, otherwise uses the provided fallback style.
- The `dirty` and `modifiers` modules are auto-imported by `define_plugin!`.
- In mutable contexts (`handle_key`, `overlay`, `on_io_event`, etc.), `bump_generation()` is called automatically when the state guard drops.
A status-widget example using `slots {}` (the prior `sel-badge` example) lives in the future external `kasane-plugin-gallery` repo — recoverable from this repo's git history before the δ-3 cleanup.
### SDK Helpers Reference
Common helpers like `plain()`, `colored()`, `is_ctrl()`, `status_badge()`, `hex()`, `redraw()`, `send_command()`, and `paste_clipboard()` are available in all plugin code (emitted by `generate!()` / `define_plugin!`). `paste_clipboard()` specifically requests insertion of the host system clipboard contents; committed text input and bracketed paste payloads already flow through the text-input pipeline without using this command. For the full list including face/color construction, overlay layout, key escaping, and attribute constants, see [plugin-api.md §4.4](./plugin-api.md#44-sdk-helpers).
### Plugin Manifest
Every plugin project ships with a `kasane-plugin.toml` manifest file. The build step embeds that manifest into the generated `.kpk` package, and the host reads its static metadata **before** compiling or instantiating WASM — the plugin never participates in its own permission decisions.
```toml
[plugin]
id = "fuzzy_finder"
abi_version = "1.0.0"
[capabilities]
wasi = ["process"]
# ADR-052: WIT capability resource declarations. Each entry names a
# service for which the plugin may acquire an unforgeable handle via
# the matching `open-*` import (e.g. `open-buffer-view`). The
# `CapabilityBroker` denies acquisition for undeclared services. Known
# service names: "buffer".
[[capabilities.services]]
name = "buffer"
[authorities]
host = ["pty-process"]
[handlers]
flags = ["overlay", "input-handler", "io-handler", "contributor"]
transform_targets = ["kasane.buffer", "kasane.menu"]
publish_topics = ["cursor.line"]
subscribe_topics = ["theme.changed"]
extensions_defined = ["myplugin.status-items"]
extensions_consumed = ["other.ext"]
[view]
deps = ["buffer-content", "buffer-cursor", "menu-structure", "menu-selection"]
```
| Section | Required | Default | Purpose |
|---|---|---|---|
| `plugin.id` | Yes | — | Plugin identifier |
| `plugin.abi_version` | Yes | — | WIT package version the plugin targets (see [ABI Versioning Policy](abi-versioning.md)) |
| `capabilities.wasi` | No | `[]` | WASI capabilities for sandbox construction |
| `[[capabilities.services]]` | No | `[]` | ADR-052 service capability declarations (each entry has `name` matching a known service like `"buffer"`) |
| `authorities.host` | No | `[]` | Host authorities for privileged effects |
| `handlers.flags` | No | `[]` (→ all) | Handler capability bitmask (empty = all-set) |
| `handlers.transform_targets` | No | `[]` | Transform target names for interference detection |
| `handlers.publish_topics` | No | `[]` | Pub/sub topics this plugin publishes |
| `handlers.subscribe_topics` | No | `[]` | Pub/sub topics this plugin subscribes to |
| `handlers.extensions_defined` | No | `[]` | Extension points defined by this plugin |
| `handlers.extensions_consumed` | No | `[]` | Extension points consumed by this plugin |
| `view.deps` | No | `[]` (→ ALL) | Dirty-flag subscription (empty = all flags) |
| `settings..type` | No | — | Setting type: `"bool"`, `"integer"`, `"float"`, `"string"` |
| `settings..default` | No | — | Default value (must match type) |
| `settings..description` | No | — | Human-readable description |
Example with settings:
```toml
[settings.enabled]
type = "bool"
default = false
description = "Enable smooth scrolling animation"
```
In `define_plugin!`, use the `manifest:` section instead of `id:`, `capabilities:`, and `authorities:`:
```rust
kasane_plugin_sdk::define_plugin! {
manifest: "kasane-plugin.toml",
state { /* ... */ },
// ...
}
```
The macro reads the TOML at compile time and generates `get_id()`, `requested_capabilities()`, `requested_authorities()`, and `view_deps()` from its contents. `manifest:` is mutually exclusive with `id:`, `capabilities:`, and `authorities:`.
At build time, Kasane packages the manifest and compiled WASM component into a single `.kpk` artifact. The plugins directory contains `.kpk` files, not loose `.wasm` binaries.
### Plugin Profiles
| Profile | Sections | Template | Example |
|---|---|---|---|
| Status widget | `state` (`#[bind]`), `slots` | `contribution` | sel-badge (gallery) |
| Line annotator | `state` (`#[bind]`), `annotate` | `annotation` | cursor-line, color-preview |
| Element transformer | `state` (`#[bind]`), `transform` | `transform` | prompt-highlight (gallery) |
| Display transform | `state`, `on_state_changed_effects`, `display_directives` | — | virtual-text-demo (gallery, native) |
| Structural projection | `state`, `define_projection`, `on_key_map` | — | semantic-zoom (builtin) |
| Interactive overlay | `state`, `handle_key`, `overlay` | `overlay` | session-ui (gallery) |
| Process launcher | Above + `on_io_event_effects`, `capabilities` | `process` | fuzzy-finder (gallery) |
| Scroll policy | `handle_default_scroll` | — | smooth-scroll (gallery) |
| Kakoune-side bindings | `on_active_session_ready_effects` + `kak::*` helpers | — | kakoune-bindings-demo (gallery) |
"(gallery)" entries refer to plugins moved to the future external
`kasane-plugin-gallery` repo (δ-3 cleanup); recover from this repo's git
history before that commit.
Available `define_plugin!` sections: `manifest` or `id`, `state` (with optional `#[bind]`), `settings`, `on_init_effects`, `on_active_session_ready_effects`, `on_state_changed_effects`, `update_effects`, `slots`, `annotate`, `transform`, `transform_patch`, `transform_priority`, `overlay`, `handle_key`, `handle_mouse`, `handle_default_scroll`, `capabilities`, `authorities`, `on_io_event_effects`.
### Registering Kakoune-side APIs at session-ready
Use `on_active_session_ready_effects` together with the
[`kasane_plugin_sdk::kak`](https://docs.rs/kasane-plugin-sdk/latest/kasane_plugin_sdk/kak/index.html)
module and the `kakoune_setup_effects!` macro to register options, commands,
user modes, and key maps when the active session becomes transport-ready.
Each `kak::*` helper encodes the correct idempotency idiom for its command
(e.g. `try %[ declare-user-mode … ]`, `define-command -override`), and the
`kakoune_setup_effects!` macro sends each entry as its own `SendKeys` for
failure isolation. See the
[Plugin Cookbook entry](./plugin-cookbook.md#register-kakoune-apis-at-session-ready).
The `kakoune-bindings-demo` example moved to the future external
`kasane-plugin-gallery` (δ-3 cleanup) — recover from git history if needed.
As of WIT 4.0.0 (ADR-041), `eval-command(string)` is a case of the
`session-ready-command` variant, so `kakoune_setup_effects!` composes
over `EvalCommand` at both session-ready and runtime callsites without
the `:cmd` keystroke-simulation wrapping.
### Structured command construction (`kak_cmd`)
For plugins that compose Kakoune commands programmatically — e.g., a
fuzzy-finder building a `define-command` body from user input — prefer
the
[`kasane_plugin_sdk::kak_cmd`](https://docs.rs/kasane-plugin-sdk/latest/kasane_plugin_sdk/kak_cmd/index.html)
module's `KakCommand` enum (ADR-043). Each variant is a Rust value
with builder methods for optional flags, and rendering centralises all
quoting / escaping rules in one place:
```rust
use kasane_plugin_sdk::kak_cmd::{KakCommand, DeclareUserMode, DefineCommand, Map, Scope};
let setup: Vec = vec![
DeclareUserMode::new("sprout").try_idempotent().into(),
DefineCommand::new("bump", "increment-counter")
.override_existing()
.docstring("bump the sprout counter")
.into(),
Map::new(Scope::Global, "sprout", "b", ":bump")
.docstring("bump")
.into(),
];
let strings: Vec = setup.iter().map(KakCommand::render).collect();
```
The renderer cannot produce an unknown flag (the builder doesn't expose
one). Plugins that prefer one-liner strings still have the `kak::*`
module; plugins that hand-compose can validate with `kak_lint!`. The
three layers cover different niches:
| Layer | Best for |
|---|---|
| `kak::*` | single command, one-liner, no composition |
| `kak_lint!` | hand-composed literal strings that need flag validation |
| `kak_cmd::KakCommand` | programmatic composition, inspection, transformation |
### Compile-time validation of raw command strings
When a plugin composes a Kakoune command string by hand — i.e., not via a
`kak::*` helper — wrap the literal in
[`kasane_plugin_sdk::kak_lint!`](https://docs.rs/kasane-plugin-sdk/latest/kasane_plugin_sdk/macro.kak_lint.html)
to catch typo'd or invalid flags at compile time.
```rust
use kasane_plugin_sdk::{kak_lint, Command};
// Valid — compiles, expands to the input string literal.
let ok = Command::EvalCommand(kak_lint!("write").to_string());
// Invalid — compile error:
// unknown flag `-override` for Kakoune command `declare-user-mode`.
// accepted flags: -docstring, -hidden
// let bad = kak_lint!("declare-user-mode -override 'sprout'");
```
The motivating bug for the linter was sprout's session-ready setup which
passed `-override` to `declare-user-mode` — a flag Kakoune silently
rejects — causing all 11 user-mode keymaps to fail to register (Issue
\#93). The linter's catalog covers the most common setup commands
(`declare-user-mode`, `define-command`, `map`, `declare-option`,
`set-option`, `evaluate-commands`, `hook`, `alias`, `echo`, `info`,
`try`, `unset-option`). Commands not in the catalog pass through
unchanged so the macro never produces a false positive against a real
Kakoune command. To expand coverage, edit `CATALOG` in
`kasane-plugin-sdk-macros/src/lint.rs`.
### Build & Deploy
```bash
kasane plugin build # Build a .kpk package (release)
kasane plugin install # Build or verify a .kpk package, then activate it
kasane plugin dev [path] # Build (debug), install, and watch for changes
kasane plugin dev --release # Same, but release builds
```
`kasane plugin install` installs a `.kpk` package into the package store under `~/.local/share/kasane/plugins/` (or the path configured in `kasane.kdl`) and updates `plugins.lock`.
`kasane plugin dev` does the same as `install`, then watches `src/`, `Cargo.toml`, and `kasane-plugin.toml` for changes and automatically rebuilds and reinstalls. By default it uses debug builds for faster iteration; add `--release` for optimized builds. A running Kasane instance picks up the updated plugin via the `.reload` sentinel file without restart.
WASM plugin ABI note: current Kasane releases expect
`kasane:plugin@6.5.0` (adds the `host-capabilities` interface and the
first capability resource — `buffer-view` — per ADR-052 chunk 1).
ABI 6.4.0 added `get-display-cells-str` cluster-aware batch primitive
per [#111](https://github.com/Yus314/kasane/issues/111).
ABI 6.3.0 added `emit-diagnostic` plugin-emitted diagnostic command per
[#106](https://github.com/Yus314/kasane/issues/106). ABI 6.2.0 added
`cell-metrics` + 3 font/cell accessors per
[#109](https://github.com/Yus314/kasane/issues/109). ABI 6.1.0 added
`get-display-cells` per [#108](https://github.com/Yus314/kasane/issues/108).
ABI 6.0.0 was Phase β-4 — removing the retired `evaluate-extension`
export per [ADR-045](decisions/adr-045-retire-the-extension-point-dispatch-path.md).
ABI 5.0.0 is the [ADR-044](decisions/adr-044-handler-effect-tier-hierarchy.md)
tier-hierarchy split. 6.x plugins (6.0.0 – 6.4.0) continue to
load against 6.5.0 hosts (additive bumps). Rebuild and reinstall any
plugin built against 5.x or earlier; those binaries are rejected at
load time. See [`docs/migration/0.6-to-0.7.md` §8.3](migration/0.6-to-0.7.md)
for the per-handler tier mapping.
### Migrating to ABI 1.0.0
If you are upgrading a plugin from `0.25.0`, the required changes are:
1. Update the SDK crate to `kasane-plugin-sdk = "0.5"` and set
`abi_version = "1.0.0"` in `kasane-plugin.toml`.
2. Rename `face`/`Face` to `style`/`Style` (struct fields, helper names,
types). The SDK ships a `style_full(fg, bg, underline_color, attrs)`
helper that decomposes the legacy `attributes::*` bitset into the new
`Style` fields (`font_weight`, `font_slant`, `underline`, etc.). Direct
`Face { … }` literals must be rewritten as `Style { … }` with all 12
fields (or use `default_style()` as a base).
3. Rename `Color`/`color` to `Brush`/`brush`: `Color::DefaultColor` →
`Brush::DefaultColor`, `Color::Named(...)` → `Brush::Named(...)`,
`Color::Rgb(...)` → `Brush::Rgb(...)`. The variant names are unchanged.
4. Rename helper functions: `default_face` → `default_style`, `face_fg` →
`style_fg`, `face_bg` → `style_bg`, `face(fg, bg)` → `style_with(fg, bg)`,
`theme_face_or` → `theme_style_or`, `get_theme_face` → `get_theme_style`,
`face_merge::*` → `style_merge::*`.
5. Rebuild and reinstall the `.wasm`. Existing artifacts built against
`0.25.0` are rejected at load time by `kasane:plugin@6.5.0` hosts.
The `style` record exposes `font_weight: u16`, `font_slant`,
`font_features`, `font_variations`, `letter_spacing`, `underline` /
`strikethrough` of `text-decoration`, plus `blink`, `reverse`, `dim`.
Plugins receive style in post-resolve form — Kakoune's `final_*`
resolution flags are a host-internal concern and do not appear in the
plugin-facing record.
To see installed plugins or diagnose environment issues:
```bash
kasane plugin list # List installed plugins
kasane plugin gc # Remove unreferenced package artifacts from the store
kasane plugin rollback # Restore the previous active plugin set
kasane plugin doctor # Check toolchain, SDK version, and plugin health
kasane plugin doctor --fix # Auto-fix missing target and plugins directory
```
Manual build & deploy
```bash
cargo build --target wasm32-wasip2 --release
kasane plugin build
kasane plugin install target/kasane/my-plugin-0.1.0.kpk
```
### Transform Example: prompt-highlight
This plugin demonstrates `transform` — the mechanism for wrapping or
replacing existing UI elements. It highlights the status bar when the editor
enters prompt mode (`:`, `/`, etc.).
```rust
kasane_plugin_sdk::define_plugin! {
id: "prompt_highlight",
state {
#[bind(host_state::get_cursor_mode(), on: dirty::STATUS)]
cursor_mode: u8 = 0,
},
transform(target, subject, _ctx) {
if *target != TransformTarget::STATUS_BAR {
return subject;
}
if state.cursor_mode != 1 {
return subject;
}
match subject {
TransformSubject::Element(element) => {
TransformSubject::Element(
container(element)
.style(face(named(NamedColor::Black), named(NamedColor::Yellow)))
.build(),
)
}
other => other,
}
},
transform_priority: 0,
}
```
**Key points:**
- **`transform(target, subject, ctx)`** receives a `TransformSubject` — either `Element(Element)` for non-overlay targets or `Overlay(Overlay)` for overlay targets. Return it unchanged for passthrough, or pattern-match and wrap.
- **`TransformTarget`** selects which UI component to transform (e.g., `StatusBar`, `Buffer`, `Menu`). Ignore targets your plugin doesn't handle.
- **`transform_priority`** (default `0`) controls ordering in the transform chain. Higher priority = applied first (inner).
- **`transform_patch(target, ctx)`** is a declarative alternative that returns `Vec` instead of imperatively transforming the subject. Pure patches are Salsa-memoizable. See [plugin-cookbook.md](./plugin-cookbook.md#declarative-transform-wasm) for an example.
### Scroll Policy Example: smooth-scroll
This plugin demonstrates `handle_default_scroll()` — a policy hook that runs
after core has classified the event as a default buffer scroll candidate, but
before fallback scroll behavior is applied. It also shows the `settings {}` block
for typed configuration.
```rust
kasane_plugin_sdk::define_plugin! {
manifest: "kasane-plugin.toml",
settings {
enabled: bool = false,
}
handle_default_scroll(candidate) {
if !__setting_enabled() {
return None;
}
Some(ScrollPolicyResult::Plan(ScrollPlan {
total_amount: candidate.resolved.amount,
line: candidate.resolved.line,
column: candidate.resolved.column,
frame_interval_ms: 16,
curve: ScrollCurve::Linear,
accumulation: ScrollAccumulationMode::Add,
}))
},
}
```
The `settings {}` block generates `__setting_enabled() -> bool` which calls
`host_state::get_setting_bool("enabled")` with the manifest default as fallback.
When `manifest:` is present, the macro validates at compile time that each setting
exists in the manifest's `[settings.*]` and types match.
**Key points:**
- **`handle_default_scroll(candidate)`** only runs for default buffer scroll candidates. It does not override info popups, drag-scroll routing, or other core-owned scroll paths.
- **Return `ScrollPolicyResult::Plan(...)`** to let the host runtime execute time-based scrolling. The plugin does not tick frames itself.
- **Return `None`** to let the next scroll-policy plugin decide. For exact `None` / `Pass` / `Suppress` / `Immediate` semantics, see [`plugin-api.md`](./plugin-api.md#34-input-handling).
- The source example was previously at `examples/wasm/smooth-scroll/`; it moved to the future external `kasane-plugin-gallery` (δ-3 cleanup). Recover from this repo's git history before that commit.
## Theme Integration with Kakoune
Plugins that emit their own overlay text (markers, virtual lines, status badges) often want to colour-match the user's active Kakoune colorscheme — e.g., rendering Markdown heading markers in the same colour as Kakoune's `keyword` face.
**This is not currently possible.** The Kakoune `kak -ui json` protocol strips face names at the wire boundary: `set-face keyword rgb:...` modifies Kakoune's internal face registry, but atoms in `draw` messages carry already-resolved `WireFace` values with no back-reference. The host has no `face_name → style` lookup to expose.
See [kakoune-protocol-constraints.md §4.5](./kakoune-protocol-constraints.md#45-face-name-stripping-plugin-theme-integration) for the constraint analysis and [upstream-dependencies.md §3 (D-005)](./upstream-dependencies.md#3-fully-blocked) for the tracker. Resolution depends on upstream [PR #4707](https://github.com/mawww/kakoune/pull/4707) merging.
### Interim guidance
Until upstream changes land, plugins should:
1. **Define plugin-specific token names** (e.g., `markdown_rich.h1_marker`, `myplugin.callout.note`) rather than referencing Kakoune face names directly.
2. **Query with a plugin-supplied fallback**:
```rust
let h1 = theme_style_or("markdown_rich.h1_marker", style_fg(named(Blue)));
```
3. **Document the user override path** in your plugin's README — users who want colorscheme-matched output add corresponding `theme { }` entries to their `kasane.kdl`:
```kdl
theme {
markdown_rich.h1_marker "rgb:093060,default+b"
}
```
4. **Optionally ship example `kasane.kdl` snippets** for popular themes in *your plugin's* repository (not in Kasane). One file per theme; document them in your plugin's README.
This shifts the colour-matching responsibility to the user as a one-time configuration step, keeps Kasane's host surface small, and avoids fragmenting the Kakoune theme ecosystem with Kasane-specific patches. When upstream PR #4707 lands, plugin authors can switch from token names like `markdown_rich.h1_marker` to direct Kakoune face references without breaking existing user `kasane.kdl` files.
## Testing
Unit tests can be written using `PluginRuntime` directly.
```rust
#[test]
fn my_plugin_contributes_gutter() {
let mut registry = PluginRuntime::new();
registry.register(MyPlugin); // Plugin trait (state-externalized)
let state = AppState::default();
let view = AppView::new(&state);
let _ = registry.init_all(&view);
let ctx = ContributeContext::new(&view, None);
let contributions = registry.collect_contributions(&SlotId::BUFFER_LEFT, &view, &ctx);
assert_eq!(contributions.len(), 1);
}
```
For WASM integration tests, see `tools/wasm-test/` and `kasane-wasm/src/tests/`.
## Debugging
### Viewing plugin load results
```bash
KASANE_LOG=info kasane file.txt
```
Plugin loading results (success, failure, skip) are logged at `info` level.
For detailed WASM instantiation errors, use `debug`.
### Common issues
| Symptom | Cause | Fix |
|---|---|---|
| `plugin X failed to load: ABI mismatch` | Plugin built against an older WIT version | Rebuild with the current `kasane-plugin-sdk` |
| `plugin X skipped: disabled` | Plugin ID is in `plugins { disabled }` | Remove from the disabled list in `kasane.kdl` |
| Plugin loads but contributes nothing | `state_hash()` returns a constant, or dirty flags don't cover the relevant state | Verify `#[bind]` flags or manual `state_hash()` implementation |
| `wasm trap: unreachable` in logs | Guest code panicked | Run `KASANE_LOG=debug` and check the backtrace |
### Inspecting plugin state at runtime
`kasane plugin list` shows loaded plugins and their IDs.
`kasane plugin doctor` checks toolchain, SDK version, and plugin health.
## Registration and Distribution
### Registration Order
Kasane registers plugins in the following order:
1. Example WASM (embedded in the binary)
2. FS-discovered packages (`~/.local/share/kasane/plugins/*.kpk`)
3. Native plugins supplied via `kasane::run_with_factories(...)` or a custom `PluginProvider`
An FS-discovered WASM plugin with the same ID can override an example plugin.
### Distribution Methods
- WASM: Place `.kpk` files in `~/.local/share/kasane/plugins/`
- Native: Distribute as a custom binary using `kasane::run_with_factories(...)` or `kasane::run(provider)`
### Control via kasane.kdl
```kdl
plugins {
enabled "cursor_line" "color_preview"
disabled "some_plugin"
// Per-plugin WASI capability denial
deny_capabilities {
untrusted_plugin "filesystem" "environment"
}
}
```
### WASI Capabilities
WASM plugins can declare required WASI capabilities via `requested_capabilities()`.
The host configures a WASI context per plugin based on the declarations.
| Capability | Effect | Default |
|---|---|---|
| `Capability::Filesystem` | Preopens `data/` (plugin-specific, read/write) and `.` (CWD, read-only) | Disabled |
| `Capability::Environment` | Inherits host environment variables | Disabled |
| `Capability::MonotonicClock` | Access to a monotonic clock | Enabled |
| `Capability::Process` | Spawn external processes | Disabled |
```rust
fn requested_capabilities() -> Vec {
vec![Capability::Filesystem]
}
```
Capabilities are granted upon declaration. Users can deny them via `deny_capabilities` in `kasane.kdl`.
Constraint: WASI capabilities are available from `on_init_effects()` onward. They are not available during component initialization (`_initialize`).
## Session-Aware Plugins
Plugins can observe and control sessions using the Tier 8 host-state API and `SessionCommand`. For type definitions and semantics, see [plugin-api.md §3.5.3](./plugin-api.md#353-session-observability). A `session-ui` example demonstrating overlay UI + keybinding + session switching previously lived at `examples/wasm/session-ui/`; it moved to the future external `kasane-plugin-gallery` (δ-3 cleanup) — recover from git history.
## Example Plugins
For the full list of bundled and source example plugins, see [using-plugins.md](./using-plugins.md#bundled-wasm-plugins).
## Appendix A: Alternative: Native Plugin {#appendix-a-alternative-native-plugin}
For use cases that require features not yet available via WASM (such as `Surface`), you can write a native plugin. Native plugins are distributed as custom binaries.
The `Plugin` trait uses `HandlerRegistry`-based registration: 2 methods + 1 associated type (`id()`, `State` type, `register()`). The framework owns the state; handlers are pure functions. Capabilities are auto-inferred from which handlers are registered.
```rust
use kasane::kasane_core::plugin_prelude::*;
#[derive(Clone, Debug, PartialEq, Default)]
struct CursorLineState {
active_line: i32,
}
struct CursorLinePlugin;
impl Plugin for CursorLinePlugin {
type State = CursorLineState;
fn id(&self) -> PluginId {
PluginId("cursor_line".into())
}
fn register(&self, r: &mut HandlerRegistry) {
r.declare_interests(DirtyFlags::BUFFER);
r.on_state_changed_tier1(|state, app, dirty| {
if dirty.intersects(DirtyFlags::BUFFER) {
(
CursorLineState {
active_line: app.cursor_line(),
},
KakouneSideEffects::default(),
)
} else {
(state.clone(), KakouneSideEffects::default())
}
});
r.on_decorate_background(|state, line, _app, _ctx| {
if line as i32 == state.active_line {
Some(BackgroundLayer {
style: Style { bg: Brush::Named(NamedColor::Blue), ..Style::default() },
z_order: 0,
blend: BlendMode::Opaque,
})
} else {
None
}
});
}
}
fn main() {
kasane::run_with_factories([host_plugin("cursor_line", || {
PluginBridge::new(CursorLinePlugin)
})]);
}
```
For a comparison of WASM vs Native plugin models, see [Appendix B](#appendix-b-wasm-vs-native-comparison) or [plugin-api.md §8](./plugin-api.md#8-wasm-plugin-constraints).
## Appendix B: WASM vs Native Comparison {#appendix-b-wasm-vs-native-comparison}
For the WASM vs Native feature gap table and runtime constraints, see [plugin-api.md §8](./plugin-api.md#8-wasm-plugin-constraints). For choosing between WASM and Native plugins, see [plugin-api.md §1.2.2](./plugin-api.md#122-choosing-a-plugin-model).
## Appendix C: Explicit WASM Pattern {#appendix-c-explicit-wasm-pattern}
The `define_plugin!` macro is recommended for most WASM plugins. For full control over state management, you can use the explicit `generate!()` + `#[plugin]` + `export!()` pattern:
```rust
kasane_plugin_sdk::generate!();
use std::cell::Cell;
use kasane_plugin_sdk::{dirty, plugin};
thread_local! {
static CURSOR_COUNT: Cell = const { Cell::new(0) };
}
struct SelBadgePlugin;
#[plugin]
impl Guest for SelBadgePlugin {
fn get_id() -> String {
"sel_badge".to_string()
}
fn on_state_changed_effects(dirty_flags: u16) -> Effects {
if dirty_flags & dirty::BUFFER != 0 {
CURSOR_COUNT.set(host_state::get_cursor_count());
}
Effects::default()
}
fn state_hash() -> u64 {
CURSOR_COUNT.get() as u64
}
kasane_plugin_sdk::slots! {
STATUS_RIGHT(dirty::BUFFER) => |_ctx| {
let count = CURSOR_COUNT.get();
(count > 1).then(|| {
auto_contribution(text(&format!(" {} sel ", count), default_face()))
})
},
}
}
export!(SelBadgePlugin);
```
Key differences from `define_plugin!`:
- You manage state manually (e.g., `thread_local!` + `Cell`/`RefCell`)
- You implement `state_hash()` explicitly
- You get direct control over struct naming and imports
- You can use `#[plugin]` on an existing `impl Guest` block
## Related Documents
- [plugin-api.md](./plugin-api.md) — API reference
- [semantics.md](./semantics.md) — Composition order and correctness conditions
- [index.md](./index.md) — Entry point for all docs