Skip to content

Latest commit

 

History

History
1413 lines (1053 loc) · 66.6 KB

File metadata and controls

1413 lines (1053 loc) · 66.6 KB

Kasane Plugin API Reference

This document is a reference for looking up the Kasane plugin API. For a quickstart guide to writing a working plugin, see plugin-development.md. For composition ordering and correctness conditions, see semantics.md.

0. Scope of the Plugin API

The Kasane plugin API is primarily designed for UI decoration, transformation, and extension.

Plugins construct Element trees based on state received from Kakoune and provide supplementary visual information to users. Side effects are issued indirectly via Command, and are limited to UI-side coordination such as sending keys to Kakoune, requesting redraws, inter-plugin messages, and timers.

The following operations are currently outside the scope of the plugin API.

Out-of-scope operation Reason
File system access WASM is prohibited by the sandbox. Native is technically possible but lacks an async infrastructure
Network communication Same as above
Text input widgets No input elements in Element. Text editing is delegated to Kakoune by design

Native plugins run within the host process and can therefore technically use std::process, std::fs, etc. However, Plugin trait hook functions are called synchronously, so the plugin developer bears the design responsibility for avoiding main thread blocking.

For WASM-specific constraints (feature gaps, runtime limits, missing state queries), see §8 WASM Plugin Constraints.

Kasane's long-term strategy is to make WASM the first-class distribution and execution path, with capabilities as close to native as possible. Accordingly, native-only APIs are treated not as "permanent advantages" but as one of the following:

  • A provisional escape hatch not yet stably exposed via WIT
  • A host-integration API requiring redesign to achieve WASM parity
  • An API intentionally kept native-only based on security boundary decisions

File system access is provided via WASI capability declarations (Phase P-1), and external process execution is provided via host-mediated Command + IoEvent (Phase P-2). See ADR-019 for design rationale.

1. Extension Points

1.1 Core Surfaces and Built-in Slots

The core UI is structured around surfaces. The extension points available to plugins are declared by each surface.

SurfaceId Surface Description
BUFFER (0) KakouneBufferSurface Main buffer display
STATUS (1) StatusBarSurface Status bar (rendered once per pane in multi-pane mode)
MENU (2) MenuSurface Menu
INFO_BASE+ (10+) InfoSurface Info popups
PLUGIN_BASE+ (100+) Plugin-defined Plugin-provided surfaces
SlotId Position Declaring Surface
kasane.buffer.left Left of buffer KakouneBufferSurface
kasane.buffer.right Right of buffer KakouneBufferSurface
kasane.buffer.above Above buffer KakouneBufferSurface
kasane.buffer.below Below buffer KakouneBufferSurface
kasane.buffer.overlay Overlay on buffer KakouneBufferSurface
kasane.status.above Above status bar StatusBarSurface
kasane.status.left Left of status bar StatusBarSurface
kasane.status.right Right of status bar StatusBarSurface

1.2 Choosing a Mechanism

Goal Mechanism to use
Add UI at a predefined location contribute_to()
Decorate individual buffer lines annotate_line_with_ctx()
Apply face to individual cells, ranges, or columns; override cursor style render_ornaments() (via OrnamentBatch)
Display floating UI contribute_overlay_with_ctx()
Modify or replace existing UI appearance transform()
Transform individual menu items transform_menu_item()
As a principle, prefer the least flexible mechanism that suffices. Do not use transform() if contribute_to() can achieve the goal.

1.2.1 Display Transformations and Display Units

As described in P-030..P-043 of requirements.md and Display Transformations and Display Units in semantics.md, Kasane allows plugins to treat display transformations as first-class concepts.

The Display Transform API (display_directives()) provides the first concrete implementation of this direction. Plugins declare DisplayDirective values describing how buffer lines map to display lines. The core builds a DisplayMap — an O(1) bidirectional mapping between buffer lines and display lines — and integrates it throughout the rendering pipeline (paint, cursor, input, patch).

Available DisplayDirective variants (4 categories, 11 variants):

Category Variant Description
Spatial Fold { range, summary } Collapse a range of buffer lines into a single summary line
Spatial Hide { range } Hide a range of buffer lines entirely
InterLine InsertBefore { line, content, priority } Insert a full Element before a buffer line
InterLine InsertAfter { line, content, priority } Insert a full Element after a buffer line
Inline InsertInline { line, byte_offset, content, interaction } Insert inline content at a byte offset
Inline HideInline { line, byte_range } Hide a byte range within a buffer line
Inline StyleInline { line, byte_range, face } Apply face styling to a byte range
Decoration StyleLine { line, face, z_order } Apply a background face to an entire line
Decoration Gutter { line, side, content, priority } Add content to the gutter of a line
Decoration VirtualText { line, position, content, priority } Add virtual text at end of line or right-aligned
Decoration EditableVirtualText { after, content, editable_spans } Editable virtual text with shadow cursor support

Key types:

  • DisplayMap: bidirectional buffer↔display line mapping with display_to_buffer(), buffer_to_display(), entry(), is_identity()
  • SourceMapping: BufferLine(usize), LineRange(Range), None (virtual text)
  • InteractionPolicy: Normal, ReadOnly (clicks suppressed), Skip (navigation skips)
  • SyntheticContent: text and face for non-buffer display lines

Multi-plugin composition (P-031):

  • Multiple plugins may contribute display directives simultaneously
  • Composition is deterministic via resolve(): Hide ranges are unioned, InsertAfter and InsertBefore lines accumulate, overlapping Folds are resolved by (priority, plugin_id) (higher wins)
  • Plugins declare priority via display_directive_priority() (default 0)
  • Folds that partially overlap hidden ranges are conservatively removed (protects summary integrity)
  • InsertAfter/InsertBefore targeting hidden or folded lines are suppressed

Constraints:

  • Display-oriented navigation (Display Units, P-040..P-043) is implemented via DisplayUnit, NavigationPolicy, and NavigationAction (see §3.4.1)
  • Kakoune controls the viewport and cursor movement, so true code folding (where folded lines are skipped during navigation) is not possible; Fold is best suited for read-only summaries
  • InsertAfter/InsertBefore (virtual text) are the primary practical use cases; InsertBefore enables Gap 0 (before the first buffer line)

See examples/virtual-text-demo/ for a working proof artifact.

For mechanisms not covered by DisplayDirective (overlay composition, element-level restructuring), plugins should use the existing combination of contribute_to(), transform(), annotate_line_with_ctx(), contribute_overlay_with_ctx(), and Surface.

1.2.2 Choosing a Plugin Model

0.6.0 (ADR-038): Plugin + HandlerRegistry is the sole sanctioned authoring path for native plugins. PluginBackend is the internal dispatch ABI consumed by PluginRuntime / WasmPlugin and is no longer presented as an alternative authoring choice. The capability traits (Lifecycle, Io, PubSubMember, ExtensionParticipant, ...) are super-traits of PluginBackend after the R1.x migration; they are blanket-implemented for Plugin + HandlerRegistry users. New extension points are added via HandlerRegistry::on_X(...) registrations.

Native plugins implement the Plugin trait:

Plugin (HandlerRegistry — public)
Registration 2 methods + 1 associated type: id(), State type, register() with HandlerRegistry
State ownership Framework holds state; handlers are pure functions
Capabilities Auto-inferred from registered handlers
Cache invalidation Automatic via PartialEq comparison (generation counter)
Salsa compatibility State transitions are pure functions; future Salsa integration path
Use cases All native plugin scenarios — UI decoration, surfaces, workspace observation, host integration. Surface and workspace observation are reachable via HandlerRegistry::on_surfaces / on_workspace_* handlers and via the Lifecycle-supertrait blanket impl

In unit tests, register via PluginRuntime::register(). In a host binary, wrap it with PluginBridge::new(...) and pass it to kasane::run_with_factories(...). (PluginBackend is documented internally for framework contributors only — see Appendix B of plugin-development.md.)

use kasane_core::plugin_prelude::*;

#[derive(Clone, Debug, PartialEq, Default)]
struct MyState { counter: u32 }

struct MyPlugin;

impl Plugin for MyPlugin {
    type State = MyState;
    fn id(&self) -> PluginId { PluginId("my_plugin".into()) }

    fn register(&self, r: &mut HandlerRegistry<MyState>) {
        r.declare_interests(DirtyFlags::BUFFER);
        r.on_state_changed_tier1(|state, _app, dirty| {
            if dirty.intersects(DirtyFlags::BUFFER) {
                (MyState { counter: state.counter + 1 }, KakouneSideEffects::default())
            } else {
                (state.clone(), KakouneSideEffects::default())
            }
        });
        r.on_decorate_background(|state, line, _app, _ctx| {
            // ... return Option<BackgroundLayer>
            None
        });
    }
}

// Unit-test registration:
registry.register(MyPlugin);

1.3 Composition Rules

The composition order for extensions is as follows:

  1. Build the seed default elements
  2. Apply the transform chain in priority order (processing decoration and replacement in a unified manner)
  3. Compose contributions and overlays

For detailed semantics, see Plugin Composition Semantics in semantics.md.

1.4 Contribution (contribute_to)

contribute_to() is the most constrained extension, contributing Elements to framework-provided extension points (SlotId).

Native:

fn contribute_to(&self, region: &SlotId, app: &AppView<'_>, _ctx: &ContributeContext) -> Option<Contribution> {
    if region != &SlotId::BUFFER_LEFT { return None; }
    Some(Contribution {
        element: Element::text("★", Style::default()),
        priority: 0,
        size_hint: ContribSizeHint::Auto,
    })
}

WASM:

fn contribute_to(region: SlotId, _ctx: ContributeContext) -> Option<Contribution> {
    kasane_plugin_sdk::route_slot_ids!(region, {
        BUFFER_LEFT => {
            Some(Contribution {
                element: element_builder::create_text("★", face),
                priority: 0,
                size_hint: ContribSizeHint::Auto,
            })
        },
    })
}

ContributeContext provides layout-aware constraints. The main fields are min_width / max_width / min_height / max_height, where None represents unbounded. Contribution consists of element, priority (composition order), and size_hint (Auto / Fixed(u16) / Flex(f32)).

The legacy u8 constants from slot::BUFFER_LEFT through slot::OVERLAY remain in the kasane_plugin_sdk::slot module, but the canonical API uses first-class SlotId. Custom slots can be specified in both Native and WASM via SlotId::new("...") / SlotId::Named("...".into()).

1.5 Line Annotation (annotate_line_with_ctx)

annotate_line_with_ctx() contributes gutter elements and backgrounds to individual buffer lines.

Native:

fn annotate_line_with_ctx(&self, line: usize, app: &AppView<'_>, _ctx: &AnnotateContext) -> Option<LineAnnotation> {
    if line == app.cursor_line() as usize {
        Some(LineAnnotation {
            left_gutter: None,
            right_gutter: None,
            background: Some(BackgroundLayer {
                style: Style { bg: Brush::rgb(40, 40, 50), ..Style::default() },
                z_order: 0,
                blend: BlendMode::Opaque,
            }),
            priority: 0,
            inline: None,
        })
    } else {
        None
    }
}

LineAnnotation consists of five fields: left_gutter, right_gutter, background, priority (controls gutter element ordering), and inline (byte-range inline decoration). BackgroundLayer has face, z_order, and blend (compositing mode); background contributions from multiple plugins are composited in z_order order. Gutter contributions are composited horizontally.

Inline Decoration

The inline field provides byte-range operations applied directly to buffer line atoms. This enables styling or hiding sub-ranges of a line without replacing the entire element tree.

InlineDecoration contains a sorted list of InlineOp:

  • InlineOp::Insert { at, content } — Insert virtual text atoms at the given byte gap position
  • InlineOp::Style { range, face } — Override the face for the given byte range
  • InlineOp::Hide { range } — Hide the given byte range (omit from output)

Ops are sorted by sort_key()(position, variant_order) where Insert (0) sorts before Style/Hide (1) at the same position. Range-based ops (Style/Hide) must be non-overlapping. Multiple Insert ops at the same position are allowed and emitted in order.

Insert ops inside a Hide range are still emitted (S1 semantics): the Hide omits the original buffer text, but any Insert whose at falls within the hidden range produces its virtual content.

Replace pattern — Hide + Insert at the same position to substitute text:

// Replace "def" (bytes 3..6) with "new" in "abcdefghi"
InlineDecoration::new(vec![
    InlineOp::Insert { at: 3, content: vec![Atom { face: red_face, contents: "new".into() }] },
    InlineOp::Hide { range: 3..6 },
])
// Result: "abc" + "new"(red) + "ghi"

Example — style bytes 6..11 ("world") in red, hide bytes 0..2 ("he"):

fn annotate_line_with_ctx(&self, line: usize, app: &AppView<'_>, _ctx: &AnnotateContext) -> Option<LineAnnotation> {
    Some(LineAnnotation {
        left_gutter: None,
        right_gutter: None,
        background: None,
        priority: 0,
        inline: Some(InlineDecoration::new(vec![
            InlineOp::Hide { range: 0..2 },
            InlineOp::Style {
                range: 6..11,
                style: Style { fg: Brush::Named(NamedColor::Red), ..Style::default() },
            },
        ])),
    })
}

Byte ranges operate on UTF-8 byte offsets within the line's atom contents. Phase 1 constraint: only one plugin may provide inline decoration per line.

1.6 Overlay (contribute_overlay_with_ctx)

contribute_overlay_with_ctx() provides floating elements that are overlaid outside the normal layout flow.

Native:

fn contribute_overlay_with_ctx(&self, app: &AppView<'_>, _ctx: &OverlayContext) -> Option<OverlayContribution> {
    Some(OverlayContribution {
        element: Element::container(child, style),
        anchor: OverlayAnchor::AnchorPoint { coord, prefer_above: true, avoid: vec![] },
        z_index: 0,
        plugin_id: self.id(),
    })
}

WASM:

fn contribute_overlay_v2(_ctx: OverlayContext) -> Option<OverlayContribution> {
    Some(OverlayContribution {
        element: element_builder::create_container_styled(child, ...),
        anchor: OverlayAnchor::Absolute(AbsoluteAnchor { x: 10, y: 5, w: 30, h: 10 }),
        z_index: 0,
    })
}

OverlayContribution consists of element, anchor, z_index, and plugin_id (used for deterministic tie-breaking). There are two types of OverlayAnchor:

  • Absolute { x, y, w, h }: Absolute position in screen coordinates
  • AnchorPoint { coord, prefer_above, avoid }: Kakoune-compatible anchor-based positioning

1.7 Transform (transform)

transform() is a unified mechanism that receives a TransformSubject (either an Element or an Overlay), transforms it, and returns the result. It serves as both decoration (formerly Decorator) and replacement (formerly Replacement). For non-overlay targets (Buffer, StatusBar), the subject is Element; for overlay targets (Menu, Info), it is Overlay (Element + OverlayAnchor), allowing plugins to modify overlay position and size.

Native:

fn transform(&self, target: &TransformTarget, subject: TransformSubject, app: &AppView<'_>, _ctx: &TransformContext) -> TransformSubject {
    subject.map_element(|element| {
        if *target == TransformTarget::BUFFER {
            Element::container(element, Style::default())
        } else {
            element
        }
    })
}

fn transform_priority(&self) -> i16 { 100 }

WASM:

fn transform(target: TransformTarget, subject: TransformSubject, _ctx: TransformContext) -> TransformSubject {
    match subject {
        TransformSubject::Element(element) => {
            TransformSubject::Element(container(element).border(BorderLineStyle::Single).build())
        }
        other => other,
    }
}

fn transform_priority() -> i16 { 100 }

TransformTarget includes Buffer, StatusBar, Menu, Info, and others.

Native (with target specification):

r.on_transform_for(0, &[TransformTarget::BUFFER, TransformTarget::STATUS_BAR], |state, target, app, ctx| {
    if *target == TransformTarget::STATUS_BAR {
        ElementPatch::Append { element: Element::text("extra", Style::default()) }
    } else {
        ElementPatch::Identity
    }
});

on_transform_for() specifies which TransformTargets the handler applies to. This populates CapabilityDescriptor::transform_targets, enabling may_interfere() to detect transform target overlap between plugins. Use on_transform() (without targets) when the handler applies to all targets.

WASM (declarative patch):

// In define_plugin!:
transform_patch(target, ctx) {
    if target == "kasane.status-bar" {
        vec![ElementPatchOp::Prepend(text("[K] ", default_face()))]
    } else {
        vec![]  // empty = identity (no patch)
    }
},

transform_patch returns Vec<ElementPatchOp> instead of imperatively transforming the subject. Pure patches are Salsa-memoizable.

Guidelines:

  • Do not assume the internal structure of the received Element
  • For lightweight decoration, prefer wrapping the Element as-is
  • Full replacement is also performed via transform() (ignore the received element and return a new one)
  • Use transform_priority() to control the application order
  • Declare handlers.transform_targets in the manifest for interference detection

1.8 Menu Transform (transform_menu_item)

transform_menu_item() is a per-menu-item transformation corresponding to the MENU_TRANSFORM capability. Use it when you want to locally transform the label or style of individual items. If you need to replace the entire menu structure, use transform() with TransformTarget::MENU.

1.10 Display Transformation API

The display transformation API allows plugins to restructure the display without falsifying protocol truth. display_directives() returns a Vec<DisplayDirective> describing how buffer lines map to display lines.

Design principles:

  • Transformations do not falsify protocol truth — they are display policy
  • The core builds a DisplayMap providing source mapping and interaction policy
  • When the inverse mapping to source is weak, read-only or restricted interaction is applied automatically
  • InsertAfter virtual text lines get InteractionPolicy::ReadOnly and SourceMapping::None
  • Fold summary lines get InteractionPolicy::ReadOnly and SourceMapping::LineRange
fn display_directives(&self, state: &Self::State, app: &AppView<'_>) -> Vec<DisplayDirective> {
    vec![DisplayDirective::InsertAfter {
        line: 2,
        content: Element::text(
            "  ⚠ TODO — address before merge",
            Style { fg: Brush::Named(NamedColor::Yellow), ..Style::default() },
        ),
        priority: 0,
    }]
}

The DisplayMap is integrated into: paint (buffer rendering), cursor positioning (buffer_to_display), mouse input (display_to_buffer with interaction policy check), and the patch optimization layer.

The display unit model (P-040..P-043) is implemented; see §3.4.1 for navigation API. Future extension: WASM WIT display-directive-priority function.

1.11 Projection API (define_projection)

Projections are named display transformation strategies registered via HandlerRegistry::define_projection(). Two categories:

Category Behavior Example
Structural Mutually exclusive — at most one active Semantic Zoom, Focus Mode
Additive Composable — any number active Error Lens, Diff Marks

Activation is managed via ProjectionPolicyState (in AppState::config). Structural projections are activated with set_structural(Some(id)), additive with toggle_additive(id). Directives from inactive projections are not collected.

r.define_projection(
    ProjectionDescriptor {
        id: ProjectionId::new("my-plugin.my-projection"),
        name: "My Projection".to_string(),
        category: ProjectionCategory::Structural,
        priority: -50,
    },
    |state: &MyState, app: &AppView<'_>| -> Vec<DisplayDirective> {
        // Return directives based on plugin state and app state
        vec![]
    },
);

Priority bands: Structural -500..0, Ambient/Legacy 0..500, Additive 500..1000.

Per-projection fold toggle state is tracked via FoldToggleState, accessible through app.projection_policy().fold_state_for(&id). The existing BuiltinFoldPlugin handles ToggleFold interaction for any projection.

The built-in kasane.semantic-zoom projection (kasane-core/src/plugin/semantic_zoom/) demonstrates the full projection pattern: state-driven directive generation, key map bindings, dual strategy dispatch (syntax-aware with tree-sitter fallback to indent-based).

1.12 Syntax Provider API

SyntaxProvider (in kasane-core/src/syntax/) provides AST-level information to plugins. The active provider is set on AppState::runtime.syntax_provider and accessible via AppView::syntax_provider().

Key trait methods:

Method Description
generation() Monotonic counter, incremented on re-parse
fold_ranges() Foldable line ranges from AST structure
declarations() Function/struct/enum/trait declarations with name, kind, signature, and body ranges
signature_summary(line) Human-readable signature text for the declaration at the given line
scopes_at(line, byte) Scope chain at a position (innermost last)
nodes_in_range(range, kind) Named AST nodes within a byte range

The kasane-syntax crate (feature-gated via --features syntax) provides TreeSitterProvider, which implements SyntaxProvider backed by tree-sitter. Grammar .so files are loaded from $XDG_DATA_HOME/kasane/grammars/ or $XDG_DATA_HOME/kak-tree-sitter/grammars/. Declaration queries are bundled for Rust, Python, Go, and TypeScript.

2. Element API

2.1 Element variants

Type Purpose WASM builder Native
Text Text + style create_text(content, style) Element::text(s, style)
StyledLine Atom sequence create_styled_line(atoms) Element::styled_line(line)
Flex (Column) Vertical layout create_column(children) / create_column_flex(entries, gap) Element::column(children)
Flex (Row) Horizontal layout create_row(children) / create_row_flex(entries, gap) Element::row(children)
Grid 2D table create_grid(cols, children, col_gap, row_gap) Element::grid(columns, children)
Container border/shadow/padding create_container(...) / create_container_styled(...) Element::container(child, style)
Stack Z-axis stacking create_stack(base, overlays) Element::stack(base, overlays)
Scrollable Scrollable region create_scrollable(child, offset, vertical) Element::Scrollable { ... }
Interactive Mouse hit test create_interactive(child, id) Element::Interactive { child, id }
Image Raster image create_image(source, w, h, fit, opacity) Element::image(source, w, h)
Empty Empty element create_empty() Element::Empty
BufferRef Buffer line reference Host-internal only Element::buffer_ref(range)

2.2 WASM element-builder API

All functions are imported from the element_builder module. The returned ElementHandle is valid only within the current plugin invocation scope.

use kasane::plugin::element_builder;

let text = element_builder::create_text("hello", face);
let col = element_builder::create_column(&[text]);
let container = element_builder::create_container(
    col,
    Some(BorderLineStyle::Single),
    false,
    Edges { top: 0, right: 1, bottom: 0, left: 1 },
);

For proportional distribution, use create_column_flex / create_row_flex with FlexEntry { child, flex }.

Image element (WIT v0.20.0)

create_image constructs a raster image element. Images are rendered natively on the GPU backend; the TUI backend renders a low-resolution approximation using Unicode halfblock characters (), with each cell representing two pixel rows (fg = top, bg = bottom). If image decoding fails, the TUI falls back to a text placeholder ([IMAGE: filename] or [IMAGE: W×H]).

ImageSource:

Variant Description
file-path(String) Path to an image file on disk
rgba-data { data: Vec<u8>, width: u32, height: u32 } Pre-decoded RGBA pixel data (4 bytes per pixel)

ImageFit:

Value Description
contain (default) Scale to fit within the area, preserving aspect ratio (letterboxing)
cover Scale to cover the entire area, preserving aspect ratio (cropping)
fill Stretch to fill the area exactly (may distort)

Size is specified in cells (width, height). opacity ranges from 0.0 (transparent) to 1.0 (opaque).

2.3 Native element construction

use kasane_core::plugin_prelude::*;

let text = Element::text("hello", Style::default());
let col = Element::column(vec![
    FlexChild::fixed(text),
    FlexChild::flexible(Element::Empty, 1.0),
]);

FlexChild::fixed(element) is fixed, and FlexChild::flexible(element, factor) is proportionally distributed.

3. State Access and Events

3.1 AppState overview

Native plugins access application state through &AppView<'_>, a zero-cost wrapper providing method-based accessors (e.g. app.cursor_line(), app.lines(), app.cols()).

Field Type Description
lines Vec<Line> Buffer lines
cursor_pos Coord Cursor position
status_line Line Status bar
menu Option<MenuState> Menu state
infos Vec<InfoState> Info popups
cols, rows u16 Terminal size
focused bool Focus state

Dirty flags primarily notify the following observable aspects:

Flag Description
BUFFER Buffer lines and cursor
STATUS Status bar
MENU_STRUCTURE Menu structure
MENU_SELECTION Menu selection
INFO Info popups
OPTIONS UI options

For semantic classification, see semantics.md.

3.2 WASM host-state API

kasane::plugin::host_state provides a tiered read API.

Basic state (Tier 0):

Function Return type
get_cursor_line() s32
get_cursor_col() s32
get_line_count() u32
get_cols() u16
get_rows() u16
is_focused() bool

Buffer lines (Tier 0.5):

Function Return type
get_line_text(line) Option<String>
get_lines_text(start, end) Vec<String>
get_lines_atoms(start, end) Vec<Vec<Atom>>
is_line_dirty(line) bool

get_lines_text and get_lines_atoms (WIT v0.18.0) retrieve all lines in the [start, end) range in a single host call, avoiding per-line round-trip overhead.

Status bar (Tier 1):

Function Return type
get_status_prompt() Vec<Atom>
get_status_content() Vec<Atom>
get_status_line() Vec<Atom>
get_status_mode_line() Vec<Atom>
get_status_default_style() Style

Menu/Info state (Tier 2):

Function Return type
has_menu() bool
get_menu_item_count() u32
get_menu_item(index) Option<Vec<Atom>>
get_menu_selected() s32
has_info() bool
get_info_count() u32

General state (Tier 3):

Function Return type
get_ui_option(key) Option<String>
get_cursor_mode() u8
get_widget_columns() u16
get_default_style() Style
get_padding_style() Style

Multi-cursor (Tier 4):

Function Return type
get_cursor_count() u32
get_secondary_cursor_count() u32
get_secondary_cursor(index) Option<Coord>

Typed Settings (Tier 5):

Function Return type
get_setting_bool(key) Option<bool>
get_setting_integer(key) Option<i64>
get_setting_float(key) Option<f64>
get_setting_string(key) Option<String>
get_config_string(key) Option<String> (deprecated, use typed settings)

Info details (Tier 6):

Function Return type
get_info_title(index) Option<Vec<Atom>>
get_info_content(index) Option<Vec<Vec<Atom>>>
get_info_style(index) Option<String>
get_info_anchor(index) Option<Coord>

Menu details (Tier 7):

Function Return type
get_menu_anchor() Option<Coord>
get_menu_mode() Option<String>
get_menu_style() Option<Style>
get_menu_selected_style() Option<Style>

Session state (Tier 8):

Function Return type
get_session_count() u32
get_session(index) Option<SessionDescriptor>
get_active_session_key() Option<String>

Selection / Time / History (Tier 9 — WIT 3.0, ADR-035):

WIT 3.0 (Kasane 0.6.0) replaces the legacy heuristic selection record and the get-selection* triplet with a canonical selection-set value-record plus first-class time-travel queries.

Function Return type
get_selection_set() SelectionSet
selection_set_to_kakoune_command(set) String (:select <args> shape)
current_version() VersionId (alias of u64)
text_at_time(time) Result<Vec<String>, Error>
selection_at_time(time) Result<SelectionSet, Error>
display_directives_at_time(time) Result<Vec<DisplayDirective>, Error>

SelectionSet fields: primary: u32 (index into selections), selections: list<selection-record> (disjoint, sorted, non-empty), direction: selection-direction (which end the user is moving). Each selection-record carries anchor: coord, cursor: coord, direction: selection-direction.

Time is a variant with now and at(version-id) arms. at(v) requires the configured history backend to retain v; otherwise the call returns Err(Error::VersionEvicted).

Removed in WIT 3.0: selection record, get-selection, get-selection-line, get-selection-column. See docs/migration/0.5-to-0.6.md §1.3 for the conversion table.

SessionDescriptor fields:

Field Type Description
key String Stable session key within the host
session_name Option<String> Kakoune session name (kak -c <name>)
buffer_name Option<String> Buffer name extracted from status_content atoms (e.g. main.rs)
mode_line Option<String> Mode line extracted from status_mode_line atoms (e.g. normal, insert)

buffer_name and mode_line are populated from the session's AppState snapshot — for the active session from the live state, for inactive sessions from the stored snapshot. These fields enable session switcher UIs that display meaningful per-session metadata without requiring plugins to access raw AppState.

3.3 Lifecycle hooks

Hook Timing Purpose
on_init_effects Immediately after plugin activation Bootstrap redraws and local startup effects
on_active_session_ready_effects After the active session is transport-ready Session-bound startup effects
on_shutdown At application exit Cleanup
on_state_changed_effects(dirty) After AppState update Synchronize plugin internal state

3.4 Input handling

The processing order for key input is as follows:

  1. Notify all plugins via observe_key()
  2. Call handle_key() in order
  3. The first plugin to return Some(commands) wins
  4. If all return None, proceed to built-in key bindings
  5. If still unhandled, forward to Kakoune

Mouse input is passed to handle_mouse(event, id, state) after observe_mouse(), followed by InteractiveId hit testing.

Default wheel scrolling has a separate policy hook. After core classifies a wheel event as a default buffer scroll candidate, it queries plugins with SCROLL_POLICY via handle_default_scroll(candidate) in registration order. The first plugin to return Some(result) wins:

  • None: pass to the next scroll-policy plugin
  • Some(Pass): stop the plugin chain and use core fallback scroll behavior
  • Some(Suppress): consume the candidate without emitting a scroll request
  • Some(Immediate(resolved)): emit a single resolved scroll request immediately
  • Some(Plan(plan)): hand a declarative scroll plan to the host runtime

This hook only applies to default buffer scroll candidates. Core-owned paths such as info-popup scrolling and drag-scroll routing do not go through it.

3.4.1 Display Units and Interaction Policy

The DisplayMap provides the first concrete implementation of source mapping and interaction policy for display lines:

  • SourceMapping::BufferLine(n): display line maps 1:1 to buffer line nInteractionPolicy::Normal
  • SourceMapping::LineRange(range): display line represents a folded range — InteractionPolicy::ReadOnly
  • SourceMapping::None: virtual text with no buffer origin — InteractionPolicy::ReadOnly

Mouse clicks on ReadOnly or Skip lines are suppressed by mouse_to_kakoune() (returns None). Cursor positioning uses buffer_to_display() to translate buffer coordinates to display coordinates.

The Display Unit model (P-040..P-043) is implemented. DisplayUnit, DisplayUnitId, SemanticRole, and UnitSource provide a first-class unit abstraction. Navigation is handled via NavigationPolicy (per-unit policy resolution through plugin dispatch, FirstWins composition) and NavigationAction / ActionResult.

InteractionPolicy (rendering/cursor suppression) and NavigationPolicy (input/navigation) are orthogonal: a fold summary has InteractionPolicy::ReadOnly (σ is weak) and NavigationPolicy::Boundary { ToggleFold } (plugin-declared).

Registration methods on HandlerRegistry:

/// Declare navigation policy for display units produced by this plugin.
pub fn on_navigation_policy(
    &mut self,
    handler: impl Fn(&S, &DisplayUnit) -> NavigationPolicy + Send + Sync + 'static,
);

/// Handle navigation actions on this plugin's display units.
pub fn on_navigation_action(
    &mut self,
    handler: impl Fn(&S, &DisplayUnit, NavigationAction) -> (S, ActionResult)
    + Send + Sync + 'static,
);

Default policies (when no plugin registers a policy):

SemanticRole Default NavigationPolicy
BufferContent Normal
FoldSummary Boundary { ToggleFold }
EditableVirtualText Boundary { ActivateShadowCursor }
Plugin(_, _) Skip

Extensibility boundaries:

Dimension Extensible? Mechanism
Unit kinds (SemanticRole) Yes Plugin(tag, id) variant
Navigation policies Yes on_navigation_policy registration
Click/activation behavior Yes on_navigation_action registration
Source mapping kinds (UnitSource) No Closed enum — core must determine σ strength for DU-INV-4
InteractionPolicy override No Derived from σ strength — prevents undefined cursor placement
Cross-plugin policy override Yes FirstWins priority

Constraints:

  • Sub-line display units (UnitSource::Span) are defined in the type but not yet produced by the builder
  • Plugins must not fabricate facts that Kakoune has not provided as the result of interactions

3.5 Commands

Hook functions issue side-effect requests through the unified Effects struct. The framework validates command legality per lifecycle phase via Effects::validate(phase):

  • Bootstrap: only RequestRedraw; no scroll plans.
  • SessionReady: SendToKakoune, Paste, PluginMessage, RequestRedraw; scroll plans allowed.
  • Runtime: all commands and scroll plans allowed.

Effects carries redraw: DirtyFlags, commands: Vec<Command>, and scroll_plans: Vec<ScrollPlan>.

Command Description
SendToKakoune(req) Send a request to Kakoune
PasteClipboard Paste from the host system clipboard
Quit Quit the application
RequestRedraw(flags) Request a redraw
ScheduleTimer { delay, target, payload } Send a message to target after a delay
PluginMessage { target, payload } Send a message to another plugin
SetConfig { key, value } Change a runtime configuration (deprecated, use SetSetting)
SetSetting { key, value } Set a typed plugin setting. Value is a SettingValue variant (bool/integer/float/string). Scoped to the calling plugin.
SpawnProcess { job_id, program, args, stdin_mode } Spawn an external process (Phase P-2)
Session(SessionCommand) Create or close a Kakoune session managed by the host runtime
WriteToProcess { job_id, data } Write to the stdin of a spawned process
CloseProcessStdin { job_id } Close a process's stdin (EOF)
KillProcess { job_id } Force-kill a process
SpawnPaneClient { surface_id, placement } Spawn a new pane backed by an independent Kakoune client
ClosePaneClient { surface_id } Close a pane and terminate its Kakoune client
Workspace(WorkspaceCommand) Workspace operations
RegisterSurface { surface, placement } Register a plugin-owned surface into the workspace
UnregisterSurface { surface_id } Unregister a plugin-owned surface
EditBuffer { edits } Apply structured buffer edits (translated to Kakoune key sequences)
InjectInput(InputEvent) Re-dispatch a synthetic input event through the update system
RegisterThemeTokens(tokens) Register custom theme tokens

SessionCommand has the following variants:

  • Spawn { key, session, args, activate }: Open a new managed session. key: Option<String> is an optional stable key within the host, session: Option<String> is the session name corresponding to kak -c <name>, and activate = true immediately switches to that session as the active session.
  • Close { key }: Close the session with the specified key. key: Option<String> = None closes the current active session. If the last session is closed, the host runtime terminates. If the active session is closed and other sessions remain, the host runtime promotes the next session in creation order to active.
  • Switch { key }: Switch the active session to the one identified by key: String.

The V1 session runtime can hold multiple sessions, but only one active session is rendered at a time. The Kakoune reader for inactive sessions remains alive, and its events continue to be reflected in the off-screen session snapshot. When activated, that snapshot is restored, but automatic generation of session-bound surfaces and multi-session dedicated UI are not yet implemented.

In WASM, these are represented as command variants. SpawnPaneClient, ClosePaneClient, Workspace, RegisterSurface, UnregisterSurface, and RegisterThemeTokens are currently not supported in WASM. Process execution commands (SpawnProcess, etc.), session management commands (spawn-session, close-session), edit-buffer, and inject-key have been introduced on the WIT side.

3.5.1 Buffer Editing

Command::EditBuffer { edits } allows plugins to apply structured edits to the buffer. Each BufferEdit specifies a range (1-indexed BufferPosition { line, column }) and a replacement string. The framework translates edits into Kakoune key sequences via edits_to_keys():

  • Edits are applied bottom-up (higher lines first) to preserve line/column validity
  • Zero-width range with non-empty replacement = insertion at point
  • Non-zero range with empty replacement = deletion
  • Non-zero range with non-empty replacement = replacement (select + c + text)

EditBuffer is an immediate command — it is executed inline during command processing. In WASM, use the edit-buffer(list<buffer-edit>) command variant.

3.5.2 Input Injection

Command::InjectInput(InputEvent) re-dispatches a synthetic input event through the update() state machine, as if the user had pressed the key. This enables plugins to programmatically trigger input-driven behavior.

  • Injection is recursive: commands produced by the injected event are processed in the same batch
  • A depth guard (MAX_INJECT_DEPTH = 10) prevents infinite recursion
  • Only InputEvent::Key is supported via WASM (inject-key(key-event) command variant)

3.5.3 Session Observability

Plugins can observe session state and control session switching:

  • Session query: AppState.session_descriptors provides the list of sessions (SessionDescriptor { key, session_name, buffer_name, mode_line }), and AppState.active_session_key identifies the current session. buffer_name is extracted from status_content atoms and mode_line from status_mode_line atoms of the session's AppState snapshot. In WASM, Tier 8 host-state functions get-session-count, get-session(index), and get-active-session-key provide equivalent access.
  • Session lifecycle notification: DirtyFlags::SESSION is set when sessions are created, closed, switched, or when a session dies. Plugins react via on_state_changed.
  • Session switch command: SessionCommand::Switch { key } (native) or command::switch-session(key) (WIT) requests activation of a specific session by key.

See ADR-023 for the boundary rationale and decision record.

WASM plugins are sandboxed by default. The host constructs the WASI context from the plugin manifest (kasane-plugin.toml[capabilities].wasi) before instantiating the WASM component — plugins never participate in their own permission decisions. Without a manifest capability declaration, access to host resources such as file system and network is unavailable. The host functions available to WASM plugins are limited to the two WIT interfaces: host-state (state reading) and element-builder (element construction). Per Phase P (ADR-019), preopened_dir / env are unlocked based on manifest capability declarations (P-1), and process execution is provided via host mediation (Command::SpawnProcess + IoEvent) (P-2). Process execution requires declaring process in the manifest's [capabilities].wasi, which can be denied via deny_capabilities in kasane.kdl.

4. Capabilities and Caching

4.1 PluginCapabilities

PluginCapabilities is a bitflag declaring the features a plugin implements, used to skip unnecessary method calls.

Flag Description
CONTRIBUTOR contribute_to()
TRANSFORMER transform()
ANNOTATOR annotate_line_with_ctx()
OVERLAY contribute_overlay_with_ctx()
MENU_TRANSFORM transform_menu_item()
RENDER_ORNAMENT render_ornaments()
INPUT_HANDLER handle_key() / handle_mouse()
SCROLL_POLICY handle_default_scroll()
PANE_LIFECYCLE Pane lifecycle hooks
PANE_RENDERER render_pane()
SURFACE_PROVIDER surfaces()
WORKSPACE_OBSERVER on_workspace_changed()
IO_HANDLER on_io_event_effects()
DISPLAY_TRANSFORM display_directives()

For native plugins the default is all(). For WASM plugins, the authoritative source is the plugin manifest (kasane-plugin.toml[handlers].flags); the WASM adapter receives pre-computed flags from the manifest without querying guest code.

PANE_LIFECYCLE, PANE_RENDERER, WORKSPACE_OBSERVER, and DISPLAY_TRANSFORM are currently native-only, but SURFACE_PROVIDER has also been introduced on the WIT side as hosted surface descriptors / render-surface. It is not assumed that the same trait signatures will be directly mapped to WIT.

4.2 State hash and caching

Plugin contribution caching is handled by Salsa incremental computation. The framework tracks dependencies automatically via PartialEq-based early cutoff on Salsa inputs.

The plugin-side caching mechanisms are:

state_hash() — signals plugin-internal state changes:

// WASM
fn state_hash() -> u64 {
    MY_STATE.get() as u64
}

PluginBackend implementors provide state_hash() to signal state changes. Plugin (state-externalized) eliminates manual state_hash() — the framework tracks state changes automatically via PartialEq comparison on the externalized state, using a generation counter.

view_deps() — declares which DirtyFlags a plugin's view methods depend on:

fn view_deps(&self) -> DirtyFlags {
    DirtyFlags::BUFFER
}

When neither the plugin's state_hash() changed nor any of its declared view_deps() flags are dirty, the framework skips re-collecting that plugin's contributions, annotations, overlays, and display directives entirely. Salsa inputs retain their previous values, so downstream memoized queries remain valid.

Default: DirtyFlags::ALL (always re-collect — safe fallback for backward compatibility). Override with the narrowest set of flags your view methods actually read. For example, a line-numbers plugin that only reads buffer content should return DirtyFlags::BUFFER.

The correctness invariant is: declared deps must be a superset of actual deps. If view_deps() omits a flag that a view method actually depends on, stale contributions may persist until the next matching dirty event.

4.3 Render Ornaments

render_ornaments() returns an OrnamentBatch containing backend-independent physical ornament proposals for the current frame. This is a unified extension point covering cell-level face overrides and cursor style changes — operations that act on the rendered grid rather than the Element tree.

Register via HandlerRegistry::on_render_ornaments(). Capability flag: RENDER_ORNAMENT.

OrnamentBatch fields (see plugin/render_ornament.rs):

Field Type Description
emphasis Vec<CellDecoration> Cell-level face overrides (see §4.3.1)
cursor_style Option<CursorStyleOrn> Cursor shape override (see §4.3.2)
cursor_effects Vec<CursorEffectOrn> Reserved for future use
surfaces Vec<SurfaceOrn> Reserved for future use

Native (HandlerRegistry):

registry.on_render_ornaments(|state, app, ctx| {
    OrnamentBatch {
        emphasis: vec![CellDecoration {
            target: DecorationTarget::Cell(Coord { line: app.cursor_line() as u32, column: 0 }),
            style: Style { bg: Brush::rgb(40, 40, 40), ..Style::default() },
            merge: FaceMerge::Background,
            priority: 0,
        }],
        cursor_style: None,
        ..Default::default()
    }
});

WASM: Implement render-ornaments(ctx: ornament-context) -> ornament-batch in the guest.

The RenderOrnamentContext provides viewport information (screen_cols, screen_rows, visible_line_start, visible_line_end) so plugins can limit work to visible cells.

4.3.1 Cell Decorations

Cell decorations (OrnamentBatch.emphasis) apply face overrides to individual cells, cell ranges, or entire columns after paint. Unlike annotate_line_with_ctx() which operates at the line level, cell decorations target arbitrary screen coordinates and are composable across plugins.

CellDecoration fields (see plugin/context.rs):

Field Type Description
target DecorationTarget Where to apply the decoration
face Face The face to apply
merge FaceMerge How to merge with the existing cell face
priority i16 Lower priority decorations are applied first

DecorationTarget variants:

Variant Description
Cell(coord) A single cell at the given buffer coordinate
Range { start, end } A contiguous range of cells (inclusive)
Column { column } An entire column across all visible rows

FaceMerge modes:

Mode WASM value Description
Replace 0 Completely replace the existing face
Overlay 1 Overlay non-default fields onto the existing face
Background 2 Only apply the background color

Decorations from multiple plugins are collected, sorted by priority (ascending), and applied in order.

4.3.2 Cursor Style Override

OrnamentBatch.cursor_style allows a plugin to change the cursor shape. When multiple plugins provide a value, resolution uses priority and OrnamentModality (Must > Approximate > May).

CursorStyleOrn fields (see plugin/render_ornament.rs):

Field Type Description
hint CursorStyleHint Cursor shape (Block, Bar, Underline, Outline)
priority i16 Lower wins among same modality
modality OrnamentModality Must / Approximate / May

Cursor shapes (WASM option<u8>):

Value Shape
0 Block (default)
1 Bar
2 Underline
3 Outline (hollow block)

4.4 SDK Helpers

The kasane_plugin_sdk::generate!() macro emits the following helper functions alongside WIT bindings, reducing boilerplate for common operations.

Style Construction

WASM plugins build Style values directly through the WIT-generated bindings. Style::default() works; for explicit fields use Brush::DefaultColor (the inheritance sentinel — equivalent to "resolve later from the parent"), Brush::Named(NamedColor::*), or Brush::Rgb(RgbColor { r, g, b }). WIT 2.0.0+ retired the legacy Face/Color/Attributes triple; Style is the single post-resolve paint description.

// Background-only highlight at column 80
let style = Style {
    bg: Brush::Rgb(RgbColor { r: 40, g: 40, b: 50 }),
    ..Style::default()
};

FontWeight is continuous (100..=900); FontWeight::BOLD (700) replaces the legacy Attributes::BOLD bit. Boolean attributes (reverse, dim, blink) are direct fields on Style.

Overlay Layout

Helper Description
centered_overlay(cols, rows, w_pct, h_pct, min_w, min_h) Compute a centered AbsoluteAnchor

Command Helpers

Helper Description
redraw() Request a full redraw (DirtyFlags::ALL)
redraw_flags(flags) Request a redraw for specific dirty flags
send_command(cmd) Build Command::SendKeys(<esc>:cmd<ret>)
paste_clipboard() Build Command::PasteClipboard for host system clipboard insertion

Key Escaping (kasane_plugin_sdk::keys)

Function Description
keys::push_literal(keys, text) Push each char as an escaped key string
keys::command(cmd) Build <esc>:cmd<ret> key sequence

Channel Serialization (kasane_plugin_sdk::channel)

Helpers for MessagePack serialization at the WASM boundary. WASM plugins work with the WIT-generated ChannelValue { data, type_hint } struct; this module handles the raw bytes.

Function Description
channel::serialize<T>(value) Serialize T into (Vec<u8>, String) for a WIT channel-value
channel::deserialize<T>(data) Deserialize &[u8] into Option<T>
use kasane_plugin_sdk::channel;

// Publishing
let (data, type_hint) = channel::serialize(&42u32);
// → ChannelValue { data, type_hint }

// Subscribing
let value: Option<u32> = channel::deserialize(&received.data);

Predicate Builder Macros

RPN-encoded predicate sequences for conditional transform patches. These macros use the WIT-generated PredicateOp type, so they must be called inside a plugin crate (after generate!() or define_plugin!).

Macro Output
pred_has_focus!() [HasFocus]
pred_surface_is!(id) [SurfaceIs(id)]
pred_line_range!(start, end) [LineRange { start, end }]
pred_not!(inner) [...inner, NotOp]
pred_and!(a, b) [...a, ...b, AndOp]
pred_or!(a, b) [...a, ...b, OrOp]
use kasane_plugin_sdk::{pred_has_focus, pred_not, pred_and, pred_surface_is};

// "has focus AND surface is 0"
let pred = pred_and!(pred_has_focus!(), pred_surface_is!(0));
// "NOT has focus"
let pred = pred_not!(pred_has_focus!());

Attribute Constants (kasane_plugin_sdk::attributes)

Constants matching kasane_core::protocol::color::Attributes bitflags: UNDERLINE, BOLD, ITALIC, REVERSE, etc.

State Macro (kasane_plugin_sdk::state!)

Generates a struct with a generation counter, Default impl, bump_generation() method, and thread_local! STATE.

kasane_plugin_sdk::state! {
    struct PluginState {
        cursor_count: u32 = 0,
        active: bool = false,
    }
}
// Access: STATE.with(|s| { let state = s.borrow(); ... })

In define_plugin!, the state {} section supports #[bind(expr, on: flags)] attributes on fields to auto-generate sync code in on_state_changed_effects(). Mutable contexts (handle_key, overlay, on_io_event_effects, etc.) use a StateMutGuard that auto-calls bump_generation() on drop, so manual calls are no longer required.

Auto Imports

generate!() also provides glob-imported auto-imports for common WIT types. Explicit use statements in existing code shadow these without conflict:

  • Guest (from exports::kasane::plugin::plugin_api)
  • host_state, element_builder (from kasane::plugin)
  • types::* (Face, Color, SlotId, Command, etc.)

5. Styling

5.1 StyleToken

StyleToken is a semantic style token that maps to a Face from the theme configuration.

Token name Purpose
buffer.text Buffer text
buffer.padding Buffer padding
status.line Status bar
status.mode Mode display
menu.item.normal Normal menu item
menu.item.selected Selected menu item
menu.scrollbar / menu.scrollbar.thumb Scrollbar
info.text / info.border Info popup
border / shadow Border / shadow

Custom tokens can be created and registered by plugins.

StyleToken::new("myplugin.highlight")

fn on_init_effects(&mut self, _app: &AppView<'_>) -> Effects {
    Effects::redraw(DirtyFlags::STATUS)
}

5.2 kasane.kdl integration

theme {
    "menu.selected" fg="black" bg="blue"
    "myplugin.highlight" fg="yellow"
}

6. Advanced API

6.1 Surface provider

Plugins with the SURFACE_PROVIDER capability can provide their own surfaces. In Native, they return Box<dyn Surface>, while in WASM, they map to a hosted surface model returning static surface-descriptor groups, render-surface(surface-key, ctx), handle-surface-event(surface-key, event, ctx), and handle-surface-state-changed(surface-key, dirty-flags).

impl PluginBackend for MyPlugin {
    fn capabilities(&self) -> PluginCapabilities {
        PluginCapabilities::SURFACE_PROVIDER
    }

    fn surfaces(&mut self) -> Vec<Box<dyn Surface>> {
        vec![Box::new(MySidebar::new())]
    }
}
Method Description
id() -> SurfaceId Unique ID
surface_key() -> CompactString Stable semantic key
size_hint() -> SizeHint Preferred size
initial_placement() -> Option<SurfacePlacementRequest> Static initial placement
view(ctx: &ViewContext) -> Element Build Element tree
handle_event(event, ctx) -> Vec<Command> Event handling
on_state_changed_effects(state, dirty) -> Effects Shared state change notification
state_hash() -> u64 Hash for view cache
declared_slots() -> &[SlotDeclaration] Extension point declarations

ViewContext provides state, rect, focused, registry, and surface_id. Collection/registration of plugin-owned surfaces and initial_placement() are evaluated during bootstrap preflight, and workspace_request() is used only as a legacy fallback during the transition period. The descriptor's initial_placement() reflects SplitFocused / SplitFrom / Tab / TabIn / Dock / Float directly from the surface path into the workspace. Dock uses SizeHint's preferred/min size to determine the ratio when the root rect is known, and falls back to a default ratio otherwise. Commands returned by handle_event() / handle-surface-event(...) / handle-surface-state-changed(...) are executed in the context of the surface owner plugin, so capability-gated deferred commands such as SpawnProcess are evaluated under the owner plugin's permissions. on_state_changed_effects(...) is called at least on shared state updates originating from the Kakoune protocol, allowing the surface owner to return additional commands based on dirty flags.

6.2 Workspace commands

WorkspaceCommand manipulates surface placement and layout.

WorkspaceCommand Description
AddSurface { surface_id, placement } Add a surface
RemoveSurface(id) Remove a surface
Focus(id) Move focus
FocusDirection(dir) Directional focus
Resize { delta } Adjust split ratio. Split divider drag also internally falls through to this command
Swap(id1, id2) Swap surfaces
Float { surface_id, rect } Make a surface floating
Unfloat(id) Return to tiled mode. If split metadata from the previous float remains, it is preferentially used for restoration
Placement Description
SplitFocused { direction, ratio } Split the focused surface
SplitFrom { target, direction, ratio } Split from a specific surface
Tab / TabIn { target } Add a tab
Dock(position) Dock to Left/Right/Bottom/Panel
Float { rect } Add as floating

6.3 Custom slots

Surfaces can define custom slots that other plugins can contribute to by returning declared_slots().

impl Surface for MySurface {
    fn declared_slots(&self) -> &[SlotDeclaration] {
        &[
            SlotDeclaration::new("myplugin.sidebar.top", SlotKind::AboveBand),
            SlotDeclaration::new("myplugin.sidebar.bottom", SlotKind::BelowBand),
        ]
    }
}

SlotDeclaration.kind is advisory metadata; the actual placement is determined by Element::SlotPlaceholder. Other plugins use contribute_to(&SlotId::new("myplugin.sidebar.top"), state, ctx). In WASM, the same slot name is specified via SlotId::Named("myplugin.sidebar.top".into()).

6.4 Plugin messages and timers

Command::PluginMessage { target, payload } enables inter-plugin message passing.

  • Native: Downcast in update(msg: Box<dyn Any>, state)
  • WASM: Receive byte array in update(payload: Vec<u8>)

Command::ScheduleTimer { delay, target, payload } performs delayed message sending.

6.5 Pane lifecycle

Plugins with the PANE_LIFECYCLE capability can observe pane creation, deletion, and focus changes.

Hook Description
on_pane_created(pane_id, state) Pane creation notification
on_pane_closed(pane_id) Pane deletion notification
on_focus_changed(from, to, state) Focus change notification

With the PANE_RENDERER capability, render_pane(pane_id, cols, rows) can render plugin-owned panes.

6.6 Inter-plugin pub/sub

Topic-based publish/subscribe enables typed inter-plugin communication. Evaluation is two-phase: publishers produce values, then subscribers receive them.

Native (typed):

// Publisher
let topic: Topic<u32> = r.publish_typed("cursor.line", |state, _app| state.line);

// Subscriber (type mismatch = compile error)
r.subscribe_typed::<u32>("cursor.line", |state, value| {
    MyState { last_line: *value, ..state.clone() }
});

WASM: Declare topics in the manifest and implement the WIT exports:

[handlers]
publish_topics = ["cursor.line"]
subscribe_topics = ["theme.changed"]
// WIT exports (default implementations provided by #[plugin])
fn publish_value(topic: String) -> Option<ChannelValue> { ... }
fn on_subscription(topic: String, values: Vec<ChannelValue>) -> RuntimeEffects { ... }

Use kasane_plugin_sdk::channel::serialize() / deserialize() for value conversion.

6.7 Extension points

Plugin-defined extension points allow plugins to define custom hooks that other plugins can contribute to.

Native:

// Definer
r.define_extension::<(), Vec<StatusItem>>(
    ExtensionPointId::new("myplugin.status-items"),
    CompositionRule::Merge,
);

// Contributor
r.on_extension::<(), Vec<StatusItem>>(
    ExtensionPointId::new("myplugin.status-items"),
    |_state, _input, _app| vec![StatusItem { ... }],
);

WASM: Declare in the manifest and implement the WIT export:

[handlers]
extensions_defined = ["myplugin.status-items"]
extensions_consumed = ["other.ext"]
// WIT export (default implementation provided by #[plugin])
fn evaluate_extension(id: String, input: ChannelValue) -> Option<ChannelValue> { ... }

6.8 Capability descriptor and interference detection

CapabilityDescriptor provides structured metadata about a plugin's transform targets, contribution slots, pub/sub topics, and extension points. The framework uses may_interfere() to warn when two plugins overlap on the same targets or slots.

Native plugins: inferred automatically from HandlerRegistry method calls (e.g., on_transform_for() populates transform_targets).

WASM plugins: declared in the manifest [handlers] section (transform_targets, publish_topics, etc.) and converted to CapabilityDescriptor at load time.

7. Inter-Plugin Cooperation

7.1 Available Mechanisms

  1. PluginMessage: Point-to-point via Command::PluginMessage { target, payload }. No type safety, no delivery guarantee, no RPC.
  2. ConfigEntry: Publish via Command::SetConfig { key, value }; read via AppView::plugin_config(). Indirect, delayed (next frame).
  3. Transform chain: Indirectly modify other plugins' contributions through ElementPatch. The transformer does not know whose contribution it is transforming.
  4. Topic-based Pub/Sub: Broadcast via TopicBus. Two-phase evaluation (collect → deliver) with cycle prevention. Type safety is runtime-enforced (downcast). See plugin/pubsub.rs.
  5. Plugin-defined Extension Points: Define ExtensionPointId with CompositionRule (Merge, FirstWins, Chain). See plugin/extension_point.rs.

7.2 Impossible Cooperation Patterns

  • Conditional contribution ("if Plugin B contributed to slot X, adjust mine") — impossible. Each plugin generates contributions independently.
  • Relative positioning ("place my overlay next to Plugin C's") — impossible. Overlays use absolute positioning or anchors.
  • Resource budget negotiation — impossible. Framework resolves via layout.
  • Synchronous RPC (Plugin A → B → A) — impossible. Pub/sub is broadcast-only.
  • Atomic transactions across multiple plugins — impossible. Each plugin's state transition is independent.

8. WASM Plugin Constraints

This section catalogs constraints of WASM plugins compared to native plugins: [By Design] (intentional boundary), [Not Yet Implemented] (planned), [Improvement] (ergonomic friction).

8.1 Feature Gaps

Feature Native WASM Gap
Dynamic surfaces Full Static only Cannot add/remove/move post-init [Not Yet Implemented]
Pane lifecycle Full None No create/close/focus hooks [Not Yet Implemented]
Pane rendering Full None Cannot own custom panes [Not Yet Implemented]
Pane commands Full None No split/close/focus commands [Not Yet Implemented]
Inter-plugin messaging Box<dyn Any> Vec<u8> Serialization required
State access Direct &AppState ~40 getter functions Guarded access [By Design]
Cache invalidation Automatic (PartialEq) Manual state_hash() [Improvement]
Execution timeout N/A Epoch interruption (~10ms)
Memory limits N/A 64 MB per plugin
Environment variables Full Allowlisted subset [By Design]

8.2 Missing State Queries [Not Yet Implemented]

State Native WASM
Selection ranges and mode state.selections Not available
Search state and regex state.search Not available
Input mode state.input_mode Not available
Named registers state.registers Not available
Completion candidates state.completions Not available
Pane tree structure state.panes Not available
Full face registry state.faces Not available

8.3 Runtime Constraints

  • Synchronous execution [By Design]: All calls block the host thread. Long-running work should use SpawnProcess.
  • No threading [By Design]: WASI does not include threading support.
  • No network access [By Design]: Spawn a helper process via SpawnProcess and communicate over stdin/stdout.
  • Element handle scope [By Design]: Handles are valid only within the current plugin call.
  • Epoch interruption: A background ticker (10ms interval) traps runaway plugins. Runtime calls have a 1-epoch (~10ms) deadline; instantiation uses 100 epochs (~1s).
  • Memory limits: Each plugin Store is limited to 64 MB memory, 10K table elements, and 10 instances via StoreLimits.
  • Environment variable allowlist [By Design]: Only a safe subset (HOME, PATH, SHELL, EDITOR, PAGER, TERM, LANG, LC_ALL, XDG_*) is exposed. Plugins can declare additional variables via manifest capabilities.env_vars.

8.4 Intentional Design Constraints

Constraint Rationale
No direct AppState mutation Pure function semantics, deterministic rendering
No commands during view phase Eliminate rendering side effects
No access to other plugins' state Plugin isolation, testability
No WASM network I/O Sandbox security

9. Related Documents