From 343a8f5e9aa272064a420e1863d36c13eee301f9 Mon Sep 17 00:00:00 2001 From: luytan Date: Sat, 8 Aug 2026 14:50:09 +0200 Subject: [PATCH 1/4] feat(cardwire-cli): fetch modes using DBus and check daemon connection before processing args --- crates/cardwire-cli/src/args.rs | 6 ++++- crates/cardwire-cli/src/dbus.rs | 11 ++++++++ crates/cardwire-cli/src/main.rs | 47 +++++++++++++++++++++++++++++---- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/crates/cardwire-cli/src/args.rs b/crates/cardwire-cli/src/args.rs index c01741d7..2ced9221 100644 --- a/crates/cardwire-cli/src/args.rs +++ b/crates/cardwire-cli/src/args.rs @@ -1,7 +1,11 @@ use clap::{Args as ClapArgs, Parser, Subcommand, ValueEnum}; use clap_complete::Shell; +use serde::{Deserialize, Serialize}; use std::fmt; -#[derive(Clone, Debug, ValueEnum)] +#[derive( + Clone, Copy, Debug, PartialEq, Eq, ValueEnum, Serialize, Deserialize, zbus::zvariant::Type, +)] +#[serde(rename_all = "snake_case")] pub enum CliMode { Integrated, Hybrid, diff --git a/crates/cardwire-cli/src/dbus.rs b/crates/cardwire-cli/src/dbus.rs index 856dea22..71c8962f 100644 --- a/crates/cardwire-cli/src/dbus.rs +++ b/crates/cardwire-cli/src/dbus.rs @@ -102,6 +102,17 @@ impl<'a> DaemonClient<'a> { proxy.get_property("Mode").await } + pub async fn get_available_modes(&self) -> zbus::Result> { + let proxy = zbus::Proxy::new( + self.proxy.connection(), + "org.opengamingcollective.cardwire", + "/org/opengamingcollective/cardwire", + "org.opengamingcollective.cardwire.Mode", + ) + .await?; + proxy.call("AvailableModes", &()).await + } + pub async fn set_gpu_block(&self, id: u32, blocked: bool) -> zbus::fdo::Result<()> { let path = format!("/org/opengamingcollective/cardwire/Gpu/{}", id); let block_proxy = zbus::Proxy::new( diff --git a/crates/cardwire-cli/src/main.rs b/crates/cardwire-cli/src/main.rs index f42232b1..096ccb14 100644 --- a/crates/cardwire-cli/src/main.rs +++ b/crates/cardwire-cli/src/main.rs @@ -37,8 +37,25 @@ async fn main() -> anyhow::Result<()> { } // Connect to D-Bus - let connection: zbus::Connection = zbus::connection::Builder::system()?.build().await?; - let client: DaemonClient<'_> = DaemonClient::connect(&connection).await?; + let connection: zbus::Connection = match zbus::connection::Builder::system() { + Ok(builder) => match builder.build().await { + Ok(conn) => conn, + Err(e) => handle_error(e), + }, + Err(e) => handle_error(e), + }; + let client: DaemonClient<'_> = match DaemonClient::connect(&connection).await { + Ok(client) => client, + Err(e) => handle_error(e), + }; + + // Verify the daemon is reachable via Cardwire Manager status before processing args + if let Err(e) = client.manager_status().await { + return Err(anyhow::anyhow!( + "Cannot communicate with cardwired, is the daemon running? - {}", + e + )); + } match args.command { Commands::Set { mode } => { @@ -55,6 +72,14 @@ async fn main() -> anyhow::Result<()> { }; } Commands::Get => { + let available_modes_str = match client.get_available_modes().await { + Ok(modes) => modes + .iter() + .map(|m| m.to_string().to_lowercase()) + .collect::>() + .join(", "), + Err(_) => "err".to_string(), + }; match client.get_mode().await { Ok(response) => { let response: CliMode = match response { @@ -66,7 +91,7 @@ async fn main() -> anyhow::Result<()> { _ => CliMode::Manual, }; println!("Current Mode: {}", response); - println!("Available Mode: integrated, hybrid, manual, smart"); + println!("Available Mode: {}", available_modes_str); } Err(e) => handle_error(e), }; @@ -223,6 +248,14 @@ async fn main() -> anyhow::Result<()> { handle_error(e); } else { println!("GPU list refreshed"); + if let Ok(modes) = client.get_available_modes().await { + let modes_str = modes + .iter() + .map(|m| m.to_string().to_lowercase()) + .collect::>() + .join(", "); + println!("Available Mode: {}", modes_str); + } } } }, @@ -309,7 +342,11 @@ async fn main() -> anyhow::Result<()> { fn handle_error(err: zbus::Error) -> ! { match err { zbus::Error::MethodError(name, description, _) => { - if let Some(msg) = description { + if name.as_str() == "org.freedesktop.DBus.Error.ServiceUnknown" + || name.as_str() == "org.freedesktop.DBus.Error.NameHasNoOwner" + { + eprintln!("error: cardwired daemon is not running. Is the service up?"); + } else if let Some(msg) = description { eprintln!("{}", msg); } else { eprintln!("{}", name); @@ -320,7 +357,7 @@ fn handle_error(err: zbus::Error) -> ! { | zbus::fdo::Error::Failed(msg) | zbus::fdo::Error::InvalidArgs(msg) | zbus::fdo::Error::NotSupported(msg) => eprintln!("{}", msg), - zbus::fdo::Error::ServiceUnknown(_) => { + zbus::fdo::Error::ServiceUnknown(_) | zbus::fdo::Error::NameHasNoOwner(_) => { eprintln!("error: cardwired daemon is not running. Is the service up?"); } _ => eprintln!("{}", fdo_err), From 47b44362f6bb72c3f967cb21131b5282227f93c9 Mon Sep 17 00:00:00 2001 From: luytan Date: Sat, 8 Aug 2026 14:53:24 +0200 Subject: [PATCH 2/4] feat(cardwire-gui): use available mode --- crates/cardwire-gui/src/app.rs | 28 ++++++++++++++++--- crates/cardwire-gui/src/gui_config.rs | 6 ++--- crates/cardwire-gui/src/helpers/dbus.rs | 11 ++++++++ crates/cardwire-gui/src/message.rs | 1 + crates/cardwire-gui/src/models.rs | 16 +++++++++-- crates/cardwire-gui/src/subscription.rs | 21 +++++++++++++++ crates/cardwire-gui/src/tray.rs | 28 +++++++++++++++---- crates/cardwire-gui/src/ui.rs | 36 ++++++++++++++++++------- 8 files changed, 124 insertions(+), 23 deletions(-) diff --git a/crates/cardwire-gui/src/app.rs b/crates/cardwire-gui/src/app.rs index caba719a..dd2abada 100644 --- a/crates/cardwire-gui/src/app.rs +++ b/crates/cardwire-gui/src/app.rs @@ -69,6 +69,7 @@ impl AppState { &message, Message::AllDevicesFetched(_) | Message::FetchedMode(_) + | Message::FetchedAvailableModes(_) | Message::TrayReady(_) | Message::UpdateGpuPowerState(..) | Message::UpdateBlockState(..) @@ -104,6 +105,15 @@ impl AppState { self.error = Some(format!("Error fetching Mode: {}", err)); } }, + // Happen when available modes are received from dbus + Message::FetchedAvailableModes(res) => match res { + Ok(modes) => { + self.main_state.available_modes = modes; + } + Err(err) => { + log::warn!("Failed to fetch available modes: {}", err); + } + }, // Send the new mode to dbus Message::SetMode(mode) => { let conn = self.zbus_conn.clone(); @@ -364,7 +374,14 @@ impl AppState { ); } Message::RefreshGpuResult(res) => match res { - Ok(()) => self.info = Some("GPU list refreshed".to_string()), + Ok(()) => { + self.info = Some("GPU list refreshed".to_string()); + let conn = self.zbus_conn.clone(); + return Task::perform( + async move { conn.get_available_modes().await.map_err(|e| e.to_string()) }, + Message::FetchedAvailableModes, + ); + } Err(err) => self.error = Some(format!("Refresh error: {}", err)), }, // Fetch the initial blocked process logs from dbus @@ -475,8 +492,11 @@ impl AppState { return Task::none(); }; let mode = self.main_state.current_mode; + let available_modes = self.main_state.available_modes.clone(); let gpus = self.gpu_list.clone(); - Task::perform(tray::update(handle, mode, gpus), |_| Message::None) + Task::perform(tray::update(handle, mode, available_modes, gpus), |_| { + Message::None + }) } fn open_or_focus_window(&mut self) -> Task { @@ -495,7 +515,9 @@ impl AppState { Page::Main => ui::main_page(&self.main_state, &self.gpu_list), Page::Pci => pci_page(&self.pci_list), Page::SmartMode => ui::smart_mode_page(&self.smart_state, self.main_state.current_mode), - Page::CardwireSettings => daemon_setting_page(&self.setting_state), + Page::CardwireSettings => { + daemon_setting_page(&self.setting_state, &self.main_state.available_modes) + } Page::Logs => ui::logs_page(&self.log_state, &self.gpu_list), Page::Advanced => ui::advanced_page(), Page::About => ui::about_page(), diff --git a/crates/cardwire-gui/src/gui_config.rs b/crates/cardwire-gui/src/gui_config.rs index fd273eb0..709ef31d 100644 --- a/crates/cardwire-gui/src/gui_config.rs +++ b/crates/cardwire-gui/src/gui_config.rs @@ -13,8 +13,8 @@ static TEMPORARY_SEQUENCE: AtomicU64 = AtomicU64::new(0); #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, VariantArray)] #[serde(rename_all = "kebab-case")] pub enum PrimaryClickAction { - #[default] SwitchMode, + #[default] OpenGui, } @@ -42,7 +42,7 @@ impl Default for GuiConfig { fn default() -> Self { Self { start_in_tray: false, - primary_click_action: PrimaryClickAction::SwitchMode, + primary_click_action: PrimaryClickAction::OpenGui, primary_click_modes: default_primary_click_modes(), } } @@ -174,7 +174,7 @@ mod tests { ); assert_eq!( GuiConfig::default().primary_click_action, - PrimaryClickAction::SwitchMode + PrimaryClickAction::OpenGui ); assert!(!GuiConfig::default().start_in_tray); } diff --git a/crates/cardwire-gui/src/helpers/dbus.rs b/crates/cardwire-gui/src/helpers/dbus.rs index b1e74aa8..902ae147 100644 --- a/crates/cardwire-gui/src/helpers/dbus.rs +++ b/crates/cardwire-gui/src/helpers/dbus.rs @@ -125,6 +125,17 @@ impl CardwireDbus { .await?; proxy.get_property("Mode").await } + pub async fn get_available_modes(&self) -> zbus::Result> { + let connection = Connection::system().await?; + let proxy = zbus::Proxy::new( + &connection, + "org.opengamingcollective.cardwire", + "/org/opengamingcollective/cardwire", + "org.opengamingcollective.cardwire.Mode", + ) + .await?; + proxy.call("AvailableModes", &()).await + } pub async fn set_mode(&self, mode: u32) -> zbus::fdo::Result<()> { let connection = Connection::system().await?; let proxy = zbus::Proxy::new( diff --git a/crates/cardwire-gui/src/message.rs b/crates/cardwire-gui/src/message.rs index 815d5d79..205e04ea 100644 --- a/crates/cardwire-gui/src/message.rs +++ b/crates/cardwire-gui/src/message.rs @@ -7,6 +7,7 @@ use std::collections::{BTreeMap, VecDeque}; pub enum Message { SwitchPage(Page), FetchedMode(Result), + FetchedAvailableModes(Result, String>), SetMode(Mode), AllDevicesFetched(Result, String>), UpdateNvidiaSetting(bool), diff --git a/crates/cardwire-gui/src/models.rs b/crates/cardwire-gui/src/models.rs index e374e185..b6cbc9b3 100644 --- a/crates/cardwire-gui/src/models.rs +++ b/crates/cardwire-gui/src/models.rs @@ -17,7 +17,7 @@ use strum::{EnumIter, FromRepr, VariantArray}; serde::Deserialize, )] #[repr(u32)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "snake_case")] pub enum Mode { Integrated = 0, Hybrid = 1, @@ -69,13 +69,25 @@ impl Display for Page { } } -#[derive(Default, Clone, Debug)] +#[derive(Clone, Debug)] pub struct MainState { pub current_mode: Option, + pub available_modes: Vec, pub open_gpu_menu: Option, pub lsof_window: Option, } +impl Default for MainState { + fn default() -> Self { + Self { + current_mode: None, + available_modes: Mode::VARIANTS.to_vec(), + open_gpu_menu: None, + lsof_window: None, + } + } +} + #[derive(serde::Deserialize, zbus::zvariant::Type, Debug, Clone)] pub struct LogEntry { pub timestamp: SystemTime, diff --git a/crates/cardwire-gui/src/subscription.rs b/crates/cardwire-gui/src/subscription.rs index 1fc9610b..669f0726 100644 --- a/crates/cardwire-gui/src/subscription.rs +++ b/crates/cardwire-gui/src/subscription.rs @@ -64,6 +64,7 @@ pub fn tray_sub() -> Subscription { trait CardwireMode { #[zbus(property)] fn mode(&self) -> zbus::Result; + fn available_modes(&self) -> zbus::Result>; } fn mode_sub() -> Subscription { Subscription::run_with("cardwire_mode_subscription", |_id| { @@ -86,6 +87,16 @@ fn mode_sub() -> Subscription { } }; // for startup + match proxy.available_modes().await { + Ok(modes) => { + let _ = output.send(Message::FetchedAvailableModes(Ok(modes))).await; + } + Err(error) => { + let _ = output + .send(Message::FetchedAvailableModes(Err(error.to_string()))) + .await; + } + } match proxy.mode().await { Ok(initial_mode) => { if let Some(mode) = Mode::from_repr(initial_mode) { @@ -425,6 +436,16 @@ fn gpu_sub() -> Subscription { .await; } } + match CardwireDbus::new().get_available_modes().await { + Ok(modes) => { + let _ = output.send(Message::FetchedAvailableModes(Ok(modes))).await; + } + Err(error) => { + let _ = output + .send(Message::FetchedAvailableModes(Err(error.to_string()))) + .await; + } + } dbus_streams.clear(); dbus_properties.clear(); if let Err(e) = build_gpu_streams( diff --git a/crates/cardwire-gui/src/tray.rs b/crates/cardwire-gui/src/tray.rs index 3f67fcbe..d7760efe 100644 --- a/crates/cardwire-gui/src/tray.rs +++ b/crates/cardwire-gui/src/tray.rs @@ -21,6 +21,7 @@ pub enum TrayAction { pub struct CardwireTray { mode: Option, + available_modes: Vec, gpus: BTreeMap, action_tx: mpsc::UnboundedSender, } @@ -29,6 +30,7 @@ impl CardwireTray { fn new(action_tx: mpsc::UnboundedSender) -> Self { Self { mode: None, + available_modes: Mode::VARIANTS.to_vec(), gpus: BTreeMap::new(), action_tx, } @@ -99,7 +101,12 @@ impl Tray for CardwireTray { ]; if self.mode.is_some() { - let options = Mode::VARIANTS + let available_modes = if self.available_modes.is_empty() { + Mode::VARIANTS.to_vec() + } else { + self.available_modes.clone() + }; + let options = available_modes .iter() .copied() .map(|mode| ksni::menu::RadioItem { @@ -108,11 +115,16 @@ impl Tray for CardwireTray { ..Default::default() }) .collect(); + let selected = self + .mode + .and_then(|current| available_modes.iter().position(|&m| m == current)) + .unwrap_or(0); + let modes_for_select = available_modes.clone(); items.push( ksni::menu::RadioGroup { - selected: self.mode.map_or(0, |mode| mode as usize), - select: Box::new(|tray: &mut Self, index| { - if let Some(mode) = Mode::from_repr(index as u32) { + selected, + select: Box::new(move |tray: &mut Self, index| { + if let Some(&mode) = modes_for_select.get(index) { let _ = tray.action_tx.send(TrayAction::SetMode(mode)); } }), @@ -204,11 +216,17 @@ pub async fn spawn() -> Result<(TrayHandle, mpsc::UnboundedReceiver) Ok((handle, action_rx)) } -pub async fn update(handle: TrayHandle, mode: Option, gpus: BTreeMap) { +pub async fn update( + handle: TrayHandle, + mode: Option, + available_modes: Vec, + gpus: BTreeMap, +) { let _ = handle .0 .update(|tray| { tray.mode = mode; + tray.available_modes = available_modes; tray.gpus = gpus; }) .await; diff --git a/crates/cardwire-gui/src/ui.rs b/crates/cardwire-gui/src/ui.rs index 44957eb4..df5dc71b 100644 --- a/crates/cardwire-gui/src/ui.rs +++ b/crates/cardwire-gui/src/ui.rs @@ -142,7 +142,7 @@ pub fn main_page<'a>( gpu_list: &'a BTreeMap, ) -> Element<'a, Message> { column![ - mode_element(main_state.current_mode), + mode_element(main_state.current_mode, &main_state.available_modes), gpu_cards(gpu_list, main_state.open_gpu_menu, main_state.current_mode) ] .spacing(20) @@ -150,10 +150,18 @@ pub fn main_page<'a>( } // A pick list containing a list of modes -fn mode_element(current_mode: Option) -> Element<'static, Message> { +fn mode_element<'a>( + current_mode: Option, + available_modes: &'a [Mode], +) -> Element<'a, Message> { + let modes = if available_modes.is_empty() { + Mode::VARIANTS + } else { + available_modes + }; row![ text!("Mode: "), - pick_list(Mode::VARIANTS, current_mode, Message::SetMode) + pick_list(modes, current_mode, Message::SetMode) ] .spacing(10) .align_y(Alignment::Center) @@ -609,11 +617,14 @@ pub fn advanced_page() -> Element<'static, Message> { column![warning, refresh_section].spacing(15).into() } -pub fn daemon_setting_page(setting_state: &SettingState) -> Element<'static, Message> { +pub fn daemon_setting_page<'a>( + setting_state: &'a SettingState, + available_modes: &'a [Mode], +) -> Element<'a, Message> { let mut col = column![].spacing(10); let nvidia_setting = container( row![ - text!("Nvidia Experimental Block"), + text!("Experimental NVIDIA PM"), horizontal(), toggler(setting_state.nvidia_checked).on_toggle(Message::UpdateNvidiaSetting), ] @@ -623,7 +634,7 @@ pub fn daemon_setting_page(setting_state: &SettingState) -> Element<'static, Mes .width(Fill); let state_setting = container( row![ - text!("Auto Apply GPU-States"), + text!("Auto apply GPU state"), horizontal(), toggler(setting_state.state_checked).on_toggle(Message::UpdateStateSetting), ] @@ -641,12 +652,17 @@ pub fn daemon_setting_page(setting_state: &SettingState) -> Element<'static, Mes ) .style(|_| box_theme!()) .width(Fill); + let modes = if available_modes.is_empty() { + Mode::VARIANTS + } else { + available_modes + }; let battery_mode = container( row![ text!("Mode: "), horizontal(), pick_list( - Mode::VARIANTS, + modes, setting_state.battery_mode, Message::UpdateBatteryMode ), @@ -655,7 +671,7 @@ pub fn daemon_setting_page(setting_state: &SettingState) -> Element<'static, Mes ) .style(|_| box_theme!()) .width(Fill); - let gui_settings = gui_setting_section(setting_state.gui_config.clone()); + let gui_settings = gui_setting_section(setting_state.gui_config.clone(), modes); col = col .push(nvidia_setting) .push(state_setting) @@ -665,12 +681,12 @@ pub fn daemon_setting_page(setting_state: &SettingState) -> Element<'static, Mes col.into() } -fn gui_setting_section(config: GuiConfig) -> Element<'static, Message> { +fn gui_setting_section<'a>(config: GuiConfig, available_modes: &'a [Mode]) -> Element<'a, Message> { let start_in_tray_config = config.clone(); let action_config = config.clone(); let primary_click_mode_settings = (config.primary_click_action == PrimaryClickAction::SwitchMode).then(|| { - Mode::VARIANTS.iter().copied().fold( + available_modes.iter().copied().fold( column![text("Modes to switch between:").size(16)].spacing(10), |column, mode| { let mode_config = config.clone(); From a8c060026bd7f4c8bb0a7cde94733338b6190d7c Mon Sep 17 00:00:00 2001 From: luytan Date: Sat, 8 Aug 2026 14:57:54 +0200 Subject: [PATCH 3/4] fix(cardwire-cli): show err --- crates/cardwire-cli/src/main.rs | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/crates/cardwire-cli/src/main.rs b/crates/cardwire-cli/src/main.rs index 096ccb14..bbae8887 100644 --- a/crates/cardwire-cli/src/main.rs +++ b/crates/cardwire-cli/src/main.rs @@ -72,14 +72,15 @@ async fn main() -> anyhow::Result<()> { }; } Commands::Get => { - let available_modes_str = match client.get_available_modes().await { - Ok(modes) => modes - .iter() - .map(|m| m.to_string().to_lowercase()) - .collect::>() - .join(", "), - Err(_) => "err".to_string(), + let available_modes = match client.get_available_modes().await { + Ok(modes) => modes, + Err(e) => handle_error(e), }; + let available_modes_str = available_modes + .iter() + .map(|m| m.to_string().to_lowercase()) + .collect::>() + .join(", "); match client.get_mode().await { Ok(response) => { let response: CliMode = match response { @@ -248,13 +249,16 @@ async fn main() -> anyhow::Result<()> { handle_error(e); } else { println!("GPU list refreshed"); - if let Ok(modes) = client.get_available_modes().await { - let modes_str = modes - .iter() - .map(|m| m.to_string().to_lowercase()) - .collect::>() - .join(", "); - println!("Available Mode: {}", modes_str); + match client.get_available_modes().await { + Ok(modes) => { + let modes_str = modes + .iter() + .map(|m| m.to_string().to_lowercase()) + .collect::>() + .join(", "); + println!("Available Mode: {}", modes_str); + } + Err(e) => handle_error(e), } } } From 9b457ea4948b932a242d1f0bebe941100be624fe Mon Sep 17 00:00:00 2001 From: luytan Date: Sat, 8 Aug 2026 14:59:03 +0200 Subject: [PATCH 4/4] fix(cardwire-gui): fix config --- crates/cardwire-gui/src/app.rs | 9 +++-- crates/cardwire-gui/src/gui_config.rs | 47 ++++++++++++++++++++++----- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/crates/cardwire-gui/src/app.rs b/crates/cardwire-gui/src/app.rs index dd2abada..5c0374fe 100644 --- a/crates/cardwire-gui/src/app.rs +++ b/crates/cardwire-gui/src/app.rs @@ -212,6 +212,7 @@ impl AppState { PrimaryClickAction::SwitchMode => { return match configured_primary_click_mode( self.main_state.current_mode, + &self.main_state.available_modes, &self.setting_state.gui_config, ) { Ok(mode) => self.update(Message::SetMode(mode)), @@ -558,10 +559,11 @@ impl AppState { fn configured_primary_click_mode( current: Option, + available_modes: &[Mode], config: &GuiConfig, ) -> Result { current - .map(|mode| config.next_primary_click_mode(mode)) + .map(|mode| config.next_primary_click_mode(mode, available_modes)) .ok_or_else(|| "Cardwire daemon is unavailable".to_string()) } @@ -576,14 +578,15 @@ mod tests { #[test] fn primary_click_uses_the_configured_modes() { assert_eq!( - configured_primary_click_mode(Some(Mode::Integrated), &GuiConfig::default()).unwrap(), + configured_primary_click_mode(Some(Mode::Integrated), &[], &GuiConfig::default()) + .unwrap(), Mode::Hybrid ); } #[test] fn primary_click_rejects_an_offline_daemon() { - assert!(configured_primary_click_mode(None, &GuiConfig::default()).is_err()); + assert!(configured_primary_click_mode(None, &[], &GuiConfig::default()).is_err()); } #[test] diff --git a/crates/cardwire-gui/src/gui_config.rs b/crates/cardwire-gui/src/gui_config.rs index 709ef31d..efd176c5 100644 --- a/crates/cardwire-gui/src/gui_config.rs +++ b/crates/cardwire-gui/src/gui_config.rs @@ -92,8 +92,12 @@ impl GuiConfig { result } - pub fn next_primary_click_mode(&self, current: Mode) -> Mode { - let modes = Mode::VARIANTS; + pub fn next_primary_click_mode(&self, current: Mode, available_modes: &[Mode]) -> Mode { + let modes = if available_modes.is_empty() { + Mode::VARIANTS + } else { + available_modes + }; let current_index = modes.iter().position(|&mode| mode == current); current_index .into_iter() @@ -106,7 +110,7 @@ impl GuiConfig { .copied() .find(|mode| self.primary_click_modes.contains(mode)) }) - .expect("validated GUI config must contain a primary-click mode") + .unwrap_or_else(|| modes[0]) } pub fn with_primary_click_mode(mut self, mode: Mode, enabled: bool) -> Self { @@ -226,15 +230,21 @@ mod tests { ..GuiConfig::default() }; assert_eq!( - config.next_primary_click_mode(Mode::Integrated), + config.next_primary_click_mode(Mode::Integrated, &[]), Mode::Manual ); - assert_eq!(config.next_primary_click_mode(Mode::Manual), Mode::Smart); assert_eq!( - config.next_primary_click_mode(Mode::Smart), + config.next_primary_click_mode(Mode::Manual, &[]), + Mode::Smart + ); + assert_eq!( + config.next_primary_click_mode(Mode::Smart, &[]), Mode::Integrated ); - assert_eq!(config.next_primary_click_mode(Mode::Hybrid), Mode::Manual); + assert_eq!( + config.next_primary_click_mode(Mode::Hybrid, &[]), + Mode::Manual + ); } #[test] @@ -243,8 +253,27 @@ mod tests { primary_click_modes: vec![Mode::Smart], ..GuiConfig::default() }; - assert_eq!(config.next_primary_click_mode(Mode::Hybrid), Mode::Smart); - assert_eq!(config.next_primary_click_mode(Mode::Smart), Mode::Smart); + assert_eq!( + config.next_primary_click_mode(Mode::Hybrid, &[]), + Mode::Smart + ); + assert_eq!( + config.next_primary_click_mode(Mode::Smart, &[]), + Mode::Smart + ); + } + + #[test] + fn skips_unavailable_configured_modes() { + let config = GuiConfig { + primary_click_modes: vec![Mode::Integrated, Mode::Hybrid, Mode::Smart], + ..GuiConfig::default() + }; + let available = vec![Mode::Hybrid, Mode::Manual]; + assert_eq!( + config.next_primary_click_mode(Mode::Hybrid, &available), + Mode::Hybrid + ); } #[test]