Allow well-formed bread.command events on the emit bus

Docs already said any module or app could publish bread.command.<app>.<verb>,
but command is reserved so both unsourced bread-emit and sourced App emit
rejected the whole namespace. Keep command unclaimable as an app id; accept
bread.command.<known-app>.<verb> (and let an app command another known app).
Also ship bread-emit and bread-module-host, and give udev enumerate the same
classification fields as a live add so boot-time devices are not all unknown.
This commit is contained in:
Breadway 2026-08-15 21:41:40 +08:00
parent a6973360bd
commit d3517d1433
52 changed files with 26405 additions and 6811 deletions

View file

@ -1588,8 +1588,8 @@ Rides the same shell-hook transport as Terminal events (`bread hooks install she
Two dotted-name segments are reserved, permanent parts of the schema — not one-off conventions: Two dotted-name segments are reserved, permanent parts of the schema — not one-off conventions:
- **`bread.<app>.*`** — inbound events published *by* a sibling `bread*` application about its own state (e.g. `bread.clip.copied`). An app may only publish within its own segment; the daemon enforces this at the IPC boundary (a socket client claiming a `source` of an app id it doesn't own is rejected the same way spoofing `power`/`hyprland` is rejected today). - **`bread.<app>.*`** — inbound events published *by* a sibling `bread*` application about its own state (e.g. `bread.clip.copied`). An app may only publish within its own segment; the daemon enforces this at the IPC boundary (a socket client claiming a `source` of an app id it doesn't own is rejected the same way spoofing `power`/`hyprland` is rejected today).
- **`bread.command.<app>.<verb>`** — outbound commands *to* a sibling application (e.g. `bread.command.clip.clear`). Any module or app may publish; only the target app subscribes. This reuses the existing event bus in both directions — there is no separate request/response protocol. - **`bread.command.<app>.<verb>`** — outbound commands *to* a sibling application (e.g. `bread.command.clip.clear`). Any module or app may publish; only the target app subscribes. This reuses the existing event bus in both directions — there is no separate request/response protocol. *Since: v1.7 — well-formed `bread.command.<known-app>.<verb>` names (`known-app``KNOWN_APPS`, verb a non-empty extra dotted segment) are allowed on the unsourced/`bread-emit` path and via sourced `AdapterSource::App` emit (an app may publish a command to another known app). `BreadClient::command` in bread-utils is the typed helper for the same path. `command` remains in `RESERVED_DOMAINS` so it cannot be claimed as an app id; `bread.command.power.off` and `bread.command.notanapp.x` are still rejected. See [Dictionary: IPC protocol](#dictionary-ipc-protocol).*
- The second dotted segment is drawn from a small known-apps registry (`bread_shared::apps::KNOWN_APPS`); daemon-internal domains (`terminal`, `git`, `hyprland`, `device`, `power`, `network`, `bluetooth`, `workspace`, `window`, `monitor`, `service`, `container`, `project`, `remote`, `system`, `profile`, `notify`, `command`, `workflow`) are reserved and cannot be claimed as app ids. *Since: v1.5 — `bluetooth`, `workspace`, `window`, and `monitor` added to this list (event families the Bluetooth and Hyprland adapters already published under, but that were missing from it); this same list is now also the boundary the IPC `emit` method's no-`source` path checks event names against, see [Dictionary: IPC protocol](#dictionary-ipc-protocol).* - The second dotted segment is drawn from a small known-apps registry (`bread_shared::apps::KNOWN_APPS` in `bread-shared/src/apps.rs`); daemon-internal domains (`terminal`, `git`, `hyprland`, `device`, `power`, `network`, `bluetooth`, `workspace`, `window`, `monitor`, `service`, `container`, `project`, `remote`, `system`, `profile`, `notify`, `command`, `workflow`) are reserved and cannot be claimed as app ids. *Since: v1.5 — `bluetooth`, `workspace`, `window`, and `monitor` added to this list (event families the Bluetooth and Hyprland adapters already published under, but that were missing from it); this same list is now also the boundary the IPC `emit` method's no-`source` path checks event names against, see [Dictionary: IPC protocol](#dictionary-ipc-protocol).*
- **Commands are best-effort.** Publishing `bread.command.<app>.<verb>` with no subscriber (the app isn't installed or isn't running) is a silent no-op — there is nothing to special-case, and no error is raised. An app that acts on a command *should* emit a corresponding `bread.<app>.<verb>.done` (or `.failed`) confirmation; a module that needs to know a command was actually honored must `bread.wait`/`bread.wait_any` on that confirmation with a timeout rather than assume success. There is no mandatory request/response correlation layer — most commands are legitimately fire-and-forget, and building one would contradict the "no listener, no-op" degradation property. - **Commands are best-effort.** Publishing `bread.command.<app>.<verb>` with no subscriber (the app isn't installed or isn't running) is a silent no-op — there is nothing to special-case, and no error is raised. An app that acts on a command *should* emit a corresponding `bread.<app>.<verb>.done` (or `.failed`) confirmation; a module that needs to know a command was actually honored must `bread.wait`/`bread.wait_any` on that confirmation with a timeout rather than assume success. There is no mandatory request/response correlation layer — most commands are legitimately fire-and-forget, and building one would contradict the "no listener, no-op" degradation property.
- **`bread.exec("<cli> ...")`** remains the zero-infrastructure fallback for triggering a sibling app that has a synchronous CLI and no need for a structured response. - **`bread.exec("<cli> ...")`** remains the zero-infrastructure fallback for triggering a sibling app that has a synchronous CLI and no need for a structured response.
@ -1599,10 +1599,11 @@ Two dotted-name segments are reserved, permanent parts of the schema — not one
This is the checklist for adding a new sibling `bread*` application to the fabric — it's deliberately short, because the whole design goal of the name-based app registry (over one `AdapterSource` enum variant per app) is that this never requires a daemon change beyond step 1. **breadclip is the reference implementation** — see its own `EVENTS.md` for a worked example of every step below. This is the checklist for adding a new sibling `bread*` application to the fabric — it's deliberately short, because the whole design goal of the name-based app registry (over one `AdapterSource` enum variant per app) is that this never requires a daemon change beyond step 1. **breadclip is the reference implementation** — see its own `EVENTS.md` for a worked example of every step below.
1. **Register your app id.** Add it to `KNOWN_APPS` in `bread-shared/src/lib.rs` (a one-line, one-word-per-app list) — this is the only change to the `bread` repo itself a new integration needs. 1. **Register your app id.** Add it to `KNOWN_APPS` in `bread-shared/src/apps.rs` (a one-line, one-word-per-app list) — this is the only change to the `bread` repo itself a new integration needs.
2. **Depend on `bread-utils` with the `bread-client` feature.** In your app's daemon (the long-running piece, if you have one — a short-lived CLI tool can use `bread-emit` instead, see below), add `bread-utils = { ..., features = ["bread-client"] }` and use `bread_utils::bread_client::BreadClient`: 2. **Depend on `bread-utils` with the `bread-client` feature.** In your app's daemon (the long-running piece, if you have one — a short-lived CLI tool can use `bread-emit` instead, see below), add `bread-utils = { ..., features = ["bread-client"] }` and use `bread_utils::bread_client::BreadClient`:
- `BreadClient::connect(app_id)` — cheap, cannot fail (there is no persistent connection to fail at construction time). - `BreadClient::connect(app_id)` — cheap, cannot fail (there is no persistent connection to fail at construction time).
- `client.emit(event, data)` — publish within your own `bread.<app_id>.*` namespace. Each call is its own short-lived connection (fire-and-forget, like `bread-emit`) — safe to call from a short-lived per-event process invocation, not just from inside a long-running loop. - `client.emit(event, data)` — publish within your own `bread.<app_id>.*` namespace. Each call is its own short-lived connection (fire-and-forget, like `bread-emit`) — safe to call from a short-lived per-event process invocation, not just from inside a long-running loop.
- `client.command(target, verb, data)` — publish `bread.command.<target>.<verb>` to another known app. Same fire-and-forget socket write as `emit`; this is the typed helper for the command-bus path that `bread-emit bread.command.<app>.<verb>` uses. *Since: v1.7 — the daemon actually accepts these on the unsourced and sourced-app emit paths; see [Namespaces](#namespaces).*
- `client.subscribe("bread.command.<app_id>.**", |event| { ... })` — receive commands addressed to you, on a background thread with its own reconnect/backoff loop. - `client.subscribe("bread.command.<app_id>.**", |event| { ... })` — receive commands addressed to you, on a background thread with its own reconnect/backoff loop.
3. **If you don't have a persistent daemon at all** (just a CLI tool invoked occasionally), skip `bread-client` entirely and shell out to `bread-emit` instead (see `bread-emit`'s own `--help`) — it's built for exactly that case (occasional callers that can't justify holding a socket open). 3. **If you don't have a persistent daemon at all** (just a CLI tool invoked occasionally), skip `bread-client` entirely and shell out to `bread-emit` instead (see `bread-emit`'s own `--help`) — it's built for exactly that case (occasional callers that can't justify holding a socket open).
4. **Emit confirmations for commands you honor.** `bread.<app_id>.<verb>.done` or `.failed` after acting on a `bread.command.<app_id>.<verb>` — optional, but it's what lets a Lua workflow `bread.wait`/`bread.wait_any` for the real outcome instead of assuming success the moment it publishes a command. 4. **Emit confirmations for commands you honor.** `bread.<app_id>.<verb>.done` or `.failed` after acting on a `bread.command.<app_id>.<verb>` — optional, but it's what lets a Lua workflow `bread.wait`/`bread.wait_any` for the real outcome instead of assuming success the moment it publishes a command.
@ -1726,7 +1727,7 @@ Available methods:
| `profile.activate` | `name` | Switch active profile | | `profile.activate` | `name` | Switch active profile |
| `events.subscribe` | — | Upgrade to streaming mode; pushes events line by line | | `events.subscribe` | — | Upgrade to streaming mode; pushes events line by line |
| `events.replay` | `since_ms` | Replay buffered events from the last N ms | | `events.replay` | `since_ms` | Replay buffered events from the last N ms |
| `emit` | `event`, `data`, optional `source`, `kind` | Inject an event. Without `source`, builds a `BreadEvent` directly, tagged `Manual` *(Since: v1.5 — previously tagged `System`; see below)*, for manually testing Lua handlers (this is what `bread emit <event>` and `bread-emit` use). With `source` set to `terminal`/`git`/`remote`, or a registered sibling-app id (see [Namespaces](#namespaces)), builds a real `RawEvent` (requires `kind` too) that goes through the normalizer like any adapter. Any other `source` value is rejected — this is the anti-spoofing boundary that stops a socket client from forging e.g. `power`/`hyprland` events. | | `emit` | `event`, `data`, optional `source`, `kind` | Inject an event. Without `source`, builds a `BreadEvent` directly, tagged `Manual` *(Since: v1.5 — previously tagged `System`; see below)*, for manually testing Lua handlers (this is what `bread emit <event>` and `bread-emit` use). Well-formed `bread.command.<known-app>.<verb>` is allowed on this path *(Since: v1.7)*; other reserved domains stay rejected. With `source` set to `terminal`/`git`/`remote`, or a registered sibling-app id (see [Namespaces](#namespaces)), builds a real `RawEvent` (requires `kind` too) that goes through the normalizer like any adapter. A sourced app may also publish a well-formed command to another known app. Any other `source` value is rejected — this is the anti-spoofing boundary that stops a socket client from forging e.g. `power`/`hyprland` events. |
| `workflows.list` | — | List running/completed workflow instances and their step/status *(Since: v1.2)* | | `workflows.list` | — | List running/completed workflow instances and their step/status *(Since: v1.2)* |
| `widgets.list` | — | List all registered widgets across every module *(Since: v1.3)* | | `widgets.list` | — | List all registered widgets across every module *(Since: v1.3)* |
@ -1763,4 +1764,5 @@ The `health` response's `api_version` field lets a client — the CLI, a Lua mod
*Since: v1.5 — `emit` without `source` closed a spoofing gap: previously any event name was accepted with zero validation and tagged `System`, the same tag the daemon uses internally for events it originates itself in Rust code (`bread.system.startup`, `bread.profile.activated`, ...). That made a manually-injected event indistinguishable from a trusted, daemon-originated one. Now:* *Since: v1.5 — `emit` without `source` closed a spoofing gap: previously any event name was accepted with zero validation and tagged `System`, the same tag the daemon uses internally for events it originates itself in Rust code (`bread.system.startup`, `bread.profile.activated`, ...). That made a manually-injected event indistinguishable from a trusted, daemon-originated one. Now:*
- *The unsourced path is tagged `AdapterSource::Manual`, not `System``System` is reserved for the daemon's own Rust-originated sends and can no longer be produced from data that arrived over the IPC socket.* - *The unsourced path is tagged `AdapterSource::Manual`, not `System``System` is reserved for the daemon's own Rust-originated sends and can no longer be produced from data that arrived over the IPC socket.*
- *The event name is rejected if its top-level dotted segment (the part right after `bread.`) is one of the reserved, adapter-owned domains in `bread_shared::apps::RESERVED_DOMAINS``terminal`, `git`, `hyprland`, `device`, `power`, `network`, `bluetooth`, `workspace`, `window`, `monitor`, `service`, `container`, `project`, `remote`, `system`, `profile`, `notify`, `command`, `workflow` (see [Namespaces](#namespaces)) — since a socket client emitting e.g. `bread.power.ac.connected` this way would otherwise be indistinguishable from the real power adapter observing it.* - *The event name is rejected if its top-level dotted segment (the part right after `bread.`) is one of the reserved, adapter-owned domains in `bread_shared::apps::RESERVED_DOMAINS``terminal`, `git`, `hyprland`, `device`, `power`, `network`, `bluetooth`, `workspace`, `window`, `monitor`, `service`, `container`, `project`, `remote`, `system`, `profile`, `notify`, `command`, `workflow` (see [Namespaces](#namespaces)) — since a socket client emitting e.g. `bread.power.ac.connected` this way would otherwise be indistinguishable from the real power adapter observing it.*
- *Since: v1.7 — well-formed `bread.command.<known-app>.<verb>` is an explicit exception to that reserved-domain reject (`command` stays reserved so it cannot be claimed as an app id). `bread.command.clip.clear` is accepted unsourced and as a sourced `AdapterSource::App` emit from another known app; `bread.command.power.off`, `bread.command.notanapp.x`, and `bread.hyprland.*` are still rejected. `API_VERSION` bumped from `1.6.0` to `1.7.0` for this addition.*
- *Freely-named custom/test event names (anything outside those reserved domains, including names with no `bread.` prefix at all) remain unrestricted — this is what keeps `bread emit <name>` useful for testing Lua handlers without unplugging cables, and what `bread-emit`'s fire-and-forget, no-reply-wait design still works against unchanged (a single JSON line write is still sufficient; no handshake was added).* - *Freely-named custom/test event names (anything outside those reserved domains, including names with no `bread.` prefix at all) remain unrestricted — this is what keeps `bread emit <name>` useful for testing Lua handlers without unplugging cables, and what `bread-emit`'s fire-and-forget, no-reply-wait design still works against unchanged (a single JSON line write is still sufficient; no handshake was added).*

View file

@ -42,10 +42,12 @@ return M
## Architecture ## Architecture
``` ```
breadd/ Rust daemon — event pipeline, state engine, IPC, adapter supervision breadd/ Rust daemon — event pipeline, state engine, IPC, adapter supervision
bread-cli/ CLI frontend — talks to breadd over a Unix socket bread-cli/ CLI frontend — talks to breadd over a Unix socket
bread-shared/ Shared types — RawEvent, BreadEvent, AdapterSource bread-emit/ Tiny fire-and-forget IPC emitter (hooks / command bus)
packaging/ Arch PKGBUILD and systemd user service bread-module-host/ Out-of-process sandboxed Lua module runtime
bread-shared/ Shared types — RawEvent, BreadEvent, AdapterSource
packaging/ systemd user service unit (bakery installs this)
``` ```
The daemon is structured in four layers: The daemon is structured in four layers:
@ -80,7 +82,7 @@ git clone https://git.breadway.dev/Breadway/bread.git
cd bread cd bread
``` ```
Run the install script — it builds, symlinks `breadd` and `bread` into `~/.local/bin` (override with `BIN_DIR=…`), installs the systemd user service, and starts the daemon: Run the install script — it builds, symlinks `breadd`, `bread`, `bread-emit`, and `bread-module-host` into `~/.local/bin` (override with `BIN_DIR=…`), installs the systemd user service, and starts the daemon:
```bash ```bash
bash scripts/install.sh bash scripts/install.sh
@ -92,13 +94,16 @@ Or step by step (system-wide install):
cargo build --release cargo build --release
sudo install -Dm755 target/release/breadd /usr/bin/breadd sudo install -Dm755 target/release/breadd /usr/bin/breadd
sudo install -Dm755 target/release/bread /usr/bin/bread sudo install -Dm755 target/release/bread /usr/bin/bread
sudo install -Dm755 target/release/bread-emit /usr/bin/bread-emit
sudo install -Dm755 target/release/bread-module-host /usr/bin/bread-module-host
``` ```
### Arch Linux (PKGBUILD) ### Via bakery
Prebuilt binaries ship through `bakery` (the bread-ecosystem package manager), not a PKGBUILD / pacman package:
```bash ```bash
cd packaging/arch bakery install bread
makepkg -si
``` ```
### systemd user service ### systemd user service

View file

@ -1,6 +1,6 @@
name = "bread" name = "bread"
description = "Reactive automation daemon and CLI for Linux desktops" description = "Reactive automation daemon and CLI for Linux desktops"
binaries = ["breadd", "bread"] binaries = ["breadd", "bread", "bread-emit", "bread-module-host"]
system_deps = ["systemd-libs", "openssl", "zlib"] system_deps = ["systemd-libs", "openssl", "zlib"]
optional_system_deps = ["bluez", "hyprland"] optional_system_deps = ["bluez", "hyprland"]
bread_deps = [] bread_deps = []

View file

@ -24,12 +24,15 @@ pub const KNOWN_APPS: &[&str] = &[
/// socket client may freely emit a custom/test event, but not one whose /// socket client may freely emit a custom/test event, but not one whose
/// top-level segment is one of these, since that would let it impersonate /// top-level segment is one of these, since that would let it impersonate
/// a real adapter (or another daemon-internal event family) rather than /// a real adapter (or another daemon-internal event family) rather than
/// producing an obviously-manual one. See [`is_reserved_domain`] and /// producing an obviously-manual one. The one exception is a well-formed
/// `breadd/src/ipc/mod.rs`'s `emit` handler. *Since: v1.5 — `bluetooth`, /// [`validate_command_event`] name (`bread.command.<known-app>.<verb>`):
/// `command` stays reserved so it cannot be claimed as an app id, but the
/// command bus itself is meant to be publishable. See [`is_reserved_domain`]
/// and `breadd/src/ipc/mod.rs`'s `emit` handler. *Since: v1.5 — `bluetooth`,
/// `workspace`, `window`, and `monitor` added (event families the Hyprland /// `workspace`, `window`, and `monitor` added (event families the Hyprland
/// and Bluetooth adapters already published under, but that were missing /// and Bluetooth adapters already published under, but that were missing
/// from this list) when this became a spoofing-prevention boundary and not /// from this list) when this became a spoofing-prevention boundary and not
/// just an app-id-conflict one.* /// just an app-id-conflict one. Since: v1.7 — command-bus exception.*
const RESERVED_DOMAINS: &[&str] = &[ const RESERVED_DOMAINS: &[&str] = &[
"terminal", "terminal",
"git", "git",
@ -66,11 +69,54 @@ pub fn is_reserved_domain(id: &str) -> bool {
/// Whether `event` is a well-formed event name for `app` — i.e. it starts /// Whether `event` is a well-formed event name for `app` — i.e. it starts
/// with `bread.<app>.`. An app may only publish within its own namespace /// with `bread.<app>.`. An app may only publish within its own namespace
/// segment; this is what the IPC boundary checks before constructing a /// segment; this is what the IPC boundary checks before constructing a
/// `RawEvent` tagged `AdapterSource::App(app)`. /// `RawEvent` tagged `AdapterSource::App(app)`. Command events addressed
/// to another known app are a separate, explicit exception — see
/// [`validate_command_event`].
pub fn validate_app_namespace(app: &str, event: &str) -> bool { pub fn validate_app_namespace(app: &str, event: &str) -> bool {
event.starts_with(&format!("bread.{app}.")) event.starts_with(&format!("bread.{app}."))
} }
/// Whether `event` is in the outbound command namespace
/// (`bread.command.*`). Does not check that the target is a known app —
/// use [`validate_command_event`] for that.
pub fn is_command_event(event: &str) -> bool {
event.starts_with("bread.command.")
}
/// The app id a command event is addressed to — the segment immediately
/// after `bread.command.`. Returns `None` if `event` is not a command
/// event or the app-id segment is empty.
pub fn command_target(event: &str) -> Option<&str> {
let rest = event.strip_prefix("bread.command.")?;
let app = rest.split('.').next()?;
if app.is_empty() {
None
} else {
Some(app)
}
}
/// Whether `event` is a well-formed command to a registered sibling app:
/// `bread.command.<known_app>.<verb>` with `known_app` in [`KNOWN_APPS`]
/// and a non-empty verb (at least one extra dotted segment).
///
/// This is the exception the IPC unsourced/`bread-emit` path (and sourced
/// `AdapterSource::App` emit) use so any module or app can publish
/// commands without `command` leaving [`is_reserved_domain`] — `command`
/// must stay unclaimable as an app id. `bread.command.power.off` and
/// `bread.command.notanapp.x` still fail because the target is not in
/// [`KNOWN_APPS`].
pub fn validate_command_event(event: &str) -> bool {
let rest = match event.strip_prefix("bread.command.") {
Some(rest) => rest,
None => return false,
};
let Some((app, verb)) = rest.split_once('.') else {
return false;
};
is_known_app(app) && !verb.is_empty()
}
/// The top-level dotted segment after `bread.` in an event name — e.g. /// The top-level dotted segment after `bread.` in an event name — e.g.
/// `Some("power")` for `"bread.power.ac.connected"`. Returns `None` for /// `Some("power")` for `"bread.power.ac.connected"`. Returns `None` for
/// event names that don't start with `bread.` at all, which are always /// event names that don't start with `bread.` at all, which are always
@ -119,8 +165,15 @@ mod tests {
// daemon itself publishes under must be reserved, or a manual/no-source // daemon itself publishes under must be reserved, or a manual/no-source
// `emit` over the IPC socket could impersonate it undetected. // `emit` over the IPC socket could impersonate it undetected.
for domain in [ for domain in [
"power", "network", "device", "bluetooth", "hyprland", "workspace", "monitor", "power",
"window", "system", "network",
"device",
"bluetooth",
"hyprland",
"workspace",
"monitor",
"window",
"system",
] { ] {
assert!( assert!(
is_reserved_domain(domain), is_reserved_domain(domain),
@ -164,4 +217,47 @@ mod tests {
// because it shares a string prefix. // because it shares a string prefix.
assert!(!validate_app_namespace("clip", "bread.clipx.copied")); assert!(!validate_app_namespace("clip", "bread.clipx.copied"));
} }
#[test]
fn is_command_event_requires_command_prefix() {
assert!(is_command_event("bread.command.clip.clear"));
assert!(is_command_event("bread.command.power.off"));
assert!(!is_command_event("bread.command"));
assert!(!is_command_event("bread.clip.copied"));
assert!(!is_command_event("command.clip.clear"));
}
#[test]
fn command_target_extracts_app_id() {
assert_eq!(command_target("bread.command.clip.clear"), Some("clip"));
assert_eq!(command_target("bread.command.cast.start.now"), Some("cast"));
assert_eq!(command_target("bread.command.clip"), Some("clip"));
assert_eq!(command_target("bread.command."), None);
assert_eq!(command_target("bread.clip.copied"), None);
}
#[test]
fn validate_command_event_accepts_known_app_with_verb() {
assert!(validate_command_event("bread.command.clip.clear"));
assert!(validate_command_event("bread.command.cast.start"));
assert!(validate_command_event("bread.command.clip.stack.clear"));
}
#[test]
fn validate_command_event_rejects_unknown_target_or_missing_verb() {
assert!(!validate_command_event("bread.command.power.off"));
assert!(!validate_command_event("bread.command.notanapp.x"));
assert!(!validate_command_event("bread.command.clip"));
assert!(!validate_command_event("bread.command.clip."));
assert!(!validate_command_event("bread.command."));
assert!(!validate_command_event("bread.hyprland.workspace.changed"));
assert!(!validate_command_event("bread.clip.copied"));
}
#[test]
fn command_stays_reserved_and_is_not_a_known_app() {
assert!(is_reserved_domain("command"));
assert!(!is_known_app("command"));
assert!(!validate_command_event("bread.command.command.x"));
}
} }

View file

@ -2,7 +2,7 @@ use std::os::unix::io::AsRawFd;
use anyhow::Result; use anyhow::Result;
use bread_shared::{now_unix_ms, AdapterSource, RawEvent}; use bread_shared::{now_unix_ms, AdapterSource, RawEvent};
use serde_json::json; use serde_json::{json, Value};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::debug; use tracing::debug;
@ -19,20 +19,13 @@ impl UdevAdapter {
} }
pub async fn enumerate_existing(&self, tx: &mpsc::Sender<RawEvent>) -> Result<()> { pub async fn enumerate_existing(&self, tx: &mpsc::Sender<RawEvent>) -> Result<()> {
let devices = enumerate_with_udev(&self.subsystems)?; let mut enumerator = udev::Enumerator::new()?;
for device in devices { for subsystem in &self.subsystems {
tx.send(RawEvent { enumerator.match_subsystem(subsystem)?;
source: AdapterSource::Udev, }
kind: "udev.enumerate".to_string(), for device in enumerator.scan_devices()? {
payload: json!({ tx.send(build_device_event(&device, "add", "udev.enumerate"))
"action": "add", .await?;
"id": device.id,
"name": device.name,
"subsystem": device.subsystem,
}),
timestamp: now_unix_ms(),
})
.await?;
} }
Ok(()) Ok(())
} }
@ -50,12 +43,6 @@ impl Adapter for UdevAdapter {
} }
} }
struct ScannedDevice {
id: String,
name: String,
subsystem: String,
}
// udev::MonitorSocket uses a non-blocking socket; calling iter().next() without // udev::MonitorSocket uses a non-blocking socket; calling iter().next() without
// first polling the fd returns None immediately and exits the loop — which is // first polling the fd returns None immediately and exits the loop — which is
// why the old code silently fell back to sysfs on every start. We use poll(2) // why the old code silently fell back to sysfs on every start. We use poll(2)
@ -110,81 +97,157 @@ fn build_event(event: &udev::Event) -> RawEvent {
.action() .action()
.map(|a| a.to_string_lossy().to_string()) .map(|a| a.to_string_lossy().to_string())
.unwrap_or_else(|| "change".to_string()); .unwrap_or_else(|| "change".to_string());
let subsystem = event build_device_event(event, &action, "udev.change")
}
/// Shared live/enumerate payload. `udev::Event` deref's to `Device`, so
/// boot-time enumerate of an already-plugged device is equivalent to an
/// `add` of that same device (same identity + classification fields
/// `resolve_device` needs).
fn build_device_event(device: &udev::Device, action: &str, kind: &str) -> RawEvent {
let subsystem = device
.subsystem() .subsystem()
.map(|s| s.to_string_lossy().to_string()) .map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "unknown".to_string()); .unwrap_or_else(|| "unknown".to_string());
let name = event let name = device
.property_value("ID_MODEL") .property_value("ID_MODEL")
.or_else(|| event.property_value("NAME")) .or_else(|| device.property_value("NAME"))
.map(|v| v.to_string_lossy().to_string()) .map(|v| v.to_string_lossy().to_string())
.or_else(|| event.devnode().map(|n| n.display().to_string())) .or_else(|| device.devnode().map(|n| n.display().to_string()))
.unwrap_or_else(|| "unknown".to_string()); .unwrap_or_else(|| "unknown".to_string());
let id = event.syspath().to_string_lossy().to_string(); let id = device.syspath().to_string_lossy().to_string();
RawEvent { RawEvent {
source: AdapterSource::Udev, source: AdapterSource::Udev,
kind: "udev.change".to_string(), kind: kind.to_string(),
payload: json!({ payload: udev_event_payload(
"action": action, action,
"id": id, &id,
"name": name, &name,
"subsystem": subsystem, &subsystem,
"id_input_keyboard": prop_bool(event, "ID_INPUT_KEYBOARD"), UdevClassification::from_device(device),
"id_input_mouse": prop_bool(event, "ID_INPUT_MOUSE"), ),
"id_input_joystick": prop_bool(event, "ID_INPUT_JOYSTICK"),
"id_input_touchpad": prop_bool(event, "ID_INPUT_TOUCHPAD"),
"id_input_tablet": prop_bool(event, "ID_INPUT_TABLET"),
"id_usb_class": prop_str(event, "ID_USB_CLASS"),
"id_usb_interfaces": prop_str(event, "ID_USB_INTERFACES"),
"id_vendor": prop_str(event, "ID_VENDOR"),
"id_model": prop_str(event, "ID_MODEL"),
"vendor_id": prop_str(event, "ID_VENDOR_ID"),
"product_id": prop_str(event, "ID_MODEL_ID"),
}),
timestamp: now_unix_ms(), timestamp: now_unix_ms(),
} }
} }
fn enumerate_with_udev(subsystems: &[String]) -> Result<Vec<ScannedDevice>> { /// Classification / identity fields copied onto every udev payload so
let mut enumerator = udev::Enumerator::new()?; /// `resolve_device` can name a device after boot the same way it names a
for subsystem in subsystems { /// live plug-in.
enumerator.match_subsystem(subsystem)?; struct UdevClassification {
} id_input_keyboard: bool,
id_input_mouse: bool,
let mut out = Vec::new(); id_input_joystick: bool,
for dev in enumerator.scan_devices()? { id_input_touchpad: bool,
let subsystem = dev id_input_tablet: bool,
.subsystem() id_usb_class: Option<String>,
.map(|s| s.to_string_lossy().to_string()) id_usb_interfaces: Option<String>,
.unwrap_or_else(|| "unknown".to_string()); id_vendor: Option<String>,
let name = dev id_model: Option<String>,
.property_value("ID_MODEL") vendor_id: Option<String>,
.or_else(|| dev.property_value("NAME")) product_id: Option<String>,
.map(|v| v.to_string_lossy().to_string())
.or_else(|| dev.sysname().to_str().map(ToString::to_string))
.unwrap_or_else(|| "unknown".to_string());
let id = dev.syspath().to_string_lossy().to_string();
out.push(ScannedDevice {
id,
name,
subsystem,
});
}
Ok(out)
} }
fn prop_bool(event: &udev::Event, key: &str) -> bool { impl UdevClassification {
event fn from_device(device: &udev::Device) -> Self {
Self {
id_input_keyboard: prop_bool(device, "ID_INPUT_KEYBOARD"),
id_input_mouse: prop_bool(device, "ID_INPUT_MOUSE"),
id_input_joystick: prop_bool(device, "ID_INPUT_JOYSTICK"),
id_input_touchpad: prop_bool(device, "ID_INPUT_TOUCHPAD"),
id_input_tablet: prop_bool(device, "ID_INPUT_TABLET"),
id_usb_class: prop_str(device, "ID_USB_CLASS"),
id_usb_interfaces: prop_str(device, "ID_USB_INTERFACES"),
id_vendor: prop_str(device, "ID_VENDOR"),
id_model: prop_str(device, "ID_MODEL"),
vendor_id: prop_str(device, "ID_VENDOR_ID"),
product_id: prop_str(device, "ID_MODEL_ID"),
}
}
}
fn udev_event_payload(
action: &str,
id: &str,
name: &str,
subsystem: &str,
class: UdevClassification,
) -> Value {
json!({
"action": action,
"id": id,
"name": name,
"subsystem": subsystem,
"id_input_keyboard": class.id_input_keyboard,
"id_input_mouse": class.id_input_mouse,
"id_input_joystick": class.id_input_joystick,
"id_input_touchpad": class.id_input_touchpad,
"id_input_tablet": class.id_input_tablet,
"id_usb_class": class.id_usb_class,
"id_usb_interfaces": class.id_usb_interfaces,
"id_vendor": class.id_vendor,
"id_model": class.id_model,
"vendor_id": class.vendor_id,
"product_id": class.product_id,
})
}
fn prop_bool(device: &udev::Device, key: &str) -> bool {
device
.property_value(key) .property_value(key)
.and_then(|v| v.to_str()) .and_then(|v| v.to_str())
.map(|v| v == "1") .map(|v| v == "1")
.unwrap_or(false) .unwrap_or(false)
} }
fn prop_str(event: &udev::Event, key: &str) -> Option<String> { fn prop_str(device: &udev::Device, key: &str) -> Option<String> {
event device
.property_value(key) .property_value(key)
.map(|v| v.to_string_lossy().to_string()) .map(|v| v.to_string_lossy().to_string())
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn enumerate_payload_includes_classification_fields() {
// Boot-time enumerate used to send only {action,id,name,subsystem},
// so resolve_device could never match vendor/product/input rules
// and bread.state.devices stayed "unknown" until the next unplug.
let payload = udev_event_payload(
"add",
"/sys/devices/pci0000:00/usb1/1-3",
"Keychron K2",
"usb",
UdevClassification {
id_input_keyboard: true,
id_input_mouse: false,
id_input_joystick: false,
id_input_touchpad: false,
id_input_tablet: false,
id_usb_class: None,
id_usb_interfaces: None,
id_vendor: Some("Keychron".into()),
id_model: Some("Keychron K2".into()),
vendor_id: Some("3434".into()),
product_id: Some("d030".into()),
},
);
assert_eq!(payload["action"], "add");
assert_eq!(payload["id"], "/sys/devices/pci0000:00/usb1/1-3");
assert_eq!(payload["name"], "Keychron K2");
assert_eq!(payload["subsystem"], "usb");
assert_eq!(payload["vendor_id"], "3434");
assert_eq!(payload["product_id"], "d030");
assert_eq!(payload["id_vendor"], "Keychron");
assert_eq!(payload["id_model"], "Keychron K2");
assert_eq!(payload["id_input_keyboard"], true);
assert_eq!(payload["id_input_mouse"], false);
assert_eq!(payload["id_input_joystick"], false);
assert_eq!(payload["id_input_touchpad"], false);
assert_eq!(payload["id_input_tablet"], false);
assert!(payload["id_usb_class"].is_null());
assert!(payload["id_usb_interfaces"].is_null());
}
}

View file

@ -1,7 +1,10 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::RwLock; use std::sync::RwLock;
use bread_shared::{apps::validate_app_namespace, AdapterSource, BreadEvent, RawEvent}; use bread_shared::{
apps::{validate_app_namespace, validate_command_event},
AdapterSource, BreadEvent, RawEvent,
};
use serde_json::{json, Value}; use serde_json::{json, Value};
/// How many multiples of `dedup_window_ms` an entry must be idle before eviction. /// How many multiples of `dedup_window_ms` an entry must be idle before eviction.
@ -208,9 +211,11 @@ impl EventNormalizer {
"workspace" | "workspacev2" => { "workspace" | "workspacev2" => {
self.emit_hyprland_dual("bread.workspace.changed", raw.payload.clone(), raw) self.emit_hyprland_dual("bread.workspace.changed", raw.payload.clone(), raw)
} }
"createworkspace" => { "createworkspace" => self.emit_hyprland_dual(
self.emit_hyprland_dual("bread.workspace.created", json!({ "workspace": data }), raw) "bread.workspace.created",
} json!({ "workspace": data }),
raw,
),
"destroyworkspace" => self.emit_hyprland_dual( "destroyworkspace" => self.emit_hyprland_dual(
"bread.workspace.destroyed", "bread.workspace.destroyed",
json!({ "workspace": data }), json!({ "workspace": data }),
@ -589,7 +594,10 @@ impl EventNormalizer {
let AdapterSource::App(app) = &raw.source else { let AdapterSource::App(app) = &raw.source else {
return vec![]; return vec![];
}; };
if !validate_app_namespace(app, &raw.kind) { // Own-namespace events plus well-formed commands to another known
// app (the command bus). Anything else — including spoofed adapter
// namespaces — is dropped here even if it somehow crossed IPC.
if !validate_app_namespace(app, &raw.kind) && !validate_command_event(&raw.kind) {
return vec![]; return vec![];
} }
vec![BreadEvent { vec![BreadEvent {
@ -997,7 +1005,11 @@ mod tests {
1, 1,
); );
let out = n.normalize(&ev); let out = n.normalize(&ev);
assert_eq!(out.len(), 2, "kind {kind} should dual-emit exactly 2 events"); assert_eq!(
out.len(),
2,
"kind {kind} should dual-emit exactly 2 events"
);
assert!( assert!(
out.iter().any(|e| &e.event == legacy_event), out.iter().any(|e| &e.event == legacy_event),
"kind {kind} missing legacy event {legacy_event}" "kind {kind} missing legacy event {legacy_event}"
@ -1006,7 +1018,12 @@ mod tests {
out.iter().any(|e| &e.event == namespaced_event), out.iter().any(|e| &e.event == namespaced_event),
"kind {kind} missing namespaced event {namespaced_event}" "kind {kind} missing namespaced event {namespaced_event}"
); );
let legacy_data = out.iter().find(|e| &e.event == legacy_event).unwrap().data.clone(); let legacy_data = out
.iter()
.find(|e| &e.event == legacy_event)
.unwrap()
.data
.clone();
let namespaced_data = out let namespaced_data = out
.iter() .iter()
.find(|e| &e.event == namespaced_event) .find(|e| &e.event == namespaced_event)
@ -1452,6 +1469,65 @@ mod tests {
} }
} }
// ─── App / command bus ─────────────────────────────────────────────────
#[test]
fn app_own_namespace_passes_through() {
let n = EventNormalizer::new(0);
let out = n.normalize(&raw(
AdapterSource::App("clip".into()),
"bread.clip.copied",
json!({"len": 4}),
1,
));
assert_eq!(out.len(), 1);
assert_eq!(out[0].event, "bread.clip.copied");
}
#[test]
fn app_command_to_another_known_app_passes_through() {
let n = EventNormalizer::new(0);
let out = n.normalize(&raw(
AdapterSource::App("cast".into()),
"bread.command.clip.clear",
json!({}),
1,
));
assert_eq!(out.len(), 1);
assert_eq!(out[0].event, "bread.command.clip.clear");
}
#[test]
fn app_wrong_namespace_is_dropped() {
let n = EventNormalizer::new(0);
let out = n.normalize(&raw(
AdapterSource::App("cast".into()),
"bread.clip.copied",
json!({}),
1,
));
assert!(out.is_empty());
}
#[test]
fn app_command_to_unknown_or_reserved_target_is_dropped() {
let n = EventNormalizer::new(0);
let power = n.normalize(&raw(
AdapterSource::App("cast".into()),
"bread.command.power.off",
json!({}),
1,
));
assert!(power.is_empty());
let spoof = n.normalize(&raw(
AdapterSource::App("cast".into()),
"bread.hyprland.workspace.changed",
json!({}),
1,
));
assert!(spoof.is_empty());
}
// ─── Helper ──────────────────────────────────────────────────────────── // ─── Helper ────────────────────────────────────────────────────────────
#[test] #[test]

View file

@ -8,7 +8,9 @@ use std::sync::Arc;
use std::time::Instant; use std::time::Instant;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use bread_shared::apps::{event_domain, is_known_app, is_reserved_domain, validate_app_namespace}; use bread_shared::apps::{
event_domain, is_known_app, is_reserved_domain, validate_app_namespace, validate_command_event,
};
use bread_shared::{now_unix_ms, AdapterSource, BreadEvent, RawEvent}; use bread_shared::{now_unix_ms, AdapterSource, BreadEvent, RawEvent};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{json, Value}; use serde_json::{json, Value};
@ -34,7 +36,12 @@ mod module_host_bridge;
/// *Since 1.6.0* — Workstream G's `module_host.*` methods (hello handshake /// *Since 1.6.0* — Workstream G's `module_host.*` methods (hello handshake
/// plus the RPC bridge a `bread-module-host` child uses in place of direct /// plus the RPC bridge a `bread-module-host` child uses in place of direct
/// in-process `bread.*` bindings). /// in-process `bread.*` bindings).
const API_VERSION: &str = "1.6.0"; /// *Since 1.7.0* — well-formed `bread.command.<known-app>.<verb>` is an
/// explicit exception to the reserved-domain reject on unsourced emit, and
/// sourced `AdapterSource::App` emit may publish commands to another known
/// app. `command` stays in `RESERVED_DOMAINS` so it cannot be claimed as
/// an app id.
const API_VERSION: &str = "1.7.0";
#[derive(Clone)] #[derive(Clone)]
pub struct Server { pub struct Server {
@ -325,13 +332,17 @@ impl Server {
}; };
// For a sibling-app source, `kind` is the full dotted event // For a sibling-app source, `kind` is the full dotted event
// name (e.g. "bread.clip.copied"), not a bare suffix — it // name (e.g. "bread.clip.copied"), not a bare suffix — it
// must live inside that app's own namespace. // must live inside that app's own namespace. Well-formed
// `bread.command.<known-app>.<verb>` is the one exception:
// an app may publish a command addressed to another known
// app (see `validate_command_event`). Adapter namespaces
// (`bread.power.*`, `bread.hyprland.*`, ...) stay rejected.
if let AdapterSource::App(app) = &source { if let AdapterSource::App(app) = &source {
if !validate_app_namespace(app, kind) { if !validate_app_namespace(app, kind) && !validate_command_event(kind) {
return Err(( return Err((
id, id,
format!( format!(
"event '{kind}' is not in the '{app}' namespace (must start with 'bread.{app}.')" "event '{kind}' is not in the '{app}' namespace (must start with 'bread.{app}.') and is not a well-formed command event"
), ),
)); ));
} }
@ -363,7 +374,11 @@ impl Server {
// `bread.hyprland.*`, ...) — otherwise this path would // `bread.hyprland.*`, ...) — otherwise this path would
// let any same-UID process impersonate a real adapter // let any same-UID process impersonate a real adapter
// event with nothing downstream able to tell the // event with nothing downstream able to tell the
// difference. // difference. Well-formed `bread.command.<known-app>.<verb>`
// is the documented exception: the command bus is
// supposed to be publishable by any module or
// `bread-emit` caller. Other reserved domains, and
// `bread.command.<not-an-app>.*`, stay rejected.
let Some(event) = req.params.get("event").and_then(Value::as_str) else { let Some(event) = req.params.get("event").and_then(Value::as_str) else {
return Err((id, "missing event name".to_string())); return Err((id, "missing event name".to_string()));
}; };
@ -429,9 +444,9 @@ impl Server {
/// impersonate a real adapter-owned event namespace. /// impersonate a real adapter-owned event namespace.
fn manual_emit(&self, event: &str, data: Value) -> std::result::Result<Value, String> { fn manual_emit(&self, event: &str, data: Value) -> std::result::Result<Value, String> {
if let Some(domain) = event_domain(event) { if let Some(domain) = event_domain(event) {
if is_reserved_domain(domain) { if is_reserved_domain(domain) && !validate_command_event(event) {
return Err(format!( return Err(format!(
"event '{event}' claims the reserved '{domain}' domain — manual emit cannot impersonate an adapter-owned event; use a custom event name, or a sourced emit if this should go through the normalizer" "event '{event}' claims the reserved '{domain}' domain — manual emit cannot impersonate an adapter-owned event; use a custom event name, a well-formed bread.command.<app>.<verb>, or a sourced emit if this should go through the normalizer"
)); ));
} }
} }

View file

@ -381,6 +381,126 @@ async fn emit_with_app_source_rejects_wrong_namespace() -> Result<()> {
Ok(()) Ok(())
} }
#[tokio::test]
async fn emit_without_source_allows_well_formed_command_event() -> Result<()> {
let harness = TestHarness::spawn()?;
harness.wait_until_ready().await?;
// Unsourced `bread-emit bread.command.clip.clear` is the documented
// command-bus path — `command` is reserved so it cannot be an app id,
// but a well-formed command to a known app must still go through.
let result = harness
.send_request(
"emit",
json!({ "event": "bread.command.clip.clear", "data": {} }),
)
.await;
assert!(
result.is_ok(),
"unsourced well-formed command event must be accepted: {result:?}"
);
assert_eq!(
result.unwrap().get("emitted").and_then(Value::as_bool),
Some(true)
);
harness.shutdown();
Ok(())
}
#[tokio::test]
async fn emit_without_source_rejects_command_to_non_app() -> Result<()> {
let harness = TestHarness::spawn()?;
harness.wait_until_ready().await?;
// `power` is reserved and is not a known app — this must not sneak
// through the command-bus exception.
let result = harness
.send_request(
"emit",
json!({ "event": "bread.command.power.off", "data": {} }),
)
.await;
assert!(
result.is_err(),
"command to a reserved/non-app target must be rejected"
);
harness.shutdown();
Ok(())
}
#[tokio::test]
async fn emit_without_source_still_rejects_hyprland_namespace() -> Result<()> {
let harness = TestHarness::spawn()?;
harness.wait_until_ready().await?;
let result = harness
.send_request(
"emit",
json!({ "event": "bread.hyprland.workspace.changed", "data": {} }),
)
.await;
assert!(
result.is_err(),
"unsourced emit must still reject adapter-owned hyprland events"
);
harness.shutdown();
Ok(())
}
#[tokio::test]
async fn emit_with_app_source_allows_command_to_another_app() -> Result<()> {
let harness = TestHarness::spawn()?;
harness.wait_until_ready().await?;
// An app may publish a command addressed to a different known app.
let result = harness
.send_request(
"emit",
json!({
"source": "cast",
"kind": "bread.command.clip.clear",
"data": {}
}),
)
.await;
assert!(
result.is_ok(),
"sourced command to another known app must be accepted: {result:?}"
);
harness.shutdown();
Ok(())
}
#[tokio::test]
async fn emit_with_app_source_still_rejects_foreign_app_namespace() -> Result<()> {
let harness = TestHarness::spawn()?;
harness.wait_until_ready().await?;
// `cast` must not be able to publish `bread.clip.*` events — only
// commands to clip, not clip's own inbound namespace.
let result = harness
.send_request(
"emit",
json!({
"source": "cast",
"kind": "bread.clip.copied",
"data": {}
}),
)
.await;
assert!(
result.is_err(),
"sourced emit must still reject a foreign app namespace"
);
harness.shutdown();
Ok(())
}
#[tokio::test] #[tokio::test]
async fn state_get_returns_specific_subtree() -> Result<()> { async fn state_get_returns_specific_subtree() -> Result<()> {
let harness = TestHarness::spawn()?; let harness = TestHarness::spawn()?;
@ -496,7 +616,10 @@ return M
.await?; .await?;
let entry = modules let entry = modules
.as_array() .as_array()
.and_then(|arr| arr.iter().find(|m| m.get("name").and_then(Value::as_str) == Some("scoped-test"))) .and_then(|arr| {
arr.iter()
.find(|m| m.get("name").and_then(Value::as_str) == Some("scoped-test"))
})
.cloned() .cloned()
.ok_or_else(|| anyhow!("scoped-test module not found in modules state; dump: {modules}"))?; .ok_or_else(|| anyhow!("scoped-test module not found in modules state; dump: {modules}"))?;
@ -511,7 +634,9 @@ return M
"a module with a manifest that declares permissions must not be flagged ungated" "a module with a manifest that declares permissions must not be flagged ungated"
); );
let store = harness.trigger_and_await_result("test.scoped_result").await?; let store = harness
.trigger_and_await_result("test.scoped_result")
.await?;
assert_eq!(store.get("state_get_ok"), Some(&json!(true))); assert_eq!(store.get("state_get_ok"), Some(&json!(true)));
assert_eq!( assert_eq!(
store.get("fs_present"), store.get("fs_present"),
@ -525,8 +650,16 @@ return M
); );
assert_eq!(store.get("exec_capture_present"), Some(&json!(false))); assert_eq!(store.get("exec_capture_present"), Some(&json!(false)));
assert_eq!(store.get("bluetooth_present"), Some(&json!(false))); assert_eq!(store.get("bluetooth_present"), Some(&json!(false)));
assert_eq!(store.get("json_present"), Some(&json!(true)), "baseline bread.json must still be present"); assert_eq!(
assert_eq!(store.get("log_present"), Some(&json!(true)), "baseline bread.log must still be present"); store.get("json_present"),
Some(&json!(true)),
"baseline bread.json must still be present"
);
assert_eq!(
store.get("log_present"),
Some(&json!(true)),
"baseline bread.log must still be present"
);
harness.shutdown(); harness.shutdown();
Ok(()) Ok(())
@ -568,7 +701,10 @@ return M
.await?; .await?;
let entry = modules let entry = modules
.as_array() .as_array()
.and_then(|arr| arr.iter().find(|m| m.get("name").and_then(Value::as_str) == Some("legacy-test"))) .and_then(|arr| {
arr.iter()
.find(|m| m.get("name").and_then(Value::as_str) == Some("legacy-test"))
})
.cloned() .cloned()
.ok_or_else(|| anyhow!("legacy-test module not found in modules state; dump: {modules}"))?; .ok_or_else(|| anyhow!("legacy-test module not found in modules state; dump: {modules}"))?;
@ -647,7 +783,9 @@ return M
.find(|m| m.get("name").and_then(Value::as_str) == Some("empty-perms-test")) .find(|m| m.get("name").and_then(Value::as_str) == Some("empty-perms-test"))
}) })
.cloned() .cloned()
.ok_or_else(|| anyhow!("empty-perms-test module not found in modules state; dump: {modules}"))?; .ok_or_else(|| {
anyhow!("empty-perms-test module not found in modules state; dump: {modules}")
})?;
assert_eq!(entry.get("status").and_then(Value::as_str), Some("loaded")); assert_eq!(entry.get("status").and_then(Value::as_str), Some("loaded"));
assert_eq!( assert_eq!(
@ -656,7 +794,9 @@ return M
"an explicit empty permissions list is a deliberate declaration, not 'undeclared'" "an explicit empty permissions list is a deliberate declaration, not 'undeclared'"
); );
let store = harness.trigger_and_await_result("test.empty_perms_result").await?; let store = harness
.trigger_and_await_result("test.empty_perms_result")
.await?;
assert_eq!(store.get("fs_present"), Some(&json!(false))); assert_eq!(store.get("fs_present"), Some(&json!(false)));
assert_eq!(store.get("state_present"), Some(&json!(false))); assert_eq!(store.get("state_present"), Some(&json!(false)));
@ -969,7 +1109,10 @@ async fn event_causality_chain_threads_caused_by_across_handlers() -> Result<()>
// Lua handler — its `caused_by` must be None. Everything downstream // Lua handler — its `caused_by` must be None. Everything downstream
// (X, Y, Z) is emitted by `bread.emit()` from inside a running handler. // (X, Y, Z) is emitted by `bread.emit()` from inside a running handler.
harness harness
.send_request("emit", json!({ "event": "bread.chain.trigger", "data": {} })) .send_request(
"emit",
json!({ "event": "bread.chain.trigger", "data": {} }),
)
.await?; .await?;
let mut events: HashMap<String, Value> = HashMap::new(); let mut events: HashMap<String, Value> = HashMap::new();
@ -1038,10 +1181,14 @@ async fn event_causality_chain_threads_caused_by_across_handlers() -> Result<()>
"Z should be caused_by Y's id" "Z should be caused_by Y's id"
); );
let ids: std::collections::HashSet<&str> = let ids: std::collections::HashSet<&str> = [
[trigger_id.as_str(), x_id.as_str(), y_id.as_str(), z_id.as_str()] trigger_id.as_str(),
.into_iter() x_id.as_str(),
.collect(); y_id.as_str(),
z_id.as_str(),
]
.into_iter()
.collect();
assert_eq!( assert_eq!(
ids.len(), ids.len(),
4, 4,
@ -1169,7 +1316,10 @@ async fn rules_toml_absent_is_a_no_op() -> Result<()> {
rules_mod.get("status").and_then(Value::as_str), rules_mod.get("status").and_then(Value::as_str),
Some("loaded") Some("loaded")
); );
assert!(rules_mod.get("last_error").and_then(Value::as_str).is_none()); assert!(rules_mod
.get("last_error")
.and_then(Value::as_str)
.is_none());
harness.shutdown(); harness.shutdown();
Ok(()) Ok(())
@ -1610,7 +1760,9 @@ enabled = false
// breadd itself would. // breadd itself would.
let deadline = Instant::now() + Duration::from_secs(55); let deadline = Instant::now() + Duration::from_secs(55);
while Instant::now() < deadline { while Instant::now() < deadline {
let modules = self.send_request("state.get", json!({"key": "modules"})).await?; let modules = self
.send_request("state.get", json!({"key": "modules"}))
.await?;
if let Some(arr) = modules.as_array() { if let Some(arr) = modules.as_array() {
for m in arr { for m in arr {
if m.get("name").and_then(Value::as_str) == Some(name) { if m.get("name").and_then(Value::as_str) == Some(name) {
@ -1626,7 +1778,9 @@ enabled = false
} }
tokio::time::sleep(Duration::from_millis(100)).await; tokio::time::sleep(Duration::from_millis(100)).await;
} }
Err(anyhow!("module '{name}' did not reach Loaded within timeout")) Err(anyhow!(
"module '{name}' did not reach Loaded within timeout"
))
} }
/// Subscribe to `result_event`, send a `test.trigger` manual emit to /// Subscribe to `result_event`, send a `test.trigger` manual emit to

View file

@ -1 +1,90 @@
{"0": "Lua Runtime Core", "1": "Adapter Framework", "2": "Adapter Configuration", "3": "App Classification", "4": "Widget Spec & Placement", "5": "Adapter Implementations", "6": "Filesystem Adapter", "7": "Git State Tracking", "8": "Integration Tests", "9": "State Engine", "10": "Module Management", "11": "Systemd Adapter", "12": "Git Hooks", "13": "CLI Core", "14": "Type Definitions", "15": "Podman Adapter", "16": "Event Routing", "17": "Udev Adapter", "18": "Shell Hooks", "19": "Bluetooth Adapter", "20": "Subscription System", "21": "Glob Pattern Matching", "22": "Event Dispatch", "23": "Network RTNetlink", "24": "Network Adapter", "25": "Power Management", "26": "Hyprland Integration", "27": "UPower Integration", "28": "CI/CD Workflows", "29": "Git Widget", "30": "Emit CLI Tool", "31": "Shared Library Init", "32": "Active Window Widget", "33": "CPU Temp Widget", "34": "Focus Mode Widget", "35": "Workflow Status Widget", "36": "Widget API Documentation", "37": "Bluetooth Widget", "38": "Media Pause Widget", "39": "Module Patterns", "40": "Monitor Dock", "41": "Workflow Dock", "42": "Battery Warning", "43": "Install Script", "44": "Filesystem Adapter Spec", "45": "Git Adapter Spec", "46": "Podman Adapter Spec", "47": "Systemd Adapter Spec", "48": "Lua Timer API (after)", "49": "Lua Bluetooth API", "50": "Lua Timer API (every)", "51": "Lua Exec API", "52": "Lua Hyprland API", "53": "Lua Notify API", "54": "Lua State Watch API", "55": "Emit Binary Build", "56": "Adapters Module", "57": "System Startup Event", "58": "Monitor Layout Config", "59": "Binds Module", "60": "Bakery Package Manager", "61": "GUI Control Center Roadmap", "62": "Cross-Device Mesh Roadmap", "63": "Version Computation"} {
"0": "lua/mod.rs",
"1": "RawEvent",
"2": "config.rs",
"3": "Server",
"4": "widget.rs",
"5": "Bread Daemon (breadd)",
"6": "filesystem.rs",
"7": "git.rs",
"8": "Result",
"9": "state_engine.rs",
"10": "modules_mgmt.rs",
"11": "systemd.rs",
"12": "hooks_git.rs",
"13": "bread-cli/src/main.rs",
"14": "types.rs",
"15": "podman.rs",
"16": "StateHandle",
"17": "run_udev_monitor",
"18": "hooks_shell.rs",
"19": "bluetooth.rs",
"20": "SubscriptionId",
"21": "glob.rs",
"22": "run_state_engine",
"23": "RtnetlinkAdapter",
"24": "network.rs",
"25": "power.rs",
"26": "hyprland.rs",
"27": "Adapter",
"28": "dl.breadway.dev Distribution",
"29": "git-branch-widget.lua",
"30": "TestHarness",
"31": "Sync",
"32": "active-window-widget.lua",
"33": "cpu-temp-widget.lua",
"34": "focus-mode-widget.lua",
"35": "workflow-status-widget.lua",
"36": "bread.widget API",
"37": "bluetooth-toggle-widget.lua",
"38": "pause-media-on-headphone-unplug.lua",
"39": "Autostart Example Module",
"40": "dock-monitors.lua",
"41": "dock-workflow.lua",
"42": "low-battery-warning.lua",
"43": "install.sh",
"44": "Filesystem Adapter",
"45": "Git Adapter",
"46": "Podman Adapter",
"47": "Systemd Adapter",
"48": "bread.after(delay_ms, fn)",
"49": "bread.bluetooth namespace",
"50": "bread.every(interval_ms, fn)",
"51": "bread.exec(cmd)",
"52": "bread.hyprland namespace",
"53": "bread.notify(message, opts)",
"54": "bread.state.watch(path, fn)",
"55": "bread-cli/src/lib.rs",
"56": "core/mod.rs",
"57": "bread.system.startup",
"58": "Monitors Configuration Example",
"59": "Binds Module (Built-in)",
"60": "Bakery Package Manager",
"61": "Phase 3: GUI Control Center",
"62": "Phase 5: Cross-Device Mesh",
"63": "Dev Version Computation",
"64": "ModuleHostLua",
"65": "ModuleHostRegistry",
"66": "xtask/src/main.rs",
"67": "Bread",
"68": "rules.rs",
"69": "Normalized events",
"70": "Bread Documentation",
"71": "udev.rs",
"72": "Dictionary: Lua API",
"73": "Out-of-process module sandboxing *(Since: v1.6)*",
"74": "Dictionary: Built-in modules",
"75": "Events",
"76": "Machine and filesystem",
"77": "Contributing",
"78": "Bluetooth",
"79": "Widgets *(Since: v1.3)*",
"80": "Getting started",
"81": "Capability-scoped modules *(Since: v1.5)*",
"82": "Workflows *(Since: v1.2)*",
"83": "Timers",
"84": "State",
"85": "init.lua",
"86": "Execution",
"87": "packaging/README.md"
}

View file

@ -1 +1 @@
/home/breadway/Projects/bread .

View file

@ -1,86 +1,115 @@
# Graph Report - . (2026-08-04) # Graph Report - bread (2026-08-15)
## Corpus Check ## Corpus Check
- 55 files · ~54,650 words - 62 files · ~88,250 words
- Verdict: corpus is large enough that graph structure adds value. - Verdict: corpus is large enough that graph structure adds value.
## Summary ## Summary
- 978 nodes · 2283 edges · 64 communities (41 shown, 23 thin omitted) - 1467 nodes · 3347 edges · 88 communities (64 shown, 24 thin omitted)
- Extraction: 99% EXTRACTED · 1% INFERRED · 0% AMBIGUOUS · INFERRED: 33 edges (avg confidence: 0.77) - Extraction: 99% EXTRACTED · 1% INFERRED · 0% AMBIGUOUS · INFERRED: 48 edges (avg confidence: 0.78)
- Token cost: 72,759 input · 0 output - Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `a6973360`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
## Community Hubs (Navigation) ## Community Hubs (Navigation)
- Lua Runtime Core - lua/mod.rs
- Adapter Framework - RawEvent
- Adapter Configuration - config.rs
- App Classification - Server
- Widget Spec & Placement - widget.rs
- Adapter Implementations - Bread Daemon (breadd)
- filesystem.rs
- git.rs
- Result
- state_engine.rs
- modules_mgmt.rs
- systemd.rs
- hooks_git.rs
- bread-cli/src/main.rs
- types.rs
- podman.rs
- StateHandle
- run_udev_monitor
- hooks_shell.rs
- bluetooth.rs
- SubscriptionId
- glob.rs
- run_state_engine
- RtnetlinkAdapter
- network.rs
- power.rs
- hyprland.rs
- Adapter
- dl.breadway.dev Distribution
- git-branch-widget.lua
- TestHarness
- Sync
- active-window-widget.lua
- cpu-temp-widget.lua
- focus-mode-widget.lua
- workflow-status-widget.lua
- bread.widget API
- bluetooth-toggle-widget.lua
- pause-media-on-headphone-unplug.lua
- Autostart Example Module
- install.sh
- Filesystem Adapter - Filesystem Adapter
- Git State Tracking - Git Adapter
- Integration Tests
- State Engine
- Module Management
- Systemd Adapter
- Git Hooks
- CLI Core
- Type Definitions
- Podman Adapter - Podman Adapter
- Event Routing - Systemd Adapter
- Udev Adapter - bread.after(delay_ms, fn)
- Shell Hooks - bread.bluetooth namespace
- Bluetooth Adapter - bread.every(interval_ms, fn)
- Subscription System - bread.exec(cmd)
- Glob Pattern Matching - bread.hyprland namespace
- Event Dispatch - bread.notify(message, opts)
- Network RTNetlink - bread.state.watch(path, fn)
- Network Adapter - bread.system.startup
- Power Management - Monitors Configuration Example
- Hyprland Integration - Binds Module (Built-in)
- UPower Integration
- CI/CD Workflows
- Git Widget
- Emit CLI Tool
- Shared Library Init
- Active Window Widget
- CPU Temp Widget
- Focus Mode Widget
- Workflow Status Widget
- Widget API Documentation
- Bluetooth Widget
- Media Pause Widget
- Module Patterns
- Install Script
- Filesystem Adapter Spec
- Git Adapter Spec
- Podman Adapter Spec
- Systemd Adapter Spec
- Lua Timer API (after)
- Lua Bluetooth API
- Lua Timer API (every)
- Lua Exec API
- Lua Hyprland API
- Lua Notify API
- Lua State Watch API
- System Startup Event
- Monitor Layout Config
- Binds Module
- Bakery Package Manager - Bakery Package Manager
- GUI Control Center Roadmap - Phase 3: GUI Control Center
- Cross-Device Mesh Roadmap - Phase 5: Cross-Device Mesh
- Version Computation - Dev Version Computation
- ModuleHostLua
- ModuleHostRegistry
- xtask/src/main.rs
- Bread
- rules.rs
- Normalized events
- Bread Documentation
- udev.rs
- Dictionary: Lua API
- Out-of-process module sandboxing *(Since: v1.6)*
- Dictionary: Built-in modules
- Events
- Machine and filesystem
- Contributing
- Bluetooth
- Widgets *(Since: v1.3)*
- Getting started
- Capability-scoped modules *(Since: v1.5)*
- Workflows *(Since: v1.2)*
- Timers
- State
- init.lua
- Execution
- packaging/README.md
## God Nodes (most connected - your core abstractions) ## God Nodes (most connected - your core abstractions)
1. `LuaEngine` - 52 edges 1. `LuaEngine` - 59 edges
2. `RawEvent` - 47 edges 2. `RawEvent` - 49 edges
3. `RuntimeState` - 34 edges 3. `BreadEvent` - 38 edges
4. `BreadEvent` - 30 edges 4. `raw()` - 37 edges
5. `raw()` - 30 edges 5. `RuntimeState` - 34 edges
6. `StateHandle` - 27 edges 6. `StateHandle` - 28 edges
7. `now_unix_ms()` - 25 edges 7. `now_unix_ms()` - 26 edges
8. `Adapter` - 25 edges 8. `Adapter` - 25 edges
9. `SubscriptionId` - 25 edges 9. `SubscriptionId` - 25 edges
10. `Server` - 22 edges 10. `ModuleHostLua` - 24 edges
## Surprising Connections (you probably didn't know these) ## Surprising Connections (you probably didn't know these)
- `parse_bluetooth_message()` --calls--> `now_unix_ms()` [INFERRED] - `parse_bluetooth_message()` --calls--> `now_unix_ms()` [INFERRED]
@ -96,6 +125,7 @@
## Import Cycles ## Import Cycles
- 2-file cycle: `breadd/src/core/state_engine.rs -> breadd/src/lua/mod.rs -> breadd/src/core/state_engine.rs` - 2-file cycle: `breadd/src/core/state_engine.rs -> breadd/src/lua/mod.rs -> breadd/src/core/state_engine.rs`
- 2-file cycle: `bread-shared/src/lib.rs -> bread-shared/src/module_host_ipc.rs -> bread-shared/src/lib.rs`
## Hyperedges (group relationships) ## Hyperedges (group relationships)
- **** — ci_dev_release, workflow_version_compute, release_track_dev, distribution_dl_breadway_dev, package_bakery [INFERRED] - **** — ci_dev_release, workflow_version_compute, release_track_dev, distribution_dl_breadway_dev, package_bakery [INFERRED]
@ -105,171 +135,263 @@
- **** — api_bread_widget, api_bread_every, example_widget_cpu_temp, api_bread_state_watch [INFERRED] - **** — api_bread_widget, api_bread_every, example_widget_cpu_temp, api_bread_state_watch [INFERRED]
- **** — branch_main, ci_dev_release, ci_rc_release, ci_stable_release, release_track_dev, release_track_beta, release_track_stable [INFERRED] - **** — branch_main, ci_dev_release, ci_rc_release, ci_stable_release, release_track_dev, release_track_beta, release_track_stable [INFERRED]
## Communities (64 total, 23 thin omitted) ## Communities (88 total, 24 thin omitted)
### Community 0 - "Lua Runtime Core" ### Community 0 - "lua/mod.rs"
Cohesion: 0.06 Cohesion: 0.06
Nodes (80): now_unix_ms(), WidgetPlacement, WidgetSpec, RuntimeState, bluetooth_connect(), bluetooth_disconnect(), bluetooth_find_adapter(), bluetooth_get_powered() (+72 more) Nodes (86): now_unix_ms(), ModulePermission, Option, String, WidgetSpec, RuntimeState, bluetooth_connect(), bluetooth_disconnect() (+78 more)
### Community 1 - "Adapter Framework" ### Community 1 - "RawEvent"
Cohesion: 0.05
Nodes (67): adapter_source_is_hashable_and_eq(), AdapterSource, bread_event_new_accepts_owned_and_borrowed_names(), bread_event_new_assigns_unique_id_and_no_cause(), bread_event_new_sets_current_timestamp(), bread_event_with_timestamp_preserves_timestamp_and_assigns_id(), BreadEvent, DaemonSection (+59 more)
### Community 2 - "config.rs"
Cohesion: 0.07 Cohesion: 0.07
Nodes (52): adapter_source_is_hashable_and_eq(), AdapterSource, bread_event_new_accepts_owned_and_borrowed_names(), bread_event_new_sets_current_timestamp(), BreadEvent, DaemonSection, expand_path(), now_unix_ms_is_monotonically_non_decreasing_across_calls() (+44 more) Nodes (50): AdaptersConfig, AdapterToggle, compat_section_defaults_legacy_hyprland_names_to_true(), CompatConfig, Config, config_path(), config_path_falls_back_to_home_when_no_xdg(), config_path_respects_xdg_config_home() (+42 more)
### Community 2 - "Adapter Configuration" ### Community 3 - "Server"
Cohesion: 0.05
Nodes (41): A, command_target(), event_domain(), is_known_app(), is_reserved_domain(), Option, validate_app_namespace(), validate_command_event() (+33 more)
### Community 4 - "widget.rs"
Cohesion: 0.07 Cohesion: 0.07
Nodes (45): AdaptersConfig, AdapterToggle, Config, config_path(), config_path_falls_back_to_home_when_no_xdg(), config_path_respects_xdg_config_home(), DaemonConfig, default_config_uses_documented_defaults() (+37 more) Nodes (32): accepts_node_count_at_max(), accepts_tree_at_max_depth(), Align, Background, box_of(), default_orientation(), FontWeight, is_valid_class() (+24 more)
### Community 3 - "App Classification" ### Community 5 - "Bread Daemon (breadd)"
Cohesion: 0.06
Nodes (36): A, is_known_app(), validate_app_namespace(), AdapterStatus, Manager, Arc, HashMap, Receiver (+28 more)
### Community 4 - "Widget Spec & Placement"
Cohesion: 0.07
Nodes (31): accepts_node_count_at_max(), accepts_tree_at_max_depth(), Align, Background, box_of(), default_orientation(), FontWeight, is_valid_class() (+23 more)
### Community 5 - "Adapter Implementations"
Cohesion: 0.06 Cohesion: 0.06
Nodes (39): Bluetooth Adapter, Hyprland Adapter, Network Adapter, Power Adapter, udev Adapter, bread.on(pattern, fn), bread.spawn(fn), bread.wait(pattern, opts) (+31 more) Nodes (39): Bluetooth Adapter, Hyprland Adapter, Network Adapter, Power Adapter, udev Adapter, bread.on(pattern, fn), bread.spawn(fn), bread.wait(pattern, opts) (+31 more)
### Community 6 - "Filesystem Adapter" ### Community 6 - "filesystem.rs"
Cohesion: 0.12 Cohesion: 0.12
Nodes (28): classify(), classify_build_artifact_created_in_target(), classify_debounces_rapid_repeat_events_for_same_path(), classify_file_changed_for_ordinary_source_file(), classify_silent_for_modify_in_target_not_create(), classify_silent_under_git(), classify_silent_under_node_modules(), detect_markers() (+20 more) Nodes (28): classify(), classify_build_artifact_created_in_target(), classify_debounces_rapid_repeat_events_for_same_path(), classify_file_changed_for_ordinary_source_file(), classify_silent_for_modify_in_target_not_create(), classify_silent_under_git(), classify_silent_under_node_modules(), detect_markers() (+20 more)
### Community 7 - "Git State Tracking" ### Community 7 - "git.rs"
Cohesion: 0.12 Cohesion: 0.12
Nodes (22): check_ahead_behind(), check_dirty(), discover_repos(), expand_roots(), expand_roots_globs_single_trailing_star(), expand_roots_skips_unreadable_glob_parent_without_panicking(), expand_roots_uses_literal_path_without_trailing_star(), GitAdapter (+14 more) Nodes (22): check_ahead_behind(), check_dirty(), discover_repos(), expand_roots(), expand_roots_globs_single_trailing_star(), expand_roots_skips_unreadable_glob_parent_without_panicking(), expand_roots_uses_literal_path_without_trailing_star(), GitAdapter (+14 more)
### Community 8 - "Integration Tests" ### Community 8 - "Result"
Cohesion: 0.24 Cohesion: 0.15
Nodes (30): daemon_survives_repeated_reloads_and_pipeline_resumes(), emit_with_app_source_rejects_wrong_namespace(), emit_with_internal_source_is_rejected(), emit_with_known_app_source_routes_through_normalizer(), emit_with_unregistered_app_source_is_rejected(), emit_without_event_errors(), event_stream_filter_excludes_non_matching_events(), events_replay_returns_buffered_events() (+22 more) Nodes (50): daemon_survives_repeated_reloads_and_pipeline_resumes(), emit_with_app_source_allows_command_to_another_app(), emit_with_app_source_rejects_wrong_namespace(), emit_with_app_source_still_rejects_foreign_app_namespace(), emit_with_internal_source_is_rejected(), emit_with_known_app_source_routes_through_normalizer(), emit_with_unregistered_app_source_is_rejected(), emit_without_event_errors() (+42 more)
### Community 9 - "State Engine" ### Community 9 - "state_engine.rs"
Cohesion: 0.12 Cohesion: 0.12
Nodes (19): apply_device_change(), apply_event_to_state(), device_connect_adds_device_with_all_fields(), device_connect_is_idempotent_for_same_id(), device_disconnect_of_unknown_id_is_noop(), device_disconnect_removes_matching_id(), ev(), monitor_connect_adds_new_monitor() (+11 more) Nodes (19): apply_device_change(), apply_event_to_state(), device_connect_adds_device_with_all_fields(), device_connect_is_idempotent_for_same_id(), device_disconnect_of_unknown_id_is_noop(), device_disconnect_removes_matching_id(), ev(), monitor_connect_adds_new_monitor() (+11 more)
### Community 10 - "Module Management" ### Community 10 - "modules_mgmt.rs"
Cohesion: 0.14 Cohesion: 0.11
Nodes (24): copy_dir(), install_from_local(), list_modules(), ModuleManifest, modules_dir(), parse_source(), read_manifest_file(), read_module_manifest() (+16 more) Nodes (36): audit_detects_fs_read_and_widget_from_cpu_temp_widget_style_module(), audit_extracts_exec_bin_hint_and_ignores_baseline_calls(), audit_module(), audit_scans_required_sibling_files_in_module_directory(), classify_call_site(), collect_lua_files(), copy_dir(), extract_first_string_arg() (+28 more)
### Community 11 - "Systemd Adapter" ### Community 11 - "systemd.rs"
Cohesion: 0.12 Cohesion: 0.12
Nodes (17): active_state_to_kind(), failure_result(), get_unit_path(), handle_message(), is_failed_transition(), query_active_state(), Connection, HashMap (+9 more) Nodes (17): active_state_to_kind(), failure_result(), get_unit_path(), handle_message(), is_failed_transition(), query_active_state(), Connection, HashMap (+9 more)
### Community 12 - "Git Hooks" ### Community 12 - "hooks_git.rs"
Cohesion: 0.15 Cohesion: 0.15
Nodes (24): all_hook_scripts_exit_0_unconditionally(), all_hook_scripts_start_with_shebang_and_marker(), branch_changed_emit_line(), commit_created_emit_line(), emit_line_for(), git_dir(), hook_script(), hook_script_rejects_unknown_name() (+16 more) Nodes (24): all_hook_scripts_exit_0_unconditionally(), all_hook_scripts_start_with_shebang_and_marker(), branch_changed_emit_line(), commit_created_emit_line(), emit_line_for(), git_dir(), hook_script(), hook_script_rejects_unknown_name() (+16 more)
### Community 13 - "CLI Core" ### Community 13 - "bread-cli/src/main.rs"
Cohesion: 0.22 Cohesion: 0.19
Nodes (27): Cli, Commands, config_directory(), daemon_socket_path(), format_timestamp(), handle_modules_cmd(), HooksCommand, install_module() (+19 more) Nodes (29): CausalityTracker, Cli, Commands, config_directory(), daemon_socket_path(), format_timestamp(), handle_modules_cmd(), HooksCommand (+21 more)
### Community 14 - "Type Definitions" ### Community 14 - "types.rs"
Cohesion: 0.15 Cohesion: 0.17
Nodes (23): Device, DeviceRule, DeviceTopology, InterfaceState, MatchCondition, ModuleStatus, Monitor, NetworkState (+15 more) Nodes (21): Device, DeviceRule, DeviceTopology, InterfaceState, MatchCondition, ModuleStatus, Monitor, NetworkState (+13 more)
### Community 15 - "Podman Adapter" ### Community 15 - "podman.rs"
Cohesion: 0.15 Cohesion: 0.15
Nodes (17): container_event(), ignores_remove_event(), ignores_stop_event_to_avoid_double_emit_with_died(), ignores_unknown_action(), map_podman_event(), maps_died_event(), maps_health_status_event(), maps_start_event() (+9 more) Nodes (17): container_event(), ignores_remove_event(), ignores_stop_event_to_avoid_double_emit_with_died(), ignores_unknown_action(), map_podman_event(), maps_died_event(), maps_health_status_event(), maps_start_event() (+9 more)
### Community 16 - "Event Routing" ### Community 16 - "StateHandle"
Cohesion: 0.13 Cohesion: 0.14
Nodes (10): condition_matches(), resolve_device(), Option, Result, String, Value, Vec, StateHandle (+2 more) Nodes (10): condition_matches(), resolve_device(), Option, Result, String, Value, Vec, StateHandle (+2 more)
### Community 17 - "Udev Adapter" ### Community 17 - "run_udev_monitor"
Cohesion: 0.23 Cohesion: 0.35
Nodes (14): build_event(), enumerate_with_udev(), prop_bool(), prop_str(), Option, Result, Self, Sender (+6 more) Nodes (9): enumerate_with_udev(), Result, Self, Sender, String, Vec, run_udev_monitor(), ScannedDevice (+1 more)
### Community 18 - "Shell Hooks" ### Community 18 - "hooks_shell.rs"
Cohesion: 0.18 Cohesion: 0.18
Nodes (11): hook_scripts_background_every_emit_call(), hooks_dir(), install_shell(), join_line_continuations(), Option, PathBuf, Result, String (+3 more) Nodes (11): hook_scripts_background_every_emit_call(), hooks_dir(), install_shell(), join_line_continuations(), Option, PathBuf, Result, String (+3 more)
### Community 19 - "Bluetooth Adapter" ### Community 19 - "bluetooth.rs"
Cohesion: 0.15 Cohesion: 0.15
Nodes (10): address_from_path(), BluetoothAdapter, parse_bluetooth_message(), Message, Option, Result, Self, Sender (+2 more) Nodes (10): address_from_path(), BluetoothAdapter, parse_bluetooth_message(), Message, Option, Result, Self, Sender (+2 more)
### Community 20 - "Subscription System" ### Community 20 - "SubscriptionId"
Cohesion: 0.29 Cohesion: 0.29
Nodes (13): HashMap, String, Vec, Subscription, SubscriptionId, SubscriptionTable, table_add_assigns_provided_id_and_finds_match(), table_clear_removes_all() (+5 more) Nodes (13): HashMap, String, Vec, Subscription, SubscriptionId, SubscriptionTable, table_add_assigns_provided_id_and_finds_match(), table_clear_removes_all() (+5 more)
### Community 22 - "Event Dispatch" ### Community 22 - "run_state_engine"
Cohesion: 0.27 Cohesion: 0.27
Nodes (12): dispatch_event(), handle_command(), Arc, AtomicU64, Receiver, RwLock, Self, Sender (+4 more) Nodes (12): dispatch_event(), handle_command(), Arc, AtomicU64, Receiver, RwLock, Self, Sender (+4 more)
### Community 23 - "Network RTNetlink" ### Community 23 - "RtnetlinkAdapter"
Cohesion: 0.19 Cohesion: 0.22
Nodes (9): Adapter, ip_from_bytes(), Option, Result, Self, Sender, String, RtnetlinkAdapter (+1 more) Nodes (7): ip_from_bytes(), Option, Result, Self, Sender, String, RtnetlinkAdapter
### Community 24 - "Network Adapter" ### Community 24 - "network.rs"
Cohesion: 0.27 Cohesion: 0.27
Nodes (9): has_default_route(), network_raw_event(), NetworkAdapter, NetworkSnapshot, read_network_state(), BTreeMap, Result, Sender (+1 more) Nodes (9): has_default_route(), network_raw_event(), NetworkAdapter, NetworkSnapshot, read_network_state(), BTreeMap, Result, Sender (+1 more)
### Community 25 - "Power Management" ### Community 25 - "power.rs"
Cohesion: 0.26 Cohesion: 0.26
Nodes (8): power_raw_event(), PowerAdapter, PowerSnapshot, read_power_state(), Option, Result, Self, Sender Nodes (8): power_raw_event(), PowerAdapter, PowerSnapshot, read_power_state(), Option, Result, Self, Sender
### Community 26 - "Hyprland Integration" ### Community 26 - "hyprland.rs"
Cohesion: 0.29 Cohesion: 0.29
Nodes (7): hyprland_event_socket(), HyprlandAdapter, parse_hyprland_line(), PathBuf, Result, Sender, String Nodes (7): hyprland_event_socket(), HyprlandAdapter, parse_hyprland_line(), PathBuf, Result, Sender, String
### Community 27 - "UPower Integration" ### Community 27 - "Adapter"
Cohesion: 0.27 Cohesion: 0.23
Nodes (6): parse_upower_message(), Message, Result, Self, Sender, UPowerAdapter Nodes (8): Adapter, parse_upower_message(), Message, Result, Self, Sender, UPowerAdapter, Send
### Community 28 - "CI/CD Workflows" ### Community 28 - "dl.breadway.dev Distribution"
Cohesion: 0.25 Cohesion: 0.25
Nodes (8): main Branch, dev-release.yml Workflow, rc-release.yml Workflow, release.yml Workflow, dl.breadway.dev Distribution, beta Release Track, dev Release Track, stable Release Track Nodes (8): main Branch, dev-release.yml Workflow, rc-release.yml Workflow, release.yml Workflow, dl.breadway.dev Distribution, beta Release Track, dev Release Track, stable Release Track
### Community 29 - "Git Widget" ### Community 29 - "git-branch-widget.lua"
Cohesion: 0.62 Cohesion: 0.62
Nodes (6): focused_tab_cwd(), git_info(), M.on_load(), shell_quote(), update(), widget_root() Nodes (6): focused_tab_cwd(), git_info(), M.on_load(), shell_quote(), update(), widget_root()
### Community 30 - "Emit CLI Tool" ### Community 30 - "TestHarness"
Cohesion: 0.40 Cohesion: 0.16
Nodes (5): main(), parse_args(), Option, String, UnixStream Nodes (16): main(), parse_args(), Option, String, killing_a_module_host_child_does_not_take_down_breadd_or_other_modules(), os_execute_and_io_open_are_denied_at_the_kernel_level_outside_granted_scope(), Child, Drop (+8 more)
### Community 31 - "Shared Library Init" ### Community 31 - "Sync"
Cohesion: 0.50 Cohesion: 0.50
Nodes (3): main(), Result, Sync Nodes (3): main(), Result, Sync
### Community 32 - "Active Window Widget" ### Community 32 - "active-window-widget.lua"
Cohesion: 0.83 Cohesion: 0.83
Nodes (3): label_for(), M.on_load(), widget_root() Nodes (3): label_for(), M.on_load(), widget_root()
### Community 33 - "CPU Temp Widget" ### Community 33 - "cpu-temp-widget.lua"
Cohesion: 0.83 Cohesion: 0.83
Nodes (3): M.on_load(), read_temp_c(), widget_root() Nodes (3): M.on_load(), read_temp_c(), widget_root()
### Community 34 - "Focus Mode Widget" ### Community 34 - "focus-mode-widget.lua"
Cohesion: 1.00 Cohesion: 1.00
Nodes (3): is_focused(), M.on_load(), widget_root() Nodes (3): is_focused(), M.on_load(), widget_root()
### Community 35 - "Workflow Status Widget" ### Community 35 - "workflow-status-widget.lua"
Cohesion: 0.83 Cohesion: 0.83
Nodes (3): M.on_load(), most_relevant(), widget_update() Nodes (3): M.on_load(), most_relevant(), widget_update()
### Community 36 - "Widget API Documentation" ### Community 36 - "bread.widget API"
Cohesion: 0.67 Cohesion: 0.67
Nodes (3): bread.widget API, CPU Temperature Widget Example, Live Widget Update Pattern Nodes (3): bread.widget API, CPU Temperature Widget Example, Live Widget Update Pattern
### Community 64 - "ModuleHostLua"
Cohesion: 0.10
Nodes (38): call(), HostMessage, IoCommand, RpcResponse, Duration, Option, PathBuf, Receiver (+30 more)
### Community 65 - "ModuleHostRegistry"
Cohesion: 0.07
Nodes (43): bin_allowed(), path_allowed(), HashMap, Option, OwnedWriteHalf, Result, Sender, String (+35 more)
### Community 66 - "xtask/src/main.rs"
Cohesion: 0.11
Nodes (36): BTreeSet, ExitCode, check(), CheckReport, clean_state_passes(), extract_cli_commands(), extract_enum_variants(), extract_ipc_methods() (+28 more)
### Community 67 - "Bread"
Cohesion: 0.06
Nodes (28): Deprecations, Hyprland legacy flat event names (since v1.5), Bread Examples, Example 1: Porting keyboard_and_display_watcher.sh (system script), Example 2: Porting autostart.lua, Example 3: Porting display/monitors.lua, Example 4: Multi-step automation workflows, Example 5: A live widget in breadbar (+20 more)
### Community 68 - "rules.rs"
Cohesion: 0.14
Nodes (27): empty_file_loads_with_no_rules(), empty_on_is_treated_as_missing(), invalid_toml_is_fatal(), load_rules(), missing_on_is_reported_with_index_and_no_on(), multiple_action_keys_is_reported(), one_bad_rule_does_not_block_other_valid_rules(), ParsedRule (+19 more)
### Community 69 - "Normalized events"
Cohesion: 0.12
Nodes (16): Bluetooth (BlueZ), Compatibility: `[compat]` config, Devices (udev / Bluetooth), Filesystem / project detection, Git (hooks + dirty-state poller), Hyprland, Network, Normalized events (+8 more)
### Community 70 - "Bread Documentation"
Cohesion: 0.14
Nodes (14): API Stability & Versioning, Bread Documentation, Contents, Debugging tips, Dictionary: Event reference, Dictionary: IPC protocol, Dictionary: Runtime state schema, Integrating a bread\* app (+6 more)
### Community 71 - "udev.rs"
Cohesion: 0.32
Nodes (10): build_device_event(), build_event(), enumerate_payload_includes_classification_fields(), prop_bool(), prop_str(), Option, Value, udev_event_payload() (+2 more)
### Community 72 - "Dictionary: Lua API"
Cohesion: 0.17
Nodes (12): `bread.debounce(delay_ms, fn) -> wrapped_fn`, `bread.log(msg)` / `bread.warn(msg)` / `bread.error(msg)`, `bread.notify(message, opts)`, `bread.profile.activate(name)`, Dictionary: Lua API, Hyprland, Module declaration, Module lifecycle hooks (+4 more)
### Community 73 - "Out-of-process module sandboxing *(Since: v1.6)*"
Cohesion: 0.20
Nodes (10): Architecture, Crash isolation, New IPC methods, Out-of-process module sandboxing *(Since: v1.6)*, RPC bridge coverage, The gap this closes, The Landlock sandbox, The token/identity handshake (+2 more)
### Community 74 - "Dictionary: Built-in modules"
Cohesion: 0.20
Nodes (10): `bread.binds`, `bread.devices`, `bread.monitors`, `bread.rules` *(Since: v1.5)*, `bread.workspaces`, Device rule options, Dictionary: Built-in modules, Example: Dock-specific setup (+2 more)
### Community 75 - "Events"
Cohesion: 0.20
Nodes (10): `bread.emit(event, data)`, `bread.filter(pattern, fn, opts) -> id`, `bread.off(id)`, `bread.on(pattern, fn) -> id`, `bread.once(pattern, fn) -> id`, `bread.spawn(fn)`, `bread.wait_all(patterns, opts) -> table` *(Since: v1.2)*, `bread.wait_any(patterns, opts) -> event | nil` *(Since: v1.2)* (+2 more)
### Community 76 - "Machine and filesystem"
Cohesion: 0.20
Nodes (10): `bread.fs.exists(path) -> bool`, `bread.fs.expand(path) -> string`, `bread.fs.read(path) -> string | nil`, `bread.fs.readlink(path) -> string | nil`, `bread.fs.write(path, content)`, `bread.json.decode(str) -> table | nil`, `bread.machine.has_tag(tag) -> bool`, `bread.machine.name() -> string` (+2 more)
### Community 77 - "Contributing"
Cohesion: 0.22
Nodes (8): Branches, CI, Contributing, Keeping the API docs honest, Local development, Questions, The release cycle, Tracks, from a user's perspective
### Community 78 - "Bluetooth"
Cohesion: 0.22
Nodes (9): Bluetooth, `bread.bluetooth.connect(address)`, `bread.bluetooth.devices() -> table | nil`, `bread.bluetooth.disconnect(address)`, `bread.bluetooth.power(enabled)`, `bread.bluetooth.powered() -> bool | nil`, `bread.bluetooth.scan(enabled)`, Example: auto-connect headphones on AC power (+1 more)
### Community 79 - "Widgets *(Since: v1.3)*"
Cohesion: 0.22
Nodes (9): `bread.widget.list() -> table`, `bread.widget.register(spec) -> ok, err`, `bread.widget.remove(id) -> bool`, `bread.widget.update(id, patch) -> ok, err`, Click events, Node types, `style` *(Since: v1.4)*, Style vs. `class` (+1 more)
### Community 80 - "Getting started"
Cohesion: 0.33
Nodes (6): 1) Create a minimal config, 2) The fast path: `rules.toml` *(Since: v1.5)*, 3) Minimal `init.lua`, 4) Start the daemon, 5) Check that it's running, Getting started
### Community 81 - "Capability-scoped modules *(Since: v1.5)*"
Cohesion: 0.33
Nodes (6): Baseline (always available, no manifest entry needed), `bread modules audit <name>`, Capability-scoped modules *(Since: v1.5)*, Gated — requires a matching `[[permissions]]` entry, `path`/`bin` enforcement depends on where the module runs, `require("bread.devices")` still works from a scoped module
### Community 82 - "Workflows *(Since: v1.2)*"
Cohesion: 0.33
Nodes (6): `bread.workflow.define(name, fn)`, `bread.workflow.list() -> table`, `bread.workflow.start(name, opts)`, `bread.workflow.status(name) -> table | nil`, `bread.workflow.step(label)`, Workflows *(Since: v1.2)*
### Community 83 - "Timers"
Cohesion: 0.50
Nodes (4): `bread.after(delay_ms, fn) -> id`, `bread.cancel(id)`, `bread.every(interval_ms, fn) -> id`, Timers
### Community 84 - "State"
Cohesion: 0.50
Nodes (4): `bread.state.get(path)`, `bread.state.watch(path, fn) -> id`, State, Typed shorthands
### Community 85 - "init.lua"
Cohesion: 0.83
Nodes (3): M.on_load(), read_temp_c(), widget_root()
### Community 86 - "Execution"
Cohesion: 0.67
Nodes (3): `bread.exec_capture(cmd, opts) -> ok, stdout`, `bread.exec(cmd)`, Execution
## Knowledge Gaps ## Knowledge Gaps
- **44 isolated node(s):** `install.sh script`, `Git Adapter`, `Filesystem Adapter`, `Systemd Adapter`, `Podman Adapter` (+39 more) - **179 isolated node(s):** `install.sh script`, `Branches`, `The release cycle`, `Tracks, from a user's perspective`, `Keeping the API docs honest` (+174 more)
These have ≤1 connection - possible missing edges or undocumented components. These have ≤1 connection - possible missing edges or undocumented components.
- **23 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes. - **24 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions ## Suggested Questions
_Questions this graph is uniquely positioned to answer:_ _Questions this graph is uniquely positioned to answer:_
- **Why does `Adapter` connect `Network RTNetlink` to `App Classification`, `Filesystem Adapter`, `Git State Tracking`, `Systemd Adapter`, `Podman Adapter`, `Udev Adapter`, `Bluetooth Adapter`, `Network Adapter`, `Power Management`, `Hyprland Integration`, `UPower Integration`, `Shared Library Init`?** - **Why does `RawEvent` connect `RawEvent` to `Server`, `filesystem.rs`, `git.rs`, `udev.rs`, `systemd.rs`, `podman.rs`, `run_udev_monitor`, `bluetooth.rs`, `RtnetlinkAdapter`, `network.rs`, `power.rs`, `hyprland.rs`, `Adapter`?**
_High betweenness centrality (0.137) - this node is a cross-community bridge._ _High betweenness centrality (0.094) - this node is a cross-community bridge._
- **Why does `RawEvent` connect `Adapter Framework` to `App Classification`, `Filesystem Adapter`, `Git State Tracking`, `Systemd Adapter`, `Podman Adapter`, `Udev Adapter`, `Bluetooth Adapter`, `Network RTNetlink`, `Network Adapter`, `Power Management`, `Hyprland Integration`, `UPower Integration`?** - **Why does `Adapter` connect `Adapter` to `Server`, `filesystem.rs`, `git.rs`, `udev.rs`, `systemd.rs`, `podman.rs`, `run_udev_monitor`, `bluetooth.rs`, `RtnetlinkAdapter`, `network.rs`, `power.rs`, `hyprland.rs`, `Sync`?**
_High betweenness centrality (0.122) - this node is a cross-community bridge._ _High betweenness centrality (0.077) - this node is a cross-community bridge._
- **Why does `Config` connect `Adapter Configuration` to `Lua Runtime Core`, `App Classification`?** - **Why does `BreadEvent` connect `RawEvent` to `ModuleHostLua`, `lua/mod.rs`, `ModuleHostRegistry`, `Server`, `state_engine.rs`, `run_state_engine`?**
_High betweenness centrality (0.076) - this node is a cross-community bridge._ _High betweenness centrality (0.063) - this node is a cross-community bridge._
- **What connects `install.sh script`, `Git Adapter`, `Filesystem Adapter` to the rest of the system?** - **What connects `install.sh script`, `Branches`, `The release cycle` to the rest of the system?**
_44 weakly-connected nodes found - possible documentation gaps or missing edges._ _179 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `Lua Runtime Core` be split into smaller, more focused modules?** - **Should `lua/mod.rs` be split into smaller, more focused modules?**
_Cohesion score 0.061946902654867256 - nodes in this community are weakly interconnected._ _Cohesion score 0.056 - nodes in this community are weakly interconnected._
- **Should `Adapter Framework` be split into smaller, more focused modules?** - **Should `RawEvent` be split into smaller, more focused modules?**
_Cohesion score 0.0727036676403765 - nodes in this community are weakly interconnected._ _Cohesion score 0.051842708517016396 - nodes in this community are weakly interconnected._
- **Should `Adapter Configuration` be split into smaller, more focused modules?** - **Should `config.rs` be split into smaller, more focused modules?**
_Cohesion score 0.0741745816372682 - nodes in this community are weakly interconnected._ _Cohesion score 0.07191780821917808 - nodes in this community are weakly interconnected._

View file

@ -0,0 +1 @@
{"nodes": [{"id": "$graphify-root$_packaging_readme_md", "label": "README.md", "file_type": "document", "source_file": "packaging/README.md", "source_location": "L1"}, {"id": "$graphify-root$_packaging_readme_systemd_user_service", "label": "systemd user service", "file_type": "document", "source_file": "packaging/README.md", "source_location": "L17"}], "edges": [{"source": "$graphify-root$_packaging_readme_md", "target": "$graphify-root$_packaging_readme_systemd_user_service", "relation": "contains", "confidence": "EXTRACTED", "source_file": "packaging/README.md", "source_location": "L17", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"nodes": [{"id": "$graphify-root$_examples_modules_readme_md", "label": "README.md", "file_type": "document", "source_file": "examples/modules/README.md", "source_location": "L1"}, {"id": "$graphify-root$_examples_modules_readme_example_bread_modules", "label": "Example bread modules", "file_type": "document", "source_file": "examples/modules/README.md", "source_location": "L1"}, {"id": "$graphify-root$_examples_modules_readme_installing", "label": "Installing", "file_type": "document", "source_file": "examples/modules/README.md", "source_location": "L7"}, {"id": "$graphify-root$_examples_modules_readme_modules", "label": "Modules", "file_type": "document", "source_file": "examples/modules/README.md", "source_location": "L33"}], "edges": [{"source": "$graphify-root$_examples_modules_readme_md", "target": "$graphify-root$_examples_modules_readme_example_bread_modules", "relation": "contains", "confidence": "EXTRACTED", "source_file": "examples/modules/README.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_examples_modules_readme_md", "target": "$graphify-root$_examples_md", "relation": "references", "confidence": "EXTRACTED", "source_file": "examples/modules/README.md", "source_location": "L4", "weight": 1.0, "target_file": "$graphify-root$/Examples.md"}, {"source": "$graphify-root$_examples_modules_readme_example_bread_modules", "target": "$graphify-root$_examples_modules_readme_installing", "relation": "contains", "confidence": "EXTRACTED", "source_file": "examples/modules/README.md", "source_location": "L7", "weight": 1.0}, {"source": "$graphify-root$_examples_modules_readme_md", "target": "$graphify-root$_examples_modules_permissions_md", "relation": "references", "confidence": "EXTRACTED", "source_file": "examples/modules/README.md", "source_location": "L18", "weight": 1.0}, {"source": "$graphify-root$_examples_modules_readme_md", "target": "$graphify-root$_documentation_md", "relation": "references", "confidence": "EXTRACTED", "source_file": "examples/modules/README.md", "source_location": "L19", "weight": 1.0, "target_file": "$graphify-root$/Documentation.md"}, {"source": "$graphify-root$_examples_modules_readme_example_bread_modules", "target": "$graphify-root$_examples_modules_readme_modules", "relation": "contains", "confidence": "EXTRACTED", "source_file": "examples/modules/README.md", "source_location": "L33", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}

View file

@ -0,0 +1 @@
{"nodes": [{"id": "$graphify-root$_examples_md", "label": "Examples.md", "file_type": "document", "source_file": "Examples.md", "source_location": "L1"}, {"id": "$graphify-root$_examples_bread_examples", "label": "Bread Examples", "file_type": "document", "source_file": "Examples.md", "source_location": "L1"}, {"id": "$graphify-root$_examples_example_1_porting_keyboard_and_display_watcher_sh_system_script", "label": "Example 1: Porting keyboard_and_display_watcher.sh (system script)", "file_type": "document", "source_file": "Examples.md", "source_location": "L7"}, {"id": "$graphify-root$_examples_example_2_porting_autostart_lua", "label": "Example 2: Porting autostart.lua", "file_type": "document", "source_file": "Examples.md", "source_location": "L93"}, {"id": "$graphify-root$_examples_example_3_porting_display_monitors_lua", "label": "Example 3: Porting display/monitors.lua", "file_type": "document", "source_file": "Examples.md", "source_location": "L130"}, {"id": "$graphify-root$_examples_example_4_multi_step_automation_workflows", "label": "Example 4: Multi-step automation workflows", "file_type": "document", "source_file": "Examples.md", "source_location": "L182"}, {"id": "$graphify-root$_examples_example_5_a_live_widget_in_breadbar", "label": "Example 5: A live widget in breadbar", "file_type": "document", "source_file": "Examples.md", "source_location": "L241"}, {"id": "$graphify-root$_examples_tips_for_porting_your_own_scripts", "label": "Tips for porting your own scripts", "file_type": "document", "source_file": "Examples.md", "source_location": "L313"}], "edges": [{"source": "$graphify-root$_examples_md", "target": "$graphify-root$_examples_bread_examples", "relation": "contains", "confidence": "EXTRACTED", "source_file": "Examples.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_examples_bread_examples", "target": "$graphify-root$_examples_example_1_porting_keyboard_and_display_watcher_sh_system_script", "relation": "contains", "confidence": "EXTRACTED", "source_file": "Examples.md", "source_location": "L7", "weight": 1.0}, {"source": "$graphify-root$_examples_bread_examples", "target": "$graphify-root$_examples_example_2_porting_autostart_lua", "relation": "contains", "confidence": "EXTRACTED", "source_file": "Examples.md", "source_location": "L93", "weight": 1.0}, {"source": "$graphify-root$_examples_bread_examples", "target": "$graphify-root$_examples_example_3_porting_display_monitors_lua", "relation": "contains", "confidence": "EXTRACTED", "source_file": "Examples.md", "source_location": "L130", "weight": 1.0}, {"source": "$graphify-root$_examples_bread_examples", "target": "$graphify-root$_examples_example_4_multi_step_automation_workflows", "relation": "contains", "confidence": "EXTRACTED", "source_file": "Examples.md", "source_location": "L182", "weight": 1.0}, {"source": "$graphify-root$_examples_md", "target": "$graphify-root$_documentation_md", "relation": "references", "confidence": "EXTRACTED", "source_file": "Examples.md", "source_location": "L235", "weight": 1.0, "target_file": "$graphify-root$/Documentation.md"}, {"source": "$graphify-root$_examples_bread_examples", "target": "$graphify-root$_examples_example_5_a_live_widget_in_breadbar", "relation": "contains", "confidence": "EXTRACTED", "source_file": "Examples.md", "source_location": "L241", "weight": 1.0}, {"source": "$graphify-root$_examples_bread_examples", "target": "$graphify-root$_examples_tips_for_porting_your_own_scripts", "relation": "contains", "confidence": "EXTRACTED", "source_file": "Examples.md", "source_location": "L313", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"nodes": [{"id": "$graphify-root$_examples_modules_dock_workflow_lua", "label": "dock-workflow.lua", "file_type": "code", "source_file": "examples/modules/dock-workflow.lua", "source_location": "L1"}, {"id": "$graphify-root$_examples_modules_dock_workflow_m_on_load", "label": "M.on_load()", "file_type": "code", "source_file": "examples/modules/dock-workflow.lua", "source_location": "L44", "_callable": true}], "edges": [{"source": "$graphify-root$_examples_modules_dock_workflow_lua", "target": "$graphify-root$_examples_modules_dock_workflow_m_on_load", "relation": "contains", "confidence": "EXTRACTED", "source_file": "examples/modules/dock-workflow.lua", "source_location": "L44", "weight": 1.0}], "raw_calls": [{"caller_nid": "$graphify-root$_examples_modules_dock_workflow_m_on_load", "callee": "bread.on", "is_member_call": false, "source_file": "examples/modules/dock-workflow.lua", "source_location": "L45", "receiver": null}, {"caller_nid": "$graphify-root$_examples_modules_dock_workflow_m_on_load", "callee": "bread.workflow.start", "is_member_call": false, "source_file": "examples/modules/dock-workflow.lua", "source_location": "L49", "receiver": null}]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"nodes": [{"id": "$graphify-root$_breadd_src_core_mod_rs", "label": "mod.rs", "file_type": "code", "source_file": "breadd/src/core/mod.rs", "source_location": "L1"}], "edges": [], "raw_calls": []}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"nodes": [{"id": "$graphify-root$_deprecations_md", "label": "DEPRECATIONS.md", "file_type": "document", "source_file": "DEPRECATIONS.md", "source_location": "L1"}, {"id": "$graphify-root$_deprecations_deprecations", "label": "Deprecations", "file_type": "document", "source_file": "DEPRECATIONS.md", "source_location": "L1"}, {"id": "$graphify-root$_deprecations_hyprland_legacy_flat_event_names_since_v1_5", "label": "Hyprland legacy flat event names (since v1.5)", "file_type": "document", "source_file": "DEPRECATIONS.md", "source_location": "L8"}], "edges": [{"source": "$graphify-root$_deprecations_md", "target": "$graphify-root$_deprecations_deprecations", "relation": "contains", "confidence": "EXTRACTED", "source_file": "DEPRECATIONS.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_deprecations_md", "target": "$graphify-root$_documentation_md", "relation": "references", "confidence": "EXTRACTED", "source_file": "DEPRECATIONS.md", "source_location": "L4", "weight": 1.0, "target_file": "$graphify-root$/Documentation.md"}, {"source": "$graphify-root$_deprecations_deprecations", "target": "$graphify-root$_deprecations_hyprland_legacy_flat_event_names_since_v1_5", "relation": "contains", "confidence": "EXTRACTED", "source_file": "DEPRECATIONS.md", "source_location": "L8", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"nodes": [{"id": "$graphify-root$_scripts_install_sh", "label": "install.sh", "file_type": "code", "source_file": "scripts/install.sh", "source_location": "L1", "metadata": {"language": "bash", "kind": "file"}}, {"id": "$graphify-root$_scripts_install_sh__entry", "label": "install.sh script", "file_type": "code", "source_file": "scripts/install.sh", "source_location": "L1", "metadata": {"language": "bash", "kind": "bash_entrypoint"}}], "edges": [{"source": "$graphify-root$_scripts_install_sh", "target": "$graphify-root$_scripts_install_sh__entry", "relation": "contains", "confidence": "EXTRACTED", "source_file": "scripts/install.sh", "source_location": "L1", "weight": 1.0}], "raw_calls": [{"language": "bash", "callee": "set", "caller_nid": "$graphify-root$_scripts_install_sh__entry", "source_file": "scripts/install.sh", "source_location": "L2"}, {"language": "bash", "callee": "echo", "caller_nid": "$graphify-root$_scripts_install_sh__entry", "source_file": "scripts/install.sh", "source_location": "L11"}, {"language": "bash", "callee": "cargo", "caller_nid": "$graphify-root$_scripts_install_sh__entry", "source_file": "scripts/install.sh", "source_location": "L12"}, {"language": "bash", "callee": "mkdir", "caller_nid": "$graphify-root$_scripts_install_sh__entry", "source_file": "scripts/install.sh", "source_location": "L17"}, {"language": "bash", "callee": "ln", "caller_nid": "$graphify-root$_scripts_install_sh__entry", "source_file": "scripts/install.sh", "source_location": "L18"}, {"language": "bash", "callee": "cat", "caller_nid": "$graphify-root$_scripts_install_sh__entry", "source_file": "scripts/install.sh", "source_location": "L39"}, {"language": "bash", "callee": "sed", "caller_nid": "$graphify-root$_scripts_install_sh__entry", "source_file": "scripts/install.sh", "source_location": "L79"}, {"language": "bash", "callee": "systemctl", "caller_nid": "$graphify-root$_scripts_install_sh__entry", "source_file": "scripts/install.sh", "source_location": "L84"}, {"language": "bash", "callee": "break", "caller_nid": "$graphify-root$_scripts_install_sh__entry", "source_file": "scripts/install.sh", "source_location": "L103"}, {"language": "bash", "callee": "sleep", "caller_nid": "$graphify-root$_scripts_install_sh__entry", "source_file": "scripts/install.sh", "source_location": "L105"}], "bash_sources": []}

View file

@ -0,0 +1 @@
{"nodes": [{"id": "$graphify-root$_contributing_md", "label": "CONTRIBUTING.md", "file_type": "document", "source_file": "CONTRIBUTING.md", "source_location": "L1"}, {"id": "$graphify-root$_contributing_contributing", "label": "Contributing", "file_type": "document", "source_file": "CONTRIBUTING.md", "source_location": "L1"}, {"id": "$graphify-root$_contributing_branches", "label": "Branches", "file_type": "document", "source_file": "CONTRIBUTING.md", "source_location": "L8"}, {"id": "$graphify-root$_contributing_the_release_cycle", "label": "The release cycle", "file_type": "document", "source_file": "CONTRIBUTING.md", "source_location": "L26"}, {"id": "$graphify-root$_contributing_tracks_from_a_user_s_perspective", "label": "Tracks, from a user's perspective", "file_type": "document", "source_file": "CONTRIBUTING.md", "source_location": "L43"}, {"id": "$graphify-root$_contributing_local_development", "label": "Local development", "file_type": "document", "source_file": "CONTRIBUTING.md", "source_location": "L63"}, {"id": "$graphify-root$_contributing_keeping_the_api_docs_honest", "label": "Keeping the API docs honest", "file_type": "document", "source_file": "CONTRIBUTING.md", "source_location": "L70"}, {"id": "$graphify-root$_contributing_ci", "label": "CI", "file_type": "document", "source_file": "CONTRIBUTING.md", "source_location": "L94"}, {"id": "$graphify-root$_contributing_questions", "label": "Questions", "file_type": "document", "source_file": "CONTRIBUTING.md", "source_location": "L106"}], "edges": [{"source": "$graphify-root$_contributing_md", "target": "$graphify-root$_contributing_contributing", "relation": "contains", "confidence": "EXTRACTED", "source_file": "CONTRIBUTING.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_contributing_contributing", "target": "$graphify-root$_contributing_branches", "relation": "contains", "confidence": "EXTRACTED", "source_file": "CONTRIBUTING.md", "source_location": "L8", "weight": 1.0}, {"source": "$graphify-root$_contributing_contributing", "target": "$graphify-root$_contributing_the_release_cycle", "relation": "contains", "confidence": "EXTRACTED", "source_file": "CONTRIBUTING.md", "source_location": "L26", "weight": 1.0}, {"source": "$graphify-root$_contributing_contributing", "target": "$graphify-root$_contributing_tracks_from_a_user_s_perspective", "relation": "contains", "confidence": "EXTRACTED", "source_file": "CONTRIBUTING.md", "source_location": "L43", "weight": 1.0}, {"source": "$graphify-root$_contributing_contributing", "target": "$graphify-root$_contributing_local_development", "relation": "contains", "confidence": "EXTRACTED", "source_file": "CONTRIBUTING.md", "source_location": "L63", "weight": 1.0}, {"source": "$graphify-root$_contributing_local_development", "target": "$graphify-root$_contributing_keeping_the_api_docs_honest", "relation": "contains", "confidence": "EXTRACTED", "source_file": "CONTRIBUTING.md", "source_location": "L70", "weight": 1.0}, {"source": "$graphify-root$_contributing_contributing", "target": "$graphify-root$_contributing_ci", "relation": "contains", "confidence": "EXTRACTED", "source_file": "CONTRIBUTING.md", "source_location": "L94", "weight": 1.0}, {"source": "$graphify-root$_contributing_contributing", "target": "$graphify-root$_contributing_questions", "relation": "contains", "confidence": "EXTRACTED", "source_file": "CONTRIBUTING.md", "source_location": "L106", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"nodes": [{"id": "$graphify-root$_examples_modules_cpu_temp_widget_init_lua", "label": "init.lua", "file_type": "code", "source_file": "examples/modules/cpu-temp-widget/init.lua", "source_location": "L1"}, {"id": "$graphify-root$_examples_modules_cpu_temp_widget_init_read_temp_c", "label": "read_temp_c()", "file_type": "code", "source_file": "examples/modules/cpu-temp-widget/init.lua", "source_location": "L32", "_callable": true}, {"id": "$graphify-root$_examples_modules_cpu_temp_widget_init_widget_root", "label": "widget_root()", "file_type": "code", "source_file": "examples/modules/cpu-temp-widget/init.lua", "source_location": "L40", "_callable": true}, {"id": "$graphify-root$_examples_modules_cpu_temp_widget_init_m_on_load", "label": "M.on_load()", "file_type": "code", "source_file": "examples/modules/cpu-temp-widget/init.lua", "source_location": "L60", "_callable": true}], "edges": [{"source": "$graphify-root$_examples_modules_cpu_temp_widget_init_lua", "target": "$graphify-root$_examples_modules_cpu_temp_widget_init_read_temp_c", "relation": "contains", "confidence": "EXTRACTED", "source_file": "examples/modules/cpu-temp-widget/init.lua", "source_location": "L32", "weight": 1.0}, {"source": "$graphify-root$_examples_modules_cpu_temp_widget_init_lua", "target": "$graphify-root$_examples_modules_cpu_temp_widget_init_widget_root", "relation": "contains", "confidence": "EXTRACTED", "source_file": "examples/modules/cpu-temp-widget/init.lua", "source_location": "L40", "weight": 1.0}, {"source": "$graphify-root$_examples_modules_cpu_temp_widget_init_lua", "target": "$graphify-root$_examples_modules_cpu_temp_widget_init_m_on_load", "relation": "contains", "confidence": "EXTRACTED", "source_file": "examples/modules/cpu-temp-widget/init.lua", "source_location": "L60", "weight": 1.0}, {"source": "$graphify-root$_examples_modules_cpu_temp_widget_init_m_on_load", "target": "$graphify-root$_examples_modules_cpu_temp_widget_init_widget_root", "relation": "calls", "context": "call", "confidence": "EXTRACTED", "source_file": "examples/modules/cpu-temp-widget/init.lua", "source_location": "L65", "weight": 1.0}, {"source": "$graphify-root$_examples_modules_cpu_temp_widget_init_m_on_load", "target": "$graphify-root$_examples_modules_cpu_temp_widget_init_read_temp_c", "relation": "calls", "context": "call", "confidence": "EXTRACTED", "source_file": "examples/modules/cpu-temp-widget/init.lua", "source_location": "L65", "weight": 1.0}], "raw_calls": [{"caller_nid": "$graphify-root$_examples_modules_cpu_temp_widget_init_read_temp_c", "callee": "bread.fs.read", "is_member_call": false, "source_file": "examples/modules/cpu-temp-widget/init.lua", "source_location": "L33", "receiver": null}, {"caller_nid": "$graphify-root$_examples_modules_cpu_temp_widget_init_read_temp_c", "callee": "tonumber", "is_member_call": false, "source_file": "examples/modules/cpu-temp-widget/init.lua", "source_location": "L37", "receiver": null}, {"caller_nid": "$graphify-root$_examples_modules_cpu_temp_widget_init_widget_root", "callee": "string.format", "is_member_call": false, "source_file": "examples/modules/cpu-temp-widget/init.lua", "source_location": "L41", "receiver": null}, {"caller_nid": "$graphify-root$_examples_modules_cpu_temp_widget_init_widget_root", "callee": "math.min", "is_member_call": false, "source_file": "examples/modules/cpu-temp-widget/init.lua", "source_location": "L53", "receiver": null}, {"caller_nid": "$graphify-root$_examples_modules_cpu_temp_widget_init_m_on_load", "callee": "bread.widget.register", "is_member_call": false, "source_file": "examples/modules/cpu-temp-widget/init.lua", "source_location": "L61", "receiver": null}, {"caller_nid": "$graphify-root$_examples_modules_cpu_temp_widget_init_m_on_load", "callee": "bread.every", "is_member_call": false, "source_file": "examples/modules/cpu-temp-widget/init.lua", "source_location": "L68", "receiver": null}, {"caller_nid": "$graphify-root$_examples_modules_cpu_temp_widget_init_m_on_load", "callee": "bread.widget.update", "is_member_call": false, "source_file": "examples/modules/cpu-temp-widget/init.lua", "source_location": "L69", "receiver": null}]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -1,252 +1,327 @@
{ {
"bread-cli/src/hooks_git.rs": { "bread-cli/src/hooks_git.rs": {
"mtime": 1784781158.0572724, "mtime": 1786800467.7200727,
"ast_hash": "7a29b5d5d90170f0aef9cececc7d8656", "ast_hash": "7a29b5d5d90170f0aef9cececc7d8656",
"semantic_hash": "7a29b5d5d90170f0aef9cececc7d8656" "semantic_hash": "7a29b5d5d90170f0aef9cececc7d8656"
}, },
"bread-cli/src/hooks_shell.rs": { "bread-cli/src/hooks_shell.rs": {
"mtime": 1784781158.0582726, "mtime": 1786800467.7201686,
"ast_hash": "c5d5b8922fcff79954ddb00496721603", "ast_hash": "c5d5b8922fcff79954ddb00496721603",
"semantic_hash": "c5d5b8922fcff79954ddb00496721603" "semantic_hash": "c5d5b8922fcff79954ddb00496721603"
}, },
"bread-cli/src/lib.rs": { "bread-cli/src/lib.rs": {
"mtime": 1778512393.4941568, "mtime": 1786800467.7201686,
"ast_hash": "d1bf6e1c239498521c42418f672bc400", "ast_hash": "d1bf6e1c239498521c42418f672bc400",
"semantic_hash": "d1bf6e1c239498521c42418f672bc400" "semantic_hash": "d1bf6e1c239498521c42418f672bc400"
}, },
"bread-cli/src/main.rs": { "bread-cli/src/main.rs": {
"mtime": 1784781158.0592725, "mtime": 1786800467.7201686,
"ast_hash": "db407455fee59858b369f8b8955de3d7", "ast_hash": "e1fbe63ea3b10e0887072ab7133d0508",
"semantic_hash": "db407455fee59858b369f8b8955de3d7" "semantic_hash": ""
}, },
"bread-cli/src/modules_mgmt.rs": { "bread-cli/src/modules_mgmt.rs": {
"mtime": 1784781158.0592725, "mtime": 1786801244.1263742,
"ast_hash": "ecef9a05ab9c0396ed7a27404b46334d", "ast_hash": "1cb6f3e4fa26108f81215ab57bb02b91",
"semantic_hash": "ecef9a05ab9c0396ed7a27404b46334d" "semantic_hash": ""
}, },
"bread-cli/tests/modules.rs": { "bread-cli/tests/modules.rs": {
"mtime": 1784781158.0602725, "mtime": 1786801244.1297076,
"ast_hash": "84fca8f87dc915b1cb523f8199e78521", "ast_hash": "84fca8f87dc915b1cb523f8199e78521",
"semantic_hash": "84fca8f87dc915b1cb523f8199e78521" "semantic_hash": "84fca8f87dc915b1cb523f8199e78521"
}, },
"bread-emit/src/main.rs": { "bread-emit/src/main.rs": {
"mtime": 1784781158.0616376, "mtime": 1786800467.7206197,
"ast_hash": "350cefbf697cfd93f7df7b7bcc45a101", "ast_hash": "350cefbf697cfd93f7df7b7bcc45a101",
"semantic_hash": "350cefbf697cfd93f7df7b7bcc45a101" "semantic_hash": "350cefbf697cfd93f7df7b7bcc45a101"
}, },
"bread-shared/src/apps.rs": { "bread-shared/src/apps.rs": {
"mtime": 1785479966.7981513, "mtime": 1786801218.1467261,
"ast_hash": "78da874557a45d9b06a9d0622c72dc5a", "ast_hash": "e8419571e7a322019d71c89559fb02fb",
"semantic_hash": "78da874557a45d9b06a9d0622c72dc5a" "semantic_hash": ""
}, },
"bread-shared/src/glob.rs": { "bread-shared/src/glob.rs": {
"mtime": 1784781158.0616376, "mtime": 1786800467.7210522,
"ast_hash": "0594a313cb1909d1cca5fd5b8f241b68", "ast_hash": "0594a313cb1909d1cca5fd5b8f241b68",
"semantic_hash": "0594a313cb1909d1cca5fd5b8f241b68" "semantic_hash": "0594a313cb1909d1cca5fd5b8f241b68"
}, },
"bread-shared/src/lib.rs": { "bread-shared/src/lib.rs": {
"mtime": 1784781173.9236407, "mtime": 1786801244.1297076,
"ast_hash": "8fabca4623d00ddfc131dea8d8c207c1", "ast_hash": "236b73bad29f1544a28fbf3ee1ccc795",
"semantic_hash": "8fabca4623d00ddfc131dea8d8c207c1" "semantic_hash": ""
}, },
"bread-shared/src/widget.rs": { "bread-shared/src/widget.rs": {
"mtime": 1784781173.9236407, "mtime": 1786801244.1297076,
"ast_hash": "33272eb38c7fffbfa180cef7c5a286fc", "ast_hash": "33272eb38c7fffbfa180cef7c5a286fc",
"semantic_hash": "33272eb38c7fffbfa180cef7c5a286fc" "semantic_hash": "33272eb38c7fffbfa180cef7c5a286fc"
}, },
"breadd/src/adapters/bluetooth.rs": { "breadd/src/adapters/bluetooth.rs": {
"mtime": 1778843727.5135255, "mtime": 1786800467.7214499,
"ast_hash": "4d1df67347d5b7c9cdbcb12921a6ae28", "ast_hash": "4d1df67347d5b7c9cdbcb12921a6ae28",
"semantic_hash": "4d1df67347d5b7c9cdbcb12921a6ae28" "semantic_hash": "4d1df67347d5b7c9cdbcb12921a6ae28"
}, },
"breadd/src/adapters/filesystem.rs": { "breadd/src/adapters/filesystem.rs": {
"mtime": 1784781158.0616376, "mtime": 1786800467.7215972,
"ast_hash": "8ec9dcd8de13f30922cfbe9b4a3fff56", "ast_hash": "8ec9dcd8de13f30922cfbe9b4a3fff56",
"semantic_hash": "8ec9dcd8de13f30922cfbe9b4a3fff56" "semantic_hash": "8ec9dcd8de13f30922cfbe9b4a3fff56"
}, },
"breadd/src/adapters/git.rs": { "breadd/src/adapters/git.rs": {
"mtime": 1784781158.0616376, "mtime": 1786800467.7215972,
"ast_hash": "6e6ff6ca318e28cd94690c30d41b520b", "ast_hash": "6e6ff6ca318e28cd94690c30d41b520b",
"semantic_hash": "6e6ff6ca318e28cd94690c30d41b520b" "semantic_hash": "6e6ff6ca318e28cd94690c30d41b520b"
}, },
"breadd/src/adapters/hyprland.rs": { "breadd/src/adapters/hyprland.rs": {
"mtime": 1784774788.7034824, "mtime": 1786800467.7215972,
"ast_hash": "71537dbd86b413d94476f8022054f1f4", "ast_hash": "71537dbd86b413d94476f8022054f1f4",
"semantic_hash": "71537dbd86b413d94476f8022054f1f4" "semantic_hash": "71537dbd86b413d94476f8022054f1f4"
}, },
"breadd/src/adapters/mod.rs": { "breadd/src/adapters/mod.rs": {
"mtime": 1784781158.0616376, "mtime": 1786800467.7215972,
"ast_hash": "63885887c2fc3ea2314d7fe095e5df61", "ast_hash": "63885887c2fc3ea2314d7fe095e5df61",
"semantic_hash": "63885887c2fc3ea2314d7fe095e5df61" "semantic_hash": "63885887c2fc3ea2314d7fe095e5df61"
}, },
"breadd/src/adapters/network.rs": { "breadd/src/adapters/network.rs": {
"mtime": 1778470615.8460202, "mtime": 1786800467.7215972,
"ast_hash": "8706dc546b7e08d5aa084ca58c48559c", "ast_hash": "8706dc546b7e08d5aa084ca58c48559c",
"semantic_hash": "8706dc546b7e08d5aa084ca58c48559c" "semantic_hash": "8706dc546b7e08d5aa084ca58c48559c"
}, },
"breadd/src/adapters/network_rtnetlink.rs": { "breadd/src/adapters/network_rtnetlink.rs": {
"mtime": 1784781158.0616376, "mtime": 1786800467.7215972,
"ast_hash": "0c9d0bb2693bc46b11807f6b8ebb7c4c", "ast_hash": "0c9d0bb2693bc46b11807f6b8ebb7c4c",
"semantic_hash": "0c9d0bb2693bc46b11807f6b8ebb7c4c" "semantic_hash": "0c9d0bb2693bc46b11807f6b8ebb7c4c"
}, },
"breadd/src/adapters/podman.rs": { "breadd/src/adapters/podman.rs": {
"mtime": 1784781158.0622723, "mtime": 1786800467.7215972,
"ast_hash": "97c08551c0fe53b0a5888d98730d35db", "ast_hash": "97c08551c0fe53b0a5888d98730d35db",
"semantic_hash": "97c08551c0fe53b0a5888d98730d35db" "semantic_hash": "97c08551c0fe53b0a5888d98730d35db"
}, },
"breadd/src/adapters/power.rs": { "breadd/src/adapters/power.rs": {
"mtime": 1778470615.838569, "mtime": 1786800467.7215972,
"ast_hash": "987d202ec0d30ec26b1d747a6e320ed7", "ast_hash": "987d202ec0d30ec26b1d747a6e320ed7",
"semantic_hash": "987d202ec0d30ec26b1d747a6e320ed7" "semantic_hash": "987d202ec0d30ec26b1d747a6e320ed7"
}, },
"breadd/src/adapters/power_upower.rs": { "breadd/src/adapters/power_upower.rs": {
"mtime": 1784781158.0622723, "mtime": 1786800467.7215972,
"ast_hash": "fc0a36ca76d340c8be77644f63e462bf", "ast_hash": "fc0a36ca76d340c8be77644f63e462bf",
"semantic_hash": "fc0a36ca76d340c8be77644f63e462bf" "semantic_hash": "fc0a36ca76d340c8be77644f63e462bf"
}, },
"breadd/src/adapters/systemd.rs": { "breadd/src/adapters/systemd.rs": {
"mtime": 1784781158.0622723, "mtime": 1786800467.7215972,
"ast_hash": "b3884dbecd39acc38abdf67589a8b954", "ast_hash": "b3884dbecd39acc38abdf67589a8b954",
"semantic_hash": "b3884dbecd39acc38abdf67589a8b954" "semantic_hash": "b3884dbecd39acc38abdf67589a8b954"
}, },
"breadd/src/adapters/udev.rs": { "breadd/src/adapters/udev.rs": {
"mtime": 1778680581.3648126, "mtime": 1786800978.7399695,
"ast_hash": "653a424baee8c436b2adb608c92c9fd2", "ast_hash": "568993c8c2dd218879eec57aee50a6b9",
"semantic_hash": "653a424baee8c436b2adb608c92c9fd2" "semantic_hash": ""
}, },
"breadd/src/core/config.rs": { "breadd/src/core/config.rs": {
"mtime": 1784781158.0632722, "mtime": 1786800467.7215972,
"ast_hash": "0bc6dd90a2398b22f5fdbf1d647b4c7d", "ast_hash": "7f479bfb51647e6131c14f0c2f153d95",
"semantic_hash": "0bc6dd90a2398b22f5fdbf1d647b4c7d" "semantic_hash": ""
}, },
"breadd/src/core/mod.rs": { "breadd/src/core/mod.rs": {
"mtime": 1778471738.0735896, "mtime": 1786800467.7223108,
"ast_hash": "3df83a958dd6ee5d09712b7cc03e15c9", "ast_hash": "e38bfdb894eabd208941bd3ffb1ef464",
"semantic_hash": "3df83a958dd6ee5d09712b7cc03e15c9" "semantic_hash": ""
}, },
"breadd/src/core/normalizer.rs": { "breadd/src/core/normalizer.rs": {
"mtime": 1784781158.0632722, "mtime": 1786801218.2233918,
"ast_hash": "5e918ed12395767b6d0c315d804f1215", "ast_hash": "1596f60df5049ff8715f66f64bee9f8a",
"semantic_hash": "5e918ed12395767b6d0c315d804f1215" "semantic_hash": ""
}, },
"breadd/src/core/state_engine.rs": { "breadd/src/core/state_engine.rs": {
"mtime": 1784781173.9246407, "mtime": 1786800467.7223108,
"ast_hash": "162df9607c8a733a7060bb732fdcf783", "ast_hash": "84a35ee78f3881261a3ce378ea3c03a0",
"semantic_hash": "162df9607c8a733a7060bb732fdcf783" "semantic_hash": ""
}, },
"breadd/src/core/subscriptions.rs": { "breadd/src/core/subscriptions.rs": {
"mtime": 1784781158.0632722, "mtime": 1786800467.7223108,
"ast_hash": "8735d4717b74523c787dab9ea2b0bbd8", "ast_hash": "8735d4717b74523c787dab9ea2b0bbd8",
"semantic_hash": "8735d4717b74523c787dab9ea2b0bbd8" "semantic_hash": "8735d4717b74523c787dab9ea2b0bbd8"
}, },
"breadd/src/core/supervisor.rs": { "breadd/src/core/supervisor.rs": {
"mtime": 1778680581.3778126, "mtime": 1786800467.7223108,
"ast_hash": "403f7ba1807c71f9b89d81bfeff2681a", "ast_hash": "403f7ba1807c71f9b89d81bfeff2681a",
"semantic_hash": "403f7ba1807c71f9b89d81bfeff2681a" "semantic_hash": "403f7ba1807c71f9b89d81bfeff2681a"
}, },
"breadd/src/core/types.rs": { "breadd/src/core/types.rs": {
"mtime": 1784781173.9246407, "mtime": 1786800467.7223108,
"ast_hash": "830da3a3b75e0cad06f07f971950bbbb", "ast_hash": "8bd070d4c09725d8717749a79aa42a92",
"semantic_hash": "830da3a3b75e0cad06f07f971950bbbb" "semantic_hash": ""
}, },
"breadd/src/ipc/mod.rs": { "breadd/src/ipc/mod.rs": {
"mtime": 1784781173.9246407, "mtime": 1786800912.9916558,
"ast_hash": "c26947d0a253ab5bec356049bb63d7e5", "ast_hash": "54af229257e555313f7d4b59d57ff8d3",
"semantic_hash": "c26947d0a253ab5bec356049bb63d7e5" "semantic_hash": ""
}, },
"breadd/src/lua/mod.rs": { "breadd/src/lua/mod.rs": {
"mtime": 1784781173.9246407, "mtime": 1786801244.1297076,
"ast_hash": "6816dd3c14657467a4e537328d6edb59", "ast_hash": "ed13e2495399d532625653a22a4fee14",
"semantic_hash": "6816dd3c14657467a4e537328d6edb59" "semantic_hash": ""
}, },
"breadd/src/main.rs": { "breadd/src/main.rs": {
"mtime": 1784781158.0642722, "mtime": 1786800467.722855,
"ast_hash": "b59d4e025c652b074a87638d99823418", "ast_hash": "4b31888cc9e76210df0c7cff3a8b1e42",
"semantic_hash": "b59d4e025c652b074a87638d99823418" "semantic_hash": ""
}, },
"breadd/tests/ipc_integration.rs": { "breadd/tests/ipc_integration.rs": {
"mtime": 1784781158.0642722, "mtime": 1786801218.36339,
"ast_hash": "116f390ea369b67ddefa95b8519de3b7", "ast_hash": "2b5bade2cc2e189a3af9c692232be258",
"semantic_hash": "116f390ea369b67ddefa95b8519de3b7" "semantic_hash": ""
}, },
"examples/modules/active-window-widget.lua": { "examples/modules/active-window-widget.lua": {
"mtime": 1784781173.9256406, "mtime": 1786800467.7243168,
"ast_hash": "a5f6fb2c54775a49ddaf29674f86571d", "ast_hash": "a5f6fb2c54775a49ddaf29674f86571d",
"semantic_hash": "a5f6fb2c54775a49ddaf29674f86571d" "semantic_hash": "a5f6fb2c54775a49ddaf29674f86571d"
}, },
"examples/modules/bluetooth-toggle-widget.lua": { "examples/modules/bluetooth-toggle-widget.lua": {
"mtime": 1784781173.9256406, "mtime": 1786800467.7243168,
"ast_hash": "c8529dc45862adf4f63db48674a28277", "ast_hash": "c8529dc45862adf4f63db48674a28277",
"semantic_hash": "c8529dc45862adf4f63db48674a28277" "semantic_hash": "c8529dc45862adf4f63db48674a28277"
}, },
"examples/modules/cpu-temp-widget.lua": {
"mtime": 1784781173.9256406,
"ast_hash": "c214e573fa1338b9e5e56ce38a1a36e3",
"semantic_hash": "c214e573fa1338b9e5e56ce38a1a36e3"
},
"examples/modules/dock-monitors.lua": { "examples/modules/dock-monitors.lua": {
"mtime": 1784781158.0663679, "mtime": 1786800467.724451,
"ast_hash": "4abe51f19614d2bf117ac35e95324801", "ast_hash": "4abe51f19614d2bf117ac35e95324801",
"semantic_hash": "4abe51f19614d2bf117ac35e95324801" "semantic_hash": "4abe51f19614d2bf117ac35e95324801"
}, },
"examples/modules/dock-workflow.lua": { "examples/modules/dock-workflow.lua": {
"mtime": 1784781158.0663679, "mtime": 1786800467.724451,
"ast_hash": "7ea9293195fdcb839acaa52bc0d040b3", "ast_hash": "84cf81cfb7780e89a46e96a6ccfbad15",
"semantic_hash": "7ea9293195fdcb839acaa52bc0d040b3" "semantic_hash": ""
}, },
"examples/modules/focus-mode-widget.lua": { "examples/modules/focus-mode-widget.lua": {
"mtime": 1784781173.9256406, "mtime": 1786800467.724451,
"ast_hash": "f62c366c2c85cfbe59eef663fc607230", "ast_hash": "f62c366c2c85cfbe59eef663fc607230",
"semantic_hash": "f62c366c2c85cfbe59eef663fc607230" "semantic_hash": "f62c366c2c85cfbe59eef663fc607230"
}, },
"examples/modules/git-branch-widget.lua": { "examples/modules/git-branch-widget.lua": {
"mtime": 1784781173.9256406, "mtime": 1786800467.724451,
"ast_hash": "25ae6c6190547d7ca7ad649bc288eac1", "ast_hash": "ebeeaebab5b5b29619f3db65c1df57d8",
"semantic_hash": "25ae6c6190547d7ca7ad649bc288eac1" "semantic_hash": ""
}, },
"examples/modules/low-battery-warning.lua": { "examples/modules/low-battery-warning.lua": {
"mtime": 1784781158.0663679, "mtime": 1786800467.724451,
"ast_hash": "e0ba79860562fc36fa8cb183bc576fb7", "ast_hash": "e0ba79860562fc36fa8cb183bc576fb7",
"semantic_hash": "e0ba79860562fc36fa8cb183bc576fb7" "semantic_hash": "e0ba79860562fc36fa8cb183bc576fb7"
}, },
"examples/modules/pause-media-on-headphone-unplug.lua": { "examples/modules/pause-media-on-headphone-unplug.lua": {
"mtime": 1784781158.0663679, "mtime": 1786800467.724451,
"ast_hash": "6c4cf82b6963eb93df224bed60d06c2d", "ast_hash": "6c4cf82b6963eb93df224bed60d06c2d",
"semantic_hash": "6c4cf82b6963eb93df224bed60d06c2d" "semantic_hash": "6c4cf82b6963eb93df224bed60d06c2d"
}, },
"examples/modules/workflow-status-widget.lua": { "examples/modules/workflow-status-widget.lua": {
"mtime": 1784781173.9256406, "mtime": 1786800467.724451,
"ast_hash": "9f913a66a0f8654dadc1c5d6c2be0938", "ast_hash": "9f913a66a0f8654dadc1c5d6c2be0938",
"semantic_hash": "9f913a66a0f8654dadc1c5d6c2be0938" "semantic_hash": "9f913a66a0f8654dadc1c5d6c2be0938"
}, },
"scripts/install.sh": { "scripts/install.sh": {
"mtime": 1778679456.2324038, "mtime": 1786800994.0764284,
"ast_hash": "c471884651bedb94c2cbceda02dee99c", "ast_hash": "35d1a7f63824e9176bd105ab3d699576",
"semantic_hash": "c471884651bedb94c2cbceda02dee99c" "semantic_hash": ""
}, },
"CONTRIBUTING.md": { "CONTRIBUTING.md": {
"mtime": 1785467202.8123527, "mtime": 1786800467.716893,
"ast_hash": "83fdc4753fadb0a6caba7d9fa5126055", "ast_hash": "6d056fcf0dae29949d19c41b41792879",
"semantic_hash": "83fdc4753fadb0a6caba7d9fa5126055" "semantic_hash": ""
}, },
"Documentation.md": { "Documentation.md": {
"mtime": 1784781173.9236407, "mtime": 1786801027.2816515,
"ast_hash": "5cbefc87f14fa998b29d8720a2c35ead", "ast_hash": "7b8471e7ae45aed70682bb858826860c",
"semantic_hash": "5cbefc87f14fa998b29d8720a2c35ead" "semantic_hash": ""
}, },
"Examples.md": { "Examples.md": {
"mtime": 1784781173.9236407, "mtime": 1786800467.716893,
"ast_hash": "6bcd7a14be53a9f67ef6912b160da8bc", "ast_hash": "6bcd7a14be53a9f67ef6912b160da8bc",
"semantic_hash": "6bcd7a14be53a9f67ef6912b160da8bc" "semantic_hash": "6bcd7a14be53a9f67ef6912b160da8bc"
}, },
"README.md": { "README.md": {
"mtime": 1784781158.0562725, "mtime": 1786800994.0964282,
"ast_hash": "4fb428200bf6ef1aef21552842249237", "ast_hash": "97ae9f7efb9f2cdceb615a47cf3dfa1a",
"semantic_hash": "4fb428200bf6ef1aef21552842249237" "semantic_hash": ""
}, },
"upgrade.md": { "bread-module-host/src/io.rs": {
"mtime": 1785826079.9260893, "mtime": 1786801244.1297076,
"ast_hash": "d298b8982ab58c0155e8f22b96dd5666", "ast_hash": "dbe04e05d1cfb02770db8d7c0bcc0b68",
"semantic_hash": "d298b8982ab58c0155e8f22b96dd5666" "semantic_hash": ""
},
"bread-module-host/src/lua_env.rs": {
"mtime": 1786801244.1297076,
"ast_hash": "97d009846e18d3c809663b1185c98bbb",
"semantic_hash": ""
},
"bread-module-host/src/main.rs": {
"mtime": 1786801244.1297076,
"ast_hash": "9a93253ae44c764b8273df40999ddede",
"semantic_hash": ""
},
"bread-shared/src/module_host_ipc.rs": {
"mtime": 1786801244.1297076,
"ast_hash": "fcdb4c03f242a5ea51fd3a096b711363",
"semantic_hash": ""
},
"bread-shared/src/permissions.rs": {
"mtime": 1786800467.7210522,
"ast_hash": "800445ef9c90bdbf8f3ac9d4f1afd98e",
"semantic_hash": ""
},
"breadd/src/core/rules.rs": {
"mtime": 1786801244.1297076,
"ast_hash": "bd430cf213f400b8d4e96ccc35a76ff1",
"semantic_hash": ""
},
"breadd/src/ipc/module_host_bridge.rs": {
"mtime": 1786801244.1297076,
"ast_hash": "caa9cf82695e48c88758fcfa835e1871",
"semantic_hash": ""
},
"breadd/src/module_host.rs": {
"mtime": 1786801244.1297076,
"ast_hash": "228826ff3c8941ea5f9bb28b66d637a9",
"semantic_hash": ""
},
"breadd/tests/module_host_sandbox.rs": {
"mtime": 1786801244.1297076,
"ast_hash": "7e637b3a950c510215a6f6a7b88dd404",
"semantic_hash": ""
},
"examples/modules/cpu-temp-widget/init.lua": {
"mtime": 1786800467.724451,
"ast_hash": "5ac893dd2f6af6d8b22dd290e3021f77",
"semantic_hash": ""
},
"xtask/src/main.rs": {
"mtime": 1786801244.1297076,
"ast_hash": "e914dd77ac3241df09e3a681228961db",
"semantic_hash": ""
},
".forgejo/workflows/dev-release.yml": {
"mtime": 1786800467.7161875,
"ast_hash": "707129aa2fe79b4539d108c03e6d7abf",
"semantic_hash": ""
},
".forgejo/workflows/rc-release.yml": {
"mtime": 1786800467.7167187,
"ast_hash": "3007d2c43d785cd72f0548096626a21d",
"semantic_hash": ""
},
".forgejo/workflows/release.yml": {
"mtime": 1786800467.7167187,
"ast_hash": "3578aa39e7b2466822c8d85247489d36",
"semantic_hash": ""
},
"DEPRECATIONS.md": {
"mtime": 1786800467.716893,
"ast_hash": "1685b60fad2ed4184e4119c30866ec12",
"semantic_hash": ""
},
"examples/modules/README.md": {
"mtime": 1786800467.7242541,
"ast_hash": "dfa8a72f41c08bd5b8036b684092a93b",
"semantic_hash": ""
},
"packaging/README.md": {
"mtime": 1786800994.0430956,
"ast_hash": "caa295eed64de126874a2e48b8aed8a2",
"semantic_hash": ""
} }
} }

View file

@ -19,7 +19,7 @@ file here.
The service unit starts `breadd` as a user service after the graphical session is available. The service unit starts `breadd` as a user service after the graphical session is available.
```bash ```bash
# Install and enable manually (if not using the PKGBUILD) # Install and enable manually (if not using bakery)
mkdir -p ~/.config/systemd/user mkdir -p ~/.config/systemd/user
cp systemd/breadd.service ~/.config/systemd/user/ cp systemd/breadd.service ~/.config/systemd/user/
systemctl --user daemon-reload systemctl --user daemon-reload

View file

@ -17,8 +17,12 @@ echo "symlinking binaries into $BIN_DIR..."
mkdir -p "$BIN_DIR" mkdir -p "$BIN_DIR"
ln -sf "$REPO_ROOT/target/release/breadd" "$BIN_DIR/breadd" ln -sf "$REPO_ROOT/target/release/breadd" "$BIN_DIR/breadd"
ln -sf "$REPO_ROOT/target/release/bread" "$BIN_DIR/bread" ln -sf "$REPO_ROOT/target/release/bread" "$BIN_DIR/bread"
ln -sf "$REPO_ROOT/target/release/bread-emit" "$BIN_DIR/bread-emit"
ln -sf "$REPO_ROOT/target/release/bread-module-host" "$BIN_DIR/bread-module-host"
echo " $BIN_DIR/breadd -> $REPO_ROOT/target/release/breadd" echo " $BIN_DIR/breadd -> $REPO_ROOT/target/release/breadd"
echo " $BIN_DIR/bread -> $REPO_ROOT/target/release/bread" echo " $BIN_DIR/bread -> $REPO_ROOT/target/release/bread"
echo " $BIN_DIR/bread-emit -> $REPO_ROOT/target/release/bread-emit"
echo " $BIN_DIR/bread-module-host -> $REPO_ROOT/target/release/bread-module-host"
if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then
echo "" echo ""