# bmux Plugin SDK
Everything you need to write a bmux plugin.
## Quick Start
A bmux plugin is a Rust crate with three files: a manifest, a library, and a `Cargo.toml`.
### 1. `plugin.toml`
```toml
id = "example.hello"
name = "Hello Plugin"
version = "0.1.0"
[[commands]]
name = "hello"
summary = "Print a greeting"
expose_in_cli = true
```
### 2. `src/lib.rs`
```rust
use bmux_plugin_sdk::prelude::*;
#[derive(Default)]
pub struct HelloPlugin;
impl RustPlugin for HelloPlugin {
fn run_command(&mut self, ctx: NativeCommandContext) -> Result<i32, PluginCommandError> {
bmux_plugin_sdk::route_command!(ctx, {
"hello" => {
let name = ctx.arguments.first().map_or("world", String::as_str);
println!("Hello, {name}!");
Ok(EXIT_OK)
},
})
}
}
bmux_plugin_sdk::export_plugin!(HelloPlugin, include_str!("../plugin.toml"));
```
### 3. `Cargo.toml`
```toml
[package]
name = "my_plugin"
edition = "2024"
version = "0.1.0"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
bmux_plugin_sdk = { version = "..." }
[features]
static-bundled = []
```
`crate-type` must include `cdylib` (for dynamic loading) and `rlib` (for static bundling into the host binary).
## The `RustPlugin` Trait
Every plugin implements `RustPlugin`. All five methods have default implementations -- override only what your plugin needs:
| `run_command` | `Result<i32, PluginCommandError>` | Plugin provides CLI commands |
| `invoke_service` | `ServiceResponse` | Plugin provides services to other plugins |
| `activate` | `Result<i32, PluginCommandError>` | Plugin needs setup on activation |
| `deactivate` | `Result<i32, PluginCommandError>` | Plugin needs cleanup on deactivation |
| `handle_event` | `Result<i32, PluginCommandError>` | Plugin subscribes to system events |
The trait requires `Default + Send + 'static`. Use `#[derive(Default)]` on your struct.
## Writing Command Plugins
### Dispatching commands
Use `route_command!` to match on the command name and auto-generate the unknown-command fallback:
```rust
fn run_command(&mut self, ctx: NativeCommandContext) -> Result<i32, PluginCommandError> {
bmux_plugin_sdk::route_command!(ctx, {
"list" => handle_list(&ctx),
"create" => handle_create(&ctx),
})
}
```
Each arm must evaluate to `Result<i32, PluginCommandError>`. Unrecognised commands automatically return `Err(PluginCommandError::unknown_command(...))`.
### Error handling
`PluginCommandError` implements `From` for common error types, so the `?` operator works naturally:
```rust
fn handle_list(ctx: &NativeCommandContext) -> Result<i32, PluginCommandError> {
let data = std::fs::read_to_string("config.toml")?; // io::Error -> PluginCommandError
let parsed: Config = toml::from_str(&data)?; // toml::de::Error -> PluginCommandError
println!("{parsed:?}");
Ok(EXIT_OK)
}
```
Supported conversions: `String`, `&str`, `std::io::Error`, `serde_json::Error`, `toml::de::Error`, `Box<dyn Error>`, `Box<dyn Error + Send + Sync>`.
For custom error codes, construct directly:
```rust
Err(PluginCommandError::new(EXIT_USAGE, "missing required argument"))
Err(PluginCommandError::unavailable("feature not supported on this platform"))
```
### Exit codes
| `EXIT_OK` | 0 | Success |
| `EXIT_ERROR` | 1 | Generic failure |
| `EXIT_USAGE` | 64 | Bad arguments or unknown command |
| `EXIT_UNAVAILABLE` | 70 | Plugin unavailable |
### Arguments
Commands receive arguments as `ctx.arguments: Vec<String>`. The host CLI layer handles parsing and validation based on the argument declarations in `plugin.toml` -- the plugin receives the parsed values as strings.
## Writing Service Plugins
Service plugins handle inbound requests from other plugins or the host runtime.
### Dispatching services
Use `route_service!` to match on `(interface_id, operation)` pairs. Each handler receives a typed request and returns a typed response:
```rust
fn invoke_service(&mut self, context: NativeServiceContext) -> ServiceResponse {
bmux_plugin_sdk::route_service!(context, {
"my-service/v1", "do_thing" => |req: DoThingRequest, _ctx| {
let result = do_the_thing(&req.input)?;
Ok(DoThingResponse { output: result })
},
})
}
```
The macro wraps each handler in `handle_service()`, which handles request deserialization, response serialization, and error conversion. Unrecognised operations return a standard "unsupported" error.
Request and response types must implement `serde::Deserialize` and `serde::Serialize` respectively.
### Returning errors from service handlers
Service handler closures return `Result<Resp, ServiceResponse>`. Use `map_err` to convert domain errors:
```rust
`use bmux_plugin_sdk::prelude::*` imports the ~16 items most plugins need:
- **Trait:** `RustPlugin`
- **Context types:** `NativeCommandContext`, `NativeLifecycleContext`, `NativeServiceContext`
- **Error type:** `PluginCommandError`
- **Exit codes:** `EXIT_OK`, `EXIT_ERROR`, `EXIT_USAGE`, `EXIT_UNAVAILABLE`
- **Service types:** `ServiceKind`, `ServiceResponse`
- **Events:** `PluginEvent`
- **Helpers:** `handle_service`, `decode_service_message`, `encode_service_message`
Types not in the prelude (import individually when needed):
- Host service DTOs: `SessionSelector`, `StorageGetRequest`, `ContextCreateRequest`, etc.
- Manifest types: `PluginCommand`, `PluginService`, `PluginEventSubscription`
- Capability types: `HostScope`, `PluginFeature`
## When to Also Depend on `bmux_plugin`
The `bmux_plugin` crate provides two traits that live outside the SDK:
- **`ServiceCaller`** -- the low-level trait for dispatching cross-plugin service calls
- **`HostRuntimeApi`** -- ergonomic methods like `ctx.session_list()`, `ctx.storage_get(...)`, `ctx.context_create(...)`
If your plugin calls host services (session management, storage, pane operations, etc.), add `bmux_plugin` as a dependency and import these traits:
```rust
use bmux_plugin::{HostRuntimeApi, ServiceCaller};
```
Simple plugins that only handle commands or receive service calls do **not** need `bmux_plugin`.
## Manifest Reference (`plugin.toml`)
### Top-Level Fields
| `id` | string | **Yes** | -- | Unique plugin identifier (e.g. `"bmux.clipboard"`) |
| `name` | string | **Yes** | -- | Human-readable display name |
| `version` | string | **Yes** | -- | Plugin version (semver) |
| `description` | string | No | -- | Optional description |
| `homepage` | string | No | -- | Optional URL |
| `runtime` | string | No | `"native"` | Runtime type (only `"native"` supported) |
| `entry` | path | No | -- | Path to `.dylib`/`.so` (only for external plugins) |
| `entry_symbol` | string | No | `"bmux_plugin_entry_v1"` | FFI entry symbol |
| `provider_priority` | integer | No | `0` | Ordering when multiple plugins provide the same capability |
| `required_capabilities` | list | No | `[]` | Host capabilities this plugin needs |
| `provided_capabilities` | list | No | `[]` | Capabilities this plugin provides |
| `provided_features` | list | No | `[]` | Feature flags this plugin provides |
### Compatibility (optional)
```toml
[plugin_api]
minimum = "1.0" # default; omit entire section if 1.0 is fine
maximum = "2.0" # optional upper bound
[native_abi]
minimum = "1.0" # default; omit entire section if 1.0 is fine
```
### Commands
```toml
[[commands]]
name = "hello" # required -- dispatch name (matches ctx.command)
summary = "Print a greeting" # required -- short description for help text
expose_in_cli = true # default: false -- set true to show as CLI subcommand
path = ["greet"] # optional -- CLI subcommand path
aliases = [["say", "hi"]] # optional -- alternative CLI paths
execution = "provider_exec" # default: "provider_exec"
description = "Longer help" # optional
```
`execution = "provider_exec"` runs the command in the plugin provider process.
Use `execution = "caller_process"` for commands that need caller-local runtime
facilities, such as an attach client's prompt/modal host.
#### Command Arguments
```toml
[[commands.arguments]]
name = "target" # required
position = 0 # optional: positional index (omit for flags/options)
long = "target" # optional: --target
short = "t" # optional: -t
multiple = true # default: false
value_name = "TARGET" # optional: displayed in help
choice_values = ["a", "b"] # for kind = "choice"
```
### Services
```toml
[[services]]
capability = "bmux.clipboard.write" # required -- host capability scope
interface_id = "clipboard-write/v1" # required -- service interface identifier
kind = "command" # required -- "command" or "query"
```
### Event Subscriptions
```toml
[[event_subscriptions]]
kinds = ["system", "window"]
names = ["server_started", "window_created"]
```
### Dependencies
```toml
[[dependencies]]
plugin_id = "bmux.permissions"
version_req = "=0.0.1-alpha.0"
required = true # default: true
```
### Keybindings
```toml
[keybindings.runtime]
c = "plugin:bmux.windows:new-window"
"alt+w" = "plugin:bmux.windows:switch-window"
[keybindings.scroll]
y = "copy_scrollback"
[keybindings.global]
# global keybindings (active outside runtime mode)
```