Bridge & Mount
Bridge is the runtime protocol a compiled binary sibling uses to talk to mnml — post toasts, drive activity-bar badges, own a statusline segment, drop context-menu entries. Mount is the highest tier, where a sibling takes over a pane and renders directly into mnml’s editor body rather than running as a Pty. Both ship as the mnml-bridge crate — one dep, four integration depths.
This page is the field guide for using and building bridged siblings. For pure launchers (no code, no binary — just a TOML manifest that shells out to an existing CLI) see Launcher manifests instead. The Bridge protocol only applies to compiled Rust siblings; launchers don’t need it.
The archetypal example is a database browser or service dashboard published by a community author — e.g. a hypothetical mnml-db-postgres on crates.io. mnml core no longer ships specific sibling binaries — everything installable comes from the Marketplace.
The four tiers
Section titled “The four tiers”Bridge is layered. Each tier is purely additive — siblings opt in to whatever depth makes sense for them.
| Tier | What | Sibling code |
|---|---|---|
| 1. Env vars | MNML_WORKSPACE, MNML_THEME, MNML_IPC_DIR set on every Pty mnml spawns. |
Read on startup; zero protocol. |
| 2. JSONL host calls | Sibling appends JSONL commands to $MNML_IPC_DIR/command. |
One file write per call. |
3. mnml-bridge SDK |
Typed Rust wrapper around tiers 1 + 2. | use mnml_bridge::* |
| 4. Mount | Sibling owns an activity-bar icon + renders frames into a pane via Unix socket. | Connect, ratatui-render, ship Frames. |
Bridge is the system as a whole. Mount is the specific tier where the sibling takes over UI space.
Tier 1 — env vars
Section titled “Tier 1 — env vars”Every Pty mnml spawns (:term <binary>, the activity-bar
shortcuts, the mount.open palette, etc.) is launched with these
in its environment:
MNML_WORKSPACE=/Users/you/Projects/your-repoMNML_THEME=cyberdreamMNML_IPC_DIR=/Users/you/Projects/your-repo/.mnml/ipcSiblings read these on startup. A theme-aware sibling can match mnml’s accent colours; a workspace-aware sibling can scope to the active project; the IPC dir is where tier 2 talks back.
If you run a sibling outside mnml the env vars are absent; the sibling should fall back to its standalone defaults.
Tier 2 — JSONL host calls
Section titled “Tier 2 — JSONL host calls”The sibling writes one JSON object per line to
$MNML_IPC_DIR/command. mnml ingests the file each tick. These
commands are recognised:
| Command | Payload | Effect |
|---|---|---|
toast |
{"text": "…"} |
Show a toast in mnml’s chrome. |
open |
{"path": "/abs/path"} |
Open the file in mnml’s editor. |
open-pty |
{"command": ["echo","hi"], "cwd": "/path"} |
Spawn a new Pty pane. |
set-activity-badge |
{"section": "agents", "count": 3} |
Drive a notification chip on an activity-bar icon. |
Example — a sibling surfacing a result:
echo '{"cmd":"toast","text":"S3 sync complete · 47 files"}' \ >> "$MNML_IPC_DIR/command"Or programmatically from Rust:
use std::io::Write;
fn toast(message: &str) { let Some(dir) = std::env::var_os("MNML_IPC_DIR") else { return }; let path = std::path::PathBuf::from(dir).join("command"); let line = serde_json::json!({"cmd": "toast", "text": message}); if let Ok(mut f) = std::fs::OpenOptions::new().append(true).create(true).open(&path) { let _ = writeln!(f, "{line}"); }}That’s tier 2 in 9 lines. Most siblings can stop here.
Activity-bar badges
Section titled “Activity-bar badges”set-activity-badge is how a sibling drives the small
notification chip ((3)) on an activity-bar icon. The section
field is one of:
| Builtin section | Key |
|---|---|
| Explorer | explorer |
| Search | search |
| Source control | git |
| Run and debug | debug |
| Integrations | integrations |
| Sessions | sessions |
| Agents (local Claude/Codex) | agents |
| Cloud agents (ECS runner) | cloud_agents |
For a manifest-registered Mount sibling, the section key is the
manifest id.
count = 0 clears the badge. The chip renders as • for 1,
the digit for 2-9, + otherwise.
Tier 3 — mnml-bridge SDK
Section titled “Tier 3 — mnml-bridge SDK”For Rust siblings, mnml-bridge is the typed wrapper. Add it as
a dep:
[dependencies]mnml-bridge = "0.3"The bare crate exposes:
- Wire types —
Cell,HostMessage,SiblingMessage,read_message/write_message. Typed wrapper around the Mount UDS protocol (tier 4). - Integration install helpers — self-registration for the
rail chip + palette commands + chord bindings + context menu +
statusline segment + notification policy. One TOML file per
integration at
~/.config/mnml/integrations/<id>.toml. - Tier-2 IPC helpers — toasts (with levels + persistent), progress notifications, activity badges, statusline segments, OS notifications. All fire-and-forget (silent no-op when the sibling isn’t running under mnml).
Install helpers
Section titled “Install helpers”use mnml_bridge::{ install_integration, ChipSpec, CommandSpec, IntegrationSpec,};
install_integration(&IntegrationSpec { id: "my-tests".into(), name: "My Tests".into(), version: Some(env!("CARGO_PKG_VERSION").into()), binary: "mnml-my-tests".into(), category: Some("test".into()), chip: Some(ChipSpec { glyph: "T".into(), fallback: "MT".into(), color: "green".into(), tooltip: Some("My Tests".into()), enabled: true, in_palette_bar: false, badge_key: Some("my-tests".into()), }), commands: vec![CommandSpec { id: "my-tests.run".into(), title: "Run tests".into(), group: Some("integrations".into()), keys: vec!["<leader>tr".into()], run: ":term mnml-my-tests".into(), }], ..Default::default()}).ok();Companion helpers: uninstall_integration(id),
list_installed_integrations(), integration_manifest_path(id).
Typical sibling pattern: expose --install / --uninstall
subcommands that call these. Users run once after cargo install
and the chip / commands appear on next mnml restart (or after
the integrations.refresh palette command).
Tier-2 IPC helpers
Section titled “Tier-2 IPC helpers”// Toasts (level-tagged + persistent)mnml_bridge::toast_info("uploaded");mnml_bridge::toast_error("build failed");mnml_bridge::toast_persistent("incident.42", "on call: DB down", mnml_bridge::ToastLevel::Error);mnml_bridge::toast_dismiss("incident.42");
// Progress (spinner + label + optional %)mnml_bridge::progress_start("build", "Compiling…");mnml_bridge::progress_update("build", Some("Linking…"), Some(80));mnml_bridge::progress_end("build", mnml_bridge::ProgressStatus::Success);
// Activity badges (numeric on rail chip)mnml_bridge::set_activity_badge("my-tests", 3);
// Statusline segments (hybrid packing)mnml_bridge::statusline_set_segment( "my-tests.status", mnml_bridge::SegmentSide::Right, "◇ 3 failing", Some("red"), Some("my-tests.run"), 150, 4, 20,);mnml_bridge::statusline_clear_segment("my-tests.status");
// OS notifications (OSC 9/777 — Ghostty/iTerm2/kitty/WezTerm)mnml_bridge::notify( "My Tests", "3 tests failed", mnml_bridge::NotifyOpts { level: mnml_bridge::ToastLevel::Error, sound: false, source: Some("my-tests".into()), },);
// Register a palette command that fires back to the sibling via// events.jsonl (as opposed to the manifest-registered// self-executing commands).mnml_bridge::register_command( "my-tests.custom", "Run custom pattern", Some("plugin"), &["<leader>tp"],);Mount client (opt-in)
Section titled “Mount client (opt-in)”For siblings that want to render into an mnml pane (not just
run as a Pty), opt in to the client feature:
mnml-bridge = { version = "0.3", features = ["client"] }Mount siblings can call every IPC helper as a method on Mount
— mount.toast_info(...), mount.notify(...),
mount.statusline_set_segment(...), etc.
Most siblings won’t need client — tier 2 covers everything
short of taking over a pane.
Tier 4 — Mount
Section titled “Tier 4 — Mount”This is the deep integration. A Mount sibling:
- Registers an icon in the activity bar via a manifest file.
- When clicked, mnml spawns the sibling with
MNML_MOUNT_SOCKET=/path/to/socket.sockset. - The sibling connects to that socket, negotiates a
Hello(geometry + theme), then streamsFramemessages back. Each Frame is a 2D array ofCells. - mnml decodes the cells and stamps them into its own
ratatui::Frame— the host terminal does the actual rendering, so font quality is always whatever the user’s terminal renders (no GPU/font involvement in mnml itself). - Input flows the other way: key + mouse events the user does in
the pane area are forwarded to the sibling as
InputEvents.
Registering an activity-bar icon
Section titled “Registering an activity-bar icon”A Mount sibling drops a manifest in either of two dirs (workspace beats user on id collision):
<workspace>/.mnml/mounts/<id>.toml~/.config/mnml/mounts/<id>.tomlManifest fields:
| Field | Required | Notes |
|---|---|---|
id |
yes | Stable unique id. Also the badge key. |
name |
yes | Display label / pane title. |
binary |
yes | PATH name or absolute path. |
icon |
yes | Single Nerd Font glyph. |
color |
no | Named theme color — red/orange/yellow/green/blue/cyan/teal/purple/pink/comment. Defaults to cyan. |
tooltip |
no | Hover text. Falls back to name. |
Example — registering a custom test-executions browser:
id = "custom-tests"name = "Test executions"binary = "mnml-custom-tests"icon = ""color = "green"mnml scans both manifest dirs at startup. To re-scan without a
restart, run the mounts.refresh palette command.
Writing a Mount sibling
Section titled “Writing a Mount sibling”The minimum Mount sibling is ~30 lines using the client
feature. Connect, render a ratatui::Buffer into a TestBackend
each tick, ship it.
use mnml_bridge::{InputEvent, Mount};use ratatui::{Terminal, backend::TestBackend};use std::time::Duration;
#[tokio::main]async fn main() -> anyhow::Result<()> { let mut mount = Mount::connect_env()?; let g = mount.geometry(); let mut terminal = Terminal::new(TestBackend::new(g.cols, g.rows))?;
while !mount.is_done() { for ev in mount.drain_inputs() { // Handle key + mouse events here. if let InputEvent::Key { spec } = ev && spec == "ctrl+q" { return Ok(()); } }
// Resize follow-up: rebuild terminal if geometry changed. let g = mount.geometry(); if terminal.size()?.width != g.cols { terminal = Terminal::new(TestBackend::new(g.cols, g.rows))?; }
terminal.draw(|f| { f.render_widget( ratatui::widgets::Paragraph::new("Hello from a Mount sibling!"), f.area(), ); })?;
let buf = terminal.backend().buffer().clone(); let _ = mount.send_frame_from_buffer(&buf); tokio::time::sleep(Duration::from_millis(33)).await; } mount.send_bye(); Ok(())}That’s it. Mount::connect_env() reads MNML_MOUNT_SOCKET,
performs the handshake, spawns a reader thread. The sibling owns
its tick cadence; ~30 fps is fine for most UI.
Lifecycle
Section titled “Lifecycle”| Event | What happens |
|---|---|
| Activity-bar icon clicked | mnml binds a Unix socket, spawns the sibling with MNML_MOUNT_SOCKET set. |
| Sibling connects | mnml sends HostMessage::Hello { geometry, theme }. |
| Pane resized | mnml sends Resize { geometry }. |
| User keys/clicks in pane | mnml sends Input { event }. |
| Sibling renders | Ships SiblingMessage::Frame { cells }. mnml stamps into ratatui buffer. |
| Sibling exits cleanly | Sends Bye; mnml shows “sibling disconnected” placeholder. |
| Pane closed | mnml sends Goodbye, SIGKILLs the sibling, removes the socket. |
Picking a tier
Section titled “Picking a tier”| If your sibling needs… | Use |
|---|---|
| Just visual consistency (theme colors) | Tier 1 (read MNML_THEME) |
| To surface progress / errors | Tier 2 (toast) |
| To open files in mnml’s editor | Tier 2 (open) |
| To dispatch a follow-on operation as a pane | Tier 2 (open-pty) |
| To drive notification counts | Tier 2 (set-activity-badge) |
| A rich, custom panel that owns rail + body | Tier 4 (Mount) |
Most siblings are happy with tiers 1 + 2. Tier 4 is for tools where Pty rendering doesn’t fit the shape of the UI (a 3-column dashboard that owns its own layout is the canonical example).
See also
Section titled “See also”- Activity bar — where mount icons appear.
- Integrations overview — the two flavors (launcher / binary) and where a manifest lives.
- Marketplace — how binary siblings get discovered.
- Building integrations — authoring a binary sibling with
mnml-bridge. mnml-bridgeon crates.io — the crate.