refactor: fixed security.yml and improved play/pause
Code Quality / quality-checks (push) Has been cancelled
Security Scan / security-audit (push) Has been cancelled

- New `--media-pause-icon` option
- Unified play/pause button action
- Move MPRIS lookup to spawn_blocking
- Speed up security CI with cargo-binstall
This commit is contained in:
2026-04-11 12:55:45 +02:00
parent 37fb09c7c6
commit 05593e0959
5 changed files with 121 additions and 74 deletions
+6 -9
View File
@@ -25,10 +25,12 @@ jobs:
- name: Install Rust - name: Install Rust
uses: dtolnay/rust-toolchain@stable 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 - name: Install security tools
run: | run: |
cargo install cargo-audit cargo binstall --no-confirm cargo-audit cargo-deny cargo-cyclonedx cargo-outdated
cargo install cargo-deny
- name: Run cargo audit - name: Run cargo audit
run: cargo audit run: cargo audit
@@ -37,21 +39,16 @@ jobs:
run: cargo deny check run: cargo deny check
- name: Generate Software Bill of Materials (SBOM) - name: Generate Software Bill of Materials (SBOM)
run: | run: cargo cyclonedx --format json --override-filename bom
cargo install cargo-cyclonedx
cargo cyclonedx --format json --output bom.json
- name: Upload SBOM - name: Upload SBOM
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: sbom name: sbom
path: bom.json path: bom.json
retention-days: 90
- name: Check for outdated dependencies - name: Check for outdated dependencies
run: | run: cargo outdated --exit-code 1 || echo "Some dependencies are outdated"
cargo install cargo-outdated
cargo outdated --exit-code 1 || echo "Some dependencies are outdated"
- name: Security summary - name: Security summary
run: | run: |
+9
View File
@@ -127,6 +127,9 @@ pub struct Config {
#[arg(long)] #[arg(long)]
pub media_play_icon: Option<String>, pub media_play_icon: Option<String>,
#[arg(long)]
pub media_pause_icon: Option<String>,
#[arg(long)] #[arg(long)]
pub media_next_icon: Option<String>, pub media_next_icon: Option<String>,
@@ -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 !is_cli("media_next_icon") {
if let Some(toml::Value::String(s)) = table.get("media_next_icon") { if let Some(toml::Value::String(s)) = table.get("media_next_icon") {
config.media_next_icon = Some(s.clone()); config.media_next_icon = Some(s.clone());
+1 -1
View File
@@ -595,7 +595,7 @@ impl PointerHandler for WaylandLock {
for (action, rx, ry, rw, rh) in &surface.renderer.media_rects { for (action, rx, ry, rw, rh) in &surface.renderer.media_rects {
if x >= *rx && x <= rx + rw && y >= *ry && y <= ry + rh { if x >= *rx && x <= rx + rw && y >= *ry && y <= ry + rh {
match action.as_str() { 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(), "stop" => self.system_manager.media_stop(),
"next" => self.system_manager.media_next(), "next" => self.system_manager.media_next(),
"prev" => self.system_manager.media_prev(), "prev" => self.system_manager.media_prev(),
+40 -4
View File
@@ -33,6 +33,7 @@ pub struct Renderer {
media_prev_icon_surface: Option<ImageSurface>, media_prev_icon_surface: Option<ImageSurface>,
media_stop_icon_surface: Option<ImageSurface>, media_stop_icon_surface: Option<ImageSurface>,
media_play_icon_surface: Option<ImageSurface>, media_play_icon_surface: Option<ImageSurface>,
media_pause_icon_surface: Option<ImageSurface>,
media_next_icon_surface: Option<ImageSurface>, media_next_icon_surface: Option<ImageSurface>,
pub media_rects: Vec<(String, f64, f64, f64, f64)>, pub media_rects: Vec<(String, f64, f64, f64, f64)>,
} }
@@ -73,6 +74,7 @@ impl Renderer {
media_prev_icon_surface: None, media_prev_icon_surface: None,
media_stop_icon_surface: None, media_stop_icon_surface: None,
media_play_icon_surface: None, media_play_icon_surface: None,
media_pause_icon_surface: None,
media_next_icon_surface: None, media_next_icon_surface: None,
media_rects: Vec::new(), media_rects: Vec::new(),
}; };
@@ -227,6 +229,25 @@ impl Renderer {
self.media_play_icon_surface = self.load_icon(&play_path); 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_names = ["media-skip-forward-symbolic", "media-skip-forward"];
let next_path = self let next_path = self
.config .config
@@ -959,13 +980,13 @@ impl Renderer {
self.context.show_text(&display_text).unwrap(); self.context.show_text(&display_text).unwrap();
let status_text = if self.system_status.media_playing { let status_text = if self.system_status.media_playing {
if let Some(ref icon) = self.media_play_icon_surface { if let Some(ref icon) = self.media_pause_icon_surface {
let play_y = start_y + 40.0; let pause_y = start_y + 40.0;
let rx = center_x - icon.width() as f64 / 2.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.draw_icon_at(rx, ry, icon);
self.media_rects.push(( self.media_rects.push((
"play".to_string(), "play_pause".to_string(),
rx, rx,
ry, ry,
icon.width() as f64, icon.width() as f64,
@@ -975,8 +996,23 @@ impl Renderer {
} else { } else {
"▶ Playing" "▶ Playing"
} }
} else {
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 { } else {
"⏸ Paused" "⏸ Paused"
}
}; };
if !status_text.is_empty() { if !status_text.is_empty() {
+52 -47
View File
@@ -38,7 +38,6 @@ impl SystemManager {
let s_clone = status.clone(); let s_clone = status.clone();
let (cmd_tx, mut cmd_rx) = mpsc::unbounded_channel::<SystemCommand>(); let (cmd_tx, mut cmd_rx) = mpsc::unbounded_channel::<SystemCommand>();
// Spawn a thread to update status periodically and handle commands
std::thread::spawn(move || { std::thread::spawn(move || {
let rt = match tokio::runtime::Runtime::new() { let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt, Ok(rt) => rt,
@@ -55,7 +54,6 @@ impl SystemManager {
let mut last_art_data: Option<Arc<Vec<u8>>> = None; let mut last_art_data: Option<Arc<Vec<u8>>> = None;
loop { loop {
// Try to connect to system DBus if not connected
if conn.is_none() { if conn.is_none() {
match Connection::system().await { match Connection::system().await {
Ok(c) => conn = Some(c), Ok(c) => conn = Some(c),
@@ -81,7 +79,9 @@ impl SystemManager {
use std::collections::HashMap; use std::collections::HashMap;
if let Ok(props) = reply.body().deserialize::<HashMap<String, zbus::zvariant::OwnedValue>>() { if let Ok(props) = reply.body().deserialize::<HashMap<String, zbus::zvariant::OwnedValue>>() {
if let Some(v) = props.get("Percentage") { if let Some(v) = props.get("Percentage") {
new_status.battery_percent = v.downcast_ref::<f64>().ok(); if let Ok(val) = v.downcast_ref::<f64>() {
new_status.battery_percent = Some(val);
}
} }
if let Some(v) = props.get("State") { if let Some(v) = props.get("State") {
if let Ok(state) = v.downcast_ref::<u32>() { if let Ok(state) = v.downcast_ref::<u32>() {
@@ -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( if let Ok(reply) = c.call_method(
Some("org.freedesktop.NetworkManager"), Some("org.freedesktop.NetworkManager"),
"/org/freedesktop/NetworkManager", "/org/freedesktop/NetworkManager",
@@ -149,7 +109,7 @@ impl SystemManager {
).await { ).await {
if let Ok(val) = dev_type_reply.body().deserialize::<zbus::zvariant::OwnedValue>() { if let Ok(val) = dev_type_reply.body().deserialize::<zbus::zvariant::OwnedValue>() {
if let Ok(dev_type) = val.downcast_ref::<u32>() { if let Ok(dev_type) = val.downcast_ref::<u32>() {
if dev_type == 2 { // WiFi if dev_type == 2 {
if let Ok(active_ap_reply) = c.call_method( if let Ok(active_ap_reply) = c.call_method(
Some("org.freedesktop.NetworkManager"), Some("org.freedesktop.NetworkManager"),
&dev_path, &dev_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() { if let Ok(mut s) = s_clone.lock() {
*s = new_status; *s = new_status;
@@ -243,9 +251,7 @@ impl SystemManager {
SystemCommand::Reboot => "Reboot", SystemCommand::Reboot => "Reboot",
SystemCommand::Suspend => "Suspend", SystemCommand::Suspend => "Suspend",
}; };
debug!("Executing system command: {}", method); debug!("Executing system command: {}", method);
// Set a timeout for the DBus call to prevent hanging the background thread
let result = tokio::time::timeout( let result = tokio::time::timeout(
tokio::time::Duration::from_secs(5), tokio::time::Duration::from_secs(5),
c.call_method( c.call_method(
@@ -256,7 +262,6 @@ impl SystemManager {
&(true), &(true),
) )
).await; ).await;
if result.is_err() { if result.is_err() {
error!("System command {} timed out", method); error!("System command {} timed out", method);
} }