Skip to content

Plugins

Tom Dringer edited this page Sep 27, 2026 · 5 revisions

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.


Available plugins

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

Installing community plugins

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 binary

mado 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-clock

Defining a plugin

Add 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

Panel positions

Value Location
"left" Left sidebar
"right" Right sidebar
"top-right" Top-right panel
"bottom-right" Bottom-right panel
"browser" Browser/wide panel

Terminal plugins (kind = "")

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 (kind = "pixel")

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

Frame protocol (plugin → Mado)

Pixel frame

[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

Action message

[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

Event protocol (Mado → plugin)

Events arrive on the plugin's stdin as newline-delimited UTF-8 JSON. The plugin must read lines continuously to avoid blocking Mado.

resize

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.

click

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.

mouse_press / mouse_move / mouse_release

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"}

key

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.

focus / blur

Sent when the plugin panel gains or loses keyboard focus.

{"type":"focus"}
{"type":"blur"}

scroll

Sent when the user scrolls inside the panel.

{"type":"scroll","delta":-3.0}

delta is negative for scroll-up, positive for scroll-down.

paste

Sent in response to the plugin emitting a {"action":"paste"} MACT message. Contains the current clipboard text.

{"type":"paste","text":"Hello world"}

chdir

Sent when the active terminal pane changes its working directory.

{"type":"chdir","path":"/Users/alice/projects/mado"}

workspace

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").


Minimal Python example

#!/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)

AI panel

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"

Options

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.


Tips

  • 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 type values.
  • Watch for workspace events — use them to scope the plugin's display to the current project.

FAQ

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"}

Clone this wiki locally