# Bread **A reactive automation fabric for Linux desktops.** Bread is a modular desktop automation runtime built around a single idea: your desktop should behave like a programmable system, not a collection of disconnected config files. Instead of scattering behavior across shell scripts, compositor configs, udev rules, and ad-hoc daemons, Bread centralizes runtime awareness into a coherent layer that can observe, interpret, and react to system state dynamically. > **Status:** Early development. The daemon (`breadd`) is stable. The Lua automation API is active and feature-complete for daily use. --- ## How it works Bread runs a long-lived daemon (`breadd`) that: 1. Ingests raw signals from your compositor, hardware, and OS 2. Normalizes them into stable, semantic events (`bread.device.dock.connected`, `bread.monitor.connected`, etc.) 3. Maintains a live model of your desktop state 4. Delivers those events to Lua modules that implement your automation Your automation lives in Lua. You subscribe to events, read state, and call APIs: ```lua local M = bread.module({ name = "dock", version = "1.0.0" }) bread.on("bread.device.dock.connected", function(event) bread.profile.activate("desk") bread.exec("waybar --config ~/.config/waybar/desk.jsonc") bread.notify("Dock connected", { urgency = "low" }) end) bread.on("bread.device.dock.disconnected", function(event) bread.profile.activate("default") end) return M ``` --- ## Architecture ``` breadd/ Rust daemon — event pipeline, state engine, IPC, adapter supervision bread-cli/ CLI frontend — talks to breadd over a Unix socket bread-shared/ Shared types — RawEvent, BreadEvent, AdapterSource bread-sync/ Sync engine — snapshot and restore system state via a Git remote packaging/ Arch PKGBUILD and systemd user service ``` The daemon is structured in four layers: - **Adapters** — interface with Hyprland IPC, udev, power state, network interfaces, and Bluetooth (BlueZ) - **Normalizer** — transforms raw adapter signals into semantic Bread events - **State engine** — maintains runtime state and dispatches events to subscribers - **Lua runtime** — loads your modules, registers handlers, executes automation --- ## Requirements - Linux (Arch recommended) - Wayland compositor (Hyprland for full functionality) - Rust toolchain (stable, 2021 edition) - `udev` (standard on systemd systems) Optional but preferred: - UPower (for battery events via D-Bus rather than sysfs polling) - rtnetlink (for network events; falls back to sysfs polling without it) - BlueZ (for Bluetooth device events and control) --- ## Installation ### From source ```bash git clone https://github.com/Breadway/bread.git cd bread ``` Run the install script — it builds, symlinks `breadd` and `bread` into `~/.local/bin` (override with `BIN_DIR=…`), installs the systemd user service, and starts the daemon: ```bash bash scripts/install.sh ``` Or step by step (system-wide install): ```bash cargo build --release sudo install -Dm755 target/release/breadd /usr/bin/breadd sudo install -Dm755 target/release/bread /usr/bin/bread ``` ### Arch Linux (PKGBUILD) ```bash cd packaging/arch makepkg -si ``` ### systemd user service ```bash mkdir -p ~/.config/systemd/user cp packaging/systemd/breadd.service ~/.config/systemd/user/ systemctl --user daemon-reload systemctl --user enable --now breadd ``` --- ## Configuration Bread reads from `~/.config/bread/breadd.toml`. All values are optional — the daemon runs with defaults if the file doesn't exist. ```toml [daemon] log_level = "info" # trace | debug | info | warn | error [lua] entry_point = "~/.config/bread/init.lua" module_path = "~/.config/bread/modules" [adapters.hyprland] enabled = true [adapters.udev] enabled = true subsystems = ["usb", "input", "drm", "power_supply"] [adapters.power] enabled = true poll_interval_secs = 30 [adapters.network] enabled = true [adapters.bluetooth] enabled = true [events] dedup_window_ms = 100 [notifications] default_timeout_ms = 5000 default_urgency = "normal" notify_send_path = "notify-send" [modules] builtin = true # load built-in modules (monitors, devices, workspaces, binds) disable = [] # list of built-in module names to disable ``` Your automation lives in `~/.config/bread/init.lua`. Modules placed in `~/.config/bread/modules/` are auto-loaded after `init.lua`: ```lua -- ~/.config/bread/init.lua bread.on("bread.system.startup", function(event) bread.profile.activate("default") end) ``` --- ## CLI reference All commands communicate with the running daemon over a Unix socket at `$XDG_RUNTIME_DIR/bread/breadd.sock`. ```bash # Daemon bread ping # Check daemon connectivity bread health # Daemon version, uptime, PID bread doctor # Diagnose daemon and module health # Lua runtime bread reload # Hot-reload all Lua modules bread reload --watch # Watch config dir and reload on changes # State and events bread state # Dump full runtime state as JSON bread events # Stream live normalized events bread events bread.device.* # Stream filtered events bread events --since 60 # Replay events from the last 60 seconds bread emit # Manually fire an event (for testing) # Profiles bread profile-list # List defined profiles bread profile-activate # Activate a named profile # Modules bread modules list # List installed modules and daemon status bread modules install github:user/repo # Install from GitHub bread modules install /local/path # Install from a local directory bread modules remove # Remove an installed module bread modules update [name] # Re-install one or all GitHub-sourced modules bread modules info # Show full manifest and daemon status # Sync bread sync init # Initialize sync for this machine bread sync push # Snapshot and push current state to remote bread sync pull # Pull and apply latest state from remote bread sync pull --install-packages # Also install packages from snapshot bread sync status # Show what has changed since last push bread sync diff # Show file-level diff vs last commit bread sync diff --remote # Show diff vs remote bread sync machines # List known machines from sync repo ``` --- ## Module system Modules are Lua files (or directories) installed to `~/.config/bread/modules/`. Each module must declare itself with `bread.module()` and have a `bread.module.toml` manifest. ### Installing modules ```bash # From GitHub (downloads latest release tarball) bread modules install github:someuser/bread-wifi # From a local path bread modules install ~/src/my-module # From a specific ref bread modules install github:someuser/bread-wifi@v1.2.0 ``` ### Writing a module A module directory looks like: ``` ~/.config/bread/modules/ └── wifi/ ├── bread.module.toml ← required manifest └── init.lua ← entry point ``` `bread.module.toml`: ```toml name = "wifi" version = "1.0.0" description = "WiFi management for Bread" author = "someuser" source = "github:someuser/bread-wifi" installed_at = "2026-01-01T00:00:00Z" ``` `init.lua`: ```lua local M = bread.module({ name = "wifi", version = "1.0.0" }) bread.on("bread.network.connected", function(event) bread.log("Network up: " .. (event.data.interface or "unknown")) end) return M ``` --- ## Sync system Bread sync snapshots your entire setup — Bread config, arbitrary dotfiles, and package lists — and stores it in a Git repository. Pull it on another machine to restore. ```bash # First-time setup bread sync init --remote git@github.com:you/bread-config.git # Push current state bread sync push # On another machine: pull and apply bread sync pull # Check what's pending bread sync status ``` Configure what gets synced in `~/.config/bread/sync.toml`: ```toml [remote] url = "git@github.com:you/bread-config.git" branch = "main" [machine] name = "hermes" tags = ["laptop", "battery"] [packages] enabled = true managers = ["pacman", "pip", "cargo"] [delegates] include = ["~/.config/nvim", "~/.config/waybar"] exclude = ["**/.git", "**/*.cache"] ``` The sync repo stores: ``` sync-repo/ ├── bread/ ← ~/.config/bread/ snapshot ├── configs/ ← delegate paths (nvim, waybar, etc.) ├── machines/ ← per-machine profiles └── packages/ ← package snapshots (pacman.txt, pip.txt, etc.) ``` --- ## Event reference Events follow the namespace convention `bread...`. | Event | Trigger | |-------|---------| | `bread.system.startup` | Daemon fully initialized | | `bread.device.connected` | Any device attached | | `bread.device.disconnected` | Any device removed | | `bread.device..connected` | Named device attached (name from `devices.lua`) | | `bread.device..disconnected` | Named device removed | | `bread.monitor.connected` | Display connected | | `bread.monitor.disconnected` | Display disconnected | | `bread.workspace.changed` | Active workspace changed | | `bread.window.focus.changed` | Focused window changed | | `bread.window.opened` | Window opened | | `bread.window.closed` | Window closed | | `bread.power.ac.connected` | AC adapter plugged in | | `bread.power.ac.disconnected` | AC adapter unplugged | | `bread.power.battery.low` | Battery ≤ 20% | | `bread.power.battery.very_low` | Battery ≤ 10% | | `bread.power.battery.critical` | Battery ≤ 5% | | `bread.power.battery.full` | Battery at 100% | | `bread.network.connected` | Network interface came online | | `bread.network.disconnected` | Network interface went offline | | `bread.bluetooth.device.paired` | Bluetooth device paired / discovered | | `bread.bluetooth.device.unpaired` | Bluetooth device removed from BlueZ | | `bread.profile.activated` | Profile switched | | `bread.notify.sent` | Desktop notification dispatched | --- ## Lua API ### Modules Every module file must declare itself. The declaration is used for dependency ordering and status tracking. ```lua local M = bread.module({ name = "my-module", version = "1.0.0", after = { "bread.devices" }, -- load after this module }) -- ... module body ... return M ``` ### Events ```lua -- Subscribe to events; returns a subscription ID local id = bread.on("bread.monitor.connected", function(event) -- event.event → "bread.monitor.connected" -- event.data → table of event-specific fields -- event.source → adapter that produced it bread.log(event.event) end) -- Unsubscribe by ID bread.off(id) -- Subscribe once, auto-unsubscribe after first delivery bread.once("bread.system.startup", function(event) bread.profile.activate("default") end) -- Subscribe with a filter predicate. The predicate goes in an opts table. bread.filter("bread.device.connected", function(event) bread.exec("xset r rate 200 40") end, { filter = function(event) return event.data.device == "keyboard" end, }) -- Emit a custom event (for cross-module communication) bread.emit("mymodule.something", { key = "value" }) ``` Pattern matching supports `*` (single segment), `**` (any depth), and `?` (single character): ```lua bread.on("bread.device.*", handler) -- matches bread.device.dock.connected bread.on("bread.device.**", handler) -- matches any depth under bread.device ``` ### State ```lua -- Read from runtime state by dot-separated path local monitors = bread.state.get("monitors") local online = bread.state.get("network.online") -- Typed shorthands local monitors = bread.state.monitors() local workspace = bread.state.active_workspace() local window = bread.state.active_window() local devices = bread.state.devices() local power = bread.state.power() local network = bread.state.network() local profile = bread.state.profile() -- Watch a state path for changes bread.state.watch("power.ac_connected", function(new_val, old_val) if new_val then bread.notify("AC connected") end end) ``` ### Profiles ```lua bread.profile.activate("desk") bread.profile.activate("default") ``` ### Execution and notifications ```lua -- Fire-and-forget shell command bread.exec("kitty") -- Desktop notification (uses notify-send) bread.notify("Title", { urgency = "normal", timeout = 3000, icon = "dialog-info" }) bread.notify("Simple message") -- title defaults to "bread" ``` ### Timers ```lua -- Run once after a delay (ms) local id = bread.after(500, function() bread.exec("some-delayed-command") end) -- Run on a repeating interval (ms) local id = bread.every(60000, function() bread.log("tick") end) -- Cancel either kind bread.cancel(id) -- Debounce a rapidly-firing handler local fn = bread.debounce(200, function(event) reconfigure_monitors() end) bread.on("bread.monitor.*", fn) ``` ### Wait (inside coroutines) ```lua -- Yield until a matching event arrives local event = bread.wait("bread.device.dock.connected", { timeout = 5000 }) if event then -- dock arrived within 5 seconds end ``` ### Machine and filesystem ```lua -- Machine identity (from sync.toml, falls back to hostname) local name = bread.machine.name() local tags = bread.machine.tags() -- array of strings local ok = bread.machine.has_tag("laptop") -- Filesystem helpers (~ is expanded) bread.fs.write("~/.config/some/file", "content") local content = bread.fs.read("~/.config/some/file") -- nil if not found local exists = bread.fs.exists("~/some/path") local abs = bread.fs.expand("~/some/path") ``` ### Logging ```lua bread.log("Module loaded") -- info level bread.warn("Unexpected state") -- warn level bread.error("Something failed") -- error level ``` ### Hyprland bindings ```lua -- Dispatch a Hyprland command bread.hyprland.dispatch("workspace", "2") bread.hyprland.dispatch("exec", "kitty") -- Set a keyword bread.hyprland.keyword("monitor", "HDMI-A-1, 2560x1440, 0x0, 1") -- Query compositor state local win = bread.hyprland.active_window() local monitors = bread.hyprland.monitors() local workspaces = bread.hyprland.workspaces() local clients = bread.hyprland.clients() -- Subscribe to raw Hyprland events (bypass normalization) bread.hyprland.on_raw("activewindow", function(raw) -- raw is the unparsed string from Hyprland's event socket end) ``` ### Bluetooth The `bread.bluetooth` namespace provides BlueZ control. All operations degrade gracefully when Bluetooth hardware is unavailable. ```lua -- Power the adapter on or off bread.bluetooth.power(true) bread.bluetooth.power(false) -- Query current power state (returns true/false, or nil if unavailable) local on = bread.bluetooth.powered() -- Connect/disconnect a paired device by MAC address -- Fire-and-forget; result arrives as bread.device.connected/disconnected bread.bluetooth.connect("AA:BB:CC:DD:EE:FF") bread.bluetooth.disconnect("AA:BB:CC:DD:EE:FF") -- Start or stop device discovery bread.bluetooth.scan(true) bread.bluetooth.scan(false) -- List all devices known to BlueZ local devs = bread.bluetooth.devices() -- Returns nil if BlueZ is unavailable, otherwise: -- { { address, name, connected, paired }, ... } ``` Example — auto-connect headphones when Bluetooth powers on: ```lua bread.state.watch("power.ac_connected", function(ac) if ac then bread.bluetooth.power(true) bread.bluetooth.connect("AA:BB:CC:DD:EE:FF") end end) ``` ### Module-scoped storage Survives hot reload; does not survive daemon restart. ```lua M.store.set("last_profile", "docked") local p = M.store.get("last_profile") -- "docked" ``` --- ## IPC protocol The daemon exposes a Unix socket at `$XDG_RUNTIME_DIR/bread/breadd.sock`. The protocol is newline-delimited JSON — useful for scripting or building tooling outside the CLI. Request: ```json { "id": "1", "method": "state.get", "params": { "key": "monitors" } } ``` Response: ```json { "id": "1", "result": [ { "name": "HDMI-A-1", "connected": true } ] } ``` Available methods: | Method | Description | |--------|-------------| | `ping` | Connectivity check | | `health` | Version, uptime, PID, adapter status | | `state.get` | Read a value from `RuntimeState` by dotted key path | | `state.dump` | Return the full `RuntimeState` as JSON | | `modules.list` | List all loaded modules and their status | | `modules.reload` | Hot-reload the Lua runtime | | `profile.list` | List defined profiles | | `profile.activate` | Switch active profile | | `events.subscribe` | Upgrade connection to streaming mode | | `events.replay` | Replay buffered events from the last N ms | | `emit` | Inject a synthetic event into the pipeline | | `sync.status` | Return sync initialization state and machine info | `events.subscribe` upgrades the connection to streaming mode — the daemon pushes events line by line until the client disconnects. --- ## Contributing Bread is early-stage software. Contributions, issues, and feedback are welcome. The daemon (`breadd`) is the most stable part of the codebase. Active development is happening across the Lua API, module system, and sync subsystem. --- ## License MIT — see [LICENSE](LICENSE).