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.
| 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
Surfacefor sidebars and dedicated panels. See Appendix A.
kasane plugin new my-hello --template hello
cd my-hello && kasane plugin buildThis 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| { ... }.
| 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() |
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 templateThis 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 --fixManual setup (without kasane plugin new)
# Cargo.toml
[package]
name = "sel-badge"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
kasane-plugin-sdk = "0.5.0"
wit-bindgen = "0.53"This plugin displays the selection count on the right side of the status bar when multiple cursors are active.
kasane_plugin_sdk::define_plugin! {
id: "sel_badge",
state {
#[bind(host_state::get_cursor_count(), on: dirty::BUFFER)]
cursor_count: u32 = 0,
},
slots {
STATUS_RIGHT(dirty::BUFFER) => |_ctx| {
status_badge(state.cursor_count > 1, &format!(" {} sel ", state.cursor_count))
},
},
}Key points:
define_plugin!combinesgenerate!(), state declaration,#[plugin], andexport!()into a single macro. All sections are optional exceptid.#[bind(expr, on: flags)]on state fields auto-generates sync code inon_state_changed_effects(). The expression is evaluated when the specified dirty flags are set.slots {}declares slot contributions with dependency tracking. Insideslotsclosures,state.fieldis available directly (read-only).status_badge()is a helper that returnsSome(auto_contribution(plain(label)))when the condition is true.- The
dirtyandmodifiersmodules are auto-imported bydefine_plugin!. - In mutable contexts (
handle_key,overlay,on_io_event, etc.),bump_generation()is called automatically when the state guard drops.
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.
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"]
[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 |
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.
| Profile | Sections | Template | Example |
|---|---|---|---|
| Status widget | state (#[bind]), slots |
contribution |
sel-badge |
| Line annotator | state (#[bind]), annotate |
annotation |
cursor-line |
| Element transformer | state (#[bind]), transform |
transform |
prompt-highlight |
| Display transform | state, on_state_changed_effects, display_directives |
— | virtual-text-demo (native) |
| Structural projection | state, define_projection, on_key_map |
— | semantic-zoom (builtin) |
| Interactive overlay | state, handle_key, overlay |
overlay |
session-ui |
| Process launcher | Above + on_io_event_effects, capabilities |
process |
fuzzy-finder |
| Scroll policy | handle_default_scroll |
— | smooth-scroll |
| Kakoune-side bindings | on_active_session_ready_effects + kak::* helpers |
— | kakoune-bindings-demo |
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.
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
and examples/wasm/kakoune-bindings-demo/.
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.
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 |
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.
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 buildskasane 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@5.0.0 (ADR-044
tier split). Rebuild and reinstall any plugin that was built against
an older version; ABI 4.x binaries are rejected at load time. See
docs/migration/0.6-to-0.7.md §8.3 for the
per-handler tier mapping.
If you are upgrading a plugin from 0.25.0, the required changes are:
- Update the SDK crate to
kasane-plugin-sdk = "0.5"and setabi_version = "1.0.0"inkasane-plugin.toml. - Rename
face/Facetostyle/Style(struct fields, helper names, types). The SDK ships astyle_full(fg, bg, underline_color, attrs)helper that decomposes the legacyattributes::*bitset into the newStylefields (font_weight,font_slant,underline, etc.). DirectFace { … }literals must be rewritten asStyle { … }with all 12 fields (or usedefault_style()as a base). - Rename
Color/colortoBrush/brush:Color::DefaultColor→Brush::DefaultColor,Color::Named(...)→Brush::Named(...),Color::Rgb(...)→Brush::Rgb(...). The variant names are unchanged. - 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::*. - Rebuild and reinstall the
.wasm. Existing artifacts built against0.25.0are rejected at load time bykasane:plugin@5.0.0hosts.
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 directoryManual build & deploy
cargo build --target wasm32-wasip2 --release
kasane plugin build
kasane plugin install target/kasane/sel-badge-0.1.0.kpkThis 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 aTransformSubject— eitherElement(Element)for non-overlay targets orOverlay(Overlay)for overlay targets. Return it unchanged for passthrough, or pattern-match and wrap.TransformTargetselects which UI component to transform (e.g.,StatusBar,Buffer,Menu). Ignore targets your plugin doesn't handle.transform_priority(default0) controls ordering in the transform chain. Higher priority = applied first (inner).transform_patch(target, ctx)is a declarative alternative that returnsVec<ElementPatchOp>instead of imperatively transforming the subject. Pure patches are Salsa-memoizable. See plugin-cookbook.md for an example.
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
Noneto let the next scroll-policy plugin decide. For exactNone/Pass/Suppress/Immediatesemantics, seeplugin-api.md. - The source example lives at
examples/wasm/smooth-scroll/. It is not part of the bundled WASM plugin set unless you build and install it yourself.
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/.
KASANE_LOG=info kasane file.txtPlugin loading results (success, failure, skip) are logged at info level.
For detailed WASM instantiation errors, use debug.
| 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 |
kasane plugin list shows loaded plugins and their IDs.
kasane plugin doctor checks toolchain, SDK version, and plugin health.
Kasane registers plugins in the following order:
- Example WASM (embedded in the binary)
- FS-discovered packages (
~/.local/share/kasane/plugins/*.kpk) - Native plugins supplied via
kasane::run_with_factories(...)or a customPluginProvider
An FS-discovered WASM plugin with the same ID can override an example plugin.
- WASM: Place
.kpkfiles in~/.local/share/kasane/plugins/ - Native: Distribute as a custom binary using
kasane::run_with_factories(...)orkasane::run(provider)
plugins {
enabled "cursor_line" "color_preview"
disabled "some_plugin"
// Per-plugin WASI capability denial
deny_capabilities {
untrusted_plugin "filesystem" "environment"
}
}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).
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. The session-ui example (examples/wasm/session-ui/src/lib.rs) demonstrates the full pattern including overlay UI, keybinding, and session switching.
For the full list of bundled and source example plugins, see using-plugins.md.
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 C or plugin-api.md §8.
Internal — do not implement in new plugins. Per ADR-038 (Plugin Authoring Path Consolidation, 0.6.0),
PluginBackendis the internal dispatch ABI consumed byPluginRuntimeandWasmPlugin. The sole sanctioned authoring path isPlugin+HandlerRegistry(Appendix A). New extension points are added viaHandlerRegistry::on_X(...)registrations, not via newPluginBackendmethods. The example below is preserved for archaeology and to illustrate the internal shape; for any new native plugin, usePlugin+HandlerRegistry.
PluginBackend is the internal mutable-state plugin model (&mut self). It provides access to all extension points including Surface and workspace observation. The capability traits (Lifecycle, Io, PubSubMember, ExtensionParticipant, ...) are super-traits of PluginBackend after the R1.x migration (0.6.0) and are blanket-implemented for Plugin + HandlerRegistry users automatically.
use kasane::kasane_core::plugin_prelude::*;
struct LineNumbersPlugin;
impl PluginBackend for LineNumbersPlugin {
fn id(&self) -> PluginId {
PluginId("line_numbers".into())
}
fn capabilities(&self) -> PluginCapabilities {
PluginCapabilities::CONTRIBUTOR
}
fn contribute_to(
&self,
region: &SlotId,
app: &AppView<'_>,
_ctx: &ContributeContext,
) -> Option<Contribution> {
if region != &SlotId::BUFFER_LEFT {
return None;
}
let total = app.line_count();
let width = total.to_string().len().max(2);
let children: Vec<_> = (0..total)
.map(|i| {
let num = format!("{:>w$} ", i + 1, w = width);
FlexChild::fixed(Element::text(
num,
Style {
fg: Brush::Named(NamedColor::Cyan),
..Style::default()
},
))
})
.collect();
Some(Contribution {
element: Element::column(children),
priority: 0,
size_hint: ContribSizeHint::Auto,
})
}
}
fn main() {
kasane::run_with_factories([host_plugin("line_numbers", || LineNumbersPlugin)]);
}Register PluginBackend implementors via host_plugin("id", || PluginType) and kasane::run_with_factories(...). See the PluginBackend trait in kasane-core/src/plugin/traits.rs for the full method list.
For the WASM vs Native feature gap table and runtime constraints, see plugin-api.md §8. For choosing a plugin model (WASM, Native Plugin, Native PluginBackend), see plugin-api.md §1.2.2.
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 existingimpl Guestblock
- plugin-api.md — API reference
- semantics.md — Composition order and correctness conditions
- index.md — Entry point for all docs