Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/cardwire-gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ ksni.workspace = true
toml.workspace = true
xdg.workspace = true
chrono.workspace = true
freedesktop-desktop-entry.workspace = true

[[bin]]
name = "cardwire-gui"
Expand Down
61 changes: 58 additions & 3 deletions crates/cardwire-gui/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use iced::{
Alignment, Element, Length::{Fill, Fixed}, Subscription, Task, widget::{column, container, row, stack, text}, window
Alignment, Element, Length::{Fill, Fixed}, Subscription, Task, widget::{column, container, row, stack}, window
};
use log::error;
use std::collections::BTreeMap;

use crate::{
gui_config::{GuiConfig, PrimaryClickAction}, helpers::{CardwireDbus, GpuDevice}, message::Message, models::{DaemonSettings, LogState, MainState, Mode, Page, PciDevice, SettingState}, tray::{self, TrayAction, TrayHandle}, ui::{self, daemon_setting_page, error_bar, info_bar, pci_page}
gui_config::{GuiConfig, PrimaryClickAction}, helpers::{CardwireDbus, GpuDevice}, message::Message, models::{
DaemonSettings, LogState, MainState, Mode, Page, PciDevice, SettingState, SmartState
}, tray::{self, TrayAction, TrayHandle}, ui::{self, daemon_setting_page, error_bar, info_bar, pci_page}
};

#[derive(Debug)]
Expand All @@ -19,6 +21,7 @@ pub struct AppState {
pub main_state: MainState,
pub setting_state: SettingState,
pub log_state: LogState,
pub smart_state: SmartState,
window_id: Option<window::Id>,
tray_handle: Option<TrayHandle>,
tray_available: bool,
Expand Down Expand Up @@ -52,6 +55,7 @@ impl AppState {
..SettingState::default()
},
log_state: LogState::default(),
smart_state: SmartState::default(),
window_id,
tray_handle: None,
tray_available: true,
Expand Down Expand Up @@ -373,6 +377,57 @@ impl AppState {
},
// Append a new blocked process log received from dbus
Message::NewLog(log) => self.log_state.push(log),
Message::FetchedAppPolicies(res) => match res {
Ok(policies) => {
let mut map = BTreeMap::new();
for (app_id, meta) in policies {
let resolved = crate::helpers::resolve_app_metadata(&app_id, &meta);
map.insert(app_id, resolved);
}
self.smart_state.app_policies = map;
self.smart_state.loading = false;
self.error = None;
}
Err(err) => {
self.smart_state.loading = false;
self.error = Some(format!("Error fetching app policies: {}", err));
}
},
Message::SetAppPolicy(app_id, policy) => {
let conn = self.zbus_conn.clone();
let app_id_clone = app_id.clone();
return Task::perform(
async move {
conn.set_app_policy(app_id_clone.clone(), policy)
.await
.map_err(|e| e.to_string())?;
Ok((app_id_clone, policy))
},
Message::AppPolicyResult,
);
}
Message::AppPolicyResult(res) => match res {
Ok((app_id, policy)) => {
if let Some(app) = self.smart_state.app_policies.get_mut(&app_id) {
app.gpu_policy = policy as u32;
}
let status = if policy == 1 { "Allowed" } else { "Blocked" };
self.info = Some(format!("App policy for {} updated to {}", app_id, status));
self.error = None;
}
Err(err) => self.error = Some(format!("App policy error: {}", err)),
},
Message::UpdateSmartSearch(query) => {
self.smart_state.search_query = query;
}
Message::RefreshSmartPolicies => {
self.smart_state.loading = true;
let conn = self.zbus_conn.clone();
return Task::perform(
async move { conn.get_app_policies().await.map_err(|e| e.to_string()) },
Message::FetchedAppPolicies,
);
}
Message::OpenUrl(url) => {
let _ = std::process::Command::new("xdg-open").arg(url).spawn();
}
Expand Down Expand Up @@ -439,7 +494,7 @@ impl AppState {
main_content = main_content.push(container(match &self.current_tab {
Page::Main => ui::main_page(&self.main_state, &self.gpu_list),
Page::Pci => pci_page(&self.pci_list),
Page::SmartMode => text("Smart Mode TODO").into(),
Page::SmartMode => ui::smart_mode_page(&self.smart_state, self.main_state.current_mode),
Page::CardwireSettings => daemon_setting_page(&self.setting_state),
Page::Logs => ui::logs_page(&self.log_state, &self.gpu_list),
Page::Advanced => ui::advanced_page(),
Expand Down
176 changes: 176 additions & 0 deletions crates/cardwire-gui/src/helpers/app_resolver.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
use std::{
collections::HashSet, path::{Path, PathBuf}
};

use crate::models::{DbusAppMetadata, ResolvedApp};
use freedesktop_desktop_entry::DesktopEntry;

/// Returns all XDG data directories to search for applications and icons.
fn get_xdg_data_dirs() -> Vec<PathBuf> {
let mut dirs = Vec::new();

if let Ok(home) = std::env::var("HOME") {
let user_share = PathBuf::from(home.clone()).join(".local/share");
if user_share.exists() {
dirs.push(user_share);
}
let user_icons = PathBuf::from(home).join(".icons");
if user_icons.exists() {
dirs.push(user_icons);
}
}

if let Ok(val) = std::env::var("XDG_DATA_DIRS") {
for p in val.split(':') {
if !p.is_empty() {
let pb = PathBuf::from(p);
if pb.exists() && !dirs.contains(&pb) {
dirs.push(pb);
}
}
}
}

// Fallback standard locations on Linux and nix
let fallbacks = [
"/run/current-system/sw/share",
"/var/lib/flatpak/exports/share",
"/usr/local/share",
"/usr/share",
];

for fb in fallbacks {
let pb = PathBuf::from(fb);
if pb.exists() && !dirs.contains(&pb) {
dirs.push(pb);
}
}

dirs
}

/// Resolves raw DbusAppMetadata into a ResolvedApp
pub fn resolve_app_metadata(app_id: &str, raw: &DbusAppMetadata) -> ResolvedApp {
let data_dirs = get_xdg_data_dirs();
let locales = freedesktop_desktop_entry::get_languages_from_env();

let mut resolved_name: Option<String> = None;
let mut resolved_icon_name: Option<String> = raw.icon_name.clone();

let mut candidate_filenames = Vec::new();
if let Some(ref dt_id) = raw.desktop_file_id {
if dt_id.ends_with(".desktop") {
candidate_filenames.push(dt_id.clone());
} else {
candidate_filenames.push(format!("{}.desktop", dt_id));
}
}
candidate_filenames.push(format!("{}.desktop", app_id));

'search_desktop: for data_dir in &data_dirs {
let apps_dir = data_dir.join("applications");
for candidate in &candidate_filenames {
let path = apps_dir.join(candidate);
if path.exists()
&& let Ok(entry) = DesktopEntry::from_path(&path, Some(&locales))
{
if let Some(name) = entry.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.icon()
{
resolved_icon_name = Some(icon.to_string());
}
if resolved_name.is_some() {
break 'search_desktop;
}
}
}
}

// Determine display name
let display_name = if let Some(name) = resolved_name {
name
} else if !raw.display_name.trim().is_empty() {
raw.display_name.clone()
} else {
app_id
.split(&['-', '_'][..])
.map(|s| {
let mut c = s.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
})
.collect::<Vec<_>>()
.join(" ")
};

let icon_path = resolve_icon_path(resolved_icon_name.as_deref(), app_id, &data_dirs);

ResolvedApp {
app_id: app_id.to_string(),
display_name,
desktop_file_id: raw.desktop_file_id.clone(),
icon_name: resolved_icon_name,
icon_path,
gpu_policy: raw.gpu_policy,
}
}

/// Resolves an icon name or app_id to a image
fn resolve_icon_path(
icon_name: Option<&str>,
app_id: &str,
data_dirs: &[PathBuf],
) -> Option<PathBuf> {
let mut names_to_check = Vec::new();
if let Some(name) = icon_name
&& !name.trim().is_empty()
{
let p = Path::new(name);
if p.is_absolute() && p.exists() {
return Some(p.to_path_buf());
}
names_to_check.push(name);
}
names_to_check.push(app_id);

let extensions = ["png", "svg", "xpm"];
let icon_subdirs = [
"icons/hicolor/128x128/apps",
"icons/hicolor/256x256/apps",
"icons/hicolor/512x512/apps",
"icons/hicolor/64x64/apps",
"icons/hicolor/48x48/apps",
"icons/hicolor/scalable/apps",
"pixmaps",
"icons/hicolor/32x32/apps",
];

let mut searched_paths = HashSet::new();

for dir in data_dirs {
for sub in &icon_subdirs {
let base = dir.join(sub);
if !base.exists() {
continue;
}
for name in &names_to_check {
for ext in &extensions {
let file_path = base.join(format!("{}.{}", name, ext));
if searched_paths.insert(file_path.clone()) && file_path.exists() {
return Some(file_path);
}
}
}
}
}

None
}
24 changes: 23 additions & 1 deletion crates/cardwire-gui/src/helpers/dbus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use zbus::{
self, Connection, fdo, names::OwnedInterfaceName, zvariant::{OwnedObjectPath, OwnedValue}
};

use crate::models::{DaemonSettings, LsofData, Mode};
use crate::models::{DaemonSettings, DbusAppMetadata, LsofData, Mode};

#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct GpuDevice {
Expand Down Expand Up @@ -209,4 +209,26 @@ impl CardwireDbus {
.await?;
proxy.call("RefreshGpu", &()).await
}
pub async fn get_app_policies(&self) -> zbus::Result<HashMap<String, DbusAppMetadata>> {
let connection = Connection::system().await?;
let proxy = zbus::Proxy::new(
&connection,
"org.opengamingcollective.cardwire",
"/org/opengamingcollective/cardwire",
"org.opengamingcollective.cardwire.SmartPolicy",
)
.await?;
proxy.call("GetAppPolicies", &()).await
}
pub async fn set_app_policy(&self, app_id: String, policy: i32) -> zbus::Result<()> {
let connection = Connection::system().await?;
let proxy = zbus::Proxy::new(
&connection,
"org.opengamingcollective.cardwire",
"/org/opengamingcollective/cardwire",
"org.opengamingcollective.cardwire.SmartPolicy",
)
.await?;
proxy.call("SetAppPolicy", &(app_id, policy)).await
}
}
2 changes: 2 additions & 0 deletions crates/cardwire-gui/src/helpers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod app_resolver;
mod dbus;

pub use app_resolver::resolve_app_metadata;
pub use dbus::{CardwireDbus, GpuDevice};
7 changes: 7 additions & 0 deletions crates/cardwire-gui/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ pub enum Message {
RefreshGpuResult(Result<(), String>),
FetchedLogs(Result<VecDeque<LogEntry>, String>),
NewLog(LogEntry),
FetchedAppPolicies(
Result<std::collections::HashMap<String, crate::models::DbusAppMetadata>, String>,
),
SetAppPolicy(String, i32),
AppPolicyResult(Result<(String, i32), String>),
UpdateSmartSearch(String),
RefreshSmartPolicies,
OpenUrl(String),
ClearError,
ClearInfo,
Expand Down
26 changes: 26 additions & 0 deletions crates/cardwire-gui/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,29 @@ pub struct PciDevice {
pub parent_pci: String,
pub child_pci: String,
}

#[derive(serde::Deserialize, serde::Serialize, zbus::zvariant::Type, Debug, Clone)]
pub struct DbusAppMetadata {
pub display_name: String,
pub desktop_file_id: Option<String>,
pub icon_name: Option<String>,
pub gpu_policy: u32,
}

#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct ResolvedApp {
pub app_id: String,
pub display_name: String,
pub desktop_file_id: Option<String>,
pub icon_name: Option<String>,
pub icon_path: Option<std::path::PathBuf>,
pub gpu_policy: u32,
}

#[derive(Default, Clone, Debug)]
pub struct SmartState {
pub app_policies: std::collections::BTreeMap<String, ResolvedApp>,
pub search_query: String,
pub loading: bool,
}
Loading