diff --git a/Cargo.lock b/Cargo.lock index 3482568..0b168bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -873,8 +873,11 @@ dependencies = [ "cosmic-config", "delegate", "env_logger", + "freedesktop-desktop-entry", + "freedesktop-icons", "futures-channel", "gbm", + "itertools 0.12.0", "libcosmic", "memmap2 0.9.0", "once_cell", @@ -1087,6 +1090,15 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" +dependencies = [ + "dirs-sys 0.3.7", +] + [[package]] name = "dirs" version = "4.0.0" @@ -1524,6 +1536,19 @@ dependencies = [ "num", ] +[[package]] +name = "freedesktop-desktop-entry" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45157175a725e81f3f594382430b6b78af5f8f72db9bd51b94f0785f80fc6d29" +dependencies = [ + "dirs 3.0.2", + "gettext-rs", + "memchr", + "thiserror", + "xdg", +] + [[package]] name = "freedesktop-icons" version = "0.2.4" @@ -1721,6 +1746,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gettext-rs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e49ea8a8fad198aaa1f9655a2524b64b70eb06b2f3ff37da407566c93054f364" +dependencies = [ + "gettext-sys", + "locale_config", +] + +[[package]] +name = "gettext-sys" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c63ce2e00f56a206778276704bbe38564c8695249fdc8f354b4ef71c57c3839d" +dependencies = [ + "cc", + "temp-dir", +] + [[package]] name = "gif" version = "0.12.0" @@ -2044,7 +2089,7 @@ dependencies = [ "iced_graphics", "iced_runtime", "iced_style", - "itertools", + "itertools 0.10.5", "lazy_static", "raw-window-handle", "smithay-client-toolkit 0.18.0", @@ -2259,6 +2304,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +dependencies = [ + "either", +] + [[package]] name = "jpeg-decoder" version = "0.3.0" @@ -2452,6 +2506,19 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d399c713b009e1604320479fddb3f029b8c4c7840715ea50217c0df599d804" +[[package]] +name = "locale_config" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934" +dependencies = [ + "lazy_static", + "objc", + "objc-foundation", + "regex", + "winapi", +] + [[package]] name = "lock_api" version = "0.4.11" @@ -2821,6 +2888,17 @@ dependencies = [ "objc_exception", ] +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + [[package]] name = "objc_exception" version = "0.1.2" @@ -2830,6 +2908,15 @@ dependencies = [ "cc", ] +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "object" version = "0.32.1" @@ -3825,6 +3912,12 @@ dependencies = [ "slotmap", ] +[[package]] +name = "temp-dir" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af547b166dd1ea4b472165569fc456cfb6818116f854690b0ff205e636523dab" + [[package]] name = "tempfile" version = "3.8.1" diff --git a/Cargo.toml b/Cargo.toml index 534c9de..6ee6a60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,8 @@ futures-channel = "0.3.25" gbm = "0.14.0" libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false, features = ["tokio", "wayland", "single-instance"] } cosmic-config = { git = "https://github.com/pop-os/libcosmic" } +freedesktop-desktop-entry = "0.5.0" +freedesktop-icons = "0.2.4" memmap2 = "0.9.0" tokio = "1.23.0" @@ -20,6 +22,7 @@ wayland-protocols = "0.31.0" zbus = { version = "3.7.0", default-features = false, features = ["tokio"] } once_cell = "1.18.0" delegate = "0.11.0" +itertools = "0.12.0" [profile.dev] # Not usable at opt-level 0, at least with software renderer diff --git a/src/desktop_info.rs b/src/desktop_info.rs new file mode 100644 index 0000000..f5971ba --- /dev/null +++ b/src/desktop_info.rs @@ -0,0 +1,95 @@ +// Coppied from cosmic-app-list +// - Put in a library? libcosmic? + +use freedesktop_desktop_entry::DesktopEntry; +use itertools::Itertools; +use std::path::PathBuf; + +pub fn icon_for_app_id(app_id: String) -> Option { + Some( + desktop_info_for_app_ids(vec![app_id]) + .into_iter() + .next()? + .icon, + ) +} + +#[derive(Debug, Clone, Default)] +struct DesktopInfo { + id: String, + wm_class: Option, + icon: PathBuf, + exec: String, + name: String, + path: PathBuf, +} + +fn default_app_icon() -> PathBuf { + freedesktop_icons::lookup("application-default") + .with_theme("Cosmic") + .force_svg() + .with_cache() + .find() + .or_else(|| { + freedesktop_icons::lookup("application-x-executable") + .with_theme("default") + .with_size(128) + .with_cache() + .find() + }) + .unwrap_or_default() +} + +fn desktop_info_for_app_ids(mut app_ids: Vec) -> Vec { + let app_ids_clone = app_ids.clone(); + let mut ret = freedesktop_desktop_entry::Iter::new(freedesktop_desktop_entry::default_paths()) + .filter_map(|path| { + std::fs::read_to_string(&path).ok().and_then(|input| { + DesktopEntry::decode(&path, &input).ok().and_then(|de| { + if let Some(i) = app_ids.iter().position(|s| { + s == de.appid || s.eq(&de.startup_wm_class().unwrap_or_default()) + }) { + let icon = freedesktop_icons::lookup(de.icon().unwrap_or(de.appid)) + .with_size(128) + .with_cache() + .find() + .unwrap_or_else(default_app_icon); + app_ids.remove(i); + + Some(DesktopInfo { + id: de.appid.to_string(), + wm_class: de.startup_wm_class().map(ToString::to_string), + icon, + exec: de.exec().unwrap_or_default().to_string(), + name: de.name(None).unwrap_or_default().to_string(), + path: path.clone(), + }) + } else { + None + } + }) + }) + }) + .collect_vec(); + ret.append( + &mut app_ids + .into_iter() + .map(|id| DesktopInfo { + id, + icon: default_app_icon(), + ..Default::default() + }) + .collect_vec(), + ); + ret.sort_by(|a, b| { + app_ids_clone + .iter() + .position(|id| id == &a.id || Some(id) == a.wm_class.as_ref()) + .cmp( + &app_ids_clone + .iter() + .position(|id| id == &b.id || Some(id) == b.wm_class.as_ref()), + ) + }); + ret +} diff --git a/src/main.rs b/src/main.rs index 01814e9..97ce56e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,9 +42,11 @@ use once_cell::sync::Lazy; use std::{ collections::{HashMap, HashSet}, mem, + path::PathBuf, str::{self, FromStr}, }; +mod desktop_info; mod view; mod wayland; mod widgets; @@ -134,6 +136,7 @@ struct Toplevel { handle: zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1, info: ToplevelInfo, img: Option, + icon: Option, } #[derive(Clone)] @@ -436,6 +439,7 @@ impl Application for App { wayland::Event::NewToplevel(handle, info) => { println!("New toplevel: {info:?}"); self.toplevels.push(Toplevel { + icon: desktop_info::icon_for_app_id(info.app_id.clone()), handle, info, img: None, @@ -445,6 +449,7 @@ impl Application for App { if let Some(toplevel) = self.toplevels.iter_mut().find(|x| x.handle == handle) { + toplevel.icon = desktop_info::icon_for_app_id(info.app_id.clone()); toplevel.info = info; } } diff --git a/src/view/mod.rs b/src/view/mod.rs index 03f5305..06fe0b8 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -194,6 +194,13 @@ fn workspaces_sidebar<'a>( } pub(crate) fn toplevel_preview(toplevel: &Toplevel) -> cosmic::Element { + let label = widget::text(&toplevel.info.title); + let label = if let Some(icon) = &toplevel.icon { + row![widget::icon(widget::icon::from_path(icon.clone())), label].spacing(4) + } else { + row![label] + } + .padding(4); column![ close_button(Msg::CloseToplevel(toplevel.handle.clone())), widget::button(capture_image(toplevel.img.as_ref())) @@ -205,8 +212,7 @@ pub(crate) fn toplevel_preview(toplevel: &Toplevel) -> cosmic::Element { ) .style(cosmic::theme::Button::Image) .on_press(Msg::ActivateToplevel(toplevel.handle.clone())), - widget::button(widget::text(&toplevel.info.title)) - .on_press(Msg::ActivateToplevel(toplevel.handle.clone())) + widget::button(label).on_press(Msg::ActivateToplevel(toplevel.handle.clone())) ] .spacing(4) .align_items(iced::Alignment::Center)