Agent-Logs-Url: https://github.com/Breadway/bread/sessions/1d380004-8d78-4a1f-9fbb-0c8a487b2e14 Co-authored-by: Breadway <108389940+Breadway@users.noreply.github.com>
12 KiB
Bread Documentation
Contents
- Overview
- Getting started
- Your first module
- Run, reload, and watch
- Debugging tips
- Dictionary: Lua API
- Dictionary: Built-in modules
- Dictionary: Event reference
- Dictionary: Runtime state schema
- Dictionary: IPC protocol
Overview
Bread is a reactive automation fabric for Linux desktops. The daemon (bread) normalizes external signals into semantic events, maintains runtime state, and dispatches events to Lua modules that implement automation.
- Daemon: long-running Rust process, source of truth for runtime state
- Lua runtime: dedicated thread inside the daemon; automation logic lives here
- CLI: talks to the daemon over a Unix socket
If you are new to Bread, start with the quick walkthrough below, then jump to the full dictionary when you need exact API details.
Getting started
1) Create a minimal config
- Daemon config:
~/.config/bread/bread.toml - Lua entry point:
~/.config/bread/init.lua - Lua modules:
~/.config/bread/modules/
2) Minimal init.lua
require("modules.devices")
require("modules.workspaces")
bread.on("bread.system.startup", function()
bread.profile.activate("default")
end)
3) Add your first module
Create a Lua file under your modules directory and load it from init.lua.
Your first module
local M = bread.module({ name = "hello", version = "0.1.0" })
function M.on_load()
bread.log("hello from bread")
bread.on("bread.device.*", function(event)
bread.log(event.event)
end)
end
return M
Why this shape?
- Every module must call
bread.moduleonce. on_loadis a good place to register subscriptions.- Use
bread.logearly to verify handlers are firing.
Run, reload, and watch
- Start the daemon, then use
bread reloadafter editing Lua. bread reload --watchwill keep reloading on changes.- See Examples.md for real-world ports.
Debugging tips
- Log event payloads with
bread.log(event.data.raw)when matching devices. - Use
bread.eventsin the CLI to see live normalized events. - Use
bread stateto see runtime state as JSON.
Lua module system
Entry point and module scanning
init.luais executed first.- Modules are discovered by scanning
~/.config/bread/modules/for.luafiles. - Every module must call
bread.moduleexactly once at top-level. - Modules are ordered by the
afterdependency list.
Module declaration
local M = bread.module({
name = "my.module",
version = "0.1.0",
after = { "bread.devices" },
})
return M
If a module does not call bread.module, it fails to load and is marked as a load error.
Require loader
require("bread.<path>") resolves to a Lua file under the module path. For example:
local utils = require("bread.lib.utils")
This loads ~/.config/bread/modules/lib/utils.lua if it exists. Non-bread.* require calls fall back to standard Lua behavior.
Lifecycle hooks
Modules may export any of the following hooks. All are optional.
function M.on_load()
-- register subscriptions, initialize module state
end
function M.on_reload()
-- called after a hot reload completes
end
function M.on_unload()
-- called before the Lua instance is dropped
end
function M.on_error(err)
-- called when a handler throws
-- return true to keep the subscription, false to cancel it
return true
end
Module storage
Each module has a scoped key-value store that survives reloads:
M.store.set("last_profile", "docked")
local value = M.store.get("last_profile")
The store lives in the daemon runtime state and is not shared across modules.
Dictionary: Lua API
Every API is exposed through the bread global table.
Events
bread.on(pattern, fn) -> id
Subscribe to matching events. Returns a numeric subscription ID.
bread.once(pattern, fn) -> id
Subscribe once. The handler is removed after the first match.
bread.filter(pattern, fn, opts) -> id
Subscribe with a predicate filter. opts must contain filter:
bread.filter("bread.device.*", function(event)
bread.exec("xset r rate 200 40")
end, {
filter = function(event)
return event.data and event.data.class == "keyboard"
end,
})
bread.off(id)
Unsubscribe an event or state watch by ID.
bread.emit(event, data)
Emit a custom event into the system pipeline.
bread.wait(pattern, opts) -> event | nil
Coroutine-only helper that waits for a matching event.
bread.spawn(function()
local event = bread.wait("bread.device.dock.connected", { timeout = 5000 })
if event then
bread.log("dock arrived")
end
end)
bread.spawn(fn)
Spawn a coroutine and surface errors if the coroutine fails.
State
bread.state.get(path)
Read a state subtree by dotted path (e.g. "network.online").
Convenience helpers
bread.state.monitors()bread.state.active_workspace()bread.state.active_window()bread.state.devices()bread.state.power()bread.state.network()bread.state.profile()
bread.state.watch(path, fn) -> id
Watch a state path. The callback receives (new, old).
bread.state.watch("power.ac_connected", function(new_val, old_val)
if new_val then
bread.exec("notify-send 'AC connected'")
end
end)
Profiles
bread.profile.activate(name)
Update the active profile. The CLI also emits bread.profile.activated over IPC; the Lua API does not emit this event on its own.
Execution
bread.exec(cmd)
Runs cmd in a sh -lc shell. Fire-and-forget (async).
Notifications
bread.notify(message, opts)
Sends a desktop notification via notify-send.
Options:
title(string, default:"bread")urgency(string, default from config)timeout(ms, default from config)icon(string, optional)
Calling bread.notify emits bread.notify.sent with { title, message, urgency }.
Timers
bread.after(delay_ms, fn) -> id
Run once after delay.
bread.every(interval_ms, fn) -> id
Run repeatedly on an interval.
bread.cancel(id)
Cancel a timer created by after or every.
Utilities
bread.debounce(delay_ms, fn) -> wrapped_fn
Returns a wrapper that only fires after quiet time.
bread.log(msg) / bread.warn(msg) / bread.error(msg)
Log helpers that accept any Lua value.
Hyprland
The bread.hyprland namespace provides compositor bindings:
bread.hyprland.dispatch(cmd, args)bread.hyprland.keyword(key, value)bread.hyprland.active_window()bread.hyprland.monitors()bread.hyprland.workspaces()bread.hyprland.clients()bread.hyprland.on_raw(kind, fn) -> id
bread.hyprland.on_raw filters raw Hyprland events by kind and delivers the full event payload (including the original raw string).
Dictionary: Built-in modules
Built-ins are enabled by default. Disable them via [modules].disable in the config.
bread.monitors
local monitors = require("bread.monitors")
monitors.layout("dock", function()
bread.exec("~/.config/bread/scripts/layout-dock.sh")
end)
monitors.on({
when = "connected",
monitors = { "HDMI-A-1" },
run = monitors.apply("dock"),
})
monitors.on({ when, monitors, run })monitors.layout(name, fn)monitors.apply(name) -> fn
when is one of connected, disconnected, changed. run may be a function or a shell command string.
bread.devices
local devices = require("bread.devices")
devices.register("Keychron", "keyboard")
devices.on({
when = "connected",
class = "keyboard",
run = function(event)
bread.exec("xset r rate 200 40")
end,
})
devices.on({ when, class, name, run })devices.register(pattern, class)
class may be dock, keyboard, mouse, tablet, display, storage, audio, unknown.
bread.workspaces
local workspaces = require("bread.workspaces")
workspaces.assign("1", "HDMI-A-1")
workspaces.pin({ app = "Firefox", workspace = "2" })
workspaces.assign(workspace, monitor)workspaces.pin({ app, workspace })workspaces.apply_assignments()
bread.binds
local binds = require("bread.binds")
binds.add({
mods = { "SUPER" },
key = "Return",
dispatch = "exec",
args = "kitty",
})
binds.add({ mods, key, dispatch, args })binds.remove(key)binds.replace(key, opts)
Dictionary: Event reference
Events are delivered as a BreadEvent:
{
"event": "bread.device.dock.connected",
"timestamp": 1710000000000,
"source": "Udev",
"data": {}
}
Pattern matching
Patterns match event names with glob-style syntax:
- Exact match:
bread.device.dock.connected *matches within a single segment (does not cross.)**matches across segments (recursive)?matches a single character within a segment
Examples:
bread.on("bread.device.*", handler)
bread.on("bread.device.**", handler)
bread.on("bread.monitor.?", handler)
Normalized events
System
bread.system.startup(data:{})
Devices (udev)
bread.device.connectedbread.device.disconnectedbread.device.changedbread.device.<class>.connectedbread.device.<class>.disconnectedbread.device.<class>.changed
Payload notes:
- Device events include
idandclass; the generic event also includesraw. <class>is one of:dock,keyboard,mouse,tablet,display,storage,audio,unknown.
Hyprland
bread.workspace.changed(raw payload)bread.workspace.created({ "workspace": "..." })bread.workspace.destroyed({ "workspace": "..." })bread.monitor.connected(raw payload)bread.monitor.disconnected(raw payload)bread.window.focus.changed(raw payload)bread.window.focused({ "address": "..." })bread.window.opened({ "address", "workspace", "class", "title" })bread.window.closed({ "address": "..." })bread.window.moved({ "address", "workspace" })bread.hyprland.event(raw payload for unhandled kinds)
Raw Hyprland payloads contain kind, raw, and data fields.
Power
bread.power.ac.connectedbread.power.ac.disconnectedbread.power.battery.lowbread.power.battery.very_lowbread.power.battery.criticalbread.power.battery.fullbread.power.changed(fallback)
Payload includes ac_connected and battery_percent.
Network
bread.network.connectedbread.network.disconnected
Payload includes online and interfaces.
Other system events
bread.profile.activated(emitted by IPC profile activation)bread.notify.sent(emitted bybread.notify)bread.state.changed.<path>(emitted when a state watch fires)
Dictionary: Runtime state schema
bread.state.get("") returns the full RuntimeState:
{
"monitors": [ { "name": "HDMI-A-1", "connected": true } ],
"workspaces": [ { "id": "1", "monitor": "HDMI-A-1" } ],
"active_workspace": "1",
"active_window": "Firefox",
"devices": { "connected": [] },
"network": { "interfaces": {}, "online": false },
"power": { "ac_connected": false, "battery_percent": null, "battery_low": false },
"profile": { "active": "default", "history": [], "profiles": {} },
"modules": [ { "name": "bread.devices", "status": "loaded", "last_error": null, "builtin": true, "store": {} } ]
}
Dictionary: IPC protocol
The daemon exposes a Unix socket at $XDG_RUNTIME_DIR/bread/bread.sock. Messages are newline-delimited JSON.
Request:
{ "id": "1", "method": "state.get", "params": { "key": "monitors" } }
Response:
{ "id": "1", "result": [ { "name": "HDMI-A-1", "connected": true } ] }
Available methods:
pinghealthstate.getstate.dumpmodules.listmodules.reloadprofile.listprofile.activateevents.subscribeevents.replayemit
events.subscribe upgrades the socket to streaming mode and sends events as they occur.