Skip to content

Latest commit

 

History

History
760 lines (586 loc) · 32.4 KB

File metadata and controls

760 lines (586 loc) · 32.4 KB

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. For composition semantics, see 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.

Quick Start

Hello World (3 lines)

kasane plugin new my-hello --template hello
cd my-hello && kasane plugin build

This creates a minimal plugin:

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

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:

rustup target add wasm32-wasip2
# or: kasane plugin doctor --fix
Manual setup (without kasane plugin new)
# 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.

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 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.

[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)
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.<key>.type No Setting type: "bool", "integer", "float", "string"
settings.<key>.default No Default value (must match type)
settings.<key>.description No Human-readable description

Example with settings:

[settings.enabled]
type = "bool"
default = false
description = "Enable smooth scrolling animation"

In define_plugin!, use the manifest: section instead of id:, capabilities:, and authorities::

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 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. 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 <esc>:cmd<ret> 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 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:

use kasane_plugin_sdk::kak_cmd::{KakCommand, DeclareUserMode, DefineCommand, Map, Scope};

let setup: Vec<KakCommand> = 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<ret>")
        .docstring("bump")
        .into(),
];

let strings: Vec<String> = 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! to catch typo'd or invalid flags at compile time.

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

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. ABI 6.3.0 added emit-diagnostic plugin-emitted diagnostic command per #106. ABI 6.2.0 added cell-metrics + 3 font/cell accessors per #109. ABI 6.1.0 added get-display-cells per #108. ABI 6.0.0 was Phase β-4 — removing the retired evaluate-extension export per ADR-045. ABI 5.0.0 is the ADR-044 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 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::DefaultColorBrush::DefaultColor, Color::Named(...)Brush::Named(...), Color::Rgb(...)Brush::Rgb(...). The variant names are unchanged.
  4. Rename helper functions: default_facedefault_style, face_fgstyle_fg, face_bgstyle_bg, face(fg, bg)style_with(fg, bg), theme_face_ortheme_style_or, get_theme_faceget_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:

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
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.).

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<ElementPatchOp> instead of imperatively transforming the subject. Pure patches are Salsa-memoizable. See plugin-cookbook.md 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.

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.
  • 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 for the constraint analysis and upstream-dependencies.md §3 (D-005) for the tracker. Resolution depends on upstream PR #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:

    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:

    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.

#[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

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

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
fn requested_capabilities() -> Vec<Capability> {
    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. 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.

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.

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<CursorLineState>) {
        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 or plugin-api.md §8.

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. For choosing between WASM and Native plugins, see plugin-api.md §1.2.2.

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:

kasane_plugin_sdk::generate!();

use std::cell::Cell;

use kasane_plugin_sdk::{dirty, plugin};

thread_local! {
    static CURSOR_COUNT: Cell<u32> = 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