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.
|
||||||
187
Examples.md
Normal file
187
Examples.md
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
# Bread Examples
|
||||||
|
|
||||||
|
These examples show how to translate existing Hyprland automation into Bread's event-driven Lua runtime.
|
||||||
|
|
||||||
|
Each snippet is designed to be drop-in friendly for a `~/.config/bread/modules/*.lua` file. Start with a new module file and `require` it from `~/.config/bread/init.lua`.
|
||||||
|
|
||||||
|
## Example 1: Porting keyboard_and_display_watcher.sh (system script)
|
||||||
|
|
||||||
|
Source inspiration: `~/.config/hypr/scripts/system/keyboard_and_display_watcher.sh`.
|
||||||
|
|
||||||
|
This example covers two parts that port cleanly to Bread:
|
||||||
|
|
||||||
|
- Start/stop the Redox layout viewer when the keyboard appears
|
||||||
|
- Start/stop a display sync service when an external monitor appears
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- ~/.config/bread/modules/redox_and_display.lua
|
||||||
|
local M = bread.module({ name = "redox_and_display", version = "1.0.0" })
|
||||||
|
|
||||||
|
local PREVIEW_CMD = "/home/breadway/redox-layout-viewer/target/release/redox-layout-viewer"
|
||||||
|
local APP_NAME = "redox-layout-vi"
|
||||||
|
|
||||||
|
local function start_viewer()
|
||||||
|
bread.exec("pgrep -f '" .. APP_NAME .. "' >/dev/null || " .. PREVIEW_CMD .. " >/dev/null 2>&1 &")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function stop_viewer()
|
||||||
|
bread.exec("pkill -f '" .. APP_NAME .. "' >/dev/null 2>&1 || true")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_redox(event)
|
||||||
|
-- Inspect event.data.raw once to find stable identifiers in your environment.
|
||||||
|
-- Typical udev fields include id_vendor, id_model, id_vendor_id, id_model_id, and name.
|
||||||
|
local raw = event.data and event.data.raw or {}
|
||||||
|
local name = tostring(raw.name or "")
|
||||||
|
local vendor = tostring(raw.id_vendor or "")
|
||||||
|
local model = tostring(raw.id_model or "")
|
||||||
|
|
||||||
|
return name:lower():find("redox", 1, true)
|
||||||
|
or vendor:lower():find("redox", 1, true)
|
||||||
|
or model:lower():find("redox", 1, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
local external_monitors = 0
|
||||||
|
|
||||||
|
local function update_display_service()
|
||||||
|
if external_monitors > 0 then
|
||||||
|
bread.exec("systemctl --user start hypr-display-sync.service")
|
||||||
|
else
|
||||||
|
bread.exec("systemctl --user stop hypr-display-sync.service")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.on_load()
|
||||||
|
bread.on("bread.device.keyboard.connected", function(event)
|
||||||
|
if is_redox(event) then
|
||||||
|
start_viewer()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
bread.on("bread.device.keyboard.disconnected", function(event)
|
||||||
|
if is_redox(event) then
|
||||||
|
stop_viewer()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
bread.on("bread.monitor.connected", function(event)
|
||||||
|
local name = event.data and (event.data.name or event.data.raw) or ""
|
||||||
|
-- ignore internal panel (eDP-1) and count only externals
|
||||||
|
if not tostring(name):match("eDP%-1") then
|
||||||
|
external_monitors = external_monitors + 1
|
||||||
|
update_display_service()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
bread.on("bread.monitor.disconnected", function(event)
|
||||||
|
local name = event.data and (event.data.name or event.data.raw) or ""
|
||||||
|
if not tostring(name):match("eDP%-1") then
|
||||||
|
external_monitors = math.max(0, external_monitors - 1)
|
||||||
|
update_display_service()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- Use `bread.log(event.data.raw)` once to see your exact udev fields for matching.
|
||||||
|
- This drops polling and relies on udev/Hyprland events.
|
||||||
|
|
||||||
|
## Example 2: Porting autostart.lua
|
||||||
|
|
||||||
|
Source inspiration: `~/.config/hypr/scripts/system/autostart.lua`.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- ~/.config/bread/modules/autostart.lua
|
||||||
|
local M = bread.module({ name = "autostart", version = "1.0.0" })
|
||||||
|
|
||||||
|
local home = os.getenv("HOME") or "/home/breadway"
|
||||||
|
local startup_commands = {
|
||||||
|
"wal -R",
|
||||||
|
home .. "/colorshell/build/colorshell",
|
||||||
|
"awww-daemon",
|
||||||
|
"awww restore",
|
||||||
|
home .. "/.config/hypr/scripts/system/keyboard_and_display_watcher.sh",
|
||||||
|
home .. "/.config/hypr/watch_hypr_scripts.sh",
|
||||||
|
"systemctl --user daemon-reload",
|
||||||
|
"systemctl --user start hypr-display-sync.service",
|
||||||
|
"systemctl --user start hyprpolkitagent",
|
||||||
|
"dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP",
|
||||||
|
"/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1",
|
||||||
|
"flatpak run dev.deedles.Trayscale",
|
||||||
|
"wificonf init",
|
||||||
|
"pkill -f hyprpaper",
|
||||||
|
}
|
||||||
|
|
||||||
|
function M.on_load()
|
||||||
|
bread.once("bread.system.startup", function()
|
||||||
|
for _, cmd in ipairs(startup_commands) do
|
||||||
|
bread.exec(cmd)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example 3: Porting display/monitors.lua
|
||||||
|
|
||||||
|
Source inspiration: `~/.config/hypr/scripts/display/monitors.lua`.
|
||||||
|
|
||||||
|
This uses Bread events and Hyprland keywords to update monitor layout when external displays change.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- ~/.config/bread/modules/monitors.lua
|
||||||
|
local M = bread.module({ name = "monitors", version = "1.0.0" })
|
||||||
|
|
||||||
|
local function apply_internal_mode(has_external)
|
||||||
|
local mode = has_external and "1920x1080@60" or "1920x1200@60"
|
||||||
|
bread.hyprland.keyword("monitor", "eDP-1, " .. mode .. ", 0x0, 1")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function apply_external()
|
||||||
|
bread.hyprland.keyword("monitor", "DP-3, 1920x1080@60, auto, 1, mirror, eDP-1")
|
||||||
|
end
|
||||||
|
|
||||||
|
local externals = 0
|
||||||
|
local function update()
|
||||||
|
apply_internal_mode(externals > 0)
|
||||||
|
if externals > 0 then
|
||||||
|
apply_external()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.on_load()
|
||||||
|
bread.on("bread.monitor.connected", function(event)
|
||||||
|
local name = tostring((event.data and (event.data.name or event.data.raw)) or "")
|
||||||
|
if not name:match("eDP%-1") then
|
||||||
|
externals = externals + 1
|
||||||
|
update()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
bread.on("bread.monitor.disconnected", function(event)
|
||||||
|
local name = tostring((event.data and (event.data.name or event.data.raw)) or "")
|
||||||
|
if not name:match("eDP%-1") then
|
||||||
|
externals = math.max(0, externals - 1)
|
||||||
|
update()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
bread.once("bread.system.startup", function()
|
||||||
|
update()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tips for porting your own scripts
|
||||||
|
|
||||||
|
- Start by logging the event payload: `bread.log(event.data.raw)`
|
||||||
|
- Replace polling loops with event subscriptions
|
||||||
|
- Use `bread.exec` for shell commands and systemd operations
|
||||||
|
- Use `bread.state.watch` for data that already lives in the runtime state
|
||||||
84
README.md
84
README.md
|
|
@ -191,29 +191,42 @@ Events follow the namespace convention `bread.<subsystem>.<noun>.<verb>`.
|
||||||
| `bread.system.startup` | Daemon fully initialized |
|
| `bread.system.startup` | Daemon fully initialized |
|
||||||
| `bread.device.connected` | Any device attached |
|
| `bread.device.connected` | Any device attached |
|
||||||
| `bread.device.disconnected` | Any device removed |
|
| `bread.device.disconnected` | Any device removed |
|
||||||
| `bread.device.dock.connected` | Dock attached |
|
| `bread.device.changed` | Any device changed |
|
||||||
| `bread.device.dock.disconnected` | Dock removed |
|
| `bread.device.<class>.connected` | Device attached by class |
|
||||||
| `bread.device.keyboard.connected` | Keyboard attached |
|
| `bread.device.<class>.disconnected` | Device removed by class |
|
||||||
|
| `bread.device.<class>.changed` | Device changed by class |
|
||||||
| `bread.monitor.connected` | Display connected |
|
| `bread.monitor.connected` | Display connected |
|
||||||
| `bread.monitor.disconnected` | Display disconnected |
|
| `bread.monitor.disconnected` | Display disconnected |
|
||||||
| `bread.workspace.changed` | Active workspace changed |
|
| `bread.workspace.changed` | Active workspace changed |
|
||||||
|
| `bread.workspace.created` | Workspace created |
|
||||||
|
| `bread.workspace.destroyed` | Workspace destroyed |
|
||||||
| `bread.window.focus.changed` | Focused window changed |
|
| `bread.window.focus.changed` | Focused window changed |
|
||||||
|
| `bread.window.focused` | Focus moved (address only) |
|
||||||
| `bread.window.opened` | Window opened |
|
| `bread.window.opened` | Window opened |
|
||||||
| `bread.window.closed` | Window closed |
|
| `bread.window.closed` | Window closed |
|
||||||
|
| `bread.window.moved` | Window moved workspaces |
|
||||||
| `bread.power.ac.connected` | AC adapter plugged in |
|
| `bread.power.ac.connected` | AC adapter plugged in |
|
||||||
| `bread.power.ac.disconnected` | AC adapter unplugged |
|
| `bread.power.ac.disconnected` | AC adapter unplugged |
|
||||||
| `bread.power.battery.low` | Battery ≤ 20% |
|
| `bread.power.battery.low` | Battery ≤ 20% |
|
||||||
| `bread.power.battery.very_low` | Battery ≤ 10% |
|
| `bread.power.battery.very_low` | Battery ≤ 10% |
|
||||||
| `bread.power.battery.critical` | Battery ≤ 5% |
|
| `bread.power.battery.critical` | Battery ≤ 5% |
|
||||||
| `bread.power.battery.full` | Battery at 100% |
|
| `bread.power.battery.full` | Battery at 100% |
|
||||||
| `bread.network.connected` | Network interface came online |
|
| `bread.power.changed` | Power state changed (fallback) |
|
||||||
| `bread.network.disconnected` | Network interface went offline |
|
| `bread.network.connected` | Network came online |
|
||||||
| `bread.profile.activated` | Profile switched |
|
| `bread.network.disconnected` | Network went offline |
|
||||||
|
| `bread.profile.activated` | Profile switched via IPC |
|
||||||
|
| `bread.notify.sent` | Notification dispatched |
|
||||||
|
| `bread.state.changed.<path>` | State watch fired |
|
||||||
|
| `bread.hyprland.event` | Raw Hyprland event (unhandled kind) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Lua API
|
## Lua API
|
||||||
|
|
||||||
|
Full reference and usage notes live in [documentation.md](documentation.md). This section is a compact quick-reference to every API that exists today.
|
||||||
|
|
||||||
|
Practical walkthroughs and ports from existing Hyprland configs live in [Examples.md](Examples.md).
|
||||||
|
|
||||||
### Events
|
### Events
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
|
|
@ -239,6 +252,14 @@ end)
|
||||||
|
|
||||||
-- Emit a custom event (for cross-module communication)
|
-- Emit a custom event (for cross-module communication)
|
||||||
bread.emit("mymodule.something", { key = "value" })
|
bread.emit("mymodule.something", { key = "value" })
|
||||||
|
|
||||||
|
-- Wait for an event (coroutine-only)
|
||||||
|
bread.spawn(function()
|
||||||
|
local event = bread.wait("bread.device.dock.connected", { timeout = 5000 })
|
||||||
|
if event then
|
||||||
|
bread.log("dock arrived")
|
||||||
|
end
|
||||||
|
end)
|
||||||
```
|
```
|
||||||
|
|
||||||
### State
|
### State
|
||||||
|
|
@ -254,6 +275,15 @@ local devices = bread.state.get("devices")
|
||||||
bread.state.watch("active_workspace", function(new, old)
|
bread.state.watch("active_workspace", function(new, old)
|
||||||
print("workspace changed from " .. tostring(old) .. " to " .. tostring(new))
|
print("workspace changed from " .. tostring(old) .. " to " .. tostring(new))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
-- Convenience helpers
|
||||||
|
local monitors = bread.state.monitors()
|
||||||
|
local active_ws = bread.state.active_workspace()
|
||||||
|
local active_win = bread.state.active_window()
|
||||||
|
local devices = bread.state.devices()
|
||||||
|
local power = bread.state.power()
|
||||||
|
local network = bread.state.network()
|
||||||
|
local profile = bread.state.profile()
|
||||||
```
|
```
|
||||||
|
|
||||||
### Profiles
|
### Profiles
|
||||||
|
|
@ -291,6 +321,10 @@ bread.cancel(id)
|
||||||
local fn = bread.debounce(200, function(event)
|
local fn = bread.debounce(200, function(event)
|
||||||
reconfigure_monitors()
|
reconfigure_monitors()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
-- Cancel a timer
|
||||||
|
local timer_id = bread.after(500, function() bread.exec("echo ready") end)
|
||||||
|
bread.cancel(timer_id)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Logging
|
### Logging
|
||||||
|
|
@ -299,6 +333,44 @@ end)
|
||||||
bread.log("Module loaded")
|
bread.log("Module loaded")
|
||||||
bread.warn("Unexpected state")
|
bread.warn("Unexpected state")
|
||||||
bread.error("Something failed")
|
bread.error("Something failed")
|
||||||
|
|
||||||
|
### Hyprland
|
||||||
|
|
||||||
|
```lua
|
||||||
|
bread.hyprland.dispatch("workspace", "2")
|
||||||
|
bread.hyprland.keyword("monitor", "HDMI-A-1, 2560x1440, 0x0, 1")
|
||||||
|
|
||||||
|
local win = bread.hyprland.active_window()
|
||||||
|
local monitors = bread.hyprland.monitors()
|
||||||
|
local workspaces = bread.hyprland.workspaces()
|
||||||
|
local clients = bread.hyprland.clients()
|
||||||
|
|
||||||
|
-- Raw Hyprland event filtering (kind matches hyprland event name)
|
||||||
|
bread.hyprland.on_raw("openwindow", function(event)
|
||||||
|
bread.log(event.data.raw)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modules
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local M = bread.module({ name = "my.module", version = "0.1.0", after = { "bread.devices" } })
|
||||||
|
|
||||||
|
function M.on_load()
|
||||||
|
bread.on("bread.device.*", function(event)
|
||||||
|
bread.log(event.event)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.on_unload()
|
||||||
|
bread.log("unloaded")
|
||||||
|
end
|
||||||
|
|
||||||
|
M.store.set("last_seen", os.time())
|
||||||
|
local last = M.store.get("last_seen")
|
||||||
|
|
||||||
|
return M
|
||||||
|
```
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue