Merge origin/master into dev and resolve conflicts
Co-authored-by: Breadway <108389940+Breadway@users.noreply.github.com>
This commit is contained in:
commit
60b2999ebf
4 changed files with 686 additions and 531 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
|
||||
527
LUA_RUNTIME.md
527
LUA_RUNTIME.md
|
|
@ -1,527 +0,0 @@
|
|||
# bread — Lua Runtime Architecture
|
||||
### The Bread Scripting and Automation Layer
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Lua runtime is the automation half of Bread. Where `breadd` maintains truth about the desktop, the Lua layer decides what to do about it.
|
||||
|
||||
Modules written in Lua subscribe to events, read state, execute shell commands, and activate profiles. The entire scripting surface is exposed through a single `bread.*` global API — stable, versioned, and designed to be hostile to accidents.
|
||||
|
||||
The runtime lives on a dedicated OS thread inside `breadd`. It is reachable from the async side only through a bounded message channel. Lua never touches sockets, sysfs, or compositor IPC directly. Everything flows through the daemon.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Runtime Core
|
||||
|
||||
These capabilities exist in the codebase today. Phase 1 is the foundation the Lua runtime is built on.
|
||||
|
||||
### Daemon Stability
|
||||
|
||||
`breadd` is a single long-running Rust process. It survives compositor restarts, module load errors, and Lua runtime panics. The daemon never terminates because a Lua file has a syntax error.
|
||||
|
||||
The Lua runtime thread is spawned once at startup:
|
||||
|
||||
```rust
|
||||
std::thread::Builder::new()
|
||||
.name("breadd-lua".to_string())
|
||||
.spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("failed to create lua runtime thread");
|
||||
|
||||
rt.block_on(async move {
|
||||
let mut engine = LuaEngine::new(config, state_handle, emit_tx)?;
|
||||
engine.reload_internal()?;
|
||||
|
||||
while let Some(msg) = rx.recv().await {
|
||||
match msg { /* ... */ }
|
||||
}
|
||||
});
|
||||
})?;
|
||||
```
|
||||
|
||||
If the initial module load fails, the daemon enters degraded mode: no Lua handlers are active, but the daemon itself remains alive. IPC stays responsive and `bread reload` can be used to recover after the user fixes their config.
|
||||
|
||||
### Event Ingestion
|
||||
|
||||
Every signal that enters `breadd` from an external system flows through a strict pipeline before it reaches Lua:
|
||||
|
||||
```
|
||||
External System (Hyprland / udev / power / network)
|
||||
│
|
||||
▼
|
||||
Adapter — raw ingestion, owns the connection
|
||||
│ RawEvent
|
||||
▼
|
||||
Normalizer — semantic interpretation
|
||||
│ BreadEvent
|
||||
▼
|
||||
State Engine — state update + fan-out
|
||||
│
|
||||
├──► RuntimeState (updated atomically)
|
||||
└──► Subscription Dispatcher
|
||||
│ BreadEvent (per subscriber)
|
||||
▼
|
||||
Lua Runtime
|
||||
(module handlers)
|
||||
```
|
||||
|
||||
Raw events never reach Lua directly. Lua never observes a `RawEvent` — it only ever sees a normalized `BreadEvent` with a stable namespace string like `bread.device.dock.connected`.
|
||||
|
||||
### Subscriptions
|
||||
|
||||
Modules subscribe to events by pattern. The subscription table maps pattern strings to `(SubscriptionId, is_once)` pairs. The state engine evaluates each incoming `BreadEvent` against the table and dispatches to every matching subscriber.
|
||||
|
||||
```rust
|
||||
pub struct SubscriptionId(pub u64);
|
||||
```
|
||||
|
||||
The Lua side registers subscriptions via `bread.on` and `bread.once`. Each call allocates a monotonically increasing `SubscriptionId`, stores the callback in the Lua registry, and registers the pattern with the state engine:
|
||||
|
||||
```lua
|
||||
bread.on("bread.device.dock.*", function(event)
|
||||
bread.exec("~/.config/bread/scripts/dock.sh")
|
||||
end)
|
||||
|
||||
bread.once("bread.system.startup", function(event)
|
||||
bread.profile.activate("default")
|
||||
end)
|
||||
```
|
||||
|
||||
`bread.once` subscriptions are automatically cancelled after first delivery. The handler is removed from both the Lua registry and the subscription table.
|
||||
|
||||
### IPC
|
||||
|
||||
`breadd` exposes a Unix domain socket at `$XDG_RUNTIME_DIR/bread/breadd.sock`. The protocol is newline-delimited JSON. All IPC requests that affect the Lua runtime route through the `LuaMessage` channel — IPC never touches the Lua thread directly.
|
||||
|
||||
Relevant IPC methods:
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `modules.list` | List loaded modules and their status |
|
||||
| `modules.reload` | Trigger a hot reload of the Lua layer |
|
||||
| `emit` | Inject a synthetic `BreadEvent` into the pipeline |
|
||||
| `state.get` | Read a value from `RuntimeState` by key path |
|
||||
| `state.dump` | Return the full `RuntimeState` as JSON |
|
||||
|
||||
The `emit` method is particularly useful for testing: it allows injecting arbitrary `BreadEvent`s without needing the real hardware event that would normally produce them.
|
||||
|
||||
### Hot Reload
|
||||
|
||||
Hot reload is a first-class feature. The daemon persists; the Lua layer restarts. No process restart required.
|
||||
|
||||
Reload sequence:
|
||||
|
||||
```
|
||||
bread reload (CLI)
|
||||
│
|
||||
▼
|
||||
IPC: modules.reload
|
||||
│
|
||||
▼
|
||||
StateEngine: pause event dispatch to Lua
|
||||
│
|
||||
▼
|
||||
LuaRuntime: receive Reload message
|
||||
│
|
||||
├── cancel all active subscriptions
|
||||
├── clear handler registry
|
||||
├── drop Lua instance (all state cleared)
|
||||
├── create fresh Lua instance
|
||||
├── re-register bread.* API
|
||||
├── re-evaluate init.lua and all modules
|
||||
└── re-register subscriptions with SubscriptionTable
|
||||
│
|
||||
▼
|
||||
StateEngine: resume event dispatch
|
||||
│
|
||||
▼
|
||||
IPC: reload complete response
|
||||
```
|
||||
|
||||
If any module fails to load during reload, the reload aborts and the daemon enters degraded mode. There is no rollback — the previous Lua state was dropped before the reload began. This is intentional for V1. A syntax error in a module produces a clear error message from `bread reload`, and the daemon stays alive.
|
||||
|
||||
### State Registry
|
||||
|
||||
The daemon maintains a live `RuntimeState` behind an `Arc<RwLock<RuntimeState>>`. It is the authoritative record of what is true about the desktop right now.
|
||||
|
||||
```rust
|
||||
pub struct RuntimeState {
|
||||
pub monitors: Vec<Monitor>,
|
||||
pub workspaces: Vec<Workspace>,
|
||||
pub active_workspace: Option<String>,
|
||||
pub active_window: Option<String>,
|
||||
pub devices: DeviceTopology,
|
||||
pub network: NetworkState,
|
||||
pub power: PowerState,
|
||||
pub profile: ProfileState,
|
||||
pub modules: Vec<ModuleStatus>,
|
||||
}
|
||||
```
|
||||
|
||||
Lua accesses this via `bread.state.get(path)`. The call takes a brief read lock, serializes the requested subtree to JSON, and converts it to a Lua value. Lua never holds the lock — the lock is dropped before control returns to the Lua callback:
|
||||
|
||||
```lua
|
||||
local monitors = bread.state.get("monitors")
|
||||
local power = bread.state.get("power")
|
||||
local active = bread.state.get("active_workspace")
|
||||
```
|
||||
|
||||
Dotted paths are supported for nested access:
|
||||
|
||||
```lua
|
||||
local online = bread.state.get("network.online")
|
||||
```
|
||||
|
||||
State is read-only from Lua. Lua cannot write to `RuntimeState` directly — it can only influence state indirectly by activating a profile or emitting an event that the state engine processes.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Lua Runtime
|
||||
|
||||
Phase 2 covers what is not yet built: the features required to make the Lua layer a complete, ergonomic automation platform.
|
||||
|
||||
### Module Loader
|
||||
|
||||
Currently, `breadd` loads modules by scanning `~/.config/bread/modules/` and executing every `.lua` file in sorted order. There is no concept of module identity, exports, or dependency declarations.
|
||||
|
||||
Phase 2 introduces a proper module system:
|
||||
|
||||
```
|
||||
~/.config/bread/
|
||||
├── init.lua ← entry point; declares module list
|
||||
└── modules/
|
||||
├── dock.lua
|
||||
├── display.lua
|
||||
├── power.lua
|
||||
└── lib/
|
||||
└── utils.lua ← shared library, loaded on require
|
||||
```
|
||||
|
||||
**`bread.module` declaration** — each module declares itself at the top of the file:
|
||||
|
||||
```lua
|
||||
local M = bread.module({
|
||||
name = "dock",
|
||||
version = "1.0.0",
|
||||
after = { "display" }, -- load after display.lua
|
||||
})
|
||||
```
|
||||
|
||||
The runtime resolves the dependency graph and loads modules in topological order. Circular dependencies are detected at load time and reported as a load error on the offending module.
|
||||
|
||||
**`require` support** — modules in `lib/` are loadable via `require`:
|
||||
|
||||
```lua
|
||||
local utils = require("bread.lib.utils")
|
||||
```
|
||||
|
||||
The module loader intercepts `require` calls that begin with `bread.` and resolves them relative to `~/.config/bread/`. Standard Lua `require` semantics apply for everything else.
|
||||
|
||||
**Module status tracking** — each module's load state is reflected in `RuntimeState.modules` and visible via `bread doctor` and `modules.list`:
|
||||
|
||||
```rust
|
||||
pub enum ModuleLoadState {
|
||||
Loaded,
|
||||
LoadError,
|
||||
NotFound,
|
||||
}
|
||||
```
|
||||
|
||||
Phase 2 extends this with `Degraded` (loaded but encountered a runtime error since last reload) and `Disabled` (explicitly disabled in config).
|
||||
|
||||
### Lifecycle Hooks
|
||||
|
||||
Currently, modules have no way to run code at load time or cleanup code at unload time. Phase 2 adds four lifecycle hooks.
|
||||
|
||||
```lua
|
||||
function M.on_load()
|
||||
-- called once when the module is first loaded
|
||||
-- register subscriptions, initialize module state
|
||||
end
|
||||
|
||||
function M.on_reload()
|
||||
-- called after a hot reload completes
|
||||
-- re-apply any external side effects the module manages
|
||||
end
|
||||
|
||||
function M.on_unload()
|
||||
-- called before the Lua instance is dropped
|
||||
-- cancel external resources, write state if needed
|
||||
end
|
||||
|
||||
function M.on_error(err)
|
||||
-- called when a subscription handler in this module throws
|
||||
-- return true to keep the subscription, false to cancel it
|
||||
end
|
||||
```
|
||||
|
||||
The runtime calls hooks in a defined order:
|
||||
|
||||
- **Load**: `on_load` is called after the module file executes successfully, in dependency order.
|
||||
- **Reload**: `on_unload` is called in reverse dependency order. After the new Lua instance is ready, `on_load` runs on every module. `on_reload` runs after all `on_load` calls complete.
|
||||
- **Error**: `on_error` is called on the Lua thread immediately after a handler throws. If the module does not define `on_error`, the default behavior is to log the error and keep the subscription alive.
|
||||
|
||||
All hooks are optional. A module with no lifecycle hooks continues to work exactly as it does today.
|
||||
|
||||
### Event APIs
|
||||
|
||||
Phase 2 expands the event surface available to Lua modules.
|
||||
|
||||
**Pattern syntax** — the current subscription API matches event names against patterns using glob-style `*` wildcards. Phase 2 adds `**` for recursive matching and `?` for single-character wildcards:
|
||||
|
||||
```lua
|
||||
bread.on("bread.device.*", handler) -- matches bread.device.dock.connected
|
||||
bread.on("bread.device.**", handler) -- matches any depth under bread.device
|
||||
bread.on("bread.monitor.?", handler) -- single-segment wildcard
|
||||
```
|
||||
|
||||
**`bread.off`** — cancel a subscription by the ID returned from `bread.on`:
|
||||
|
||||
```lua
|
||||
local id = bread.on("bread.power.*", handler)
|
||||
-- later:
|
||||
bread.off(id)
|
||||
```
|
||||
|
||||
**`bread.wait`** — yield until a matching event arrives, with an optional timeout:
|
||||
|
||||
```lua
|
||||
local event = bread.wait("bread.device.dock.connected", { timeout = 5000 })
|
||||
if event then
|
||||
-- dock arrived within 5 seconds
|
||||
end
|
||||
```
|
||||
|
||||
`bread.wait` is syntactic sugar over a `bread.once` subscription combined with a coroutine yield. It can only be used inside a coroutine context; calling it from a top-level module body is a load error.
|
||||
|
||||
**`bread.filter`** — attach a predicate to a subscription. The handler is only called when the predicate returns true:
|
||||
|
||||
```lua
|
||||
bread.on("bread.device.*", handler, {
|
||||
filter = function(event)
|
||||
return event.data.class == "dock"
|
||||
end
|
||||
})
|
||||
```
|
||||
|
||||
**Timers** — schedule callbacks without relying on an external timer process:
|
||||
|
||||
```lua
|
||||
local id = bread.after(500, function()
|
||||
-- called once, 500ms from now
|
||||
end)
|
||||
|
||||
local id = bread.every(30000, function()
|
||||
-- called every 30 seconds
|
||||
end)
|
||||
|
||||
bread.cancel(id) -- cancel either kind
|
||||
```
|
||||
|
||||
Timers are cancelled automatically on reload. A module does not need to track its own timer IDs for cleanup.
|
||||
|
||||
### State Access
|
||||
|
||||
Phase 2 extends `bread.state` from a read-only snapshot query into a richer interface.
|
||||
|
||||
**Typed helpers** — convenience wrappers for the most common state subtrees:
|
||||
|
||||
```lua
|
||||
bread.state.monitors() -- Vec<Monitor>
|
||||
bread.state.active_workspace() -- string | nil
|
||||
bread.state.active_window() -- string | nil
|
||||
bread.state.devices() -- Vec<Device>
|
||||
bread.state.power() -- PowerState
|
||||
bread.state.network() -- NetworkState
|
||||
bread.state.profile() -- ProfileState
|
||||
```
|
||||
|
||||
These are thin wrappers over `bread.state.get` — they add no locking overhead.
|
||||
|
||||
**Reactive state** — watch a state path for changes and receive a callback when it changes:
|
||||
|
||||
```lua
|
||||
bread.state.watch("power.ac_connected", function(new_val, old_val)
|
||||
if new_val then
|
||||
bread.exec("notify-send 'AC connected'")
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
||||
State watches are implemented as synthetic subscriptions: the state engine compares the watched path before and after each `RuntimeState` update and synthesizes a `bread.state.changed.<path>` event when a difference is detected. From the Lua runtime's perspective, watches are ordinary subscriptions.
|
||||
|
||||
**Module-scoped storage** — a key-value store persisted across reloads (but not across daemon restarts):
|
||||
|
||||
```lua
|
||||
M.store.set("last_profile", "docked")
|
||||
local p = M.store.get("last_profile") -- "docked"
|
||||
```
|
||||
|
||||
Storage is scoped per module. A module cannot read another module's store. The store is backed by a `HashMap<String, serde_json::Value>` in the `RuntimeState.modules` entry for that module, so it survives hot reload.
|
||||
|
||||
### Hyprland Bindings
|
||||
|
||||
Phase 2 exposes a `bread.hyprland` namespace for direct interaction with the Hyprland compositor. This is the only place in the Lua API that is compositor-specific; all other APIs are compositor-agnostic.
|
||||
|
||||
The bindings communicate over Hyprland's IPC request socket (`$HYPRLAND_INSTANCE_SIGNATURE/.socket.sock`), not the event socket. Calls are dispatched to a Tokio task on the async side and awaited transparently from Lua via coroutine suspension.
|
||||
|
||||
**Dispatch**
|
||||
|
||||
```lua
|
||||
bread.hyprland.dispatch("workspace", "2")
|
||||
bread.hyprland.dispatch("movetoworkspace", "2,address:0x...")
|
||||
bread.hyprland.dispatch("exec", "kitty")
|
||||
```
|
||||
|
||||
**Keyword**
|
||||
|
||||
```lua
|
||||
local result = bread.hyprland.keyword("monitor", "HDMI-A-1, 2560x1440, 0x0, 1")
|
||||
```
|
||||
|
||||
**Active window**
|
||||
|
||||
```lua
|
||||
local win = bread.hyprland.active_window()
|
||||
-- { address, title, class, workspace, monitor, ... }
|
||||
```
|
||||
|
||||
**Monitor and workspace queries**
|
||||
|
||||
```lua
|
||||
local monitors = bread.hyprland.monitors()
|
||||
local workspaces = bread.hyprland.workspaces()
|
||||
local clients = bread.hyprland.clients()
|
||||
```
|
||||
|
||||
All calls return deserialized Lua tables matching Hyprland's JSON response shape. Errors from the compositor (malformed dispatch, unknown keyword) are surfaced as Lua errors catchable with `pcall`.
|
||||
|
||||
**Hyprland-specific events** — the existing `bread.monitor.*` and `bread.workspace.*` event namespaces already cover the most common Hyprland signals. The Phase 2 bindings add lower-level passthrough for events that do not yet have a normalized `BreadEvent` representation:
|
||||
|
||||
```lua
|
||||
bread.hyprland.on_raw("activewindow", function(raw)
|
||||
-- raw is the unparsed string from Hyprland's event socket
|
||||
end)
|
||||
```
|
||||
|
||||
Raw subscriptions bypass normalization. They are intended for power users and for features not yet covered by the normalized event namespace. Once a raw event pattern is common enough, it graduates to a stable `BreadEvent` and the raw subscription is deprecated.
|
||||
|
||||
---
|
||||
|
||||
## Lua ↔ Rust Boundary
|
||||
|
||||
All calls across the boundary go through `mlua`'s safe API. Rust functions registered as Lua globals return `mlua::Result` and handle their own error mapping. Panics inside registered Rust functions are caught by mlua and converted to Lua errors — they do not unwind into the Lua thread and they do not crash the daemon.
|
||||
|
||||
The `LuaMessage` enum is the only channel between the async Tokio runtime and the Lua thread:
|
||||
|
||||
```rust
|
||||
pub enum LuaMessage {
|
||||
Event {
|
||||
subscription_id: SubscriptionId,
|
||||
event: BreadEvent,
|
||||
},
|
||||
SubscriptionCancelled {
|
||||
id: SubscriptionId,
|
||||
},
|
||||
Reload {
|
||||
reply: oneshot::Sender<Result<(), String>>,
|
||||
},
|
||||
Shutdown,
|
||||
}
|
||||
```
|
||||
|
||||
Lua is not `Send`. The `LuaEngine` and the `Lua` instance live exclusively on the dedicated Lua OS thread. The async side communicates only by sending `LuaMessage` values through the channel — it never holds a reference to anything inside the Lua VM.
|
||||
|
||||
---
|
||||
|
||||
## Error Isolation
|
||||
|
||||
### Handler errors
|
||||
|
||||
Lua errors during event handler execution are caught with `pcall` at the Rust boundary:
|
||||
|
||||
```rust
|
||||
fn handle_event(&self, id: SubscriptionId, event: BreadEvent) -> Result<()> {
|
||||
let callback: Function = self.lua.registry_value(reg)?;
|
||||
let event_value = self.lua.to_value(&event)?;
|
||||
if let Err(err) = callback.call::<_, ()>(event_value) {
|
||||
error!(subscription = id.0, error = %err, "lua callback failed");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
The error is logged with the subscription ID and full Lua stack trace. The handler remains registered and will fire again on the next matching event. A persistently failing handler is the module's responsibility to cancel via `bread.off`.
|
||||
|
||||
Phase 2's `on_error` hook gives modules a structured way to respond to handler failures rather than relying solely on the daemon log.
|
||||
|
||||
### Module load errors
|
||||
|
||||
Errors during module load are fatal to that module but not to the daemon or to other modules. The failed module is marked `LoadError` in `RuntimeState.modules`. Remaining modules continue loading in dependency order; only modules that declared `after` the failed module are also skipped (their dependency is broken).
|
||||
|
||||
### Degraded mode
|
||||
|
||||
If the initial load or a hot reload fails such that no Lua instance is running, the daemon enters degraded mode:
|
||||
|
||||
- No Lua handlers are active.
|
||||
- IPC remains fully operational.
|
||||
- `bread reload` can be retried after the user fixes their config.
|
||||
- `bread doctor` reports the load error with the full stack trace.
|
||||
|
||||
The daemon never requires a full restart to recover from a Lua error.
|
||||
|
||||
---
|
||||
|
||||
## `bread.*` API Surface Summary
|
||||
|
||||
### Phase 1 (implemented)
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `bread.on(pattern, fn)` | Subscribe to a pattern; returns subscription ID |
|
||||
| `bread.once(pattern, fn)` | Subscribe once; auto-cancelled after first delivery |
|
||||
| `bread.emit(event, payload)` | Inject a synthetic `BreadEvent` |
|
||||
| `bread.exec(cmd)` | Fire-and-forget shell command |
|
||||
| `bread.state.get(path)` | Read a value from `RuntimeState` by dotted path |
|
||||
| `bread.profile.activate(name)` | Activate a named profile |
|
||||
|
||||
### Phase 2 (planned)
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `bread.off(id)` | Cancel a subscription by ID |
|
||||
| `bread.wait(pattern, opts)` | Yield until a matching event arrives |
|
||||
| `bread.filter(pattern, fn, opts)` | Subscribe with a predicate guard |
|
||||
| `bread.after(ms, fn)` | One-shot timer |
|
||||
| `bread.every(ms, fn)` | Repeating timer |
|
||||
| `bread.cancel(id)` | Cancel a timer |
|
||||
| `bread.state.watch(path, fn)` | React to state changes at a path |
|
||||
| `bread.state.monitors()` | Typed shorthand for `bread.state.get("monitors")` |
|
||||
| `bread.state.power()` | Typed shorthand for `bread.state.get("power")` |
|
||||
| `bread.state.network()` | Typed shorthand for `bread.state.get("network")` |
|
||||
| `bread.hyprland.dispatch(cmd, args)` | Send a Hyprland dispatch |
|
||||
| `bread.hyprland.keyword(key, val)` | Set a Hyprland keyword |
|
||||
| `bread.hyprland.active_window()` | Query the active window |
|
||||
| `bread.hyprland.monitors()` | Query all monitors |
|
||||
| `bread.hyprland.workspaces()` | Query all workspaces |
|
||||
| `bread.hyprland.clients()` | Query all open clients |
|
||||
| `bread.hyprland.on_raw(event, fn)` | Subscribe to a raw Hyprland event string |
|
||||
| `bread.module(decl)` | Declare a module with name, version, and dependencies |
|
||||
| `M.store.get(key)` | Read from module-scoped persistent storage |
|
||||
| `M.store.set(key, val)` | Write to module-scoped persistent storage |
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
The Lua runtime is where Bread becomes useful. The daemon provides a reliable, normalized view of the desktop; the Lua layer acts on it.
|
||||
|
||||
Phase 1 delivers the mechanical minimum: a stable thread, a working `bread.*` API, event subscriptions, state access, hot reload, and IPC. That foundation is in the codebase today.
|
||||
|
||||
Phase 2 builds the ergonomics: module identity, lifecycle hooks, reactive state, timers, richer event APIs, and Hyprland control bindings. Each Phase 2 feature is additive — nothing in Phase 1 needs to change to support it.
|
||||
|
||||
The boundary between Rust and Lua is intentionally narrow. The daemon knows nothing about what modules do. Modules know nothing about how events arrive. The `bread.*` API is the entire contract between them.
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
# Maintainer: Your Name <you@example.com>
|
||||
|
||||
pkgname=breadd
|
||||
pkgname=bread
|
||||
pkgver=0.1.0
|
||||
pkgrel=1
|
||||
pkgdesc="Bread daemon - event normalizer and automation runtime"
|
||||
pkgdesc="Bread - event normalizer and automation runtime"
|
||||
arch=('x86_64')
|
||||
url="https://github.com/Breadway/bread"
|
||||
license=('MIT')
|
||||
|
|
@ -19,7 +19,6 @@ build() {
|
|||
|
||||
package() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
install -Dm755 target/release/breadd "${pkgdir}/usr/bin/breadd"
|
||||
install -Dm755 target/release/bread "${pkgdir}/usr/bin/bread"
|
||||
install -Dm644 packaging/systemd/breadd.service "${pkgdir}/usr/lib/systemd/user/breadd.service"
|
||||
install -Dm644 packaging/systemd/bread.service "${pkgdir}/usr/lib/systemd/user/bread.service"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue