Reads notification history via makoctl, formats entries as pango markup with resolved app icons, and pipes them to wofi in dmenu mode for selection and invocation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
75 lines
2.1 KiB
Rust
75 lines
2.1 KiB
Rust
mod format;
|
|
mod icon;
|
|
mod notification;
|
|
|
|
use std::io::Write;
|
|
use std::process::{Command, Stdio};
|
|
|
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let notifications = notification::history()?;
|
|
if notifications.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
let entries: Vec<String> = notifications
|
|
.iter()
|
|
.map(|n| {
|
|
let app_name = n.app_name.as_deref().unwrap_or("unknown");
|
|
let summary = n.summary.as_deref().unwrap_or("");
|
|
let body = n.body.as_deref().unwrap_or("");
|
|
let text = format::entry_text(app_name, summary, body);
|
|
|
|
match icon::resolve(
|
|
n.app_icon.as_deref(),
|
|
n.desktop_entry.as_deref(),
|
|
n.app_name.as_deref(),
|
|
) {
|
|
Some(path) => format!("img:{}:text:{text}", path.display()),
|
|
None => text,
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
let Some(index) = prompt_selection(&entries)? else {
|
|
return Ok(());
|
|
};
|
|
|
|
if let Some(notification) = notifications.get(index) {
|
|
notification::invoke(notification.id)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Run wofi in dmenu mode with the given entries and return the selected
|
|
/// index, or `None` if the user cancelled (Escape / clicked outside).
|
|
fn prompt_selection(entries: &[String]) -> Result<Option<usize>, Box<dyn std::error::Error>> {
|
|
let mut child = Command::new("wofi")
|
|
.args([
|
|
"--dmenu",
|
|
"-p",
|
|
"Notifications",
|
|
"-D",
|
|
"dmenu-print_line_num=true",
|
|
])
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.spawn()?;
|
|
|
|
let mut stdin = child.stdin.take().expect("stdin was piped");
|
|
stdin.write_all(entries.join("\n").as_bytes())?;
|
|
drop(stdin);
|
|
|
|
let output = child.wait_with_output()?;
|
|
if !output.status.success() {
|
|
return Ok(None);
|
|
}
|
|
|
|
let selection = String::from_utf8(output.stdout)?;
|
|
let selection = selection.trim();
|
|
if selection.is_empty() {
|
|
return Ok(None);
|
|
}
|
|
|
|
Ok(selection.parse::<usize>().ok())
|
|
}
|