This commit is contained in:
Breadway 2026-05-12 00:35:09 +08:00
parent 70464b2439
commit acbf8e1b1b

View file

@ -6,6 +6,8 @@
- [Getting started](#getting-started) - [Getting started](#getting-started)
- [Your first module](#your-first-module) - [Your first module](#your-first-module)
- [Run, reload, and watch](#run-reload-and-watch) - [Run, reload, and watch](#run-reload-and-watch)
- [Modules: install and manage](#modules-install-and-manage)
- [Sync: snapshot and restore](#sync-snapshot-and-restore)
- [Debugging tips](#debugging-tips) - [Debugging tips](#debugging-tips)
- [Dictionary: Lua API](#dictionary-lua-api) - [Dictionary: Lua API](#dictionary-lua-api)
- [Dictionary: Built-in modules](#dictionary-built-in-modules) - [Dictionary: Built-in modules](#dictionary-built-in-modules)
@ -15,11 +17,11 @@
## Overview ## 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. 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 - **Daemon** (`breadd`) — long-running Rust process; source of truth for runtime state
- Lua runtime: dedicated thread inside the daemon; automation logic lives here - **Lua runtime** dedicated thread inside the daemon; automation logic lives here
- CLI: talks to the daemon over a Unix socket - **CLI** (`bread`) — 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. If you are new to Bread, start with the quick walkthrough below, then jump to the full dictionary when you need exact API details.
@ -27,75 +29,189 @@ If you are new to Bread, start with the quick walkthrough below, then jump to th
### 1) Create a minimal config ### 1) Create a minimal config
- Daemon config: `~/.config/bread/bread.toml` - Daemon config: `~/.config/bread/breadd.toml` (all values optional)
- Lua entry point: `~/.config/bread/init.lua` - Lua entry point: `~/.config/bread/init.lua`
- Lua modules: `~/.config/bread/modules/` - Lua modules: `~/.config/bread/modules/`
### 2) Minimal `init.lua` ### 2) Minimal `init.lua`
```lua ```lua
require("modules.devices") bread.on("bread.system.startup", function(event)
require("modules.workspaces")
bread.on("bread.system.startup", function()
bread.profile.activate("default") bread.profile.activate("default")
bread.log("bread started on " .. bread.machine.name())
end) end)
``` ```
### 3) Add your first module ### 3) Start the daemon
Create a Lua file under your modules directory and load it from `init.lua`. ```bash
systemctl --user start breadd
# Or directly:
breadd
```
### 4) Check that it's running
```bash
bread ping
bread doctor
```
## Your first module ## Your first module
Create a file at `~/.config/bread/modules/hello.lua`. It is discovered and loaded automatically after `init.lua`.
```lua ```lua
local M = bread.module({ name = "hello", version = "0.1.0" }) local M = bread.module({ name = "hello", version = "0.1.0" })
function M.on_load() function M.on_load()
bread.log("hello from bread") bread.log("hello from bread on " .. bread.machine.name())
bread.on("bread.device.*", function(event) bread.on("bread.device.*", function(event)
bread.log(event.event) bread.log("device event: " .. event.event)
end) end)
end end
return M return M
``` ```
Why this shape? Key rules:
- Every module must call `bread.module` once. - Every module must call `bread.module` exactly once at the top level.
- `on_load` is a good place to register subscriptions. - Register subscriptions inside `M.on_load` so they are cleaned up properly on hot reload.
- Use `bread.log` early to verify handlers are firing. - Use `bread.log` early to verify handlers are firing.
## Run, reload, and watch ## Run, reload, and watch
- Start the daemon, then use `bread reload` after editing Lua. ```bash
- `bread reload --watch` will keep reloading on changes. # Hot-reload the Lua runtime after editing config
- See [Examples.md](Examples.md) for real-world ports. bread reload
# Watch for file changes and reload automatically
bread reload --watch
```
If any module fails to load, `bread reload` prints the error with a full Lua stack trace. The daemon stays running — fix the file and reload again.
## Modules: install and manage
Modules are Lua packages installed to `~/.config/bread/modules/`. The CLI manages the install lifecycle.
```bash
# Install from GitHub (downloads and extracts the default branch tarball)
bread modules install github:someuser/bread-wifi
# Install from a local directory
bread modules install ~/src/my-module
# Install a specific ref
bread modules install github:someuser/bread-wifi@v1.2.0
# List installed modules and their daemon status
bread modules list
# Show full manifest for one module
bread modules info bread-wifi
# Re-install all GitHub-sourced modules (pick up upstream changes)
bread modules update
# Remove a module
bread modules remove bread-wifi
bread modules remove bread-wifi --yes # skip confirmation
```
Each installed module has a `bread.module.toml` manifest:
```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"
```
## Sync: snapshot and restore
Bread sync snapshots your Bread config, arbitrary dotfiles, and installed package lists into a Git repository. Pull it on another machine to restore state.
```bash
# First-time setup
bread sync init --remote git@github.com:you/bread-config.git
# Snapshot and push
bread sync push
# On another machine: pull and apply
bread sync pull
# Also reinstall packages from snapshot
bread sync pull --install-packages
# See what has changed
bread sync status
bread sync diff
bread sync diff --remote
# List known machines
bread sync machines
```
Configure sync 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:
```
~/.local/share/bread/sync-repo/
├── bread/ ← ~/.config/bread/ snapshot
├── configs/ ← delegate paths (nvim, waybar, etc.)
├── machines/ ← per-machine profiles with tags and last-sync time
└── packages/ ← package snapshots (pacman.txt, pip.txt, etc.)
```
## Debugging tips ## Debugging tips
- Log event payloads with `bread.log(event.data.raw)` when matching devices. - Run `bread events` to see live normalized events.
- Use `bread.events` in the CLI to see live normalized events. - Run `bread state` to see full runtime state as JSON.
- Use `bread state` to see runtime state as JSON. - Run `bread doctor` to check adapter and module health.
- Log event payloads with `bread.log(tostring(event.data))`.
- Use `RUST_LOG=debug breadd` for verbose daemon output.
## Lua module system ---
### Entry point and module scanning ## Dictionary: Lua API
- `init.lua` is executed first. Every API is exposed through the `bread` global table.
- 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 ### Module declaration
Every module must call `bread.module` exactly once at the top level.
```lua ```lua
local M = bread.module({ local M = bread.module({
name = "my.module", name = "my.module",
version = "0.1.0", version = "0.1.0",
after = { "bread.devices" }, after = { "bread.devices" }, -- optional: load after this module
}) })
return M return M
@ -103,65 +219,25 @@ return M
If a module does not call `bread.module`, it fails to load and is marked as a load error. 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 ### Events
#### `bread.on(pattern, fn) -> id` #### `bread.on(pattern, fn) -> id`
Subscribe to matching events. Returns a numeric subscription ID. Subscribe to matching events. Returns a numeric subscription ID.
```lua
local id = bread.on("bread.device.*", function(event)
-- event.event → the full event name string
-- event.data → table of event-specific fields
-- event.source → adapter that produced it ("Udev", "Hyprland", etc.)
bread.log(event.event)
end)
```
#### `bread.once(pattern, fn) -> id` #### `bread.once(pattern, fn) -> id`
Subscribe once. The handler is removed after the first match. Subscribe once. The handler is removed after the first match.
#### `bread.filter(pattern, fn, opts) -> id` #### `bread.filter(pattern, fn, opts) -> id`
Subscribe with a predicate filter. `opts` must contain `filter`: Subscribe with a predicate. `opts` must contain a `filter` function:
```lua ```lua
bread.filter("bread.device.*", function(event) bread.filter("bread.device.*", function(event)
@ -174,13 +250,13 @@ end, {
``` ```
#### `bread.off(id)` #### `bread.off(id)`
Unsubscribe an event or state watch by ID. Unsubscribe an event handler or state watch by ID.
#### `bread.emit(event, data)` #### `bread.emit(event, data)`
Emit a custom event into the system pipeline. Emit a custom event into the system pipeline. Useful for cross-module communication.
#### `bread.wait(pattern, opts) -> event | nil` #### `bread.wait(pattern, opts) -> event | nil`
Coroutine-only helper that waits for a matching event. Coroutine-only helper that suspends until a matching event arrives.
```lua ```lua
bread.spawn(function() bread.spawn(function()
@ -192,30 +268,37 @@ end)
``` ```
#### `bread.spawn(fn)` #### `bread.spawn(fn)`
Spawn a coroutine and surface errors if the coroutine fails. Spawn a coroutine and surface errors if it fails. Required for using `bread.wait`.
### State ### State
#### `bread.state.get(path)` #### `bread.state.get(path)`
Read a state subtree by dotted path (e.g. `"network.online"`). Read a state subtree by dotted path.
#### Convenience helpers ```lua
local monitors = bread.state.get("monitors")
local online = bread.state.get("network.online")
```
- `bread.state.monitors()` #### Typed shorthands
- `bread.state.active_workspace()`
- `bread.state.active_window()` ```lua
- `bread.state.devices()` bread.state.monitors()
- `bread.state.power()` bread.state.active_workspace()
- `bread.state.network()` bread.state.active_window()
- `bread.state.profile()` bread.state.devices()
bread.state.power()
bread.state.network()
bread.state.profile()
```
#### `bread.state.watch(path, fn) -> id` #### `bread.state.watch(path, fn) -> id`
Watch a state path. The callback receives `(new, old)`. Watch a state path for changes. The callback receives `(new_value, old_value)`.
```lua ```lua
bread.state.watch("power.ac_connected", function(new_val, old_val) bread.state.watch("power.ac_connected", function(new_val, old_val)
if new_val then if new_val then
bread.exec("notify-send 'AC connected'") bread.notify("AC connected")
end end
end) end)
``` ```
@ -223,66 +306,147 @@ end)
### Profiles ### Profiles
#### `bread.profile.activate(name)` #### `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. Activate a named profile. Emits `bread.profile.activated` over IPC.
### Execution ### Execution
#### `bread.exec(cmd)` #### `bread.exec(cmd)`
Runs `cmd` in a `sh -lc` shell. Fire-and-forget (async). Run a shell command. Fire-and-forget (async, does not block Lua).
### Notifications ### Notifications
#### `bread.notify(message, opts)` #### `bread.notify(message, opts)`
Sends a desktop notification via `notify-send`. Send a desktop notification via `notify-send`.
Options: Options:
- `title` (string, default: `"bread"`) | Key | Type | Default |
- `urgency` (string, default from config) |-----|------|---------|
- `timeout` (ms, default from config) | `title` | string | `"bread"` |
- `icon` (string, optional) | `urgency` | string | from config |
| `timeout` | ms | from config |
| `icon` | string | none |
Calling `bread.notify` emits `bread.notify.sent` with `{ title, message, urgency }`. Calling `bread.notify` emits `bread.notify.sent` with `{ title, message, urgency }`.
### Timers ### Timers
#### `bread.after(delay_ms, fn) -> id` #### `bread.after(delay_ms, fn) -> id`
Run once after delay. Run once after a delay.
#### `bread.every(interval_ms, fn) -> id` #### `bread.every(interval_ms, fn) -> id`
Run repeatedly on an interval. Run on a repeating interval.
#### `bread.cancel(id)` #### `bread.cancel(id)`
Cancel a timer created by `after` or `every`. Cancel a timer created by `after` or `every`. Timers are also cancelled automatically on reload.
### Utilities ### Utilities
#### `bread.debounce(delay_ms, fn) -> wrapped_fn` #### `bread.debounce(delay_ms, fn) -> wrapped_fn`
Returns a wrapper that only fires after quiet time. Returns a wrapper that fires only after `delay_ms` of quiet time.
```lua
local fn = bread.debounce(200, function(event)
reconfigure_monitors()
end)
bread.on("bread.monitor.**", fn)
```
#### `bread.log(msg)` / `bread.warn(msg)` / `bread.error(msg)` #### `bread.log(msg)` / `bread.warn(msg)` / `bread.error(msg)`
Log helpers that accept any Lua value. Logging helpers. Accept any Lua value (coerced via `tostring`).
### Machine and filesystem
#### `bread.machine.name() -> string`
Returns the machine name from `sync.toml`. Falls back to the system hostname if sync is not initialized.
#### `bread.machine.tags() -> string[]`
Returns the tags array from `sync.toml`, or `{}` if sync is not initialized.
#### `bread.machine.has_tag(tag) -> bool`
Returns true if the machine has the given tag.
#### `bread.fs.write(path, content)`
Write a file. Creates parent directories as needed. `~` is expanded.
#### `bread.fs.read(path) -> string | nil`
Read a file. Returns `nil` if the file does not exist. `~` is expanded.
#### `bread.fs.exists(path) -> bool`
Returns true if the path exists. `~` is expanded.
#### `bread.fs.expand(path) -> string`
Expand `~` to the home directory.
### Hyprland ### Hyprland
The `bread.hyprland` namespace provides compositor bindings: The `bread.hyprland` namespace provides compositor bindings.
- `bread.hyprland.dispatch(cmd, args)` ```lua
- `bread.hyprland.keyword(key, value)` -- Dispatch a Hyprland command
- `bread.hyprland.active_window()` bread.hyprland.dispatch("workspace", "2")
- `bread.hyprland.monitors()` bread.hyprland.dispatch("exec", "kitty")
- `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). -- Set a keyword
bread.hyprland.keyword("monitor", "HDMI-A-1, 2560x1440, 0x0, 1")
-- Query compositor state (returns deserialized Lua tables)
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 (bypasses normalization)
bread.hyprland.on_raw("activewindow", function(raw)
-- raw payload includes: kind, raw (original string), data
end)
```
### Module lifecycle hooks
All hooks are optional.
```lua
function M.on_load()
-- Called after the module loads. Register subscriptions here.
end
function M.on_reload()
-- Called after a hot reload completes across all modules.
end
function M.on_unload()
-- Called before the Lua instance is dropped.
end
function M.on_error(err)
-- Called when a subscription handler in this module throws.
-- Return true to keep the subscription alive, false to cancel it.
return true
end
```
### Module storage
Survives hot reload; does not survive daemon restart.
```lua
M.store.set("last_profile", "docked")
local value = M.store.get("last_profile")
```
Storage is scoped per module and is not shared across modules.
---
## Dictionary: Built-in modules ## Dictionary: Built-in modules
Built-ins are enabled by default. Disable them via `[modules].disable` in the config. Built-ins are loaded before user modules. Disable them via `[modules].disable` in the daemon config.
### `bread.monitors` ### `bread.monitors`
High-level declarative monitor event handlers.
```lua ```lua
local monitors = require("bread.monitors") local monitors = require("bread.monitors")
@ -291,41 +455,51 @@ monitors.layout("dock", function()
end) end)
monitors.on({ monitors.on({
when = "connected", when = "connected",
monitors = { "HDMI-A-1" }, monitors = { "HDMI-A-1" },
run = monitors.apply("dock"), run = monitors.apply("dock"),
}) })
``` ```
- `monitors.on({ when, monitors, run })` | Function | Description |
- `monitors.layout(name, fn)` |----------|-------------|
- `monitors.apply(name) -> fn` | `M.on(opts)` | Register a monitor workflow. `opts`: `when`, `monitors` (optional list), `run` (function or shell string) |
| `M.layout(name, fn)` | Register a named layout function |
| `M.apply(name) -> fn` | Returns a function that calls the named layout |
`when` is one of `connected`, `disconnected`, `changed`. `run` may be a function or a shell command string. `when` is one of `connected`, `disconnected`, `changed`.
### `bread.devices` ### `bread.devices`
Device connection rules with class-based matching.
```lua ```lua
local devices = require("bread.devices") local devices = require("bread.devices")
-- Register a name pattern → class mapping
devices.register("CalDigit", "dock")
devices.register("Keychron", "keyboard") devices.register("Keychron", "keyboard")
devices.on({ devices.on({
when = "connected", when = "connected",
class = "keyboard", class = "keyboard",
run = function(event) run = function(event)
bread.exec("xset r rate 200 40") bread.exec("xset r rate 200 40")
end, end,
}) })
``` ```
- `devices.on({ when, class, name, run })` | Function | Description |
- `devices.register(pattern, class)` |----------|-------------|
| `M.on(opts)` | Register a device rule. `opts`: `when`, `class` (optional), `name` (optional pattern), `run` |
| `M.register(pattern, class)` | Map a device name pattern to a class string |
`class` may be `dock`, `keyboard`, `mouse`, `tablet`, `display`, `storage`, `audio`, `unknown`. `class` values: `dock`, `keyboard`, `mouse`, `tablet`, `display`, `storage`, `audio`, `unknown`.
### `bread.workspaces` ### `bread.workspaces`
Workspace-to-monitor assignment and app pinning.
```lua ```lua
local workspaces = require("bread.workspaces") local workspaces = require("bread.workspaces")
@ -333,26 +507,34 @@ workspaces.assign("1", "HDMI-A-1")
workspaces.pin({ app = "Firefox", workspace = "2" }) workspaces.pin({ app = "Firefox", workspace = "2" })
``` ```
- `workspaces.assign(workspace, monitor)` | Function | Description |
- `workspaces.pin({ app, workspace })` |----------|-------------|
- `workspaces.apply_assignments()` | `M.assign(workspace, monitor)` | Assign a workspace to a monitor |
| `M.pin(opts)` | Pin an app class to a workspace. `opts`: `app`, `workspace` |
| `M.apply_assignments()` | Apply all registered assignments via Hyprland dispatch |
### `bread.binds` ### `bread.binds`
Runtime keybind management via Hyprland.
```lua ```lua
local binds = require("bread.binds") local binds = require("bread.binds")
binds.add({ binds.add({
mods = { "SUPER" }, mods = { "SUPER" },
key = "Return", key = "Return",
dispatch = "exec", dispatch = "exec",
args = "kitty", args = "kitty",
}) })
``` ```
- `binds.add({ mods, key, dispatch, args })` | Function | Description |
- `binds.remove(key)` |----------|-------------|
- `binds.replace(key, opts)` | `M.add(opts)` | Add a keybind. `opts`: `mods`, `key`, `dispatch`, `args` |
| `M.remove(key)` | Remove a keybind by key |
| `M.replace(key, opts)` | Remove and re-add a keybind |
---
## Dictionary: Event reference ## Dictionary: Event reference
@ -369,103 +551,136 @@ Events are delivered as a `BreadEvent`:
### Pattern matching ### Pattern matching
Patterns match event names with glob-style syntax: | Pattern | Matches |
|---------|---------|
- Exact match: `bread.device.dock.connected` | `bread.device.dock.connected` | Exact match only |
- `*` matches within a single segment (does not cross `.`) | `bread.device.*` | One segment wildcard (does not cross `.`) |
- `**` matches across segments (recursive) | `bread.device.**` | Any depth under `bread.device` |
- `?` matches a single character within a segment | `bread.monitor.?` | Single character within one segment |
Examples:
```lua
bread.on("bread.device.*", handler)
bread.on("bread.device.**", handler)
bread.on("bread.monitor.?", handler)
```
### Normalized events ### Normalized events
#### System #### System
- `bread.system.startup` (data: `{}`) | Event | Data |
|-------|------|
| `bread.system.startup` | `{}` |
#### Devices (udev) #### Devices (udev)
- `bread.device.connected` | Event | Data |
- `bread.device.disconnected` |-------|------|
- `bread.device.changed` | `bread.device.connected` | `{ id, class, name, subsystem, vendor_id?, product_id? }` |
- `bread.device.<class>.connected` | `bread.device.disconnected` | `{ id, class, name, subsystem, vendor_id?, product_id? }` |
- `bread.device.<class>.disconnected` | `bread.device.<class>.connected` | same |
- `bread.device.<class>.changed` | `bread.device.<class>.disconnected` | same |
Payload notes: `class`: `dock`, `keyboard`, `mouse`, `tablet`, `display`, `storage`, `audio`, `unknown`.
- 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 #### Hyprland
- `bread.workspace.changed` (raw payload) | Event | Data |
- `bread.workspace.created` (`{ "workspace": "..." }`) |-------|------|
- `bread.workspace.destroyed` (`{ "workspace": "..." }`) | `bread.workspace.changed` | raw payload |
- `bread.monitor.connected` (raw payload) | `bread.workspace.created` | `{ workspace }` |
- `bread.monitor.disconnected` (raw payload) | `bread.workspace.destroyed` | `{ workspace }` |
- `bread.window.focus.changed` (raw payload) | `bread.monitor.connected` | raw payload |
- `bread.window.focused` (`{ "address": "..." }`) | `bread.monitor.disconnected` | raw payload |
- `bread.window.opened` (`{ "address", "workspace", "class", "title" }`) | `bread.window.focus.changed` | raw payload |
- `bread.window.closed` (`{ "address": "..." }`) | `bread.window.focused` | `{ address }` |
- `bread.window.moved` (`{ "address", "workspace" }`) | `bread.window.opened` | `{ address, workspace, class, title }` |
- `bread.hyprland.event` (raw payload for unhandled kinds) | `bread.window.closed` | `{ address }` |
| `bread.window.moved` | `{ address, workspace }` |
Raw Hyprland payloads contain `kind`, `raw`, and `data` fields. | `bread.hyprland.event` | `{ kind, raw, data }` (unhandled kinds) |
#### Power #### Power
- `bread.power.ac.connected` | Event | Data |
- `bread.power.ac.disconnected` |-------|------|
- `bread.power.battery.low` | `bread.power.ac.connected` | `{ ac_connected, battery_percent }` |
- `bread.power.battery.very_low` | `bread.power.ac.disconnected` | `{ ac_connected, battery_percent }` |
- `bread.power.battery.critical` | `bread.power.battery.low` | `{ battery_percent }` |
- `bread.power.battery.full` | `bread.power.battery.very_low` | `{ battery_percent }` |
- `bread.power.changed` (fallback) | `bread.power.battery.critical` | `{ battery_percent }` |
| `bread.power.battery.full` | `{ battery_percent }` |
Payload includes `ac_connected` and `battery_percent`. | `bread.power.changed` | `{ ac_connected, battery_percent }` |
#### Network #### Network
- `bread.network.connected` | Event | Data |
- `bread.network.disconnected` |-------|------|
| `bread.network.connected` | `{ online, interfaces }` |
| `bread.network.disconnected` | `{ online, interfaces }` |
Payload includes `online` and `interfaces`. #### System events
#### Other system events | Event | Data |
|-------|------|
| `bread.profile.activated` | `{ name }` |
| `bread.notify.sent` | `{ title, message, urgency }` |
| `bread.state.changed.<path>` | emitted by state watches |
- `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 ## Dictionary: Runtime state schema
`bread.state.get("")` returns the full `RuntimeState`: `bread state` and `bread.state.get("")` return the full `RuntimeState`:
```json ```json
{ {
"monitors": [ { "name": "HDMI-A-1", "connected": true } ], "monitors": [
"workspaces": [ { "id": "1", "monitor": "HDMI-A-1" } ], { "name": "HDMI-A-1", "connected": true, "resolution": null, "position": null }
],
"workspaces": [
{ "id": "1", "monitor": "HDMI-A-1" }
],
"active_workspace": "1", "active_workspace": "1",
"active_window": "Firefox", "active_window": "0x...",
"devices": { "connected": [] }, "devices": {
"network": { "interfaces": {}, "online": false }, "connected": [
"power": { "ac_connected": false, "battery_percent": null, "battery_low": false }, {
"profile": { "active": "default", "history": [], "profiles": {} }, "id": "/sys/...",
"modules": [ { "name": "bread.devices", "status": "loaded", "last_error": null, "builtin": true, "store": {} } ] "name": "CalDigit TS4",
"class": "dock",
"subsystem": "usb",
"vendor_id": "0x35f5",
"product_id": "0x0104"
}
]
},
"network": {
"interfaces": { "eth0": { "up": true } },
"online": true
},
"power": {
"ac_connected": true,
"battery_percent": 87,
"battery_low": false
},
"profile": {
"active": "default",
"history": [],
"profiles": {}
},
"modules": [
{
"name": "bread.monitors",
"status": "loaded",
"last_error": null,
"builtin": true,
"store": {}
}
]
} }
``` ```
`status` values: `loaded`, `load_error`, `not_found`, `degraded`, `disabled`.
---
## Dictionary: IPC protocol ## Dictionary: IPC protocol
The daemon exposes a Unix socket at `$XDG_RUNTIME_DIR/bread/bread.sock`. Messages are newline-delimited JSON. The daemon exposes a Unix socket at `$XDG_RUNTIME_DIR/bread/breadd.sock`. Messages are newline-delimited JSON.
Request: Request:
@ -481,16 +696,17 @@ Response:
Available methods: Available methods:
- `ping` | Method | Params | Description |
- `health` |--------|--------|-------------|
- `state.get` | `ping` | — | Connectivity check |
- `state.dump` | `health` | — | Version, uptime, PID, adapter status |
- `modules.list` | `state.get` | `key` (dotted path) | Read a value from `RuntimeState` |
- `modules.reload` | `state.dump` | — | Return the full `RuntimeState` as JSON |
- `profile.list` | `modules.list` | — | List all loaded modules and their status |
- `profile.activate` | `modules.reload` | — | Hot-reload the Lua runtime |
- `events.subscribe` | `profile.list` | — | List defined profiles |
- `events.replay` | `profile.activate` | `name` | Switch active profile |
- `emit` | `events.subscribe` | — | Upgrade to streaming mode; pushes events line by line |
| `events.replay` | `since_ms` | Replay buffered events from the last N ms |
`events.subscribe` upgrades the socket to streaming mode and sends events as they occur. | `emit` | `event`, `data` | Inject a synthetic event into the pipeline |
| `sync.status` | — | Return sync init state: `{ initialized, machine?, remote? }` |