Repository navigation
Plugins
Mado supports external plugins that render custom UI into sidebar panels. Plugins are standalone executables — any language works as long as the process can read/write binary data on stdin/stdout.
| Plugin | Description |
|---|---|
| Clock | Live clock with weather |
| World Time | Current time across up to 4 cities |
| News Feed | RSS/Atom feed reader |
| Pomodoro | Pomodoro timer with circular progress ring |
| Timer | Workspace billing timer with CSV export |
| Stats | Tasku task statistics dashboard |
| Clipboard | Clipboard history manager |
Community plugins are available at github.com/tomdringer/mado-plugins. Install them with the mado plugin command — no manual config editing required.
mado plugin install clock # download and register a plugin
mado plugin list # show installed plugins
mado plugin update clock # update to the latest release
mado plugin remove clock # unregister and remove the binarymado plugin install downloads the correct binary for your platform, places it in ~/.config/mado/plugins/, and adds the entry to your config.toml automatically. Restart Mado to activate it.
You can also install any plugin by its GitHub repo directly:
mado plugin install tomdringer/mado-clockAdd one or more [[plugins]] entries to ~/.config/mado/config.toml:
[[plugins]]
id = "pomodoro"
command = "/usr/local/bin/mado-pomodoro"
kind = "pixel"
icon = "\uf017"
position = "left"| Key | Required | Description |
|---|---|---|
id |
Yes | Unique identifier — used in logs and workspace state |
command |
Yes | Path to the plugin executable |
kind |
Yes |
"pixel" (custom rendered UI) or "" (PTY terminal) |
icon |
No | Nerd Font glyph shown in the sidebar tab bar |
position |
No | Where to place the panel — see below |
| Value | Location |
|---|---|
"left" |
Left sidebar |
"right" |
Right sidebar |
"top-right" |
Top-right panel |
"bottom-right" |
Bottom-right panel |
"browser" |
Browser/wide panel |
Terminal plugins are the simplest kind — Mado spawns the command in a PTY and renders it as a terminal pane using the same VT100 emulator as regular panes.
Use this for:
- Shell scripts that output ANSI-coloured text
- TUI apps that use ncurses or similar
No protocol knowledge needed — the plugin just behaves like a shell program.
Pixel plugins render their own UI as RGBA frames. Mado composites those frames directly into the panel, bypassing the terminal emulator. This enables fully custom interfaces — graphics, charts, custom input handling.
The plugin communicates via two channels:
- stdin — receives events from Mado as newline-delimited JSON
- stdout — sends rendered frames and action messages back to Mado
[4 bytes] magic: "MADO"
[4 bytes] width as u32 little-endian (physical pixels)
[4 bytes] height as u32 little-endian (physical pixels)
[w×h×4 B] RGBA8 pixel data, row-major, top-to-bottom
Write a new frame whenever the display needs to update. Mado composites the latest frame on the next render tick.
Constraints:
- Dimensions must be non-zero and ≤ 8192 on each axis
- Send new frames only when something changed — there is no need for a fixed frame rate
[4 bytes] magic: "MACT"
[4 bytes] JSON length as u32 little-endian
[N bytes] UTF-8 JSON
Action messages let the plugin request Mado to perform an operation on its behalf.
Supported actions:
| JSON | Effect |
|---|---|
{"action":"paste"} |
Mado reads the clipboard and sends a paste event back to the plugin |
{"action":"notify","title":"My Plugin","message":"Done!"} |
Sends a macOS notification. title defaults to "Mado" if omitted |
{"action":"navigate","url":"https://..."} |
Opens the URL in the Mado browser panel, making it visible if hidden. Only http:// and https:// URLs are accepted |
{"action":"browser_back"} |
Navigates the browser panel back in history |
Events arrive on the plugin's stdin as newline-delimited UTF-8 JSON. The plugin must read lines continuously to avoid blocking Mado.
Sent on spawn and whenever the panel size changes.
{"type":"resize","width":600,"height":800}width and height are physical pixel dimensions. Render a frame at this size immediately after receiving a resize event.
Sent when the user clicks inside the panel.
{"type":"click","x":42.0,"y":17.0,"button":"left"}Coordinates are physical pixels relative to the panel's top-left corner.
Low-level mouse events for drag/selection interactions.
{"type":"mouse_press","x":10.0,"y":20.0}
{"type":"mouse_move","x":15.0,"y":25.0}
{"type":"mouse_release"}Sent when a key is pressed while the plugin panel is focused.
{"type":"key","text":"Return","ctrl":false,"meta":false,"alt":false,"shift":false}| Field | Type | Description |
|---|---|---|
text |
string | The key string — printable character, or a named key such as "Return", "Backspace", "Tab", "Escape", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Delete", "Home", "End", "PageUp", "PageDown"
|
ctrl |
bool | Control modifier held |
meta |
bool | Command (⌘) modifier held |
alt |
bool | Option/Alt modifier held |
shift |
bool | Shift modifier held |
Note: bare modifier keypresses (Shift, Ctrl, Alt, Meta, CapsLock) are not forwarded as key events.
Sent when the plugin panel gains or loses keyboard focus.
{"type":"focus"}
{"type":"blur"}Sent when the user scrolls inside the panel.
{"type":"scroll","delta":-3.0}delta is negative for scroll-up, positive for scroll-down.
Sent in response to the plugin emitting a {"action":"paste"} MACT message. Contains the current clipboard text.
{"type":"paste","text":"Hello world"}Sent when the active terminal pane changes its working directory.
{"type":"chdir","path":"/Users/alice/projects/mado"}Sent when the user switches to a different workspace.
{"type":"workspace","code":"MDO"}code matches the section name in projects.toml (e.g. "MDO", "TCL").
#!/usr/bin/env python3
import sys, struct, json
def send_frame(width, height, rgba_bytes):
sys.stdout.buffer.write(b"MADO")
sys.stdout.buffer.write(struct.pack("<II", width, height))
sys.stdout.buffer.write(rgba_bytes)
sys.stdout.buffer.flush()
width, height = 300, 200
for line in sys.stdin:
event = json.loads(line)
if event["type"] == "resize":
width = event["width"]
height = event["height"]
# Fill with a solid colour as a placeholder
pixels = bytes([0x1e, 0x20, 0x30, 0xff]) * width * height
send_frame(width, height, pixels)Mado has a built-in AI panel that runs an AI CLI tool in an embedded terminal. It works with any AI CLI — just point the command at whichever tool you prefer.
The panel is configured as a plugin in config.toml. Run mado config to open it and uncomment the AI section:
[[plugins]]
id = "ai"
command = "mado-claude" # see options below
kind = "pixel"
icon = "\uF086"
position = "top-right"| Command | Requires | Notes |
|---|---|---|
mado-claude |
ANTHROPIC_API_KEY |
Built-in Claude Code panel, ships with Mado |
mado-ai + gemini in shell |
Gemini CLI installed | Opens a shell — run gemini
|
mado-ai + sgpt in shell |
Shell GPT installed | Opens a shell — run sgpt
|
The mado-claude command uses Claude Code — install it with npm install -g @anthropic-ai/claude-code and set your ANTHROPIC_API_KEY environment variable. No other setup is required.
- Match frame size to resize events — rendering at a different size causes Slint to scale the image, which blurs text.
- Flush after every write — buffered output will not reach Mado until the buffer fills.
-
Handle missing events gracefully — future Mado versions may add new event types. Ignore unknown
typevalues. -
Watch for
workspaceevents — use them to scope the plugin's display to the current project.
My plugin panel is blank after install. macOS kills unsigned binaries spawned by a signed app. After installing a plugin binary, it must be signed:
codesign --force --sign "Developer ID Application: Your Name (TEAMID)" ~/.config/mado/plugins/<name>mado plugin install handles this automatically. If you copied a binary manually, sign it yourself. The symptom of a missing signature is a blank panel with no output (exit code 137 / SIGKILL).
How do I update a plugin?
mado plugin update <name>This fetches the latest GitHub release, replaces the binary, and re-signs it. Restart Mado after updating.
Can I write a plugin in any language? Yes. The protocol is just binary stdin/stdout — any language that can do binary I/O works. Python, Go, Swift, JavaScript (via Bun or Node), and shell scripts are all viable options.
My pixel plugin receives key events but modifier keys insert garbage.
Slint fires key events for bare modifier keypresses using private Unicode codepoints. Mado filters these before forwarding, but if you are building a plugin that handles keys internally, filter out strings that are a single character in the range \u{0010}–\u{0019}.
How do I open a URL from a plugin? Send a MACT action message:
{"action":"navigate","url":"https://example.com"}Mado opens the URL in the browser panel and makes the panel visible.
Can a plugin show macOS notifications? Yes — send a MACT action:
{"action":"notify","title":"My Plugin","message":"Task complete"}Getting Started
Plugins