breadmon: clear error for missing BOS hyprctl eval extension, doc, version fix

- apply_monitors() shells out to `hyprctl eval` with a `hl.monitor()`
  Lua call, a BOS-only Hyprland extension. On vanilla Hyprland the
  eval request itself isn't recognized and failed with a confusing
  raw hyprctl response. Detect that case and fail with an explicit
  'this feature requires BOS' message instead, with tests.
- README: documented the BOS dependency for the apply step under
  Requirements; noted everything else in the TUI works without it.
- Cargo.toml: 0.1.0 -> 0.1.1, matching the latest tag (v0.1.1).
This commit is contained in:
Breadway 2026-07-17 03:22:52 +08:00
parent 4bbe976e7a
commit ddbc9e63d3
4 changed files with 44 additions and 3 deletions

2
Cargo.lock generated
View file

@ -22,7 +22,7 @@ checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
[[package]] [[package]]
name = "breadmon" name = "breadmon"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"crossterm", "crossterm",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "breadmon" name = "breadmon"
version = "0.1.0" version = "0.1.1"
edition = "2021" edition = "2021"
description = "TUI monitor manager for Hyprland" description = "TUI monitor manager for Hyprland"
license = "MIT" license = "MIT"

View file

@ -4,7 +4,8 @@ A terminal UI monitor manager for Hyprland. Lets you position, configure, and mi
## Requirements ## Requirements
- Hyprland compositor (the `hyprctl` binary must be on `PATH`) - **[BOS (Bread OS)](https://git.breadway.dev/breadway/bos)'s patched Hyprland build.** Applying changes (the `a` key / Global keys "Apply") runs `hyprctl eval` with a `hl.monitor({...})` Lua call — a BOS-specific extension that does not exist on vanilla/upstream Hyprland. On a non-BOS Hyprland install, `hyprctl eval` itself is not a recognized request, and breadmon will fail to apply with an explicit error explaining this instead of the raw hyprctl response. Everything else in the TUI (viewing/arranging/saving profiles) works regardless; only the live-apply step needs BOS.
- The `hyprctl` binary must be on `PATH`
- Rust toolchain (to build from source) - Rust toolchain (to build from source)
## Build ## Build

View file

@ -367,6 +367,15 @@ pub async fn apply_monitors(monitors: &[Monitor]) -> Result<()> {
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_owned(); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_owned();
if stdout != "ok" { if stdout != "ok" {
if is_missing_eval_extension(&stdout) {
return Err(anyhow::anyhow!(
"breadmon: `hyprctl eval` (and the `hl.monitor()` Lua extension it uses \
to apply monitor changes) is not available on this compositor. \
This feature requires BOS (Bread OS)'s patched Hyprland build it \
does not exist on vanilla/upstream Hyprland. See the breadmon README \
for details. (raw hyprctl response: {stdout:?})"
));
}
return Err(anyhow::anyhow!("hyprctl: {}", stdout)); return Err(anyhow::anyhow!("hyprctl: {}", stdout));
} }
} }
@ -382,6 +391,37 @@ pub async fn apply_monitors(monitors: &[Monitor]) -> Result<()> {
Ok(()) Ok(())
} }
/// Vanilla/upstream Hyprland doesn't recognize the `eval` request at all
/// (BOS's Hyprland fork adds it, along with the `hl.monitor()` Lua global
/// used above), and responds with an "unknown request" style message rather
/// than an error about `hl`. Detect that case so we can point the user at
/// the actual cause instead of surfacing the raw hyprctl text.
fn is_missing_eval_extension(hyprctl_stdout: &str) -> bool {
let s = hyprctl_stdout.to_lowercase();
s.contains("unknown request") || s.contains("unknown command")
}
#[cfg(test)]
mod eval_extension_tests {
use super::is_missing_eval_extension;
#[test]
fn detects_unknown_request() {
assert!(is_missing_eval_extension("unknown request"));
assert!(is_missing_eval_extension("Unknown Request"));
}
#[test]
fn does_not_flag_lua_errors() {
// A real Lua/apply-time error from a BOS build should still surface
// as a normal hyprctl error, not the "requires BOS" message.
assert!(!is_missing_eval_extension(
"error: attempt to call a nil value"
));
assert!(!is_missing_eval_extension("ok"));
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;