From 3d921728c6bf587947c9922f3adb79bee1d8feff Mon Sep 17 00:00:00 2001 From: luytan Date: Sat, 8 Aug 2026 16:55:33 +0200 Subject: [PATCH 01/11] feat(cardwire-gui): redesign and fix issues --- crates/cardwire-daemon/src/analyzer/models.rs | 4 + .../src/analyzer/static_analysis.rs | 6 + crates/cardwire-gui/src/app.rs | 44 +- .../cardwire-gui/src/helpers/app_resolver.rs | 80 +- crates/cardwire-gui/src/ui.rs | 811 ++++++++++++------ 5 files changed, 684 insertions(+), 261 deletions(-) diff --git a/crates/cardwire-daemon/src/analyzer/models.rs b/crates/cardwire-daemon/src/analyzer/models.rs index 32db6109..085988c3 100644 --- a/crates/cardwire-daemon/src/analyzer/models.rs +++ b/crates/cardwire-daemon/src/analyzer/models.rs @@ -304,6 +304,9 @@ impl CardwireAnalyzer { // Check the database now, we can take our time since if we reached it, the app would've // been blocked let mut lookup_name = comm.to_lowercase(); + if lookup_name.contains("xdg-desktop-portal") { + return None; + } if let Some(steam_app) = get_steam_app_id(&environ) { lookup_name = steam_app; } @@ -325,6 +328,7 @@ impl CardwireAnalyzer { let meta = meta.clone(); drop(xdg_list); self.discover_app(&lookup_name, meta).await; + return Some((false, PidType::Allowed, 0)); } } // Fallback for steam games diff --git a/crates/cardwire-daemon/src/analyzer/static_analysis.rs b/crates/cardwire-daemon/src/analyzer/static_analysis.rs index 3c6577ea..f1b33fbd 100644 --- a/crates/cardwire-daemon/src/analyzer/static_analysis.rs +++ b/crates/cardwire-daemon/src/analyzer/static_analysis.rs @@ -175,6 +175,12 @@ pub async fn watch_fdo_folders( fn parse_fdo_app(app_fdo: &DesktopEntry, name: &str, path: &Path) -> HashMap { let mut app_list: HashMap = HashMap::new(); + if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) + && file_name.contains("xdg-desktop-portal") + { + return app_list; + } + let display_name = name.to_string(); let icon_name = app_fdo.icon().map(|icon| icon.to_string()); diff --git a/crates/cardwire-gui/src/app.rs b/crates/cardwire-gui/src/app.rs index 9be22db1..27f2b009 100644 --- a/crates/cardwire-gui/src/app.rs +++ b/crates/cardwire-gui/src/app.rs @@ -27,6 +27,14 @@ pub struct AppState { tray_available: bool, } +fn default_window_settings() -> window::Settings { + window::Settings { + size: iced::Size::new(1150.0, 800.0), + min_size: Some(iced::Size::new(850.0, 600.0)), + ..window::Settings::default() + } +} + impl AppState { pub fn new() -> (Self, Task) { let (gui_config, error) = match GuiConfig::load() { @@ -39,7 +47,7 @@ impl AppState { let (window_id, open_window) = if gui_config.start_in_tray { (None, Task::none()) } else { - let (id, task) = window::open(window::Settings::default()); + let (id, task) = window::open(default_window_settings()); (Some(id), task.discard()) }; let state = AppState { @@ -399,7 +407,22 @@ impl AppState { Ok(policies) => { let mut map = BTreeMap::new(); for (app_id, meta) in policies { + if app_id.contains("xdg-desktop-portal") + || meta + .desktop_file_id + .as_deref() + .is_some_and(|d| d.contains("xdg-desktop-portal")) + { + continue; + } let resolved = crate::helpers::resolve_app_metadata(&app_id, &meta); + if resolved + .desktop_file_id + .as_deref() + .is_some_and(|d| d.contains("xdg-desktop-portal")) + { + continue; + } map.insert(app_id, resolved); } self.smart_state.app_policies = map; @@ -412,8 +435,21 @@ impl AppState { } }, Message::NewAppDiscovered((app_id, meta)) => { - let resolved = crate::helpers::resolve_app_metadata(&app_id, &meta); - self.smart_state.app_policies.insert(app_id, resolved); + if !app_id.contains("xdg-desktop-portal") + && !meta + .desktop_file_id + .as_deref() + .is_some_and(|d| d.contains("xdg-desktop-portal")) + { + let resolved = crate::helpers::resolve_app_metadata(&app_id, &meta); + if !resolved + .desktop_file_id + .as_deref() + .is_some_and(|d| d.contains("xdg-desktop-portal")) + { + self.smart_state.app_policies.insert(app_id, resolved); + } + } } Message::SetAppPolicy(app_id, policy) => { let conn = self.zbus_conn.clone(); @@ -508,7 +544,7 @@ impl AppState { if let Some(id) = self.window_id { window::gain_focus(id) } else { - let (id, task) = window::open(window::Settings::default()); + let (id, task) = window::open(default_window_settings()); self.window_id = Some(id); task.discard() } diff --git a/crates/cardwire-gui/src/helpers/app_resolver.rs b/crates/cardwire-gui/src/helpers/app_resolver.rs index f8a2cfff..82aa7bc2 100644 --- a/crates/cardwire-gui/src/helpers/app_resolver.rs +++ b/crates/cardwire-gui/src/helpers/app_resolver.rs @@ -100,11 +100,56 @@ pub fn resolve_app_metadata(app_id: &str, raw: &DbusAppMetadata) -> ResolvedApp } } + let mut resolved_desktop_id = raw.desktop_file_id.clone(); + + // If it's a Steam app (e.g. "steam_app_1070560"), scan desktop files for steam://rungameid/ + if resolved_name.is_none() + && let Some(steam_id) = app_id.strip_prefix("steam_app_") + { + let rungame_uri = format!("steam://rungameid/{}", steam_id); + 'search_steam: for data_dir in &data_dirs { + let apps_dir = data_dir.join("applications"); + if let Ok(entries) = std::fs::read_dir(&apps_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if let Some(ext) = path.extension() + && ext == "desktop" + && let Ok(entry_fdo) = DesktopEntry::from_path(&path, Some(&locales)) + && let Some(exec) = entry_fdo.exec() + && exec.contains(&rungame_uri) + { + if let Some(name) = entry_fdo.name(&locales) { + let name_str = name.to_string(); + if !name_str.trim().is_empty() { + resolved_name = Some(name_str); + } + } + if resolved_icon_name.is_none() + && let Some(icon) = entry_fdo.icon() + { + resolved_icon_name = Some(icon.to_string()); + } + if resolved_desktop_id.is_none() { + resolved_desktop_id = path.file_name().map(|s| { + s.to_string_lossy().trim_end_matches(".desktop").to_string() + }); + } + if resolved_name.is_some() { + break 'search_steam; + } + } + } + } + } + } + // Determine display name let display_name = if let Some(name) = resolved_name { name - } else if !raw.display_name.trim().is_empty() { + } else if !raw.display_name.trim().is_empty() && !raw.display_name.starts_with("Steam Game ") { raw.display_name.clone() + } else if let Some(steam_id) = app_id.strip_prefix("steam_app_") { + format!("Steam Game {}", steam_id) } else { app_id .split(&['-', '_'][..]) @@ -124,7 +169,7 @@ pub fn resolve_app_metadata(app_id: &str, raw: &DbusAppMetadata) -> ResolvedApp ResolvedApp { app_id: app_id.to_string(), display_name, - desktop_file_id: raw.desktop_file_id.clone(), + desktop_file_id: resolved_desktop_id, icon_name: resolved_icon_name, icon_path, gpu_policy: raw.gpu_policy, @@ -200,3 +245,34 @@ fn resolve_icon_path( None } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_resolve_app_metadata_steam_fallback() { + let raw = DbusAppMetadata { + display_name: "".to_string(), + desktop_file_id: None, + icon_name: None, + gpu_policy: 0, + }; + let resolved = resolve_app_metadata("steam_app_1070560", &raw); + assert_eq!(resolved.app_id, "steam_app_1070560"); + assert_eq!(resolved.display_name, "Steam Game 1070560"); + } + + #[test] + fn test_resolve_app_metadata_generic_name() { + let raw = DbusAppMetadata { + display_name: "blender".to_string(), + desktop_file_id: None, + icon_name: None, + gpu_policy: 1, + }; + let resolved = resolve_app_metadata("blender", &raw); + assert_eq!(resolved.app_id, "blender"); + assert_eq!(resolved.gpu_policy, 1); + } +} diff --git a/crates/cardwire-gui/src/ui.rs b/crates/cardwire-gui/src/ui.rs index df5dc71b..7f0a1659 100644 --- a/crates/cardwire-gui/src/ui.rs +++ b/crates/cardwire-gui/src/ui.rs @@ -121,35 +121,189 @@ fn page_btn_style(theme: &iced::Theme, selected: bool, status: button::Status) - } } +// Styling used for text inputs to avoid white border on hover +fn search_input_style(theme: &iced::Theme, status: text_input::Status) -> text_input::Style { + let mut style = text_input::default(theme, status); + if status == text_input::Status::Hovered { + let active = text_input::default(theme, text_input::Status::Active); + style.border = active.border; + } + style +} + +// Standardized page header component for all pages +pub fn page_header<'a>( + title: &'a str, + subtitle: Option<&'a str>, + action: Option>, +) -> Element<'a, Message> { + let mut title_col = column![ + text(title) + .size(24) + .color(Color::from_rgb(0.96, 0.96, 0.96)), + ]; + + if let Some(sub) = subtitle { + title_col = title_col.push(text(sub).size(14).color(Color::from_rgb(0.72, 0.72, 0.75))); + } + + let mut header_row = row![title_col.spacing(4)].align_y(Alignment::Center); + + if let Some(act) = action { + header_row = header_row.push(horizontal()).push(act); + } + + header_row.width(Fill).into() +} + +pub fn count_badge<'a>(label: impl Into) -> Element<'a, Message> { + container( + text(label.into()) + .size(14) + .color(Color::from_rgb(0.9, 0.9, 0.9)), + ) + .padding([6, 14]) + .style(|_| container::Style { + background: Some(Color::from_rgb(0.2, 0.2, 0.22).into()), + border: Border { + radius: 4.0.into(), + width: 1.0, + color: Color::from_rgb(0.32, 0.32, 0.35), + }, + ..Default::default() + }) + .into() +} + +fn power_state_badge<'a>(power_state: &Option) -> Element<'a, Message> { + match power_state { + Some(raw) => { + let trimmed = raw.trim(); + if trimmed.eq_ignore_ascii_case("d0") { + container( + text(format!("Active ({})", trimmed)) + .size(14) + .color(Color::from_rgb(0.4, 0.95, 0.55)), + ) + .padding([5, 12]) + .style(|_| container::Style { + background: Some(Color::from_rgb(0.1, 0.28, 0.16).into()), + border: Border { + radius: 6.0.into(), + width: 1.0, + color: Color::from_rgb(0.22, 0.55, 0.32), + }, + ..Default::default() + }) + .into() + } else if trimmed.to_lowercase().starts_with("d3") { + container( + text(format!("Inactive ({})", trimmed)) + .size(14) + .color(Color::from_rgb(0.7, 0.8, 0.98)), + ) + .padding([5, 12]) + .style(|_| container::Style { + background: Some(Color::from_rgb(0.14, 0.2, 0.3).into()), + border: Border { + radius: 6.0.into(), + width: 1.0, + color: Color::from_rgb(0.28, 0.42, 0.6), + }, + ..Default::default() + }) + .into() + } else { + container( + text(format!("Active ({})", trimmed)) + .size(14) + .color(Color::from_rgb(0.9, 0.9, 0.9)), + ) + .padding([5, 12]) + .style(|_| container::Style { + background: Some(Color::from_rgb(0.2, 0.2, 0.22).into()), + border: Border { + radius: 6.0.into(), + width: 1.0, + color: Color::from_rgb(0.35, 0.35, 0.38), + }, + ..Default::default() + }) + .into() + } + } + None => text("N/A").color(Color::from_rgb(0.5, 0.5, 0.5)).into(), + } +} + +pub fn warning_banner<'a>(msg: impl Into) -> Element<'a, Message> { + container( + row![ + text("⚠ ").size(20).color(Color::from_rgb(1.0, 0.8, 0.0)), + text(msg.into()) + .size(15) + .color(Color::from_rgb(1.0, 0.85, 0.2)), + ] + .spacing(8) + .align_y(Alignment::Center) + .padding(12), + ) + .style(|_| container::Style { + background: Some(Color::from_rgb(0.25, 0.2, 0.05).into()), + border: Border { + radius: 8.0.into(), + width: 1.0, + color: Color::from_rgb(0.5, 0.4, 0.1), + }, + ..Default::default() + }) + .width(Fill) + .into() +} + // Iter over the Page enum and make a button for each variants, Used to navigate around the GUI pub fn page_bar(current_page: Page) -> Element<'static, Message> { let buttons = Page::iter().fold(column![].spacing(10), |col, page| { let selected = page == current_page; col.push( - button(text!("{}", page)) + button(text!("{}", page).size(15)) .on_press(Message::SwitchPage(page)) .width(Fill) - .padding([8, 12]) + .padding([10, 14]) .style(move |theme, status| page_btn_style(theme, selected, status)), ) }); buttons.into() } -// The main page contain the mode and the GPU Cards +// The main page contains the mode selection and connected GPU Cards pub fn main_page<'a>( main_state: &'a MainState, gpu_list: &'a BTreeMap, ) -> Element<'a, Message> { - column![ - 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) - .into() + let header = page_header( + "System Overview", + Some("Manage active Cardwire mode and inspect connected hardware"), + Some(count_badge(if gpu_list.len() == 1 { + "1 GPU".to_string() + } else { + format!("{} GPUs", gpu_list.len()) + })), + ); + + let mode_card = mode_element(main_state.current_mode, &main_state.available_modes); + let gpus = gpu_cards(gpu_list, main_state.open_gpu_menu, main_state.current_mode); + + let content = column![mode_card, gpus].spacing(20); + + column![header, scrollable(content).height(Fill)] + .spacing(16) + .width(Fill) + .height(Fill) + .into() } -// A pick list containing a list of modes +// A structured card containing the Cardwire mode selector fn mode_element<'a>( current_mode: Option, available_modes: &'a [Mode], @@ -159,13 +313,28 @@ fn mode_element<'a>( } else { available_modes }; - row![ - text!("Mode: "), - pick_list(modes, current_mode, Message::SetMode) + + let mode_info = column![ + text("Cardwire Mode") + .size(17) + .color(Color::from_rgb(0.95, 0.95, 0.95)), + text("Select how applications route to available graphics processors.") + .size(14) + .color(Color::from_rgb(0.72, 0.72, 0.75)), ] - .spacing(10) - .align_y(Alignment::Center) - .into() + .spacing(3); + + let picker = pick_list(modes, current_mode, Message::SetMode); + + let card_content = row![mode_info, horizontal(), picker] + .align_y(Alignment::Center) + .spacing(12); + + container(card_content) + .style(|_| box_theme!()) + .width(Fill) + .padding(18) + .into() } fn gpu_cards( @@ -253,90 +422,117 @@ fn gpu_cards( .width(Fixed(120.0)) ] .into(); + + let width = 110; let details = if is_available { column![ row![ text("Discrete: ") - .color(Color::from_rgb(0.6, 0.6, 0.6)) - .width(80), + .size(15) + .color(Color::from_rgb(0.72, 0.72, 0.75)) + .width(width), text(gpu.discrete) + .size(15) + .color(Color::from_rgb(0.92, 0.92, 0.92)) ], row![ text("Vendor: ") - .color(Color::from_rgb(0.6, 0.6, 0.6)) - .width(80), + .size(15) + .color(Color::from_rgb(0.72, 0.72, 0.75)) + .width(width), text(gpu.vendor.clone()) + .size(15) + .color(Color::from_rgb(0.92, 0.92, 0.92)) ], row![ text("Driver: ") - .color(Color::from_rgb(0.6, 0.6, 0.6)) - .width(80), - text(gpu.driver.clone()), + .size(15) + .color(Color::from_rgb(0.72, 0.72, 0.75)) + .width(width), + text(gpu.driver.clone()) + .size(15) + .color(Color::from_rgb(0.92, 0.92, 0.92)), ], row![ text("PCI: ") - .color(Color::from_rgb(0.6, 0.6, 0.6)) - .width(80), + .size(15) + .color(Color::from_rgb(0.72, 0.72, 0.75)) + .width(width), text(&gpu.pci) + .size(15) + .color(Color::from_rgb(0.92, 0.92, 0.92)) ], row![ text("Nodes: ") - .color(Color::from_rgb(0.6, 0.6, 0.6)) - .width(80), + .size(15) + .color(Color::from_rgb(0.72, 0.72, 0.75)) + .width(width), text(format!("card{} / renderD{}", gpu.card, gpu.render)) + .size(15) + .color(Color::from_rgb(0.92, 0.92, 0.92)) ], row![ text("Virtual: ") - .color(Color::from_rgb(0.6, 0.6, 0.6)) - .width(80), + .size(15) + .color(Color::from_rgb(0.72, 0.72, 0.75)) + .width(width), text(gpu.virtual_gpu) + .size(15) + .color(Color::from_rgb(0.92, 0.92, 0.92)) ], row![ text("Blocked: ") - .color(Color::from_rgb(0.6, 0.6, 0.6)) - .width(80), - text(gpu.blocked), - horizontal(), - match &gpu.power_state { - Some(power_state) => { - match power_state.trim() { - "D0" => text!("D0").color(Color::from_rgb(1.0, 0.0, 0.0)), - "D3cold" => { - text!("D3Cold").color(Color::from_rgb(0.0, 1.0, 0.0)) - } - _ => text!("{}", power_state), - } + .size(15) + .color(Color::from_rgb(0.72, 0.72, 0.75)) + .width(width), + text(if gpu.blocked { "Yes" } else { "No" }).size(15).color( + if gpu.blocked { + Color::from_rgb(0.95, 0.3, 0.3) + } else { + Color::from_rgb(0.4, 0.8, 0.4) } - None => text("err"), - } + ), + horizontal(), + power_state_badge(&gpu.power_state), ] + .align_y(Alignment::Center), ] .spacing(8) } else { column![ row![ text("Vendor: ") - .color(Color::from_rgb(0.6, 0.6, 0.6)) - .width(80), + .size(15) + .color(Color::from_rgb(0.72, 0.72, 0.75)) + .width(width), text(gpu.vendor.clone()) + .size(15) + .color(Color::from_rgb(0.92, 0.92, 0.92)) ], row![ text("Driver: ") - .color(Color::from_rgb(0.6, 0.6, 0.6)) - .width(80), - text(gpu.driver.clone()), + .size(15) + .color(Color::from_rgb(0.72, 0.72, 0.75)) + .width(width), + text(gpu.driver.clone()) + .size(15) + .color(Color::from_rgb(0.92, 0.92, 0.92)), ], row![ text("PCI: ") - .color(Color::from_rgb(0.6, 0.6, 0.6)) - .width(80), + .size(15) + .color(Color::from_rgb(0.72, 0.72, 0.75)) + .width(width), text(&gpu.pci) + .size(15) + .color(Color::from_rgb(0.92, 0.92, 0.92)) ], row![ text("Available: ") - .color(Color::from_rgb(0.6, 0.6, 0.6)) - .width(80), - text(gpu.available).color(Color::from_rgb( + .size(15) + .color(Color::from_rgb(0.72, 0.72, 0.75)) + .width(width), + text(gpu.available).size(15).color(Color::from_rgb( 239.0 / 255.0, 68.0 / 255.0, 68.0 / 255.0 @@ -453,18 +649,15 @@ pub fn logs_page<'a>( log_state: &'a LogState, gpu_list: &'a BTreeMap, ) -> Element<'a, Message> { - let header = row![ - text("Blocked Process Logs") - .size(20) - .color(Color::from_rgb(0.9, 0.9, 0.9)), - horizontal(), - text!("{} entries", log_state.logs.len()).color(Color::from_rgb(0.6, 0.6, 0.6)), - ] - .align_y(Alignment::Center); + let header = page_header( + "Blocked Process Logs", + Some("Real-time log of processes attempting unauthorized GPU access"), + Some(count_badge(format!("{} entries", log_state.logs.len()))), + ); let content = if log_state.logs.is_empty() { column![ - text("No blocked process logs yet") + text("No blocked process logs recorded yet") .color(Color::from_rgb(0.5, 0.5, 0.5)) .font(Font::MONOSPACE) ] @@ -477,7 +670,7 @@ pub fn logs_page<'a>( }) }; - let terminal = container(scrollable(content)) + let terminal = container(scrollable(content).height(Fill)) .width(Fill) .height(Fill) .padding(16) @@ -491,7 +684,11 @@ pub fn logs_page<'a>( ..Default::default() }); - column![header, terminal].spacing(12).into() + column![header, terminal] + .spacing(16) + .width(Fill) + .height(Fill) + .into() } // One color-coded log line, the gpu id is replaced by the gpu name when known @@ -538,147 +735,211 @@ fn log_line<'a>( pub fn about_page() -> Element<'static, Message> { let version = env!("CARGO_PKG_VERSION"); - let content = column![ - text("Cardwire").size(40), - text!("Version {}", version) - .size(18) - .color(Color::from_rgb(0.6, 0.6, 0.6)), - row![ - text("Author: ").color(Color::from_rgb(0.6, 0.6, 0.6)), - text("luytan") - ], - row![ - text("Other contributors: ").color(Color::from_rgb(0.6, 0.6, 0.6)), - text("SeawolfTony") - ], - row![ - text("License: ").color(Color::from_rgb(0.6, 0.6, 0.6)), - text("GPL-3.0") - ], - row![ - text("Repository: ").color(Color::from_rgb(0.6, 0.6, 0.6)), - button( - text("github.com/OpenGamingCollective/cardwire") - .color(Color::from_rgb(0.4, 0.6, 1.0)) - ) - .style(link_btn_style) - .padding(0) - .on_press(Message::OpenUrl( - "https://github.com/OpenGamingCollective/cardwire".to_string() - )) - ], - ] - .spacing(10); + let header = page_header( + "About Cardwire", + Some("GPU management and process isolation for Linux"), + Some(count_badge(format!("v{}", version))), + ); - container(content) - .style(|_| box_theme!()) + let info_card = container( + column![ + row![ + text("Cardwire") + .size(24) + .color(Color::from_rgb(0.96, 0.96, 0.96)), + horizontal(), + count_badge(format!("Version {}", version)), + ] + .align_y(Alignment::Center), + text("Dynamic GPU management, power control, and per-process hardware isolation.") + .size(14) + .color(Color::from_rgb(0.75, 0.75, 0.75)), + column![ + row![ + text("Author:") + .size(15) + .color(Color::from_rgb(0.72, 0.72, 0.75)) + .width(150), + text("luytan") + .size(15) + .color(Color::from_rgb(0.92, 0.92, 0.92)), + ], + row![ + text("Other Contributors:") + .size(15) + .color(Color::from_rgb(0.72, 0.72, 0.75)) + .width(150), + text("SeawolfTony") + .size(15) + .color(Color::from_rgb(0.92, 0.92, 0.92)), + ], + row![ + text("License:") + .size(15) + .color(Color::from_rgb(0.72, 0.72, 0.75)) + .width(150), + text("GPL-3.0") + .size(15) + .color(Color::from_rgb(0.92, 0.92, 0.92)), + ], + row![ + text("Repository:") + .size(15) + .color(Color::from_rgb(0.72, 0.72, 0.75)) + .width(150), + button( + text("github.com/OpenGamingCollective/cardwire") + .size(15) + .color(Color::from_rgb(0.45, 0.68, 1.0)) + ) + .style(link_btn_style) + .padding(0) + .on_press(Message::OpenUrl( + "https://github.com/OpenGamingCollective/cardwire".to_string() + )), + ], + ] + .spacing(10), + ] + .spacing(16), + ) + .style(|_| box_theme!()) + .width(Fill) + .padding(20); + + let content = column![info_card].spacing(16); + + column![header, scrollable(content).height(Fill)] + .spacing(16) .width(Fill) - .padding(20) + .height(Fill) .into() } pub fn advanced_page() -> Element<'static, Message> { - let warning = container( - row![ - text("⚠ ").size(20).color(Color::from_rgb(1.0, 0.8, 0.0)), - text("Warning: These actions are for advanced users.") - .color(Color::from_rgb(1.0, 0.8, 0.0)), - ] - .align_y(Alignment::Center) - .padding(10), - ) - .style(|_| container::Style { - background: Some(Color::from_rgb(0.25, 0.2, 0.05).into()), - border: Border { - radius: 8.0.into(), - width: 1.0, - color: Color::from_rgb(0.5, 0.4, 0.1), - }, - ..Default::default() - }) - .width(Fill); + let header = page_header( + "Advanced Controls", + Some("Low-level operations and maintenance actions"), + None, + ); + + let warning = warning_banner( + "Warning: These actions are intended for advanced users and troubleshooting.", + ); - //Send refresh_gpu to dbus, automatic GUI gpu_list refreshing isn't implemented yet let refresh_section = container( column![ - text("Refresh GPU List").size(20), - text("Re-scan PCI devices and update the internal GPU list.") - .color(Color::from_rgb(0.6, 0.6, 0.6)), - button("Refresh GPU") + text("Hardware & Rescan") + .size(18) + .color(Color::from_rgb(0.92, 0.92, 0.92)), + text("Re-scan PCI devices and update the internal GPU device list in the daemon.") + .size(14) + .color(Color::from_rgb(0.72, 0.72, 0.75)), + button(text("Refresh GPU List").size(15)) .on_press(Message::RefreshGpu) - .padding([8, 16]) + .padding([10, 18]) ] - .spacing(10), + .spacing(14), ) .style(|_| box_theme!()) .width(Fill) .padding(20); - column![warning, refresh_section].spacing(15).into() + let content = column![warning, refresh_section].spacing(16); + + column![header, scrollable(content).height(Fill)] + .spacing(16) + .width(Fill) + .height(Fill) + .into() } 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!("Experimental NVIDIA PM"), - horizontal(), - toggler(setting_state.nvidia_checked).on_toggle(Message::UpdateNvidiaSetting), - ] - .padding(10), - ) - .style(|_| box_theme!()) - .width(Fill); - let state_setting = container( - row![ - text!("Auto apply GPU state"), - horizontal(), - toggler(setting_state.state_checked).on_toggle(Message::UpdateStateSetting), - ] - .padding(10), - ) - .style(|_| box_theme!()) - .width(Fill); - let battery_setting = container( - row![ - text!("Switch Mode on battery"), - horizontal(), - toggler(setting_state.battery_checked).on_toggle(Message::UpdateBatterySetting), - ] - .padding(10), - ) - .style(|_| box_theme!()) - .width(Fill); + let header = page_header( + "Settings", + Some("Configure Cardwire daemon behavior, power management, and GUI options"), + None, + ); + let modes = if available_modes.is_empty() { Mode::VARIANTS } else { available_modes }; - let battery_mode = container( - row![ - text!("Mode: "), - horizontal(), - pick_list( - modes, - setting_state.battery_mode, - Message::UpdateBatteryMode - ), + + let daemon_section = container( + column![ + text("Daemon & Power Management").size(18).color(Color::from_rgb(0.92, 0.92, 0.92)), + text("Manage kernel and runtime power management policies.") + .size(14) + .color(Color::from_rgb(0.72, 0.72, 0.75)), + row![ + column![ + text("Experimental NVIDIA block").size(15).color(Color::from_rgb(0.95, 0.95, 0.95)), + text("Block shared NVIDIA device files (/dev/nvidiactl) to prevent unwanted GPU wakeups.") + .size(14) + .color(Color::from_rgb(0.72, 0.72, 0.75)), + ], + horizontal(), + toggler(setting_state.nvidia_checked).on_toggle(Message::UpdateNvidiaSetting), + ] + .align_y(Alignment::Center), + row![ + column![ + text("Auto apply GPU state").size(15).color(Color::from_rgb(0.95, 0.95, 0.95)), + text("Automatically restore configured GPU power and block states upon daemon startup.") + .size(14) + .color(Color::from_rgb(0.72, 0.72, 0.75)), + ], + horizontal(), + toggler(setting_state.state_checked).on_toggle(Message::UpdateStateSetting), + ] + .align_y(Alignment::Center), + row![ + column![ + text("Switch mode on battery").size(15).color(Color::from_rgb(0.95, 0.95, 0.95)), + text("Automatically switch to a designated power-saving mode when on battery.") + .size(14) + .color(Color::from_rgb(0.72, 0.72, 0.75)), + ], + horizontal(), + toggler(setting_state.battery_checked).on_toggle(Message::UpdateBatterySetting), + ] + .align_y(Alignment::Center), + row![ + column![ + text("Battery target mode").size(15).color(Color::from_rgb(0.95, 0.95, 0.95)), + text("Cardwire mode to activate when disconnected from AC power.") + .size(14) + .color(Color::from_rgb(0.72, 0.72, 0.75)), + ], + horizontal(), + pick_list( + modes, + setting_state.battery_mode, + Message::UpdateBatteryMode + ), + ] + .align_y(Alignment::Center), ] - .padding(10), + .spacing(16), ) .style(|_| box_theme!()) - .width(Fill); - let gui_settings = gui_setting_section(setting_state.gui_config.clone(), modes); - col = col - .push(nvidia_setting) - .push(state_setting) - .push(battery_setting) - .push(battery_mode) - .push(gui_settings); - col.into() + .width(Fill) + .padding(20); + + let gui_section = gui_setting_section(setting_state.gui_config.clone(), modes); + + let content = column![daemon_section, gui_section].spacing(16); + + column![header, scrollable(content).height(Fill)] + .spacing(16) + .width(Fill) + .height(Fill) + .into() } fn gui_setting_section<'a>(config: GuiConfig, available_modes: &'a [Mode]) -> Element<'a, Message> { @@ -687,7 +948,12 @@ fn gui_setting_section<'a>(config: GuiConfig, available_modes: &'a [Mode]) -> El let primary_click_mode_settings = (config.primary_click_action == PrimaryClickAction::SwitchMode).then(|| { available_modes.iter().copied().fold( - column![text("Modes to switch between:").size(16)].spacing(10), + column![ + text("Modes to switch between:") + .size(15) + .color(Color::from_rgb(0.9, 0.9, 0.9)) + ] + .spacing(10), |column, mode| { let mode_config = config.clone(); let enabled = config.primary_click_modes.contains(&mode); @@ -695,7 +961,7 @@ fn gui_setting_section<'a>(config: GuiConfig, available_modes: &'a [Mode]) -> El column.push( row![ - text(mode.to_string()).width(Fixed(130.0)), + text(mode.to_string()).size(15).width(Fixed(130.0)), toggler(enabled).on_toggle_maybe(can_disable.then_some( move |enabled| { Message::UpdateGuiConfig( @@ -710,15 +976,20 @@ fn gui_setting_section<'a>(config: GuiConfig, available_modes: &'a [Mode]) -> El ) }); let mut content = column![ - text("GUI Settings").size(20), - text("Configure Cardwire's startup and tray icon behavior.") - .color(Color::from_rgb(0.6, 0.6, 0.6)), + text("GUI & Tray Settings") + .size(18) + .color(Color::from_rgb(0.92, 0.92, 0.92)), + text("Configure Cardwire's startup and system tray icon behavior.") + .size(14) + .color(Color::from_rgb(0.72, 0.72, 0.75)), row![ column![ - text("Start in tray"), - text("Do not open the GUI when Cardwire starts. Takes effect next launch.") - .size(13) - .color(Color::from_rgb(0.6, 0.6, 0.6)), + text("Start in tray") + .size(15) + .color(Color::from_rgb(0.95, 0.95, 0.95)), + text("Do not open the GUI window when Cardwire starts. Takes effect next launch.") + .size(14) + .color(Color::from_rgb(0.72, 0.72, 0.75)), ], horizontal(), toggler(config.start_in_tray).on_toggle(move |start_in_tray| { @@ -730,7 +1001,15 @@ fn gui_setting_section<'a>(config: GuiConfig, available_modes: &'a [Mode]) -> El ] .align_y(Alignment::Center), row![ - text("Primary click:").width(Fixed(130.0)), + column![ + text("Primary click action") + .size(15) + .color(Color::from_rgb(0.95, 0.95, 0.95)), + text("Action triggered when clicking the system tray icon.") + .size(14) + .color(Color::from_rgb(0.72, 0.72, 0.75)), + ], + horizontal(), pick_list( PrimaryClickAction::VARIANTS, Some(config.primary_click_action), @@ -742,7 +1021,7 @@ fn gui_setting_section<'a>(config: GuiConfig, available_modes: &'a [Mode]) -> El ] .align_y(Alignment::Center), ] - .spacing(10); + .spacing(16); if let Some(settings) = primary_click_mode_settings { content = content.push(settings); @@ -756,88 +1035,126 @@ fn gui_setting_section<'a>(config: GuiConfig, available_modes: &'a [Mode]) -> El } pub fn pci_page<'a>(pci_list: &'a BTreeMap) -> Element<'a, Message> { + let copy_btn = button("Copy to Clipboard") + .on_press(Message::PciListToClipboard()) + .padding([6, 12]); + + let header = page_header( + "PCI Devices", + Some("Inspect detected PCI hardware devices, active drivers, and IOMMU groupings"), + Some( + row![copy_btn, count_badge(format!("{} devices", pci_list.len()))] + .spacing(10) + .align_y(Alignment::Center) + .into(), + ), + ); + let cards = scrollable( pci_list .iter() - .fold(column![].spacing(15), |col, (pci_id, device)| { - let title_color = Color::from_rgb(0.9, 0.9, 0.9); + .fold(column![].spacing(14), |col, (pci_id, device)| { + let title_color = Color::from_rgb(0.92, 0.92, 0.92); - let header: iced::Element<'_, Message> = row![ + let card_header: iced::Element<'_, Message> = row![ text!("{}", device.device_name) - .size(20) + .size(18) .color(title_color) .width(FillPortion(1)), ] .into(); - let width = 150; + let width = 140; let details = column![ row![ - text("PCI: ") - .color(Color::from_rgb(0.6, 0.6, 0.6)) + text("PCI ID: ") + .size(15) + .color(Color::from_rgb(0.72, 0.72, 0.75)) .width(width), text!("{}", pci_id) + .size(15) + .color(Color::from_rgb(0.92, 0.92, 0.92)) ], row![ text("IOMMU group: ") - .color(Color::from_rgb(0.6, 0.6, 0.6)) + .size(15) + .color(Color::from_rgb(0.72, 0.72, 0.75)) .width(width), text!("{}", device.iommu_group) + .size(15) + .color(Color::from_rgb(0.92, 0.92, 0.92)) ], row![ text("Vendor: ") - .color(Color::from_rgb(0.6, 0.6, 0.6)) + .size(15) + .color(Color::from_rgb(0.72, 0.72, 0.75)) .width(width), text!("{}", device.vendor_name) + .size(15) + .color(Color::from_rgb(0.92, 0.92, 0.92)) ], row![ text("Driver: ") - .color(Color::from_rgb(0.6, 0.6, 0.6)) + .size(15) + .color(Color::from_rgb(0.72, 0.72, 0.75)) .width(width), match device.driver.is_empty() { - true => text("N/A"), - false => text!("{}", device.driver), + true => text("N/A").size(15).color(Color::from_rgb(0.5, 0.5, 0.5)), + false => text!("{}", device.driver) + .size(15) + .color(Color::from_rgb(0.92, 0.92, 0.92)), } ], row![ text("Class: ") - .color(Color::from_rgb(0.6, 0.6, 0.6)) + .size(15) + .color(Color::from_rgb(0.72, 0.72, 0.75)) .width(width), text!("{}", device.class) + .size(15) + .color(Color::from_rgb(0.92, 0.92, 0.92)) ], row![ text("Parent Device: ") - .color(Color::from_rgb(0.6, 0.6, 0.6)) + .size(15) + .color(Color::from_rgb(0.72, 0.72, 0.75)) .width(width), match device.parent_pci.is_empty() { - true => text("N/A"), - false => text!("{}", device.parent_pci), + true => text("N/A").size(15).color(Color::from_rgb(0.5, 0.5, 0.5)), + false => text!("{}", device.parent_pci) + .size(15) + .color(Color::from_rgb(0.92, 0.92, 0.92)), } ], row![ text("Child Device: ") - .color(Color::from_rgb(0.6, 0.6, 0.6)) + .size(15) + .color(Color::from_rgb(0.72, 0.72, 0.75)) .width(width), match device.child_pci.is_empty() { - true => text!("N/A"), - false => text!("{}", device.child_pci), + true => text!("N/A").size(15).color(Color::from_rgb(0.5, 0.5, 0.5)), + false => text!("{}", device.child_pci) + .size(15) + .color(Color::from_rgb(0.92, 0.92, 0.92)), } ] ] .spacing(8); - let card = container(column![header, details].spacing(10)) + let card = container(column![card_header, details].spacing(10)) .width(Fill) - .padding(20) + .padding(18) .style(|_| box_theme!()); col.push(card) }), - ); - let title = row![ - text!("List of PCI Devices").size(30).center().width(Fill), - button("Copy to Clipboard").on_press(Message::PciListToClipboard()) - ]; - column![title, cards].spacing(15).into() + ) + .height(Fill); + + column![header, cards] + .spacing(16) + .width(Fill) + .height(Fill) + .into() } pub fn error_bar(msg: &str) -> Element<'_, Message> { @@ -879,38 +1196,21 @@ pub fn smart_mode_page<'a>( ) -> Element<'a, Message> { let is_smart = current_mode == Some(Mode::Smart); - let header = row![ - text("Smart Mode App Policies") - .size(20) - .color(Color::from_rgb(0.9, 0.9, 0.9)), - horizontal(), - text!("{} apps", smart_state.app_policies.len()).color(Color::from_rgb(0.6, 0.6, 0.6)), - ] - .align_y(Alignment::Center); + let header = page_header( + "Smart Mode Policies", + Some("Configure dynamic per-application GPU routing and isolation rules"), + Some(count_badge(format!( + "{} apps", + smart_state.app_policies.len() + ))), + ); let warning = if !is_smart { let current_mode_str = current_mode.map_or("Unknown".to_string(), |m| m.to_string()); - Some( - container( - row![ - text("⚠ ").size(20).color(Color::from_rgb(1.0, 0.8, 0.0)), - text!("Warning: Smart Mode is inactive (Current mode: {}). App policies are not enforced.", current_mode_str) - .color(Color::from_rgb(1.0, 0.8, 0.0)), - ] - .align_y(Alignment::Center) - .padding(10), - ) - .style(|_| container::Style { - background: Some(Color::from_rgb(0.25, 0.2, 0.05).into()), - border: Border { - radius: 8.0.into(), - width: 1.0, - color: Color::from_rgb(0.5, 0.4, 0.1), - }, - ..Default::default() - }) - .width(Fill), - ) + Some(warning_banner(format!( + "Warning: Smart Mode is inactive (Current mode: {}). App policies are not enforced.", + current_mode_str + ))) } else { None }; @@ -918,6 +1218,7 @@ pub fn smart_mode_page<'a>( // Search input & Refresh button let search_bar = text_input("Search app name or binary ID...", &smart_state.search_query) .on_input(Message::UpdateSmartSearch) + .style(search_input_style) .padding(8) .width(Fill); @@ -997,14 +1298,14 @@ pub fn smart_mode_page<'a>( // App title & binary ID let text_color = if !is_smart { - Color::from_rgb(0.5, 0.5, 0.5) + Color::from_rgb(0.6, 0.6, 0.6) } else { Color::WHITE }; let subtext_color = if !is_smart { - Color::from_rgb(0.4, 0.4, 0.4) + Color::from_rgb(0.5, 0.5, 0.5) } else { - Color::from_rgb(0.6, 0.6, 0.6) + Color::from_rgb(0.72, 0.72, 0.75) }; let dt_id = app.desktop_file_id.as_deref(); @@ -1015,23 +1316,23 @@ pub fn smart_mode_page<'a>( }; let app_info = column![ - text!("{}", app.display_name).color(text_color), - text!("{}", sub_text).color(subtext_color), + text!("{}", app.display_name).size(16).color(text_color), + text!("{}", sub_text).size(13).color(subtext_color), ] .spacing(2); // Policy toggle widget & status label let policy_label = if is_allowed { - text("Allowed (dGPU)").color(if is_smart { - Color::from_rgb(0.3, 0.8, 0.4) + text("Allowed (dGPU)").size(15).color(if is_smart { + Color::from_rgb(0.35, 0.85, 0.45) } else { - Color::from_rgb(0.4, 0.6, 0.4) + Color::from_rgb(0.4, 0.65, 0.4) }) } else { - text("Blocked (iGPU)").color(if is_smart { - Color::from_rgb(0.8, 0.4, 0.4) + text("Blocked (iGPU)").size(15).color(if is_smart { + Color::from_rgb(0.9, 0.4, 0.4) } else { - Color::from_rgb(0.6, 0.4, 0.4) + Color::from_rgb(0.65, 0.4, 0.4) }) }; @@ -1079,7 +1380,7 @@ pub fn smart_mode_page<'a>( let list_scrollable = scrollable(app_list_col).height(Fill); - let mut content = column![header].spacing(12).width(Fill).height(Fill); + let mut content = column![header].spacing(16).width(Fill).height(Fill); if let Some(w) = warning { content = content.push(w); From 89d75a1cb77f36afac723254773fc46dfc36593b Mon Sep 17 00:00:00 2001 From: luytan Date: Sat, 8 Aug 2026 17:07:54 +0200 Subject: [PATCH 02/11] feat(cardwire-gui): add images --- Cargo.lock | 945 +++++++++++++++++++++++++++++++++- Cargo.toml | 2 + crates/cardwire-gui/src/ui.rs | 22 +- 3 files changed, 959 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1dad23f..20f3c07f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "ahash" version = "0.8.12" @@ -40,6 +46,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "android-activity" version = "0.6.1" @@ -139,6 +163,23 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -157,6 +198,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "ash" version = "0.38.0+1.3.281" @@ -315,6 +365,49 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey 0.1.1", + "rayon", + "thiserror 2.0.19", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", +] + [[package]] name = "aya" version = "0.14.0" @@ -455,6 +548,12 @@ dependencies = [ "thiserror 2.0.19", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bit-set" version = "0.8.0" @@ -470,6 +569,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -482,6 +587,15 @@ version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + [[package]] name = "block" version = "0.1.6" @@ -530,6 +644,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "built" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" + [[package]] name = "bumpalo" version = "3.20.3" @@ -556,6 +676,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.12.1" @@ -858,6 +984,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.5" @@ -986,6 +1118,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.13" @@ -1026,6 +1177,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + [[package]] name = "defmt" version = "1.1.1" @@ -1103,6 +1260,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + [[package]] name = "endi" version = "1.1.1" @@ -1153,6 +1316,26 @@ dependencies = [ "log", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1214,6 +1397,23 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.74.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "num-complex", + "pulp", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -1232,12 +1432,43 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + [[package]] name = "float_next_after" version = "1.0.0" @@ -1493,6 +1724,26 @@ dependencies = [ "temp-dir", ] +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gl_generator" version = "0.14.0" @@ -1728,6 +1979,7 @@ dependencies = [ "iced_runtime", "iced_widget", "iced_winit", + "image", "thiserror 2.0.19", ] @@ -1826,6 +2078,8 @@ dependencies = [ "half", "iced_core", "iced_futures", + "image", + "kamadak-exif", "log", "lyon_path", "raw-window-handle", @@ -1880,8 +2134,9 @@ dependencies = [ "cosmic-text", "iced_debug", "iced_graphics", - "kurbo", + "kurbo 0.10.4", "log", + "resvg", "rustc-hash 2.1.3", "softbuffer", "tiny-skia", @@ -1903,6 +2158,7 @@ dependencies = [ "iced_graphics", "log", "lyon", + "resvg", "rustc-hash 2.1.3", "thiserror 2.0.19", "wgpu", @@ -1941,6 +2197,52 @@ dependencies = [ "winit", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif 0.14.2", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core 0.5.1", + "zune-jpeg 0.5.15", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + +[[package]] +name = "imgref" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7" + [[package]] name = "indexmap" version = "2.14.0" @@ -1973,6 +2275,17 @@ dependencies = [ "libc", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "io-lifetimes" version = "1.0.11" @@ -1990,6 +2303,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -2111,6 +2433,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kamadak-exif" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1130d80c7374efad55a117d715a3af9368f0fa7a2c54573afc15a188cd984837" +dependencies = [ + "mutate_once", +] + [[package]] name = "khronos-egl" version = "6.0.0" @@ -2135,7 +2466,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "814b44c24cd2cb236c3b8a41c7f08237b452a8e76ecaa81f1cec40b5b678215b" dependencies = [ "futures-util", - "pastey", + "pastey 0.2.3", "serde", "tokio", "zbus", @@ -2151,18 +2482,45 @@ dependencies = [ "smallvec", ] +[[package]] +name = "kurbo" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "libc" version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libloading" version = "0.8.9" @@ -2273,6 +2631,15 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "lru" version = "0.16.4" @@ -2340,6 +2707,16 @@ dependencies = [ "libc", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.8.3" @@ -2385,6 +2762,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.2" @@ -2396,6 +2783,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "mundy" version = "0.2.3" @@ -2421,6 +2818,12 @@ dependencies = [ "zbus", ] +[[package]] +name = "mutate_once" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d2233c9842d08cfe13f9eac96e207ca6a2ea10b80259ebe8ad0268be27d2af" + [[package]] name = "naga" version = "27.0.3" @@ -2478,17 +2881,78 @@ dependencies = [ ] [[package]] -name = "nom" -version = "7.1.3" +name = "new_debug_unreachable" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" dependencies = [ "memchr", - "minimal-lexical", ] [[package]] -name = "num-format" +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "bytemuck", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num-format" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" @@ -2497,6 +2961,26 @@ dependencies = [ "itoa", ] +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3017,12 +3501,24 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + [[package]] name = "pastey" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "pin-project" version = "1.1.13" @@ -3078,6 +3574,32 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.11.0" @@ -3107,6 +3629,15 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "presser" version = "0.3.1" @@ -3148,6 +3679,63 @@ name = "profiling" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pulp" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "paste", + "pulp-wasm-simd-flag", + "raw-cpuid", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp-wasm-simd-flag" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" @@ -3179,6 +3767,35 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "range-alloc" version = "0.1.5" @@ -3191,6 +3808,65 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "thiserror 2.0.19", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.13.1", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -3209,6 +3885,26 @@ dependencies = [ "objc2-quartz-core 0.3.2", ] +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "read-fonts" version = "0.35.0" @@ -3231,6 +3927,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "reborrow" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" + [[package]] name = "redox_syscall" version = "0.4.1" @@ -3293,6 +3995,32 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "resvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" +dependencies = [ + "gif 0.13.3", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", + "zune-jpeg 0.4.21", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + [[package]] name = "roxmltree" version = "0.20.0" @@ -3377,6 +4105,24 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.13.1", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + [[package]] name = "same-file" version = "1.0.6" @@ -3506,6 +4252,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + [[package]] name = "simd_cesu8" version = "1.2.0" @@ -3516,12 +4268,36 @@ dependencies = [ "simdutf8", ] +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "simdutf8" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + [[package]] name = "skrifa" version = "0.37.0" @@ -3700,6 +4476,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "static_assertions" version = "1.1.0" @@ -3711,6 +4493,9 @@ name = "strict-num" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] [[package]] name = "strsim" @@ -3745,6 +4530,16 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo 0.11.3", + "siphasher", +] + [[package]] name = "swash" version = "0.2.10" @@ -3864,6 +4659,20 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg 0.5.15", +] + [[package]] name = "tiny-skia" version = "0.11.4" @@ -3875,6 +4684,7 @@ dependencies = [ "bytemuck", "cfg-if", "log", + "png 0.17.16", "tiny-skia-path", ] @@ -4071,6 +4881,18 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -4083,6 +4905,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unicode-script" version = "0.5.8" @@ -4095,12 +4923,45 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + [[package]] name = "unicode-width" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "usvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb", + "imagesize", + "kurbo 0.11.3", + "log", + "pico-args", + "roxmltree", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + [[package]] name = "utf8parse" version = "0.2.2" @@ -4118,6 +4979,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -4153,7 +5025,7 @@ dependencies = [ "heck 0.4.1", "indexmap", "libloading", - "nom", + "nom 7.1.3", "once_cell", "parking_lot", "proc-macro2", @@ -4432,6 +5304,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "wgpu" version = "27.0.1" @@ -5097,6 +5975,18 @@ version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yazi" version = "0.2.1" @@ -5197,6 +6087,45 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core 0.5.1", +] + [[package]] name = "zvariant" version = "5.13.1" diff --git a/Cargo.toml b/Cargo.toml index eb924779..161f29d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,7 +57,9 @@ udev = { version = "^0.9.3", features = ["send", "sync"] } # cardwire doesn't support X11 and will never support it unless someone maintains it iced = { version = "0.14.0", default-features = false, features = [ "crisp", + "image", "linux-theme-detection", + "svg", "tokio", "tiny-skia", "wayland", diff --git a/crates/cardwire-gui/src/ui.rs b/crates/cardwire-gui/src/ui.rs index 7f0a1659..7b62e27e 100644 --- a/crates/cardwire-gui/src/ui.rs +++ b/crates/cardwire-gui/src/ui.rs @@ -1280,7 +1280,24 @@ pub fn smart_mode_page<'a>( Color::from_rgb(0.35, 0.2, 0.2) }; - let icon_element: Element<'_, Message> = + let icon_element: Element<'_, Message> = if let Some(ref path) = app.icon_path { + let ext = path + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + .to_lowercase(); + if ext == "svg" { + iced::widget::svg(iced::widget::svg::Handle::from_path(path)) + .width(38) + .height(38) + .into() + } else { + iced::widget::image(iced::widget::image::Handle::from_path(path)) + .width(38) + .height(38) + .into() + } + } else { container(text!("{}", initial).color(Color::WHITE)) .width(38) .height(38) @@ -1294,7 +1311,8 @@ pub fn smart_mode_page<'a>( }, ..Default::default() }) - .into(); + .into() + }; // App title & binary ID let text_color = if !is_smart { From dbeb7c585b1e5e20d188f7b300e0240331551b38 Mon Sep 17 00:00:00 2001 From: luytan Date: Sat, 8 Aug 2026 17:23:15 +0200 Subject: [PATCH 03/11] fix: pin rust date --- flake.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 598620cf..1b01d56e 100644 --- a/flake.nix +++ b/flake.nix @@ -25,7 +25,8 @@ let tc = (fenixpkgs system).toolchainOf { channel = "nightly"; - sha256 = "sha256-w0X+EGECIpZs67m1KoS/4Ac+q1n/PGzhlOJ+0z/BkO8="; + date = "2026-08-08"; + sha256 = "sha256-lYQxtgBt4zZfd4p7QDOaE26H7qwHwl9Pny1he+Nmn+s="; }; in (fenixpkgs system).combine [ From b2cef3213e04a0f08bf8760b816a62efcf2963b6 Mon Sep 17 00:00:00 2001 From: luytan Date: Sat, 8 Aug 2026 18:21:34 +0200 Subject: [PATCH 04/11] fix(cardwired): match all apps now --- .../cardwire-daemon/src/analyzer/helpers.rs | 69 +++++++++++++++++++ crates/cardwire-daemon/src/analyzer/models.rs | 33 ++++++--- crates/cardwire-daemon/src/file/sql.rs | 20 +++++- 3 files changed, 111 insertions(+), 11 deletions(-) diff --git a/crates/cardwire-daemon/src/analyzer/helpers.rs b/crates/cardwire-daemon/src/analyzer/helpers.rs index ceab6053..a2df52ac 100644 --- a/crates/cardwire-daemon/src/analyzer/helpers.rs +++ b/crates/cardwire-daemon/src/analyzer/helpers.rs @@ -72,6 +72,26 @@ pub fn parse_cmdline_name(cmdline_bytes: &[u8]) -> Option { } } + // Electron apps, the real app name is in the .asar path argument + if base_name == "electron" || base_name.ends_with("-electron") { + for arg in args.iter().skip(1) { + if arg.starts_with('-') { + continue; + } + if arg.ends_with(".asar") || arg.contains("resources/app") { + let path = Path::new(arg); + for component in path.components().rev() { + let part = component.as_os_str().to_string_lossy(); + if part == "app.asar" || part == "resources" || part == "app" || part == "share" + { + continue; + } + return Some(part.to_string()); + } + } + } + } + // Fix for discord or other apps: if base_name.contains("--") { return base_name.split_whitespace().next().map(|s| s.to_string()); @@ -84,6 +104,18 @@ pub fn is_proc_still_alive(pid: u32) -> bool { Path::new(&format!("/proc/{}", pid)).exists() } +/// Unwrap NixOS-style wrapper names into lookups, eg: +/// ".discord-wrapped" -> ["discord-wrapped", "discord"] +/// "steamwebhelper" -> ["steamwebhelper"] +pub fn normalized_candidates(name: &str) -> Vec { + let trimmed = name.trim_start_matches('.'); + let mut candidates = vec![trimmed.to_string()]; + if let Some(rest) = trimmed.strip_suffix("-wrapped") { + candidates.push(rest.to_string()); + } + candidates +} + /// Decode the 16-byte kernel comm into a String, trimming trailing NULs pub fn comm_to_string(comm: [u8; 16]) -> String { match String::from_utf8(comm.to_vec()) { @@ -167,4 +199,41 @@ mod tests { assert!(is_proc_still_alive(std::process::id())); assert!(!is_proc_still_alive(0)); } + + #[test] + fn test_parse_cmdline_name_extracts_electron_app_from_asar() { + // NixOS-style Obsidian wrapped in electron + let cmdline_bytes = b"/nix/store/abc-electron-41.10.3/libexec/electron/electron\0/nix/store/xyz-obsidian-1.12.7/share/obsidian/app.asar\0--ozone-platform=wayland"; + assert_eq!( + parse_cmdline_name(cmdline_bytes), + Some("obsidian".to_string()) + ); + } + + #[test] + fn test_parse_cmdline_name_electron_without_asar_falls_back() { + // Plain electron without .asar argument + let cmdline_bytes = b"/usr/bin/electron\0--no-sandbox"; + assert_eq!( + parse_cmdline_name(cmdline_bytes), + Some("electron".to_string()) + ); + } + + #[test] + fn test_normalized_candidates_unwraps_nix_wrapper() { + assert_eq!( + normalized_candidates(".discord-wrapped"), + vec!["discord-wrapped".to_string(), "discord".to_string()] + ); + } + + #[test] + fn test_normalized_candidates_plain_name_unchanged() { + assert_eq!( + normalized_candidates("steamwebhelper"), + vec!["steamwebhelper".to_string()] + ); + assert_eq!(normalized_candidates("steam"), vec!["steam".to_string()]); + } } diff --git a/crates/cardwire-daemon/src/analyzer/models.rs b/crates/cardwire-daemon/src/analyzer/models.rs index 085988c3..51051205 100644 --- a/crates/cardwire-daemon/src/analyzer/models.rs +++ b/crates/cardwire-daemon/src/analyzer/models.rs @@ -12,7 +12,9 @@ use zbus::object_server::SignalEmitter; use crate::{ analyzer::{ - dynamic_analysis::{check_env, get_app_id_wayland_with_retry, get_steam_app_id}, helpers::{comm_to_string, get_real_process_name, is_proc_still_alive}, static_analysis::{self, AppMetadata, watch_fdo_folders} + dynamic_analysis::{check_env, get_app_id_wayland_with_retry, get_steam_app_id}, helpers::{ + comm_to_string, get_real_process_name, is_proc_still_alive, normalized_candidates + }, static_analysis::{self, AppMetadata, watch_fdo_folders} }, file::{DbusAppMetadata, GpuPolicy}, interface::{LogEntry, LoggerInterfaceSignals, SmartPolicyInterface} }; #[repr(C)] @@ -324,7 +326,18 @@ impl CardwireAnalyzer { { let xdg_list = self.xdg_list.read().await; - if let Some(meta) = xdg_list.get(&lookup_name) { + for candidate in normalized_candidates(&lookup_name) { + if let Some(meta) = xdg_list.get(&candidate) { + let meta = meta.clone(); + drop(xdg_list); + self.discover_app(&lookup_name, meta).await; + return Some((false, PidType::Allowed, 0)); + } + } + if let Some((_key, meta)) = xdg_list + .iter() + .find(|(key, _)| key.len() >= 3 && lookup_name.starts_with(key.as_str())) + { let meta = meta.clone(); drop(xdg_list); self.discover_app(&lookup_name, meta).await; @@ -358,6 +371,12 @@ impl CardwireAnalyzer { } let dbus_meta = DbusAppMetadata::from_app_metadata(&meta, GpuPolicy::Blocked as u32); + // Mirror in the cache regardless of the outcome, the app is blocked by default + // and this prevents re-discovering it on every new process spawn + self.db_cache + .write() + .await + .insert(lookup_name.to_string(), GpuPolicy::Blocked); let (reply_tx, reply_rx) = tokio::sync::oneshot::channel(); let res = self .db_tx @@ -366,11 +385,6 @@ impl CardwireAnalyzer { match res { Ok(_) => match reply_rx.await { Ok(true) => { - // Mirror in the cache - self.db_cache - .write() - .await - .insert(lookup_name.to_string(), GpuPolicy::Blocked); if let Some(emitter) = self.new_app_signal.get() && let Err(e) = SmartPolicyInterface::new_app_added( emitter, @@ -385,9 +399,8 @@ impl CardwireAnalyzer { lookup_name ); } - Ok(false) => { - error!("Couldn't write {} to the database", lookup_name) - } + // Duplicate entry or write failure, nothing to do + Ok(false) => {} Err(err) => { error!("DB worker dropped the reply for {}: {}", lookup_name, err) } diff --git a/crates/cardwire-daemon/src/file/sql.rs b/crates/cardwire-daemon/src/file/sql.rs index d86fb391..96b8ff4e 100644 --- a/crates/cardwire-daemon/src/file/sql.rs +++ b/crates/cardwire-daemon/src/file/sql.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::Arc}; use crate::{STATE_PATH, analyzer::AppMetadata}; use log::error; -use rusqlite::{Connection, Result}; +use rusqlite::{Connection, OptionalExtension, Result}; use tokio::sync::{RwLock, mpsc, oneshot}; use zbus::zvariant; @@ -95,6 +95,24 @@ impl CardwireDatabase { tokio::task::spawn_blocking(move || { let conn = conn; while let Some((binary_name, meta, reply)) = rx.blocking_recv() { + if let Some(desktop_file_id) = meta.desktop_file_id.as_deref() + && conn + .query_row( + "SELECT 1 FROM app_policies + WHERE desktop_file_id IS NOT NULL + AND desktop_file_id = ?1 + AND binary_name != ?2 + LIMIT 1", + rusqlite::params![desktop_file_id, binary_name], + |_| Ok(()), + ) + .optional() + .unwrap_or(None) + .is_some() + { + let _ = reply.send(false); + continue; + } let res = conn.execute( "INSERT INTO app_policies (binary_name, display_name, desktop_file_id, icon_name, policy) VALUES (?1, ?2, ?3, ?4, 0) From a29f7c104a60105e1f59635c0d260b92255550c8 Mon Sep 17 00:00:00 2001 From: luytan Date: Sat, 8 Aug 2026 18:27:39 +0200 Subject: [PATCH 05/11] fix(cardwire-gui): scrolling in log, duplicate version and blocked color --- crates/cardwire-gui/src/ui.rs | 43 ++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/crates/cardwire-gui/src/ui.rs b/crates/cardwire-gui/src/ui.rs index 7b62e27e..0a374039 100644 --- a/crates/cardwire-gui/src/ui.rs +++ b/crates/cardwire-gui/src/ui.rs @@ -485,13 +485,9 @@ fn gpu_cards( .size(15) .color(Color::from_rgb(0.72, 0.72, 0.75)) .width(width), - text(if gpu.blocked { "Yes" } else { "No" }).size(15).color( - if gpu.blocked { - Color::from_rgb(0.95, 0.3, 0.3) - } else { - Color::from_rgb(0.4, 0.8, 0.4) - } - ), + text(if gpu.blocked { "Yes" } else { "No" }) + .size(15) + .color(Color::from_rgb(0.92, 0.92, 0.92)), horizontal(), power_state_badge(&gpu.power_state), ] @@ -670,19 +666,24 @@ pub fn logs_page<'a>( }) }; - let terminal = container(scrollable(content).height(Fill)) - .width(Fill) - .height(Fill) - .padding(16) - .style(|_| container::Style { - background: Some(Color::from_rgb(0.07, 0.07, 0.08).into()), - border: Border { - radius: 8.0.into(), - width: 1.0, - color: Color::from_rgb(0.2, 0.2, 0.2), - }, - ..Default::default() - }); + let terminal = container(scrollable(content).width(Fill).height(Fill).direction( + scrollable::Direction::Both { + vertical: Default::default(), + horizontal: Default::default(), + }, + )) + .width(Fill) + .height(Fill) + .padding(16) + .style(|_| container::Style { + background: Some(Color::from_rgb(0.07, 0.07, 0.08).into()), + border: Border { + radius: 8.0.into(), + width: 1.0, + color: Color::from_rgb(0.2, 0.2, 0.2), + }, + ..Default::default() + }); column![header, terminal] .spacing(16) @@ -738,7 +739,7 @@ pub fn about_page() -> Element<'static, Message> { let header = page_header( "About Cardwire", Some("GPU management and process isolation for Linux"), - Some(count_badge(format!("v{}", version))), + None, ); let info_card = container( From 82c633b3c1e636982c3475751f8f8beabcb6204d Mon Sep 17 00:00:00 2001 From: luytan Date: Sat, 8 Aug 2026 18:45:33 +0200 Subject: [PATCH 06/11] fix(cardwired): in smart, default should not be reported blocked --- crates/cardwire-daemon/src/interface/gpu.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/cardwire-daemon/src/interface/gpu.rs b/crates/cardwire-daemon/src/interface/gpu.rs index 30883e91..04ae63ce 100644 --- a/crates/cardwire-daemon/src/interface/gpu.rs +++ b/crates/cardwire-daemon/src/interface/gpu.rs @@ -232,6 +232,10 @@ impl GpuInterface { #[zbus(property)] pub async fn block(&self) -> fdo::Result { + let mode = self.mode_state.read().await.mode(); + if mode == Modes::Smart && (self.device.is_default() && !self.device.is_discrete()) { + return Ok(false); + } self.gpu_blocked().await } From 31b9884da93b292eb56b389c4f2d3e4c1b65879d Mon Sep 17 00:00:00 2001 From: luytan Date: Sat, 8 Aug 2026 18:52:49 +0200 Subject: [PATCH 07/11] fix(cardwire-gui): show unknown on power state --- crates/cardwire-gui/src/ui.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cardwire-gui/src/ui.rs b/crates/cardwire-gui/src/ui.rs index 0a374039..3b39d733 100644 --- a/crates/cardwire-gui/src/ui.rs +++ b/crates/cardwire-gui/src/ui.rs @@ -215,7 +215,7 @@ fn power_state_badge<'a>(power_state: &Option) -> Element<'a, Message> { .into() } else { container( - text(format!("Active ({})", trimmed)) + text("Unknown") .size(14) .color(Color::from_rgb(0.9, 0.9, 0.9)), ) From 680790dce6c2d5de2e64a9746e22006d4550da29 Mon Sep 17 00:00:00 2001 From: luytan Date: Sat, 8 Aug 2026 19:13:01 +0200 Subject: [PATCH 08/11] feat(cardwired): send block_changed signal on mode change --- crates/cardwire-daemon/src/daemon.rs | 11 +++++++---- crates/cardwire-daemon/src/interface/gpu.rs | 2 ++ crates/cardwire-daemon/src/interface/mode.rs | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/crates/cardwire-daemon/src/daemon.rs b/crates/cardwire-daemon/src/daemon.rs index 3a1cf221..8f770183 100644 --- a/crates/cardwire-daemon/src/daemon.rs +++ b/crates/cardwire-daemon/src/daemon.rs @@ -147,12 +147,15 @@ async fn spawn_dbus_api( object_server .at(path.clone(), gpu_interface.as_ref().clone()) .await?; + let gpu_ref = object_server + .interface::<_, crate::interface::GpuInterface>(path) + .await?; + gpu_interface + .signal_emitter + .get_or_init(|| gpu_ref.signal_emitter().to_owned()); // spawn power state watcher only for available GPUs if gpu_interface.device.is_available() { - let handle = task::spawn(watch_power_state( - Arc::clone(gpu_interface), - object_server.interface(path).await?, - )); + let handle = task::spawn(watch_power_state(Arc::clone(gpu_interface), gpu_ref)); power_tasks.insert(*id, handle); } } diff --git a/crates/cardwire-daemon/src/interface/gpu.rs b/crates/cardwire-daemon/src/interface/gpu.rs index 04ae63ce..2d630cd6 100644 --- a/crates/cardwire-daemon/src/interface/gpu.rs +++ b/crates/cardwire-daemon/src/interface/gpu.rs @@ -34,6 +34,7 @@ pub struct GpuInterface { pci_list: Arc>>, gpu_state: Arc>, mode_state: Arc>, + pub signal_emitter: Arc>>, } impl GpuInterface { @@ -54,6 +55,7 @@ impl GpuInterface { pci_list, gpu_state, mode_state, + signal_emitter: Arc::new(OnceLock::new()), }) } } diff --git a/crates/cardwire-daemon/src/interface/mode.rs b/crates/cardwire-daemon/src/interface/mode.rs index 0fa582c7..22dda38f 100644 --- a/crates/cardwire-daemon/src/interface/mode.rs +++ b/crates/cardwire-daemon/src/interface/mode.rs @@ -67,6 +67,20 @@ impl ModeInterface { if let Some(emitter) = self.signal_emitter.get() { self.mode_changed(emitter).await?; } + + // Emit block_changed signal after the mode has been applied + let gpu_list = self.gpu_list.read().await; + for gpu in gpu_list.values().filter(|gpu| gpu.device.is_available()) { + let Some(emitter) = gpu.signal_emitter.get() else { + continue; + }; + if let Err(e) = gpu.block_changed(emitter).await { + warn!( + "failed to emit Block property change for {}: {e}", + gpu.device.name() + ); + } + } Ok(()) } From 612b7fa229316685c4961b184ce00d0a034f26d0 Mon Sep 17 00:00:00 2001 From: luytan Date: Sat, 8 Aug 2026 22:28:09 +0200 Subject: [PATCH 09/11] fix(cardiwred): handle emitter error and init emitter before mode --- crates/cardwire-daemon/src/daemon.rs | 9 +++++---- crates/cardwire-daemon/src/interface/mode.rs | 12 +++++++----- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/crates/cardwire-daemon/src/daemon.rs b/crates/cardwire-daemon/src/daemon.rs index 8f770183..e6742352 100644 --- a/crates/cardwire-daemon/src/daemon.rs +++ b/crates/cardwire-daemon/src/daemon.rs @@ -132,10 +132,6 @@ async fn spawn_dbus_api( let path = "/org/opengamingcollective/cardwire"; let gpu_interfaces = daemon.inner.gpu_list.read().await; - // cardwire.Mode - object_server - .at(path, daemon.mode_interface.clone()) - .await?; // cardwire.Config object_server .at(path, daemon.config_interface.clone()) @@ -159,6 +155,11 @@ async fn spawn_dbus_api( power_tasks.insert(*id, handle); } } + + // cardwire.Mode + object_server + .at(path, daemon.mode_interface.clone()) + .await?; // Cardwire logger object_server .at(path, daemon.logger_interface.clone()) diff --git a/crates/cardwire-daemon/src/interface/mode.rs b/crates/cardwire-daemon/src/interface/mode.rs index 22dda38f..19cd52a5 100644 --- a/crates/cardwire-daemon/src/interface/mode.rs +++ b/crates/cardwire-daemon/src/interface/mode.rs @@ -64,9 +64,11 @@ impl ModeInterface { } } - if let Some(emitter) = self.signal_emitter.get() { - self.mode_changed(emitter).await?; - } + if let Some(emitter) = self.signal_emitter.get() + && let Err(err) = self.mode_changed(emitter).await + { + warn!("failed to emit mode change signal: {err}"); + }; // Emit block_changed signal after the mode has been applied let gpu_list = self.gpu_list.read().await; @@ -74,9 +76,9 @@ impl ModeInterface { let Some(emitter) = gpu.signal_emitter.get() else { continue; }; - if let Err(e) = gpu.block_changed(emitter).await { + if let Err(err) = gpu.block_changed(emitter).await { warn!( - "failed to emit Block property change for {}: {e}", + "failed to emit Block property change for {}: {err}", gpu.device.name() ); } From 09a7c4872ba297197597f966311df0a9bf137d04 Mon Sep 17 00:00:00 2001 From: luytan Date: Sat, 8 Aug 2026 22:41:38 +0200 Subject: [PATCH 10/11] fix(cardwired): init gpu signal on refresh --- crates/cardwire-daemon/src/interface/debug.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/cardwire-daemon/src/interface/debug.rs b/crates/cardwire-daemon/src/interface/debug.rs index b1bd73c1..6215281e 100644 --- a/crates/cardwire-daemon/src/interface/debug.rs +++ b/crates/cardwire-daemon/src/interface/debug.rs @@ -125,15 +125,16 @@ impl DebugInterface { object_server .at(path.clone(), gpu_interface.as_ref().clone()) .await?; + let gpu_ref = object_server + .interface::<_, GpuInterface>(path) + .await + .map_err(|err| fdo::Error::Failed(err.to_string()))?; + gpu_interface + .signal_emitter + .get_or_init(|| gpu_ref.signal_emitter().to_owned()); // spawn power state tasks only for available GPUs if gpu_interface.device.is_available() { - let handle = task::spawn(watch_power_state( - Arc::clone(gpu_interface), - object_server - .interface(path) - .await - .map_err(|err| fdo::Error::Failed(err.to_string()))?, - )); + let handle = task::spawn(watch_power_state(Arc::clone(gpu_interface), gpu_ref)); power_tasks.insert(*id, handle); } } From 6eb98f19cff4867463e425c1bb25a34f859c487e Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 9 Aug 2026 10:22:00 +0200 Subject: [PATCH 11/11] fix(cardwire-gui): remove scrollable gpu --- crates/cardwire-gui/src/ui.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/cardwire-gui/src/ui.rs b/crates/cardwire-gui/src/ui.rs index 3b39d733..99671380 100644 --- a/crates/cardwire-gui/src/ui.rs +++ b/crates/cardwire-gui/src/ui.rs @@ -293,10 +293,7 @@ pub fn main_page<'a>( let mode_card = mode_element(main_state.current_mode, &main_state.available_modes); let gpus = gpu_cards(gpu_list, main_state.open_gpu_menu, main_state.current_mode); - - let content = column![mode_card, gpus].spacing(20); - - column![header, scrollable(content).height(Fill)] + column![header, mode_card, gpus] .spacing(16) .width(Fill) .height(Fill)