Enhance installation process, update service paths, and improve device classification
This commit is contained in:
parent
e339660084
commit
f0ef411697
12 changed files with 323 additions and 70 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -1,3 +1,6 @@
|
|||
target/
|
||||
Overview.md
|
||||
DAEMON.md
|
||||
DAEMON.md
|
||||
.claude
|
||||
CLAUDE.md
|
||||
.github/
|
||||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -289,6 +289,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"bread-shared",
|
||||
"clap",
|
||||
"libc",
|
||||
"notify",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
|
|||
84
README.md
84
README.md
|
|
@ -72,16 +72,20 @@ Optional but preferred:
|
|||
```bash
|
||||
git clone https://github.com/Breadway/bread.git
|
||||
cd bread
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
Binaries will be at `target/release/breadd` and `target/release/bread`.
|
||||
|
||||
Install them:
|
||||
Run the install script — it builds, installs to `/usr/bin`, sets up the systemd user service, and starts the daemon:
|
||||
|
||||
```bash
|
||||
sudo install -Dm755 target/release/breadd /usr/local/bin/breadd
|
||||
sudo install -Dm755 target/release/bread /usr/local/bin/bread
|
||||
bash scripts/install.sh
|
||||
```
|
||||
|
||||
Or do it step by step:
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
sudo install -Dm755 target/release/breadd /usr/bin/breadd
|
||||
sudo install -Dm755 target/release/bread /usr/bin/bread
|
||||
```
|
||||
|
||||
### Arch Linux (PKGBUILD)
|
||||
|
|
@ -130,6 +134,15 @@ enabled = true
|
|||
|
||||
[events]
|
||||
dedup_window_ms = 100
|
||||
|
||||
[notifications]
|
||||
default_timeout_ms = 5000
|
||||
default_urgency = "normal"
|
||||
notify_send_path = "notify-send"
|
||||
|
||||
[modules]
|
||||
builtin = true # load built-in modules (monitors, devices, etc.)
|
||||
disable = [] # list of built-in module names to disable
|
||||
```
|
||||
|
||||
Your automation lives in `~/.config/bread/init.lua`:
|
||||
|
|
@ -153,15 +166,18 @@ All commands communicate with the running daemon over a Unix socket at `$XDG_RUN
|
|||
|
||||
```bash
|
||||
bread reload # Hot-reload all Lua modules
|
||||
bread reload --watch # Watch config dir and reload on changes
|
||||
bread state # Dump full runtime state as JSON
|
||||
bread events # Stream live normalized events
|
||||
bread events --filter bread.device.* # Stream filtered events
|
||||
bread events --since 60 # Replay events from the last 60 seconds
|
||||
bread modules # List loaded modules and status
|
||||
bread profile-list # List defined profiles
|
||||
bread profile-activate <name> # Activate a named profile
|
||||
bread emit <event> --data '{}' # Manually fire an event (for testing)
|
||||
bread ping # Check daemon connectivity
|
||||
bread health # Daemon version, uptime, PID
|
||||
bread doctor # Diagnose daemon and module health
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -201,16 +217,26 @@ Events follow the namespace convention `bread.<subsystem>.<noun>.<verb>`.
|
|||
### Events
|
||||
|
||||
```lua
|
||||
-- Subscribe to an event
|
||||
bread.on("bread.monitor.connected", function(event)
|
||||
-- Subscribe to an event; returns a numeric ID
|
||||
local id = bread.on("bread.monitor.connected", function(event)
|
||||
print(event.data.name)
|
||||
end)
|
||||
|
||||
-- Unsubscribe by ID
|
||||
bread.off(id)
|
||||
|
||||
-- Subscribe once, then auto-unsubscribe
|
||||
bread.once("bread.system.startup", function(event)
|
||||
-- runs exactly once
|
||||
end)
|
||||
|
||||
-- Subscribe with a predicate filter
|
||||
bread.filter("bread.device.connected", function(event)
|
||||
return event.data.class == "keyboard"
|
||||
end, function(event)
|
||||
bread.exec("xset r rate 200 40")
|
||||
end)
|
||||
|
||||
-- Emit a custom event (for cross-module communication)
|
||||
bread.emit("mymodule.something", { key = "value" })
|
||||
```
|
||||
|
|
@ -222,6 +248,12 @@ bread.emit("mymodule.something", { key = "value" })
|
|||
local monitors = bread.state.get("monitors")
|
||||
local workspace = bread.state.get("active_workspace")
|
||||
local power = bread.state.get("power")
|
||||
local devices = bread.state.get("devices")
|
||||
|
||||
-- Watch a state key and fire on changes
|
||||
bread.state.watch("active_workspace", function(new, old)
|
||||
print("workspace changed from " .. tostring(old) .. " to " .. tostring(new))
|
||||
end)
|
||||
```
|
||||
|
||||
### Profiles
|
||||
|
|
@ -231,12 +263,42 @@ bread.profile.activate("desk")
|
|||
bread.profile.activate("default")
|
||||
```
|
||||
|
||||
### Execution
|
||||
### Execution and notifications
|
||||
|
||||
```lua
|
||||
-- Fire-and-forget: returns immediately, process runs in background
|
||||
bread.exec("kitty")
|
||||
bread.exec("notify-send 'Dock connected'")
|
||||
|
||||
-- Desktop notification
|
||||
bread.notify("Dock connected", { urgency = "normal", timeout = 3000 })
|
||||
```
|
||||
|
||||
### Timers
|
||||
|
||||
```lua
|
||||
-- Run once after a delay (ms)
|
||||
bread.after(500, function()
|
||||
bread.exec("some-delayed-command")
|
||||
end)
|
||||
|
||||
-- Run on a repeating interval (ms); returns a timer ID
|
||||
local id = bread.every(60000, function()
|
||||
bread.log("tick")
|
||||
end)
|
||||
bread.cancel(id)
|
||||
|
||||
-- Debounce a rapidly-firing handler
|
||||
local fn = bread.debounce(200, function(event)
|
||||
reconfigure_monitors()
|
||||
end)
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
```lua
|
||||
bread.log("Module loaded")
|
||||
bread.warn("Unexpected state")
|
||||
bread.error("Something failed")
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -255,7 +317,7 @@ Response:
|
|||
{ "id": "1", "result": [ { "name": "HDMI-A-1", "connected": true } ] }
|
||||
```
|
||||
|
||||
Available methods: `ping`, `health`, `state.get`, `state.dump`, `modules.list`, `modules.reload`, `profile.list`, `profile.activate`, `events.subscribe`, `emit`.
|
||||
Available methods: `ping`, `health`, `state.get`, `state.dump`, `modules.list`, `modules.reload`, `profile.list`, `profile.activate`, `events.subscribe`, `events.replay`, `emit`.
|
||||
|
||||
`events.subscribe` upgrades the connection to a streaming mode — the daemon pushes events line by line until the client disconnects.
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ name = "bread-cli"
|
|||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "bread"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
bread-shared = { path = "../bread-shared" }
|
||||
serde.workspace = true
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use serde_json::{json, Value};
|
|||
use std::env;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, UNIX_EPOCH};
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::UnixStream;
|
||||
use tokio::sync::mpsc;
|
||||
|
|
@ -331,18 +331,6 @@ fn print_reload(value: &Value) {
|
|||
}
|
||||
}
|
||||
|
||||
async fn watch_reload(socket: &Path) -> Result<()> {
|
||||
let config_dir = config_directory();
|
||||
println!("watching {} for changes...", config_dir.display());
|
||||
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
let mut watcher: RecommendedWatcher = notify::recommended_watcher(move |res| {
|
||||
let _ = tx.send(res);
|
||||
})?;
|
||||
watcher.watch(&config_dir, RecursiveMode::Recursive)?;
|
||||
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
async fn watch_reload(socket: &Path) -> Result<()> {
|
||||
let config_dir = config_directory();
|
||||
println!("watching {} for changes...", config_dir.display());
|
||||
|
|
@ -360,7 +348,7 @@ async fn watch_reload(socket: &Path) -> Result<()> {
|
|||
|
||||
// Debounce: drain any follow-up events that arrive within 150ms.
|
||||
// A single file save typically generates 2-3 fs events in rapid succession.
|
||||
sleep(Duration::from_millis(150)).await;
|
||||
tokio::time::sleep(Duration::from_millis(150)).await;
|
||||
while rx.try_recv().is_ok() {}
|
||||
|
||||
let response = send_request(socket, "modules.reload", json!({})).await?;
|
||||
|
|
@ -370,9 +358,6 @@ async fn watch_reload(socket: &Path) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn print_doctor(socket: &Path) -> Result<()> {
|
||||
let stream = match UnixStream::connect(socket).await {
|
||||
Ok(stream) => stream,
|
||||
|
|
|
|||
|
|
@ -52,18 +52,23 @@ impl Adapter for UdevAdapter {
|
|||
|
||||
async fn run(&self, tx: mpsc::Sender<RawEvent>) -> Result<()> {
|
||||
debug!("udev adapter started");
|
||||
if let Ok(()) = run_udev_monitor(self.subsystems.clone(), tx.clone()).await {
|
||||
return Ok(());
|
||||
match run_udev_monitor(self.subsystems.clone(), tx.clone()).await {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(err) => {
|
||||
tracing::warn!(error = %err, "udev netlink monitor unavailable, falling back to sysfs polling (add user to 'plugdev' group for real-time events)");
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for environments where monitor sockets are unavailable.
|
||||
let mut known: HashMap<String, ScannedDevice> = scan_devices(&self.subsystems)?
|
||||
// Fallback: poll sysfs every 2 seconds for environments where the
|
||||
// netlink socket is unavailable (missing plugdev membership, containers, etc).
|
||||
let mut known: HashMap<String, ScannedDevice> = scan_devices(&self.subsystems)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|d| (d.id.clone(), d))
|
||||
.collect();
|
||||
|
||||
loop {
|
||||
let current = scan_devices(&self.subsystems)?;
|
||||
let current = scan_devices(&self.subsystems).unwrap_or_default();
|
||||
let current_map: HashMap<String, ScannedDevice> = current
|
||||
.into_iter()
|
||||
.map(|d| (d.id.clone(), d))
|
||||
|
|
@ -71,13 +76,17 @@ impl Adapter for UdevAdapter {
|
|||
|
||||
for (id, dev) in ¤t_map {
|
||||
if !known.contains_key(id) {
|
||||
tx.send(raw_change_event("add", dev)).await?;
|
||||
if tx.send(raw_change_event("add", dev)).await.is_err() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (id, dev) in &known {
|
||||
if !current_map.contains_key(id) {
|
||||
tx.send(raw_change_event("remove", dev)).await?;
|
||||
if tx.send(raw_change_event("remove", dev)).await.is_err() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -130,6 +139,15 @@ async fn run_udev_monitor(subsystems: Vec<String>, tx: mpsc::Sender<RawEvent>) -
|
|||
"id": id,
|
||||
"name": name,
|
||||
"subsystem": subsystem,
|
||||
"id_input_keyboard": prop_bool(&event, "ID_INPUT_KEYBOARD"),
|
||||
"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"),
|
||||
}),
|
||||
timestamp: now_unix_ms(),
|
||||
};
|
||||
|
|
@ -263,3 +281,17 @@ fn scan_devices(subsystems: &[String]) -> Result<Vec<ScannedDevice>> {
|
|||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn prop_bool(event: &udev::Event, key: &str) -> bool {
|
||||
event
|
||||
.property_value(key)
|
||||
.and_then(|v| v.to_str())
|
||||
.map(|v| v == "1")
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn prop_str(event: &udev::Event, key: &str) -> Option<String> {
|
||||
event
|
||||
.property_value(key)
|
||||
.map(|v| v.to_string_lossy().to_string())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -289,33 +289,104 @@ fn split_hyprland_fields(data: &str) -> Vec<&str> {
|
|||
}
|
||||
|
||||
fn classify_device(payload: &Value) -> DeviceClass {
|
||||
let name = payload
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
let subsystem = payload
|
||||
.get("subsystem")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
|
||||
if name.contains("dock") {
|
||||
return DeviceClass::Dock;
|
||||
}
|
||||
if subsystem == "input" && name.contains("keyboard") {
|
||||
// --- Property-based classification (reliable, hardware-agnostic) ---
|
||||
|
||||
// udev sets ID_INPUT_KEYBOARD=1 for anything that presents as a keyboard HID device.
|
||||
if payload.get("id_input_keyboard").and_then(Value::as_bool).unwrap_or(false) {
|
||||
return DeviceClass::Keyboard;
|
||||
}
|
||||
if subsystem == "input" && name.contains("mouse") {
|
||||
|
||||
// ID_INPUT_MOUSE=1 covers mice and trackballs.
|
||||
if payload.get("id_input_mouse").and_then(Value::as_bool).unwrap_or(false) {
|
||||
return DeviceClass::Mouse;
|
||||
}
|
||||
|
||||
// ID_INPUT_TABLET=1 covers drawing tablets (Wacom etc).
|
||||
if payload.get("id_input_tablet").and_then(Value::as_bool).unwrap_or(false) {
|
||||
return DeviceClass::Tablet;
|
||||
}
|
||||
|
||||
// USB class 0x09 = Hub. Docks expose a hub interface; they also typically
|
||||
// expose video (0x0e), audio (0x01), and ethernet (CDC 0x02) interfaces.
|
||||
// We check for hub + at least one of those secondary interfaces.
|
||||
if let Some(ifaces) = payload.get("id_usb_interfaces").and_then(Value::as_str) {
|
||||
let ifaces_lc = ifaces.to_lowercase();
|
||||
let has_hub = ifaces_lc.contains(":0900") || ifaces_lc.contains(":0902");
|
||||
let has_secondary = ifaces_lc.contains(":0e") // video
|
||||
|| ifaces_lc.contains(":0200") // CDC ethernet
|
||||
|| ifaces_lc.contains(":0100") // audio
|
||||
|| ifaces_lc.contains(":0801"); // mass storage
|
||||
if has_hub && has_secondary {
|
||||
return DeviceClass::Dock;
|
||||
}
|
||||
}
|
||||
|
||||
// USB class 0x01 = Audio.
|
||||
if let Some(cls) = payload.get("id_usb_class").and_then(Value::as_str) {
|
||||
if cls == "01" || cls.to_lowercase() == "0x01" {
|
||||
return DeviceClass::Audio;
|
||||
}
|
||||
// USB class 0x08 = Mass Storage.
|
||||
if cls == "08" || cls.to_lowercase() == "0x08" {
|
||||
return DeviceClass::Storage;
|
||||
}
|
||||
}
|
||||
|
||||
// DRM subsystem = display connector.
|
||||
if subsystem == "drm" {
|
||||
return DeviceClass::Display;
|
||||
}
|
||||
if subsystem == "sound" || name.contains("audio") {
|
||||
|
||||
// Block devices = storage.
|
||||
if subsystem == "block" {
|
||||
return DeviceClass::Storage;
|
||||
}
|
||||
|
||||
// Sound subsystem = audio.
|
||||
if subsystem == "sound" {
|
||||
return DeviceClass::Audio;
|
||||
}
|
||||
if subsystem == "block" || name.contains("storage") {
|
||||
|
||||
// --- Name-based fallback (catches user-registered patterns and obvious names) ---
|
||||
// This runs last so the property-based rules above always win.
|
||||
|
||||
let name = payload
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| payload.get("id_model").and_then(Value::as_str))
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
|
||||
let vendor = payload
|
||||
.get("id_vendor")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
|
||||
let combined = format!("{name} {vendor}");
|
||||
|
||||
if combined.contains("dock") || combined.contains("hub") || combined.contains("thunderbolt") {
|
||||
return DeviceClass::Dock;
|
||||
}
|
||||
if combined.contains("keyboard") || combined.contains("kbd") {
|
||||
return DeviceClass::Keyboard;
|
||||
}
|
||||
if combined.contains("mouse") || combined.contains("trackball") || combined.contains("trackpoint") {
|
||||
return DeviceClass::Mouse;
|
||||
}
|
||||
if combined.contains("tablet") || combined.contains("wacom") || combined.contains("stylus") {
|
||||
return DeviceClass::Tablet;
|
||||
}
|
||||
if combined.contains("audio") || combined.contains("headset") || combined.contains("speaker") || combined.contains("dac") {
|
||||
return DeviceClass::Audio;
|
||||
}
|
||||
if combined.contains("storage") || combined.contains("drive") || combined.contains("flash") || combined.contains("disk") {
|
||||
return DeviceClass::Storage;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -270,14 +270,16 @@ impl Server {
|
|||
"events.replay" => {
|
||||
let since_ms = req.params.get("since_ms").and_then(Value::as_u64).unwrap_or(0);
|
||||
let cutoff = now_unix_ms().saturating_sub(since_ms);
|
||||
let mut replay = Vec::new();
|
||||
if let Ok(buf) = self.event_buffer.lock() {
|
||||
for event in buf.iter() {
|
||||
if event.timestamp >= cutoff {
|
||||
replay.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
let replay: Vec<BreadEvent> = self
|
||||
.event_buffer
|
||||
.lock()
|
||||
.map(|buf| {
|
||||
buf.iter()
|
||||
.filter(|e| e.timestamp >= cutoff)
|
||||
.cloned()
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Ok(serde_json::to_value(replay).unwrap_or_else(|_| json!([])))
|
||||
}
|
||||
_ => Err("unknown method".to_string()),
|
||||
|
|
|
|||
|
|
@ -1499,7 +1499,15 @@ fn state_value_to_lua<'lua>(
|
|||
state_arc: &Arc<RwLock<RuntimeState>>,
|
||||
path: &str,
|
||||
) -> mlua::Result<Value<'lua>> {
|
||||
let snapshot = state_arc.blocking_read();
|
||||
// The Lua thread runs a current_thread runtime. blocking_read and block_in_place
|
||||
// both require the multi-thread runtime and panic here. try_read succeeds
|
||||
// immediately in the common case; the write lock is held for microseconds.
|
||||
let snapshot = loop {
|
||||
if let Ok(g) = state_arc.try_read() {
|
||||
break g;
|
||||
}
|
||||
std::hint::spin_loop();
|
||||
};
|
||||
let mut value = serde_json::to_value(&*snapshot)
|
||||
.map_err(|e| LuaError::external(e.to_string()))?;
|
||||
if path.is_empty() {
|
||||
|
|
@ -1518,13 +1526,23 @@ fn state_value_to_lua<'lua>(
|
|||
}
|
||||
|
||||
fn module_store_get(state_arc: &Arc<RwLock<RuntimeState>>, module: &str, key: &str) -> Option<JsonValue> {
|
||||
let guard = state_arc.blocking_read();
|
||||
let guard = loop {
|
||||
if let Ok(g) = state_arc.try_read() {
|
||||
break g;
|
||||
}
|
||||
std::hint::spin_loop();
|
||||
};
|
||||
let entry = guard.modules.iter().find(|m| m.name == module)?;
|
||||
entry.store.get(key).cloned()
|
||||
}
|
||||
|
||||
fn module_store_set(state_arc: &Arc<RwLock<RuntimeState>>, module: &str, key: String, value: JsonValue) {
|
||||
let mut guard = state_arc.blocking_write();
|
||||
let mut guard = loop {
|
||||
if let Ok(g) = state_arc.try_write() {
|
||||
break g;
|
||||
}
|
||||
std::hint::spin_loop();
|
||||
};
|
||||
if let Some(entry) = guard.modules.iter_mut().find(|m| m.name == module) {
|
||||
entry.store.insert(key, value);
|
||||
return;
|
||||
|
|
@ -1616,6 +1634,7 @@ const BUILTIN_DEVICES: &str = r#"
|
|||
local M = bread.module({ name = "bread.devices", version = "1.0.0" })
|
||||
|
||||
local rules = {}
|
||||
local user_patterns = {} -- { { pattern = "...", class = "..." }, ... }
|
||||
|
||||
local function matches_rule(rule, event)
|
||||
local class = rule.class
|
||||
|
|
@ -1651,15 +1670,55 @@ local function run_rule(rule, event)
|
|||
end
|
||||
end
|
||||
|
||||
-- Reclassify an event's data.class based on user-registered name patterns.
|
||||
-- Called before rule matching so that user-registered patterns take effect
|
||||
-- even for devices that the daemon classified as Unknown.
|
||||
local function apply_user_patterns(event)
|
||||
if not event.data then return event end
|
||||
local name = tostring(event.data.name or ""):lower()
|
||||
local vendor = tostring(event.data.vendor or ""):lower()
|
||||
local combined = name .. " " .. vendor
|
||||
for _, p in ipairs(user_patterns) do
|
||||
if combined:find(p.pattern, 1, true) then
|
||||
-- Return a shallow copy with the class overridden so we don't
|
||||
-- mutate the original event that other handlers may receive.
|
||||
local patched = {}
|
||||
for k, v in pairs(event) do patched[k] = v end
|
||||
patched.data = {}
|
||||
for k, v in pairs(event.data) do patched.data[k] = v end
|
||||
patched.data.class = p.class
|
||||
return patched
|
||||
end
|
||||
end
|
||||
return event
|
||||
end
|
||||
|
||||
function M.on(opts)
|
||||
table.insert(rules, opts)
|
||||
end
|
||||
|
||||
-- Register a user-defined device pattern so the daemon can correctly classify
|
||||
-- hardware that the automatic classifier doesn't recognise.
|
||||
--
|
||||
-- Usage:
|
||||
-- local devices = require("bread.devices")
|
||||
-- devices.register("CalDigit", "dock")
|
||||
-- devices.register("Keychron", "keyboard")
|
||||
-- devices.register("MX Master", "mouse")
|
||||
--
|
||||
-- The pattern is matched case-insensitively against the device name and vendor
|
||||
-- combined. The class must be one of: dock, keyboard, mouse, tablet, display,
|
||||
-- storage, audio, unknown.
|
||||
function M.register(pattern, class)
|
||||
table.insert(user_patterns, { pattern = pattern:lower(), class = class })
|
||||
end
|
||||
|
||||
function M.on_load()
|
||||
bread.on("bread.device.**", function(event)
|
||||
local patched = apply_user_patterns(event)
|
||||
for _, rule in ipairs(rules) do
|
||||
if matches_rule(rule, event) then
|
||||
run_rule(rule, event)
|
||||
if matches_rule(rule, patched) then
|
||||
run_rule(rule, patched)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
|
@ -1811,13 +1870,11 @@ fn hyprland_request(request: &str) -> Result<String> {
|
|||
use std::os::unix::net::UnixStream;
|
||||
|
||||
let socket = hyprland_request_socket()?;
|
||||
tokio::task::block_in_place(|| {
|
||||
let mut stream = UnixStream::connect(&socket)?;
|
||||
stream.write_all(request.as_bytes())?;
|
||||
let mut buffer = String::new();
|
||||
stream.read_to_string(&mut buffer)?;
|
||||
Ok(buffer)
|
||||
})
|
||||
let mut stream = UnixStream::connect(&socket)?;
|
||||
stream.write_all(request.as_bytes())?;
|
||||
let mut buffer = String::new();
|
||||
stream.read_to_string(&mut buffer)?;
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
fn list_lua_files(root: &Path) -> Result<Vec<PathBuf>> {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,6 @@ build() {
|
|||
package() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
install -Dm755 target/release/breadd "${pkgdir}/usr/bin/breadd"
|
||||
install -Dm755 target/release/bread-cli "${pkgdir}/usr/bin/bread-cli"
|
||||
install -Dm755 target/release/bread "${pkgdir}/usr/bin/bread"
|
||||
install -Dm644 packaging/systemd/breadd.service "${pkgdir}/usr/lib/systemd/user/breadd.service"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ Wants=graphical-session.target
|
|||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=%h/.cargo/bin/breadd
|
||||
ExecStart=/usr/bin/breadd
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
UMask=0077
|
||||
|
|
|
|||
36
scripts/install.sh
Executable file
36
scripts/install.sh
Executable file
|
|
@ -0,0 +1,36 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
INSTALL_PREFIX="${INSTALL_PREFIX:-/usr/bin}"
|
||||
SERVICE_DIR="${HOME}/.config/systemd/user"
|
||||
|
||||
# ── build ──────────────────────────────────────────────────────────────────────
|
||||
echo "building bread (release)..."
|
||||
cargo build --release --manifest-path "$REPO_ROOT/Cargo.toml"
|
||||
|
||||
# ── install binaries ───────────────────────────────────────────────────────────
|
||||
echo "installing binaries to $INSTALL_PREFIX (requires sudo)..."
|
||||
sudo install -Dm755 "$REPO_ROOT/target/release/breadd" "$INSTALL_PREFIX/breadd"
|
||||
sudo install -Dm755 "$REPO_ROOT/target/release/bread" "$INSTALL_PREFIX/bread"
|
||||
echo " installed $INSTALL_PREFIX/breadd"
|
||||
echo " installed $INSTALL_PREFIX/bread"
|
||||
|
||||
# ── systemd user service ───────────────────────────────────────────────────────
|
||||
echo "installing systemd user service..."
|
||||
mkdir -p "$SERVICE_DIR"
|
||||
install -Dm644 "$REPO_ROOT/packaging/systemd/breadd.service" "$SERVICE_DIR/breadd.service"
|
||||
echo " installed $SERVICE_DIR/breadd.service"
|
||||
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now breadd
|
||||
echo " breadd enabled and started"
|
||||
|
||||
# ── verify ─────────────────────────────────────────────────────────────────────
|
||||
sleep 0.5
|
||||
if bread ping &>/dev/null; then
|
||||
echo ""
|
||||
bread doctor
|
||||
else
|
||||
echo "warning: daemon did not respond to ping — check: journalctl --user -u breadd -n 20"
|
||||
fi
|
||||
Loading…
Add table
Add a link
Reference in a new issue