Some checks failed
dev bread-theme / build (push) Successful in 17s
dev bakery / build (push) Successful in 43s
beta (rc) bakery / build (push) Has been skipped
beta (rc) bread-theme / build (push) Has been skipped
Build and publish package / package (push) Successful in 1m46s
release bakery / build (push) Failing after 52s
release bread-theme / build (push) Failing after 14s
bread-app is the GTK bootstrap new tools should use instead of another copied main.rs: com.breadway.* app id, singleton lock, optional gtk_popup re-export, optional bread.command.<app>.** listen loop. Tests cover app-id helpers and command-verb parse. Existing apps are not migrated. bread-polkit is an own PolicyKit1 session authentication agent with a bread-theme GTK4 password prompt (not a polkit-gnome wrapper). Autostart via contrib/bread-polkit.desktop or exec-once. Not a bakery product; not added to the BOS ISO lockfile.
121 lines
3.9 KiB
Rust
121 lines
3.9 KiB
Rust
//! Command-bus helpers for `bread.command.<app>.**`.
|
|
//!
|
|
//! The `command_id` here is the breadd sibling-app id (`clip`, `box`,
|
|
//! `shot`) — often shorter than the GTK / singleton name (`breadclip`).
|
|
|
|
use crate::id::{parse_app_name, InvalidAppId};
|
|
use bread_utils::bread_client::{BreadClient, BreadEvent, Subscription};
|
|
|
|
/// Same charset as [`parse_app_name`]: a single command-bus segment.
|
|
pub fn parse_command_id(command_id: &str) -> Result<&str, InvalidAppId> {
|
|
parse_app_name(command_id)
|
|
}
|
|
|
|
/// Subscribe glob: `bread.command.<app>.**`.
|
|
pub fn command_pattern(command_id: &str) -> Result<String, InvalidAppId> {
|
|
let id = parse_command_id(command_id)?;
|
|
Ok(format!("bread.command.{id}.**"))
|
|
}
|
|
|
|
/// The verb segment of `bread.command.<app>.<verb>` (and extra trailing
|
|
/// segments, if any). `None` when the event is not addressed to
|
|
/// `command_id` or the verb is missing.
|
|
///
|
|
/// Extra dotted remainder (`bread.command.clip.stack.clear`) yields the
|
|
/// first remaining segment (`stack`) — a verb is one segment, matching
|
|
/// [`BreadClient::command`].
|
|
pub fn command_verb<'a>(event: &'a str, command_id: &str) -> Option<&'a str> {
|
|
if command_id.is_empty() {
|
|
return None;
|
|
}
|
|
let prefix = format!("bread.command.{command_id}.");
|
|
let rest = event.strip_prefix(&prefix)?;
|
|
let verb = rest.split('.').next()?;
|
|
if verb.is_empty() {
|
|
None
|
|
} else {
|
|
Some(verb)
|
|
}
|
|
}
|
|
|
|
/// Subscribe to `bread.command.<command_id>.**` and invoke `on_verb` with
|
|
/// the parsed verb plus the raw event.
|
|
///
|
|
/// Fail-silent: constructing the client and holding the subscription never
|
|
/// requires breadd to be running. Drop the returned [`Subscription`] (or
|
|
/// call [`Subscription::stop`]) to end the loop.
|
|
pub fn listen_commands<F>(command_id: &str, on_verb: F) -> Result<Subscription, InvalidAppId>
|
|
where
|
|
F: Fn(&str, BreadEvent) + Send + 'static,
|
|
{
|
|
let id = parse_command_id(command_id)?.to_string();
|
|
let client = BreadClient::connect(id.clone());
|
|
let pattern = format!("bread.command.{id}.**");
|
|
Ok(client.subscribe(pattern, move |event| {
|
|
let Some(verb) = command_verb(&event.event, &id).map(str::to_owned) else {
|
|
return;
|
|
};
|
|
on_verb(&verb, event);
|
|
}))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn command_pattern_uses_double_star() {
|
|
assert_eq!(command_pattern("clip").unwrap(), "bread.command.clip.**");
|
|
assert_eq!(command_pattern("shot").unwrap(), "bread.command.shot.**");
|
|
}
|
|
|
|
#[test]
|
|
fn command_pattern_rejects_invalid_id() {
|
|
assert!(command_pattern("").is_err());
|
|
assert!(command_pattern("clip.clear").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn command_verb_strips_app_prefix() {
|
|
assert_eq!(
|
|
command_verb("bread.command.clip.clear", "clip"),
|
|
Some("clear")
|
|
);
|
|
assert_eq!(
|
|
command_verb("bread.command.shot.region", "shot"),
|
|
Some("region")
|
|
);
|
|
assert_eq!(
|
|
command_verb("bread.command.shot.annotate", "shot"),
|
|
Some("annotate")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn command_verb_takes_first_segment_only() {
|
|
assert_eq!(
|
|
command_verb("bread.command.clip.stack.clear", "clip"),
|
|
Some("stack")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn command_verb_rejects_other_apps_and_missing_verb() {
|
|
assert_eq!(command_verb("bread.command.clip.clear", "shot"), None);
|
|
assert_eq!(command_verb("bread.command.clip", "clip"), None);
|
|
assert_eq!(command_verb("bread.command.clip.", "clip"), None);
|
|
assert_eq!(command_verb("bread.clip.copied", "clip"), None);
|
|
assert_eq!(command_verb("bread.command.clip.clear", ""), None);
|
|
}
|
|
|
|
#[test]
|
|
fn listen_commands_rejects_invalid_id() {
|
|
assert!(listen_commands("", |_, _| {}).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn listen_commands_stop_joins_without_a_daemon() {
|
|
let sub = listen_commands("clip", |_, _| {}).unwrap();
|
|
sub.stop();
|
|
}
|
|
}
|