pub extern crate simple_osd_common as osd; pub extern crate mpris; pub use std::sync::{Arc, Mutex}; use std::sync::atomic::{AtomicBool, Ordering}; use std::ops::{Deref}; pub use osd::notify::{OSD, OSDContents, OSDProgressText}; pub use osd::config::Config; use mpris::{PlaybackStatus, PlayerFinder}; pub use std::time::{Duration, SystemTime}; use std::vec::Vec; fn format_duration(d: Duration) -> String { let s = d.as_secs(); let secs = s % 60; let mins = s / 60; format!("{:02}:{:02}", mins, secs) } #[cfg(test)] mod format_duration_tests { use super::*; #[test] fn seconds() { assert_eq!(&format_duration(Duration::from_secs(10)), "00:10"); } #[test] fn minutes_seconds() { assert_eq!(&format_duration(Duration::from_secs(70)), "01:10"); } #[test] fn many_minutes_seconds() { assert_eq!(&format_duration(Duration::from_secs(7210)), "120:10"); } } fn format_artists(artists: Vec<&str>) -> Option { let mut v = artists.clone(); v.reverse(); if v.len() < 2 { return Some(v.pop()?.to_string()) } let mut s = String::new(); for _ in 0..v.len()-2 { s.push_str(v.pop()?); s.push_str(", ") } s.push_str(v.pop()?); s.push_str(" & "); s.push_str(v.pop()?); Some(s) } #[cfg(test)] mod format_artists_test { use super::*; #[test] fn none() { assert_eq!(format_artists([].to_vec()), None); } #[test] fn one() { assert_eq!(format_artists(["John Doe"].to_vec()), Some("John Doe".to_string())); } #[test] fn two() { assert_eq!(format_artists(["John Doe", "Jane Doe"].to_vec()), Some("John Doe & Jane Doe".to_string())); } #[test] fn many() { assert_eq!(format_artists(["John Doe", "Jane Doe", "Chris P. Bacon", "Seymore Clevarge"].to_vec()), Some("John Doe, Jane Doe, Chris P. Bacon & Seymore Clevarge".to_string())); } } #[cfg(feature = "display_on_volume_changes")] mod volume_changes { extern crate libpulse_binding as pulse; use super::*; use pulse::mainloop::standard::Mainloop; use pulse::mainloop::standard::IterateResult; use pulse::context::Context; use pulse::context::subscribe::{subscription_masks, Operation, Facility}; pub(super) struct VolumeMonitor { mainloop: Arc>, #[allow(dead_code)] context: Arc> } impl VolumeMonitor { pub fn new(config: Arc>, trigger: Arc>, dismissed: Arc>) -> VolumeMonitor { let mainloop = Arc::new(Mutex::new (Mainloop::new().expect("Failed to create mainloop"))); let context = Arc::new(Mutex::new(Context::new( mainloop.lock().unwrap().deref(), osd::APPNAME ).expect("Failed to create new context"))); context.lock().unwrap().connect(config.lock().unwrap().get::("pulseaudio", "server").as_deref(), 0, None) .expect("Failed to connect context"); // Wait for context to be ready loop { match mainloop.lock().unwrap().iterate(false) { IterateResult::Quit(_) | IterateResult::Err(_) => { panic!("Iterate state was not success, quitting..."); }, IterateResult::Success(_) => {}, } match context.lock().unwrap().get_state() { pulse::context::State::Ready => { break; }, pulse::context::State::Failed | pulse::context::State::Unconnected | pulse::context::State::Terminated => { panic!("Context state failed/terminated, quitting..."); }, _ => {} } } context.lock().unwrap().subscribe(subscription_masks::SINK, |success| { if ! success { eprintln!("failed to subscribe to events"); return; } }); let subscribe_callback = move |facility, operation, _index| { if facility == Some(Facility::Sink) && operation == Some(Operation::Changed) { *trigger.lock().unwrap() = SystemTime::now(); dismissed.lock().unwrap().store(false, Ordering::Relaxed); } }; context.lock().unwrap().set_subscribe_callback(Some(Box::new(subscribe_callback))); VolumeMonitor { mainloop, context } } pub fn tick(& self) { match self.mainloop.lock().unwrap().iterate(false) { IterateResult::Quit(_) | IterateResult::Err(_) => { panic!("Iterate state was not success, quitting..."); }, IterateResult::Success(_) => { }, } } } } fn main() { let config = Arc::new(Mutex::new(Config::new("mpris"))); let mut osd = OSD::new(); let mut waiting_on_close = false; let dismissed = Arc::new(Mutex::new(AtomicBool::new(false))); let player = PlayerFinder::new().unwrap().find_active().unwrap(); let mut progress_tracker = player.track_progress(100).unwrap(); let update_on_volume_change = config.lock().unwrap().get_default("default", "update on volume change", true); let timeout = config.lock().unwrap().get_default("default", "notification display time", 5); let trigger = Arc::new(Mutex::new(SystemTime::now())); #[cfg(feature = "display_on_volume_changes")] let vc = if update_on_volume_change { Some(volume_changes::VolumeMonitor::new(config.clone(), trigger.clone(), dismissed.clone())) } else { None }; drop(config); let mut title; let mut old_title = "".to_string(); let mut playback_status; let mut old_playback_status = PlaybackStatus::Stopped; loop { let progress = progress_tracker.tick().progress; title = progress.metadata().title().unwrap_or("Unknown").to_string(); playback_status = progress.playback_status(); if title != old_title || playback_status != old_playback_status { *trigger.lock().unwrap() = SystemTime::now(); dismissed.lock().unwrap().store(false, Ordering::Relaxed); } let elapsed = trigger.lock().unwrap().elapsed().unwrap_or_else(|_| Duration::from_secs(timeout + 1)); if elapsed.as_secs() < timeout && playback_status != PlaybackStatus::Stopped && ! dismissed.lock().unwrap().load(Ordering::Relaxed) { let metadata = progress.metadata(); let artists = metadata.artists().and_then(format_artists).unwrap_or_else(|| "Unknown".to_string()); let position = progress.position(); let length = progress.length().unwrap_or_else(|| Duration::from_secs(100000000)); osd.title = Some(format!("{:?}: {} - {}", playback_status, title, artists)); let ratio = position.as_secs_f32() / length.as_secs_f32(); let text = format!("{} / {}", format_duration(position), format_duration(length)); osd.contents = OSDContents::Progress(ratio, OSDProgressText::Text(Some(text))); osd.timeout = 1; osd.icon = match playback_status { PlaybackStatus::Playing => Some("media-playback-start".to_string()), PlaybackStatus::Paused => Some("media-playback-pause".to_string()), _ => None }; osd.update().unwrap(); if ! waiting_on_close { waiting_on_close = true; let dismissed_clone = dismissed.clone(); osd.on_close(move || { dismissed_clone.lock().unwrap().store(true, Ordering::Relaxed); }); } } else { waiting_on_close = false; osd.close() } old_title = title; old_playback_status = playback_status; #[cfg(feature = "display_on_volume_changes")] if let Some(v) = vc.as_ref() { v.tick() }; } }