Add filesystem/git/podman/systemd adapters, git/shell hooks, bread-emit CLI, app-detection helpers

This commit is contained in:
Breadway 2026-07-19 03:05:57 +08:00
parent 89c5849539
commit 1208c5d1b7
29 changed files with 4098 additions and 339 deletions

296
README.md
View file

@ -76,7 +76,7 @@ Optional but preferred:
### From source
```bash
git clone https://github.com/Breadway/bread.git
git clone https://git.breadway.dev/Breadway/bread.git
cd bread
```
@ -256,295 +256,15 @@ return M
---
## Event reference
## Event reference, Lua API, and IPC protocol
Events follow the namespace convention `bread.<subsystem>.<noun>.<verb>`.
These are fully documented in [`Documentation.md`](Documentation.md) — the single canonical reference (event catalogue, per-function Lua API, runtime-state schema, and IPC protocol), versioned as **Bread Automation API v1**. This README no longer keeps a parallel copy, to avoid the two drifting apart.
| Event | Trigger |
|-------|---------|
| `bread.system.startup` | Daemon fully initialized |
| `bread.device.connected` | Any device attached |
| `bread.device.disconnected` | Any device removed |
| `bread.device.<device>.connected` | Named device attached (name from `devices.lua`) |
| `bread.device.<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)
-- First arg is the message body; opts.title sets the notification title (default: "bread")
bread.notify("Something happened", { title = "My Module", urgency = "normal", timeout = 3000, icon = "dialog-info" })
bread.notify("AC connected") -- 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 (system 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 |
`events.subscribe` upgrades the connection to streaming mode — the daemon pushes events line by line until the client disconnects.
- [Dictionary: Event reference](Documentation.md#dictionary-event-reference)
- [Dictionary: Lua API](Documentation.md#dictionary-lua-api)
- [Dictionary: Runtime state schema](Documentation.md#dictionary-runtime-state-schema)
- [Dictionary: IPC protocol](Documentation.md#dictionary-ipc-protocol)
- [API Stability & Versioning](Documentation.md#api-stability--versioning)
---