diff --git a/Cargo.lock b/Cargo.lock index 055c2c5..07aa126 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -216,6 +216,20 @@ dependencies = [ "libloading", ] +[[package]] +name = "ashpd" +version = "0.13.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8421aaa9644a5faf26735f258b669b15f063313ef8f8e2bdb28912a1a6f111" +dependencies = [ + "enumflags2", + "futures-util", + "getrandom 0.4.3", + "serde", + "tokio", + "zbus", +] + [[package]] name = "assert_matches" version = "1.5.0" @@ -816,6 +830,7 @@ dependencies = [ name = "cardwire-gui" version = "0.13.0-alpha.1" dependencies = [ + "ashpd", "chrono", "clap", "env_logger", @@ -1349,7 +1364,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4096,7 +4111,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4595,10 +4610,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5492,7 +5507,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8e4368e..972c424 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ toml = "1.0.3" vulkano = "0.35.2" khronos-egl = { version= "6.0.0", features = ["dynamic", "1_5"] } rusqlite = { version = "0.40.1", features = ["bundled"] } +ashpd = { version = "0.13.13", features = ["global_shortcuts"] } # EBPF aya = "0.14.0" diff --git a/assets/cardwire-gui.desktop b/assets/cardwire-gui.desktop index 197eae5..605a289 100644 --- a/assets/cardwire-gui.desktop +++ b/assets/cardwire-gui.desktop @@ -8,3 +8,24 @@ Terminal=false Type=Application Categories=System;Settings;HardwareSettings; Keywords=GPU;Graphics;Hardware;Management; +Actions=Hybrid;Integrated;Smart;Manual; + +[Desktop Action Hybrid] +Name=Switch to Hybrid Mode +Exec=cardwire set hybrid +Icon=org.opengamingcollective.cardwire.tray-hybrid + +[Desktop Action Integrated] +Name=Switch to Integrated Mode +Exec=cardwire set integrated +Icon=org.opengamingcollective.cardwire.tray-integrated + +[Desktop Action Smart] +Name=Switch to Smart Mode +Exec=cardwire set smart +Icon=org.opengamingcollective.cardwire.tray-smart + +[Desktop Action Manual] +Name=Switch to Manual Mode +Exec=cardwire set manual +Icon=org.opengamingcollective.cardwire.tray-manual diff --git a/crates/cardwire-gui/Cargo.toml b/crates/cardwire-gui/Cargo.toml index ac720a6..36656f6 100644 --- a/crates/cardwire-gui/Cargo.toml +++ b/crates/cardwire-gui/Cargo.toml @@ -25,6 +25,7 @@ xdg.workspace = true chrono.workspace = true freedesktop-desktop-entry.workspace = true clap.workspace = true +ashpd.workspace = true [[bin]] name = "cardwire-gui" diff --git a/crates/cardwire-gui/src/app.rs b/crates/cardwire-gui/src/app.rs index 4c63491..6df31f3 100644 --- a/crates/cardwire-gui/src/app.rs +++ b/crates/cardwire-gui/src/app.rs @@ -277,6 +277,23 @@ impl AppState { return self.open_or_focus_window(); } } + Message::GlobalShortcutTriggered(id) => match id.as_str() { + "cycle_mode" => { + let modes = &self.main_state.available_modes; + if !modes.is_empty() { + let current = self.main_state.current_mode.unwrap_or(modes[0]); + let idx = modes.iter().position(|&m| m == current).unwrap_or(0); + let next_mode = modes[(idx + 1) % modes.len()]; + return self.update(Message::SetMode(next_mode)); + } + } + "set_hybrid" => return self.update(Message::SetMode(Mode::Hybrid)), + "set_integrated" => return self.update(Message::SetMode(Mode::Integrated)), + "set_smart" => return self.update(Message::SetMode(Mode::Smart)), + "set_manual" => return self.update(Message::SetMode(Mode::Manual)), + "toggle_gui" => return self.open_or_focus_window(), + _ => {} + }, Message::TrayShutdownComplete => return iced::exit(), Message::WindowClosed(id) => { if self.window_id == Some(id) { diff --git a/crates/cardwire-gui/src/message.rs b/crates/cardwire-gui/src/message.rs index ede0f41..b6f8e58 100644 --- a/crates/cardwire-gui/src/message.rs +++ b/crates/cardwire-gui/src/message.rs @@ -47,5 +47,6 @@ pub enum Message { OpenUrl(String), ClearError, ClearInfo, + GlobalShortcutTriggered(String), None, } diff --git a/crates/cardwire-gui/src/subscription.rs b/crates/cardwire-gui/src/subscription.rs index 4d346c3..e12725d 100644 --- a/crates/cardwire-gui/src/subscription.rs +++ b/crates/cardwire-gui/src/subscription.rs @@ -15,6 +15,10 @@ use zbus::{ Connection, Proxy, names::OwnedInterfaceName, proxy, zvariant::{OwnedObjectPath, OwnedValue} }; +use ashpd::desktop::{ + CreateSessionOptions, global_shortcuts::{BindShortcutsOptions, GlobalShortcuts, NewShortcut} +}; + pub fn tray_sub() -> Subscription { Subscription::run_with("cardwire_tray_subscription", |_id| { stream::channel(10, |mut output: Sender| async move { @@ -720,6 +724,99 @@ fn smart_sub() -> Subscription { }) } +pub fn shortcuts_sub() -> Subscription { + Subscription::run_with("cardwire_shortcuts_subscription", |_id| { + stream::channel(10, |mut output: Sender| async move { + let conn = match Connection::session().await { + Ok(c) => c, + Err(err) => { + log::warn!("Failed to open session bus for shortcuts: {err}"); + std::future::pending::<()>().await; + return; + } + }; + + // Register this connection as cardwire-gui with the portal + let options = HashMap::::new(); + if let Err(err) = conn + .call_method( + Some("org.freedesktop.portal.Desktop"), + "/org/freedesktop/portal/desktop", + Some("org.freedesktop.host.portal.Registry"), + "Register", + &("cardwire-gui", options), + ) + .await + { + log::warn!("Failed to register app ID with portal: {err}"); + } + + // Connect to GlobalShortcuts using the same connection + let proxy = match GlobalShortcuts::with_connection(conn).await { + Ok(p) => p, + Err(err) => { + log::warn!("Global shortcuts portal unavailable: {err}"); + std::future::pending::<()>().await; + return; + } + }; + + // Create session with the portal + let session = match proxy.create_session(CreateSessionOptions::default()).await { + Ok(s) => s, + Err(err) => { + log::warn!("Failed to create global shortcuts session: {err}"); + std::future::pending::<()>().await; + return; + } + }; + + // Define the shortcuts + let shortcuts = [ + NewShortcut::new("cycle_mode", "Cycle GPU Mode"), + NewShortcut::new("set_hybrid", "Switch to Hybrid Mode"), + NewShortcut::new("set_integrated", "Switch to Integrated Mode"), + NewShortcut::new("set_smart", "Switch to Smart Mode"), + NewShortcut::new("set_manual", "Switch to Manual Mode"), + NewShortcut::new("toggle_gui", "Open / Focus Cardwire Window"), + ]; + + if let Err(err) = proxy + .bind_shortcuts(&session, &shortcuts, None, BindShortcutsOptions::default()) + .await + { + log::warn!("Failed to bind global shortcuts: {err}"); + std::future::pending::<()>().await; + return; + } + + // Listen for activation signals + let mut stream = match proxy.receive_activated().await { + Ok(s) => s, + Err(err) => { + log::warn!("Failed to receive activated stream: {err}"); + std::future::pending::<()>().await; + return; + } + }; + + // keep session alive while listening + let _keep_session = session; + + while let Some(activated) = stream.next().await { + let shortcut_id = activated.shortcut_id().to_string(); + if output + .send(Message::GlobalShortcutTriggered(shortcut_id)) + .await + .is_err() + { + return; + } + } + }) + }) +} + pub fn dbus_sub() -> Subscription { Subscription::batch([ config_sub(), @@ -728,5 +825,6 @@ pub fn dbus_sub() -> Subscription { pci_sub(), logger_sub(), smart_sub(), + shortcuts_sub(), ]) }