mod args;
mod mpd;
use std::collections::HashMap;
use std::env::args_os;
use std::path::Path;
use std::thread::sleep;
use std::time::Duration;
use anyhow::ensure;
use anyhow::Context as _;
use anyhow::Result;
use clap::error::ErrorKind;
use clap::Parser as _;
use inotify::Inotify;
use inotify::WatchMask;
use zbus::blocking::connection::Builder as ConnectionBuilder;
use zbus::names::WellKnownName;
use zbus::zvariant::Value;
use zbus::Address;
use crate::args::Args;
fn send_notification(summary: &str) -> Result<()> {
let appname = env!("CARGO_PKG_NAME");
let replaces_id = 1u32;
let icon = "";
let body = "";
let hints = HashMap::<&str, Value>::new();
let timeout = 5000i32;
let address = Address::session().context("failed to get D-Bus session address")?;
let connection = ConnectionBuilder::address(address.clone())
.with_context(|| format!("failed to create connection builder for address {address}"))?
.build()
.with_context(|| format!("failed to establish D-Bus session connection to {address}"))?;
let bus = WellKnownName::from_static_str_unchecked("org.freedesktop.Notifications");
let destination = Some(bus);
let path = "/org/freedesktop/Notifications";
let interface = "org.freedesktop.Notifications";
let method = "Notify";
let _msg_id = connection
.call_method(
destination.clone(),
path,
Some(interface),
method,
&(
appname,
replaces_id,
icon,
summary,
body,
[""; 0].as_slice(),
&hints,
timeout,
),
)
.with_context(|| format!("failed to call {method} method on {interface}"))?
.body()
.deserialize::<u32>()
.context("failed to deserialize D-Bus message body")?;
Ok(())
}
pub fn run() -> Result<()> {
let _args = match Args::try_parse_from(args_os()) {
Ok(args) => args,
Err(err) => match err.kind() {
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => {
print!("{err}");
return Ok(())
},
_ => return Err(err.into()),
},
};
let config_path = mpd::find_config()?;
let config = mpd::parse_config_file(&config_path).context("failed to parse MPD config file")?;
let state_file = config
.get("state_file")
.context("MPD configuration does not specify `state_file`")?;
let mut inotify = Inotify::init().context("failed to create file watcher")?;
let mut buffer = [0u8; 1024];
let mut previous = None;
loop {
let _descriptor = inotify
.watches()
.add(state_file, WatchMask::CREATE)
.with_context(|| format!("failed to add file watch for `{state_file}`"))?;
let mut events = inotify
.read_events_blocking(&mut buffer)
.with_context(|| format!("failed to wait for inotify event on `{state_file}`"))?;
if events.next().is_some() {
let path = Path::new(state_file);
let mut i = 0;
while !path.exists() {
i += 1;
ensure!(
i < 500,
"failed to find MPD state file at `{}`",
path.display()
);
let () = sleep(Duration::from_millis(1));
}
let current =
mpd::parse_state_file_current(path).context("failed to parse MPD state file")?;
if current != previous {
if let Some(current) = ¤t {
let () = send_notification(current).context("failed to send DBus notification")?;
}
}
previous = current;
}
}
}