Update README and add documentation and examples for Bread automation
This commit is contained in:
parent
45d5979252
commit
f05d6ba602
3 changed files with 761 additions and 6 deletions
496
Documentation.md
Normal file
496
Documentation.md
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
# Bread Documentation
|
||||
|
||||
## Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Getting started](#getting-started)
|
||||
- [Your first module](#your-first-module)
|
||||
- [Run, reload, and watch](#run-reload-and-watch)
|
||||
- [Debugging tips](#debugging-tips)
|
||||
- [Dictionary: Lua API](#dictionary-lua-api)
|
||||
- [Dictionary: Built-in modules](#dictionary-built-in-modules)
|
||||
- [Dictionary: Event reference](#dictionary-event-reference)
|
||||
- [Dictionary: Runtime state schema](#dictionary-runtime-state-schema)
|
||||
- [Dictionary: IPC protocol](#dictionary-ipc-protocol)
|
||||
|
||||
## Overview
|
||||
|
||||
Bread is a reactive automation fabric for Linux desktops. The daemon (`breadd`) 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/breadd.toml`
|
||||
- Lua entry point: `~/.config/bread/init.lua`
|
||||
- Lua modules: `~/.config/bread/modules/`
|
||||
|
||||
### 2) Minimal `init.lua`
|
||||
|
||||
```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
|
||||
|
||||
```lua
|
||||
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.module` once.
|
||||
- `on_load` is a good place to register subscriptions.
|
||||
- Use `bread.log` early to verify handlers are firing.
|
||||
|
||||
## Run, reload, and watch
|
||||
|
||||
- Start the daemon, then use `bread reload` after editing Lua.
|
||||
- `bread reload --watch` will keep reloading on changes.
|
||||
- See [Examples.md](Examples.md) for real-world ports.
|
||||
|
||||
## Debugging tips
|
||||
|
||||
- Log event payloads with `bread.log(event.data.raw)` when matching devices.
|
||||
- Use `bread.events` in the CLI to see live normalized events.
|
||||
- Use `bread state` to see runtime state as JSON.
|
||||
|
||||
## Lua module system
|
||||
|
||||
### Entry point and module scanning
|
||||
|
||||
- `init.lua` is executed first.
|
||||
- Modules are discovered by scanning `~/.config/bread/modules/` for `.lua` files.
|
||||
- Every module must call `bread.module` exactly once at top-level.
|
||||
- Modules are ordered by the `after` dependency list.
|
||||
|
||||
### Module declaration
|
||||
|
||||
```lua
|
||||
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:
|
||||
|
||||
```lua
|
||||
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.
|
||||
|
||||
```lua
|
||||
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:
|
||||
|
||||
```lua
|
||||
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`:
|
||||
|
||||
```lua
|
||||
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.
|
||||
|
||||
```lua
|
||||
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)`.
|
||||
|
||||
```lua
|
||||
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`
|
||||
|
||||
```lua
|
||||
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`
|
||||
|
||||
```lua
|
||||
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`
|
||||
|
||||
```lua
|
||||
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`
|
||||
|
||||
```lua
|
||||
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`:
|
||||
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
|
||||
```lua
|
||||
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.connected`
|
||||
- `bread.device.disconnected`
|
||||
- `bread.device.changed`
|
||||
- `bread.device.<class>.connected`
|
||||
- `bread.device.<class>.disconnected`
|
||||
- `bread.device.<class>.changed`
|
||||
|
||||
Payload notes:
|
||||
|
||||
- Device events include `id` and `class`; the generic event also includes `raw`.
|
||||
- `<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.connected`
|
||||
- `bread.power.ac.disconnected`
|
||||
- `bread.power.battery.low`
|
||||
- `bread.power.battery.very_low`
|
||||
- `bread.power.battery.critical`
|
||||
- `bread.power.battery.full`
|
||||
- `bread.power.changed` (fallback)
|
||||
|
||||
Payload includes `ac_connected` and `battery_percent`.
|
||||
|
||||
#### Network
|
||||
|
||||
- `bread.network.connected`
|
||||
- `bread.network.disconnected`
|
||||
|
||||
Payload includes `online` and `interfaces`.
|
||||
|
||||
#### Other system events
|
||||
|
||||
- `bread.profile.activated` (emitted by IPC profile activation)
|
||||
- `bread.notify.sent` (emitted by `bread.notify`)
|
||||
- `bread.state.changed.<path>` (emitted when a state watch fires)
|
||||
|
||||
## Dictionary: Runtime state schema
|
||||
|
||||
`bread.state.get("")` returns the full `RuntimeState`:
|
||||
|
||||
```json
|
||||
{
|
||||
"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/breadd.sock`. Messages are newline-delimited JSON.
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{ "id": "1", "method": "state.get", "params": { "key": "monitors" } }
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{ "id": "1", "result": [ { "name": "HDMI-A-1", "connected": true } ] }
|
||||
```
|
||||
|
||||
Available methods:
|
||||
|
||||
- `ping`
|
||||
- `health`
|
||||
- `state.get`
|
||||
- `state.dump`
|
||||
- `modules.list`
|
||||
- `modules.reload`
|
||||
- `profile.list`
|
||||
- `profile.activate`
|
||||
- `events.subscribe`
|
||||
- `events.replay`
|
||||
- `emit`
|
||||
|
||||
`events.subscribe` upgrades the socket to streaming mode and sends events as they occur.
|
||||
Loading…
Add table
Add a link
Reference in a new issue