Skip to content

Bridge & Mount

mnml’s family is a constellation of small sibling binaries (mnml-aws-cloudwatch-logs, mnml-db-postgres, mnml-forge-github, mnml-fs-s3, …) — each one a focused TUI for a specific service. The Bridge is the protocol that lets those siblings talk to mnml; Mount is the highest tier where a sibling takes over a pane and renders directly into mnml’s editor body. Both ship as mnml-bridge — one crate, two integration depths.

This page is the field guide for using and building bridged siblings.

Bridge is layered. Each tier is purely additive — siblings opt in to whatever depth makes sense for them.

TierWhatSibling code
1. Env varsMNML_WORKSPACE, MNML_THEME, MNML_IPC_DIR set on every Pty mnml spawns.Read on startup; zero protocol.
2. JSONL host callsSibling appends JSONL commands to $MNML_IPC_DIR/command.One file write per call.
3. mnml-bridge SDKTyped Rust wrapper around tiers 1 + 2.use mnml_bridge::*
4. MountSibling 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.

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-repo
MNML_THEME=cyberdream
MNML_IPC_DIR=/Users/you/Projects/your-repo/.mnml/ipc

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

The sibling writes one JSON object per line to $MNML_IPC_DIR/command. mnml ingests the file each tick. These commands are recognised:

CommandPayloadEffect
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:

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

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 sectionKey
Explorerexplorer
Searchsearch
Source controlgit
Run and debugdebug
Integrationsintegrations
Sessionssessions
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.

For Rust siblings, mnml-bridge is the typed wrapper. Add it as a dep:

[dependencies]
mnml-bridge = "0.3"

The bare crate exposes:

  • Wire typesCell, 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).
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).

// 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"],
);

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

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.sock set.
  • The sibling connects to that socket, negotiates a Hello (geometry + theme), then streams Frame messages back. Each Frame is a 2D array of Cells.
  • 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.

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

Manifest fields:

FieldRequiredNotes
idyesStable unique id. Also the badge key.
nameyesDisplay label / pane title.
binaryyesPATH name or absolute path.
iconyesSingle Nerd Font glyph.
colornoNamed theme color — red/orange/yellow/green/blue/cyan/teal/purple/pink/comment. Defaults to cyan.
tooltipnoHover 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.

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.

EventWhat happens
Activity-bar icon clickedmnml binds a Unix socket, spawns the sibling with MNML_MOUNT_SOCKET set.
Sibling connectsmnml sends HostMessage::Hello { geometry, theme }.
Pane resizedmnml sends Resize { geometry }.
User keys/clicks in panemnml sends Input { event }.
Sibling rendersShips SiblingMessage::Frame { cells }. mnml stamps into ratatui buffer.
Sibling exits cleanlySends Bye; mnml shows “sibling disconnected” placeholder.
Pane closedmnml sends Goodbye, SIGKILLs the sibling, removes the socket.
If your sibling needs…Use
Just visual consistency (theme colors)Tier 1 (read MNML_THEME)
To surface progress / errorsTier 2 (toast)
To open files in mnml’s editorTier 2 (open)
To dispatch a follow-on operation as a paneTier 2 (open-pty)
To drive notification countsTier 2 (set-activity-badge)
A rich, custom panel that owns rail + bodyTier 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).