From 05593e0959b19e5d0f0edd698d3ca6cb54ac3eb0 Mon Sep 17 00:00:00 2001 From: Jory Severijnse Date: Sat, 11 Apr 2026 12:55:45 +0200 Subject: [PATCH] refactor: fixed security.yml and improved play/pause - New `--media-pause-icon` option - Unified play/pause button action - Move MPRIS lookup to spawn_blocking - Speed up security CI with cargo-binstall --- .github/workflows/security.yml | 15 ++-- src/config.rs | 9 +++ src/main.rs | 2 +- src/render.rs | 46 ++++++++++-- src/system.rs | 123 +++++++++++++++++---------------- 5 files changed, 121 insertions(+), 74 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 40fbc3e..ab306f9 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -25,10 +25,12 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable + - name: Install cargo-binstall + run: curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install.sh | sh + - name: Install security tools run: | - cargo install cargo-audit - cargo install cargo-deny + cargo binstall --no-confirm cargo-audit cargo-deny cargo-cyclonedx cargo-outdated - name: Run cargo audit run: cargo audit @@ -37,21 +39,16 @@ jobs: run: cargo deny check - name: Generate Software Bill of Materials (SBOM) - run: | - cargo install cargo-cyclonedx - cargo cyclonedx --format json --output bom.json + run: cargo cyclonedx --format json --override-filename bom - name: Upload SBOM uses: actions/upload-artifact@v4 with: name: sbom path: bom.json - retention-days: 90 - name: Check for outdated dependencies - run: | - cargo install cargo-outdated - cargo outdated --exit-code 1 || echo "Some dependencies are outdated" + run: cargo outdated --exit-code 1 || echo "Some dependencies are outdated" - name: Security summary run: | diff --git a/src/config.rs b/src/config.rs index 04c33d4..afa2ad0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -127,6 +127,9 @@ pub struct Config { #[arg(long)] pub media_play_icon: Option, + #[arg(long)] + pub media_pause_icon: Option, + #[arg(long)] pub media_next_icon: Option, @@ -251,6 +254,12 @@ impl Config { } } + if !is_cli("media_pause_icon") { + if let Some(toml::Value::String(s)) = table.get("media_pause_icon") { + config.media_pause_icon = Some(s.clone()); + } + } + if !is_cli("media_next_icon") { if let Some(toml::Value::String(s)) = table.get("media_next_icon") { config.media_next_icon = Some(s.clone()); diff --git a/src/main.rs b/src/main.rs index b0362dd..ffc423a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -595,7 +595,7 @@ impl PointerHandler for WaylandLock { for (action, rx, ry, rw, rh) in &surface.renderer.media_rects { if x >= *rx && x <= rx + rw && y >= *ry && y <= ry + rh { match action.as_str() { - "play" => self.system_manager.media_play_pause(), + "play_pause" => self.system_manager.media_play_pause(), "stop" => self.system_manager.media_stop(), "next" => self.system_manager.media_next(), "prev" => self.system_manager.media_prev(), diff --git a/src/render.rs b/src/render.rs index 2d80d2d..b0f7690 100644 --- a/src/render.rs +++ b/src/render.rs @@ -33,6 +33,7 @@ pub struct Renderer { media_prev_icon_surface: Option, media_stop_icon_surface: Option, media_play_icon_surface: Option, + media_pause_icon_surface: Option, media_next_icon_surface: Option, pub media_rects: Vec<(String, f64, f64, f64, f64)>, } @@ -73,6 +74,7 @@ impl Renderer { media_prev_icon_surface: None, media_stop_icon_surface: None, media_play_icon_surface: None, + media_pause_icon_surface: None, media_next_icon_surface: None, media_rects: Vec::new(), }; @@ -227,6 +229,25 @@ impl Renderer { self.media_play_icon_surface = self.load_icon(&play_path); } + let pause_names = ["media-playback-pause-symbolic", "media-playback-pause"]; + let pause_path = self + .config + .media_pause_icon + .clone() + .or_else(|| { + for name in &pause_names { + if let Some(path) = self.find_system_icon(name) { + return Some(path); + } + } + None + }) + .unwrap_or_default(); + if !pause_path.is_empty() { + log::debug!("Resolved Media Pause icon path: {}", pause_path); + self.media_pause_icon_surface = self.load_icon(&pause_path); + } + let next_names = ["media-skip-forward-symbolic", "media-skip-forward"]; let next_path = self .config @@ -959,13 +980,13 @@ impl Renderer { self.context.show_text(&display_text).unwrap(); let status_text = if self.system_status.media_playing { - if let Some(ref icon) = self.media_play_icon_surface { - let play_y = start_y + 40.0; + if let Some(ref icon) = self.media_pause_icon_surface { + let pause_y = start_y + 40.0; let rx = center_x - icon.width() as f64 / 2.0; - let ry = play_y - icon.height() as f64 / 2.0; + let ry = pause_y - icon.height() as f64 / 2.0; self.draw_icon_at(rx, ry, icon); self.media_rects.push(( - "play".to_string(), + "play_pause".to_string(), rx, ry, icon.width() as f64, @@ -976,7 +997,22 @@ impl Renderer { "▶ Playing" } } else { - "⏸ Paused" + if let Some(ref icon) = self.media_play_icon_surface { + let play_y = start_y + 40.0; + let rx = center_x - icon.width() as f64 / 2.0; + let ry = play_y - icon.height() as f64 / 2.0; + self.draw_icon_at(rx, ry, icon); + self.media_rects.push(( + "play_pause".to_string(), + rx, + ry, + icon.width() as f64, + icon.height() as f64, + )); + "" + } else { + "⏸ Paused" + } }; if !status_text.is_empty() { diff --git a/src/system.rs b/src/system.rs index c6d72ca..d261544 100644 --- a/src/system.rs +++ b/src/system.rs @@ -38,7 +38,6 @@ impl SystemManager { let s_clone = status.clone(); let (cmd_tx, mut cmd_rx) = mpsc::unbounded_channel::(); - // Spawn a thread to update status periodically and handle commands std::thread::spawn(move || { let rt = match tokio::runtime::Runtime::new() { Ok(rt) => rt, @@ -55,7 +54,6 @@ impl SystemManager { let mut last_art_data: Option>> = None; loop { - // Try to connect to system DBus if not connected if conn.is_none() { match Connection::system().await { Ok(c) => conn = Some(c), @@ -81,7 +79,9 @@ impl SystemManager { use std::collections::HashMap; if let Ok(props) = reply.body().deserialize::>() { if let Some(v) = props.get("Percentage") { - new_status.battery_percent = v.downcast_ref::().ok(); + if let Ok(val) = v.downcast_ref::() { + new_status.battery_percent = Some(val); + } } if let Some(v) = props.get("State") { if let Ok(state) = v.downcast_ref::() { @@ -90,47 +90,7 @@ impl SystemManager { } } } - } - // 2. MPRIS status - if let Ok(finder) = PlayerFinder::new() { - if let Ok(player) = finder.find_active() { - if let Ok(metadata) = player.get_metadata() { - new_status.media_title = metadata.title().map(|s| s.to_string()); - new_status.media_artist = metadata.artists().map(|a| a.join(", ")); - new_status.media_art_url = metadata.art_url().map(|u| u.to_string()); - - if new_status.media_art_url != last_art_url { - last_art_url = new_status.media_art_url.clone(); - last_art_data = None; - if let Some(ref url) = last_art_url { - if url.starts_with("file://") { - let path = url.trim_start_matches("file://"); - if let Ok(data) = std::fs::read(path) { - last_art_data = Some(Arc::new(data)); - } - } else if url.starts_with("http") { - #[cfg(feature = "networking")] - if let Ok(resp) = reqwest::get(url).await { - if let Ok(bytes) = resp.bytes().await { - last_art_data = Some(Arc::new(bytes.to_vec())); - } - } - #[cfg(not(feature = "networking"))] - { - log::debug!("Networking disabled, skipping remote album art: {}", url); - } - } - } - } - new_status.media_art_data = last_art_data.clone(); - } - new_status.media_playing = player.get_playback_status().map(|s| matches!(s, mpris::PlaybackStatus::Playing)).unwrap_or(false); - } - } - - // 3. WiFi status (NetworkManager) - if let Some(ref c) = conn { if let Ok(reply) = c.call_method( Some("org.freedesktop.NetworkManager"), "/org/freedesktop/NetworkManager", @@ -149,7 +109,7 @@ impl SystemManager { ).await { if let Ok(val) = dev_type_reply.body().deserialize::() { if let Ok(dev_type) = val.downcast_ref::() { - if dev_type == 2 { // WiFi + if dev_type == 2 { if let Ok(active_ap_reply) = c.call_method( Some("org.freedesktop.NetworkManager"), &dev_path, @@ -160,20 +120,20 @@ impl SystemManager { if let Ok(ap_val) = active_ap_reply.body().deserialize::() { if let Ok(ap_path) = ap_val.downcast_ref::() { if ap_path.as_str() != "/" { - if let Ok(ssid_reply) = c.call_method( - Some("org.freedesktop.NetworkManager"), - &ap_path, - Some("org.freedesktop.DBus.Properties"), - "Get", - &("org.freedesktop.NetworkManager.AccessPoint", "Ssid"), - ).await { - if let Ok(ssid_val) = ssid_reply.body().deserialize::() { - let ssid_bytes: Result, _> = ssid_val.try_into(); - if let Ok(ssid_bytes) = ssid_bytes { - new_status.wifi_ssid = Some(String::from_utf8_lossy(&ssid_bytes).to_string()); - } + if let Ok(ssid_reply) = c.call_method( + Some("org.freedesktop.NetworkManager"), + &ap_path, + Some("org.freedesktop.DBus.Properties"), + "Get", + &("org.freedesktop.NetworkManager.AccessPoint", "Ssid"), + ).await { + if let Ok(ssid_val) = ssid_reply.body().deserialize::() { + let ssid_bytes: Result, _> = ssid_val.try_into(); + if let Ok(ssid_bytes) = ssid_bytes { + new_status.wifi_ssid = Some(String::from_utf8_lossy(&ssid_bytes).to_string()); } } + } if let Ok(strength_reply) = c.call_method( Some("org.freedesktop.NetworkManager"), &ap_path, @@ -230,6 +190,54 @@ impl SystemManager { } } + let mpris_status = tokio::task::spawn_blocking(move || { + let mut media_title = None; + let mut media_artist = None; + let mut media_art_url = None; + let mut media_playing = false; + if let Ok(finder) = PlayerFinder::new() { + if let Ok(player) = finder.find_active() { + if let Ok(metadata) = player.get_metadata() { + media_title = metadata.title().map(|s| s.to_string()); + media_artist = metadata.artists().map(|a| a.join(", ")); + media_art_url = metadata.art_url().map(|u| u.to_string()); + } + media_playing = player.get_playback_status().map(|s| matches!(s, mpris::PlaybackStatus::Playing)).unwrap_or(false); + } + } + (media_title, media_artist, media_art_url, media_playing) + }).await.unwrap_or((None, None, None, false)); + + new_status.media_title = mpris_status.0; + new_status.media_artist = mpris_status.1; + new_status.media_art_url = mpris_status.2; + new_status.media_playing = mpris_status.3; + + if new_status.media_art_url != last_art_url { + last_art_url = new_status.media_art_url.clone(); + last_art_data = None; + if let Some(ref url) = last_art_url { + if url.starts_with("file://") { + let path = url.trim_start_matches("file://"); + if let Ok(data) = std::fs::read(path) { + last_art_data = Some(Arc::new(data)); + } + } else if url.starts_with("http") { + #[cfg(feature = "networking")] + if let Ok(resp) = reqwest::get(url).await { + if let Ok(bytes) = resp.bytes().await { + last_art_data = Some(Arc::new(bytes.to_vec())); + } + } + #[cfg(not(feature = "networking"))] + { + log::debug!("Networking disabled, skipping remote album art: {}", url); + } + } + } + } + new_status.media_art_data = last_art_data.clone(); + { if let Ok(mut s) = s_clone.lock() { *s = new_status; @@ -243,9 +251,7 @@ impl SystemManager { SystemCommand::Reboot => "Reboot", SystemCommand::Suspend => "Suspend", }; - debug!("Executing system command: {}", method); - // Set a timeout for the DBus call to prevent hanging the background thread let result = tokio::time::timeout( tokio::time::Duration::from_secs(5), c.call_method( @@ -256,7 +262,6 @@ impl SystemManager { &(true), ) ).await; - if result.is_err() { error!("System command {} timed out", method); }