diff --git a/Cargo.lock b/Cargo.lock index 854f8496..2951c161 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -792,8 +792,6 @@ dependencies = [ "anyhow", "cosmic-client-toolkit", "cosmic-protocols", - "freedesktop-desktop-entry", - "freedesktop-icons", "futures", "futures-util", "i18n-embed 0.13.9", @@ -808,13 +806,14 @@ dependencies = [ "rust-embed 6.8.1", "rust-embed-utils 7.8.1", "serde", - "shlex", + "switcheroo-control", "tokio", "tracing", "tracing-log", "tracing-subscriber", "url", "xdg", + "zbus", ] [[package]] @@ -844,6 +843,7 @@ name = "cosmic-applet-battery" version = "0.1.0" dependencies = [ "cosmic-time", + "drm 0.11.1", "futures", "i18n-embed 0.13.9", "i18n-embed-fl 0.6.7", @@ -855,6 +855,7 @@ dependencies = [ "tracing", "tracing-log", "tracing-subscriber", + "udev", "zbus", ] @@ -1034,7 +1035,7 @@ dependencies = [ [[package]] name = "cosmic-config" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#912e8b0a4478e67ad4b3ce46e327c70dbe9887d8" +source = "git+https://github.com/pop-os/libcosmic#bf0508816b7e7098cfcc6eb16ee288207ef0cc31" dependencies = [ "atomicwrites", "cosmic-config-derive", @@ -1042,17 +1043,19 @@ dependencies = [ "dirs 5.0.1", "futures-util", "iced_futures", + "known-folders", "notify", "once_cell", "ron", "serde", + "xdg", "zbus", ] [[package]] name = "cosmic-config-derive" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#912e8b0a4478e67ad4b3ce46e327c70dbe9887d8" +source = "git+https://github.com/pop-os/libcosmic#bf0508816b7e7098cfcc6eb16ee288207ef0cc31" dependencies = [ "quote", "syn 1.0.109", @@ -1061,7 +1064,7 @@ dependencies = [ [[package]] name = "cosmic-dbus-networkmanager" version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#3644bc909984842f35adb1057ef6e0a277c1aa6a" +source = "git+https://github.com/pop-os/dbus-settings-bindings#5dea929b730460f883935357a1a8fb9736f36f95" dependencies = [ "bitflags 2.4.2", "derive_builder", @@ -1162,7 +1165,7 @@ dependencies = [ [[package]] name = "cosmic-theme" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#912e8b0a4478e67ad4b3ce46e327c70dbe9887d8" +source = "git+https://github.com/pop-os/libcosmic#bf0508816b7e7098cfcc6eb16ee288207ef0cc31" dependencies = [ "almost", "cosmic-config", @@ -1605,21 +1608,44 @@ checksum = "97fb1b703ffbc7ebd216eba7900008049a56ace55580ecb2ee7fa801e8d8be87" dependencies = [ "bitflags 2.4.2", "bytemuck", - "drm-ffi", + "drm-ffi 0.6.0", "drm-fourcc", "nix 0.27.1", ] +[[package]] +name = "drm" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0f8a69e60d75ae7dab4ef26a59ca99f2a89d4c142089b537775ae0c198bdcde" +dependencies = [ + "bitflags 2.4.2", + "bytemuck", + "drm-ffi 0.7.1", + "drm-fourcc", + "rustix 0.38.30", +] + [[package]] name = "drm-ffi" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba7d1c19c4b6270e89d59fb27dc6d02a317c658a8a54e54781e1db9b5947595d" dependencies = [ - "drm-sys", + "drm-sys 0.5.0", "nix 0.27.1", ] +[[package]] +name = "drm-ffi" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41334f8405792483e32ad05fbb9c5680ff4e84491883d2947a4757dc54cb2ac6" +dependencies = [ + "drm-sys 0.6.1", + "rustix 0.38.30", +] + [[package]] name = "drm-fourcc" version = "2.2.0" @@ -1632,6 +1658,16 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a4f1c0468062a56cd5705f1e3b5409eb286d5596a2028ec8e947595d7e715ae" +[[package]] +name = "drm-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d09ff881f92f118b11105ba5e34ff8f4adf27b30dae8f12e28c193af1c83176" +dependencies = [ + "libc", + "linux-raw-sys 0.6.4", +] + [[package]] name = "either" version = "1.9.0" @@ -2654,7 +2690,7 @@ dependencies = [ [[package]] name = "iced" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic#912e8b0a4478e67ad4b3ce46e327c70dbe9887d8" +source = "git+https://github.com/pop-os/libcosmic#bf0508816b7e7098cfcc6eb16ee288207ef0cc31" dependencies = [ "iced_accessibility", "iced_core", @@ -2669,7 +2705,7 @@ dependencies = [ [[package]] name = "iced_accessibility" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#912e8b0a4478e67ad4b3ce46e327c70dbe9887d8" +source = "git+https://github.com/pop-os/libcosmic#bf0508816b7e7098cfcc6eb16ee288207ef0cc31" dependencies = [ "accesskit", "accesskit_unix", @@ -2678,7 +2714,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic#912e8b0a4478e67ad4b3ce46e327c70dbe9887d8" +source = "git+https://github.com/pop-os/libcosmic#bf0508816b7e7098cfcc6eb16ee288207ef0cc31" dependencies = [ "bitflags 1.3.2", "iced_accessibility", @@ -2696,7 +2732,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic#912e8b0a4478e67ad4b3ce46e327c70dbe9887d8" +source = "git+https://github.com/pop-os/libcosmic#bf0508816b7e7098cfcc6eb16ee288207ef0cc31" dependencies = [ "futures", "iced_core", @@ -2709,7 +2745,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic#912e8b0a4478e67ad4b3ce46e327c70dbe9887d8" +source = "git+https://github.com/pop-os/libcosmic#bf0508816b7e7098cfcc6eb16ee288207ef0cc31" dependencies = [ "bitflags 1.3.2", "bytemuck", @@ -2732,7 +2768,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic#912e8b0a4478e67ad4b3ce46e327c70dbe9887d8" +source = "git+https://github.com/pop-os/libcosmic#bf0508816b7e7098cfcc6eb16ee288207ef0cc31" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -2745,7 +2781,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic#912e8b0a4478e67ad4b3ce46e327c70dbe9887d8" +source = "git+https://github.com/pop-os/libcosmic#bf0508816b7e7098cfcc6eb16ee288207ef0cc31" dependencies = [ "iced_accessibility", "iced_core", @@ -2757,7 +2793,7 @@ dependencies = [ [[package]] name = "iced_sctk" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#912e8b0a4478e67ad4b3ce46e327c70dbe9887d8" +source = "git+https://github.com/pop-os/libcosmic#bf0508816b7e7098cfcc6eb16ee288207ef0cc31" dependencies = [ "enum-repr", "float-cmp", @@ -2781,7 +2817,7 @@ dependencies = [ [[package]] name = "iced_style" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic#912e8b0a4478e67ad4b3ce46e327c70dbe9887d8" +source = "git+https://github.com/pop-os/libcosmic#bf0508816b7e7098cfcc6eb16ee288207ef0cc31" dependencies = [ "iced_core", "once_cell", @@ -2791,7 +2827,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic#912e8b0a4478e67ad4b3ce46e327c70dbe9887d8" +source = "git+https://github.com/pop-os/libcosmic#bf0508816b7e7098cfcc6eb16ee288207ef0cc31" dependencies = [ "bytemuck", "cosmic-text", @@ -2809,7 +2845,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic#912e8b0a4478e67ad4b3ce46e327c70dbe9887d8" +source = "git+https://github.com/pop-os/libcosmic#bf0508816b7e7098cfcc6eb16ee288207ef0cc31" dependencies = [ "bitflags 1.3.2", "bytemuck", @@ -2829,7 +2865,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic#912e8b0a4478e67ad4b3ce46e327c70dbe9887d8" +source = "git+https://github.com/pop-os/libcosmic#bf0508816b7e7098cfcc6eb16ee288207ef0cc31" dependencies = [ "iced_renderer", "iced_runtime", @@ -3035,6 +3071,15 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "known-folders" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4397c789f2709d23cfcb703b316e0766a8d4b17db2d47b0ab096ef6047cae1d8" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "kqueue" version = "1.0.8" @@ -3085,7 +3130,7 @@ checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libcosmic" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#912e8b0a4478e67ad4b3ce46e327c70dbe9887d8" +source = "git+https://github.com/pop-os/libcosmic#bf0508816b7e7098cfcc6eb16ee288207ef0cc31" dependencies = [ "apply", "ashpd", @@ -3097,6 +3142,7 @@ dependencies = [ "css-color", "derive_setters", "fraction", + "freedesktop-desktop-entry", "freedesktop-icons", "iced", "iced_core", @@ -3112,6 +3158,7 @@ dependencies = [ "palette", "rfd", "ron", + "shlex", "slotmap", "taffy", "thiserror", @@ -3218,6 +3265,16 @@ dependencies = [ "redox_syscall 0.4.1", ] +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -3230,6 +3287,12 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +[[package]] +name = "linux-raw-sys" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b5399f6804fbab912acbd8878ed3532d506b7c951b8f9f164ef90fef39e3f4" + [[package]] name = "locale_config" version = "0.3.0" @@ -3445,7 +3508,7 @@ dependencies = [ [[package]] name = "mpris2-zbus" version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#3644bc909984842f35adb1057ef6e0a277c1aa6a" +source = "git+https://github.com/pop-os/dbus-settings-bindings#5dea929b730460f883935357a1a8fb9736f36f95" dependencies = [ "serde", "thiserror", @@ -4793,7 +4856,7 @@ dependencies = [ "cfg_aliases", "cocoa", "core-graphics", - "drm", + "drm 0.10.0", "fastrand 2.0.1", "foreign-types", "js-sys", @@ -4901,6 +4964,14 @@ dependencies = [ "zeno", ] +[[package]] +name = "switcheroo-control" +version = "0.1.0" +source = "git+https://github.com/pop-os/dbus-settings-bindings#5dea929b730460f883935357a1a8fb9736f36f95" +dependencies = [ + "zbus", +] + [[package]] name = "syn" version = "1.0.109" @@ -5327,6 +5398,18 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "udev" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50051c6e22be28ee6f217d50014f3bc29e81c20dc66ff7ca0d5c5226e1dcc5a1" +dependencies = [ + "io-lifetimes 1.0.11", + "libc", + "libudev-sys", + "pkg-config", +] + [[package]] name = "uds_windows" version = "1.1.0" @@ -5909,7 +5992,7 @@ dependencies = [ "js-sys", "log", "naga", - "parking_lot 0.12.1", + "parking_lot 0.11.2", "profiling", "raw-window-handle", "smallvec", @@ -5934,7 +6017,7 @@ dependencies = [ "codespan-reporting", "log", "naga", - "parking_lot 0.12.1", + "parking_lot 0.11.2", "profiling", "raw-window-handle", "rustc-hash", @@ -5974,7 +6057,7 @@ dependencies = [ "naga", "objc", "once_cell", - "parking_lot 0.12.1", + "parking_lot 0.11.2", "profiling", "range-alloc", "raw-window-handle", diff --git a/Cargo.toml b/Cargo.toml index 0aae1ab7..fdc64e75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = fa "applet-token", "tokio", "wayland", - "process", + "desktop", "dbus-config" ] } zbus = { version = "3.14", default-features = false, features = ["tokio"] } diff --git a/cosmic-app-list/Cargo.toml b/cosmic-app-list/Cargo.toml index b6f03d59..598b2cfb 100644 --- a/cosmic-app-list/Cargo.toml +++ b/cosmic-app-list/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" cctk.workspace = true cosmic-protocols.workspace = true libcosmic.workspace = true +zbus.workspace = true # libcosmic = { path = "../../libcosmic", default-features = false, features = ["wayland", "tokio", "applet"] } ron = "0.8" futures = "0.3" @@ -18,17 +19,15 @@ tracing-subscriber.workspace = true tracing-log.workspace = true tracing.workspace = true nix = "0.26" -shlex = "1.3.0" anyhow = "1.0" serde = { version = "1.0", features = ["derive"] } log = "0.4" tokio = { version = "1.17.0", features = ["sync", "rt", "rt-multi-thread", "macros", "process"] } itertools = "*" -freedesktop-desktop-entry = "0.5.0" -freedesktop-icons = "0.2.4" i18n-embed = { version = "0.13", features = ["fluent-system", "desktop-requester"] } i18n-embed-fl = "0.6" rust-embed = "6.3" url = "2.3.1" rust-embed-utils = "7.5.0" rand = "0.8.5" +switcheroo-control = { git = "https://github.com/pop-os/dbus-settings-bindings" } \ No newline at end of file diff --git a/cosmic-app-list/i18n/de/cosmic_app_list.ftl b/cosmic-app-list/i18n/de/cosmic_app_list.ftl index 763860f2..6537a383 100644 --- a/cosmic-app-list/i18n/de/cosmic_app_list.ftl +++ b/cosmic-app-list/i18n/de/cosmic_app_list.ftl @@ -3,4 +3,7 @@ favorite = Favorisieren unfavorite = Entfavorisieren quit = Beenden quit-all = Alle beenden -new-window = Neues Fenster \ No newline at end of file +new-window = Neues Fenster +run = Ausführen +run-on = Ausführen auf {$gpu} +run-on-default = (Standard) \ No newline at end of file diff --git a/cosmic-app-list/i18n/en/cosmic_app_list.ftl b/cosmic-app-list/i18n/en/cosmic_app_list.ftl index d3bffb22..0a722919 100644 --- a/cosmic-app-list/i18n/en/cosmic_app_list.ftl +++ b/cosmic-app-list/i18n/en/cosmic_app_list.ftl @@ -3,4 +3,7 @@ favorite = Favorite unfavorite = Un-Favorite quit = Quit quit-all = Quit All -new-window = New Window \ No newline at end of file +new-window = New Window +run = Run +run-on = Run on {$gpu} +run-on-default = (Default) \ No newline at end of file diff --git a/cosmic-app-list/src/app.rs b/cosmic-app-list/src/app.rs index 57b8c176..6f766d89 100755 --- a/cosmic-app-list/src/app.rs +++ b/cosmic-app-list/src/app.rs @@ -11,7 +11,8 @@ use cctk::sctk::reexports::calloop::channel::Sender; use cctk::toplevel_info::ToplevelInfo; use cctk::wayland_client::protocol::wl_data_device_manager::DndAction; use cctk::wayland_client::protocol::wl_seat::WlSeat; -use cosmic::cosmic_config::{self, Config, CosmicConfigEntry}; +use cosmic::cosmic_config::{Config, CosmicConfigEntry}; +use cosmic::desktop::{load_applications_for_app_ids, DesktopEntryData}; use cosmic::iced; use cosmic::iced::event::listen_with; use cosmic::iced::wayland::actions::data_device::DataFromMimeType; @@ -24,7 +25,7 @@ use cosmic::iced::widget::vertical_space; use cosmic::iced::widget::{column, dnd_source, mouse_area, row, Column, Row}; use cosmic::iced::Color; use cosmic::iced::{window, Subscription}; -use cosmic::iced_core::window::Icon; +use cosmic::iced_core::Padding; use cosmic::iced_runtime::core::alignment::Horizontal; use cosmic::iced_runtime::core::event; use cosmic::iced_sctk::commands::data_device::accept_mime_type; @@ -44,7 +45,6 @@ use cosmic::{ }; use cosmic::{Element, Theme}; use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1; -use freedesktop_desktop_entry::DesktopEntry; use futures::future::pending; use iced::widget::container; use iced::Alignment; @@ -52,12 +52,11 @@ use iced::Background; use iced::Length; use itertools::Itertools; use rand::{thread_rng, Rng}; -use std::borrow::Cow; use std::collections::HashMap; -use std::path::Path; use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; +use switcheroo_control::Gpu; use tokio::time::sleep; use url::Url; @@ -71,14 +70,14 @@ pub fn run() -> cosmic::iced::Result { struct DockItem { id: u32, toplevels: Vec<(ZcosmicToplevelHandleV1, ToplevelInfo)>, - desktop_info: DesktopInfo, + desktop_info: DesktopEntryData, } impl DataFromMimeType for DockItem { fn from_mime_type(&self, mime_type: &str) -> Option> { - if mime_type == MIME_TYPE { + if mime_type == MIME_TYPE && self.desktop_info.path.is_some() { Some( - Url::from_file_path(self.desktop_info.path.clone()) + Url::from_file_path(self.desktop_info.path.as_deref().unwrap()) .ok()? .to_string() .as_bytes() @@ -94,7 +93,7 @@ impl DockItem { fn new( id: u32, toplevels: Vec<(ZcosmicToplevelHandleV1, ToplevelInfo)>, - desktop_info: DesktopInfo, + desktop_info: DesktopEntryData, ) -> Self { Self { id, @@ -108,6 +107,7 @@ impl DockItem { applet: &Context, rectangle_tracker: Option<&RectangleTracker>, interaction_enabled: bool, + gpus: Option<&[Gpu]>, ) -> Element<'_, Message> { let Self { toplevels, @@ -167,11 +167,24 @@ impl DockItem { dnd_source( mouse_area( icon_button - .on_press( + .on_press_maybe( toplevels .first() .map(|t| Message::Activate(t.0.clone())) - .unwrap_or_else(|| Message::Exec(desktop_info.exec.clone())), + .or_else(|| { + let gpu_idx = gpus.map(|gpus| { + if desktop_info.prefers_dgpu { + gpus.iter().position(|gpu| !gpu.default).unwrap_or(0) + } else { + gpus.iter().position(|gpu| gpu.default).unwrap_or(0) + } + }); + + desktop_info + .exec + .clone() + .map(|exec| Message::Exec(exec, gpu_idx)) + }), ) .width(Length::Shrink) .height(Length::Shrink), @@ -202,7 +215,7 @@ struct DndOffer { #[derive(Clone, Default)] struct CosmicAppList { core: cosmic::app::Core, - popup: Option<(window::Id, DockItem)>, + popup: Option<(window::Id, u32)>, subscription_ctr: u32, item_ctr: u32, active_list: Vec, @@ -215,6 +228,7 @@ struct CosmicAppList { rectangles: HashMap, dnd_offer: Option, is_listening_for_dnd: bool, + gpus: Option>, } // TODO DnD after sctk merges DnD @@ -224,10 +238,11 @@ enum Message { Favorite(String), UnFavorite(String), Popup(String), + GpuRequest(Option>), CloseRequested(window::Id), ClosePopup, Activate(ZcosmicToplevelHandleV1), - Exec(String), + Exec(String, Option), Quit(String), Ignore, NewSeat(WlSeat), @@ -246,109 +261,6 @@ enum Message { ConfigUpdated(AppListConfig), } -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum IconSource { - Name(String), - Path(PathBuf), -} - -impl IconSource { - fn from_unknown(icon: &str) -> Self { - let icon_path = Path::new(icon); - if icon_path.is_absolute() && icon_path.exists() { - Self::Path(icon_path.into()) - } else { - Self::Name(icon.into()) - } - } - - fn as_cosmic_icon(&self) -> cosmic::widget::icon::Icon { - match self { - Self::Name(name) => cosmic::widget::icon::from_name(name.as_str()) - .size(128) - .fallback(Some(cosmic::widget::icon::IconFallback::Names(vec![ - "application-default".into(), - "application-x-executable".into(), - ]))) - .into(), - Self::Path(path) => cosmic::widget::icon(cosmic::widget::icon::from_path(path.clone())), - } - } -} - -impl Default for IconSource { - fn default() -> Self { - Self::Name("application-default".to_string()) - } -} - -#[derive(Debug, Clone, Default)] -struct DesktopInfo { - id: String, - wm_class: Option, - icon: IconSource, - exec: String, - name: String, - path: PathBuf, -} - -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()) - }) { - // check if absolute path exists and otherwise treat it as a name - let icon = de.icon().unwrap_or(de.appid); - let icon_path = Path::new(icon); - let icon = if icon_path.is_absolute() && icon_path.exists() { - IconSource::Path(icon_path.into()) - } else { - IconSource::Name(icon.into()) - }; - 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: IconSource::default(), - ..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 -} - fn index_in_list( mut list_len: usize, item_size: f32, @@ -388,6 +300,39 @@ fn index_in_list( } } +async fn try_get_gpus() -> Option> { + let connection = zbus::Connection::system().await.ok()?; + let proxy = switcheroo_control::SwitcherooControlProxy::new(&connection) + .await + .ok()?; + + if !proxy.has_dual_gpu().await.ok()? { + return None; + } + + let gpus = proxy.get_gpus().await.ok()?; + if gpus.is_empty() { + return None; + } + + Some(gpus) +} + +pub fn menu_button<'a, Message>( + content: impl Into>, +) -> cosmic::widget::Button<'a, Message, cosmic::Renderer> { + cosmic::widget::Button::new(content) + .style(Button::AppletMenu) + .padding(menu_control_padding()) + .width(Length::Fill) +} + +pub fn menu_control_padding() -> Padding { + let theme = cosmic::theme::active(); + let cosmic = theme.cosmic(); + [cosmic.space_xxs(), cosmic.space_m()].into() +} + impl cosmic::Application for CosmicAppList { type Message = Message; type Executor = cosmic::SingleThreadExecutor; @@ -404,21 +349,30 @@ impl cosmic::Application for CosmicAppList { .unwrap_or_default(); let mut self_ = Self { core, - favorite_list: desktop_info_for_app_ids(config.favorites.clone()) - .into_iter() - .enumerate() - .map(|(favorite_ctr, e)| DockItem { - id: favorite_ctr as u32, - toplevels: Default::default(), - desktop_info: e, - }) - .collect(), + favorite_list: load_applications_for_app_ids( + None, + config.favorites.iter().map(|s| &**s), + true, + ) + .into_iter() + .enumerate() + .map(|(favorite_ctr, e)| DockItem { + id: favorite_ctr as u32, + toplevels: Default::default(), + desktop_info: e, + }) + .collect(), config, ..Default::default() }; self_.item_ctr = self_.favorite_list.len() as u32; - (self_, Command::none()) + ( + self_, + Command::perform(try_get_gpus(), |gpus| { + cosmic::app::Message::App(Message::GpuRequest(gpus)) + }), + ) } fn core(&self) -> &cosmic::app::Core { @@ -450,7 +404,7 @@ impl cosmic::Application for CosmicAppList { }; let new_id = window::Id::unique(); - self.popup = Some((new_id, toplevel_group.clone())); + self.popup = Some((new_id, toplevel_group.id)); let mut popup_settings = self.core.applet.get_popup_settings( window::Id::MAIN, @@ -471,7 +425,11 @@ impl cosmic::Application for CosmicAppList { width: width as i32, height: height as i32, }; - return get_popup(popup_settings); + + let gpu_update = Command::perform(try_get_gpus(), |gpus| { + cosmic::app::Message::App(Message::GpuRequest(gpus)) + }); + return Command::batch([gpu_update, get_popup(popup_settings)]); } } Message::Favorite(id) => { @@ -511,12 +469,12 @@ impl cosmic::Application for CosmicAppList { } } Message::Activate(handle) => { - if let Some(p) = self.popup.take() { - return destroy_popup(p.0); - } if let Some(tx) = self.wayland_sender.as_ref() { let _ = tx.send(WaylandRequest::Toplevel(ToplevelRequest::Activate(handle))); } + if let Some(p) = self.popup.take() { + return destroy_popup(p.0); + } } Message::Quit(id) => { if let Some(toplevel_group) = self @@ -649,18 +607,7 @@ impl cosmic::Application for CosmicAppList { } Message::DndData(file_path) => { if let Some(DndOffer { dock_item, .. }) = self.dnd_offer.as_mut() { - if let Some(di) = std::fs::read_to_string(&file_path).ok().and_then(|input| { - DesktopEntry::decode(&file_path, &input) - .ok() - .map(|de| DesktopInfo { - id: de.id().to_string(), - wm_class: de.startup_wm_class().map(ToString::to_string), - icon: IconSource::from_unknown(de.icon().unwrap_or(de.appid)), - exec: de.exec().unwrap_or_default().to_string(), - name: de.name(None).unwrap_or_default().to_string(), - path: file_path.clone(), - }) - }) { + if let Some(di) = cosmic::desktop::load_desktop_file(None, file_path) { self.item_ctr += 1; *dock_item = Some(DockItem::new(self.item_ctr, Vec::new(), di)); } @@ -758,17 +705,14 @@ impl cosmic::Application for CosmicAppList { info.app_id = format!("Unknown Application {}", self.item_ctr); } self.item_ctr += 1; - let desktop_info = - desktop_info_for_app_ids(vec![info.app_id.clone()]) - .pop() - .unwrap_or_else(|| DesktopInfo { - id: info.app_id.clone(), - wm_class: None, - icon: IconSource::default(), - exec: String::new(), - name: info.app_id.clone(), - path: PathBuf::new(), - }); + let desktop_info = load_applications_for_app_ids( + None, + std::iter::once(&*info.app_id), + true, + ) + .into_iter() + .next() + .unwrap(); self.active_list.push(DockItem { id: self.item_ctr, toplevels: vec![(handle, info)], @@ -805,24 +749,26 @@ impl cosmic::Application for CosmicAppList { } } }, - WaylandUpdate::ActivationToken { token, exec } => { - let mut exec = shlex::Shlex::new(&exec); - let mut cmd = match exec.next() { - Some(cmd) if !cmd.contains('=') => std::process::Command::new(cmd), - _ => return Command::none(), - }; - for arg in exec { - // TODO handle "%" args here if necessary? - if !arg.starts_with('%') { - cmd.arg(arg); - } - } + WaylandUpdate::ActivationToken { + token, + exec, + gpu_idx, + } => { + let mut envs = Vec::new(); if let Some(token) = token { - cmd.env("XDG_ACTIVATION_TOKEN", &token); - cmd.env("DESKTOP_STARTUP_ID", &token); + envs.push(("XDG_ACTIVATION_TOKEN".to_string(), token.clone())); + envs.push(("DESKTOP_STARTUP_ID".to_string(), token)); + } + if let (Some(gpus), Some(idx)) = (self.gpus.as_ref(), gpu_idx) { + envs.extend( + gpus[idx] + .environment + .iter() + .map(|(k, v)| (k.clone(), v.clone())), + ); } tokio::task::spawn_blocking(|| { - crate::process::spawn(cmd); + cosmic::desktop::spawn_desktop_exec(exec, envs); }); } } @@ -833,11 +779,12 @@ impl cosmic::Application for CosmicAppList { Message::RemovedSeat(_) => { self.seat.take(); } - Message::Exec(exec) => { + Message::Exec(exec, gpu_idx) => { if let Some(tx) = self.wayland_sender.as_ref() { let _ = tx.send(WaylandRequest::TokenRequest { app_id: Self::APP_ID.to_string(), exec, + gpu_idx, }); } } @@ -872,31 +819,38 @@ impl cosmic::Application for CosmicAppList { } // pull back configured items into the favorites list - self.favorite_list = desktop_info_for_app_ids(self.config.favorites.clone()) - .into_iter() - .map(|new_dock_item| { - if let Some(p) = self - .active_list - .iter() - .position(|dock_item| dock_item.desktop_info.id == new_dock_item.id) - { - self.active_list.remove(p) - } else { - self.item_ctr += 1; - DockItem { - id: self.item_ctr, - toplevels: Default::default(), - desktop_info: new_dock_item, - } + self.favorite_list = load_applications_for_app_ids( + None, + self.config.favorites.iter().map(|s| &**s), + true, + ) + .into_iter() + .map(|new_dock_item| { + if let Some(p) = self + .active_list + .iter() + .position(|dock_item| dock_item.desktop_info.id == new_dock_item.id) + { + self.active_list.remove(p) + } else { + self.item_ctr += 1; + DockItem { + id: self.item_ctr, + toplevels: Default::default(), + desktop_info: new_dock_item, } - }) - .collect(); + } + }) + .collect(); } Message::CloseRequested(id) => { if Some(id) == self.popup.as_ref().map(|p| p.0) { self.popup = None; } } + Message::GpuRequest(gpus) => { + self.gpus = gpus; + } } Command::none() @@ -915,6 +869,7 @@ impl cosmic::Application for CosmicAppList { &self.core.applet, self.rectangle_tracker.as_ref(), self.popup.is_none(), + self.gpus.as_deref(), ) }) .collect(); @@ -924,7 +879,10 @@ impl cosmic::Application for CosmicAppList { .as_ref() .and_then(|o| o.dock_item.as_ref().map(|item| (item, o.preview_index))) { - favorites.insert(index, item.as_icon(&self.core.applet, None, false)); + favorites.insert( + index, + item.as_icon(&self.core.applet, None, false, self.gpus.as_deref()), + ); } else if self.is_listening_for_dnd && self.favorite_list.is_empty() { // show star indicating favorite_list is drag target favorites.push( @@ -945,6 +903,7 @@ impl cosmic::Application for CosmicAppList { &self.core.applet, self.rectangle_tracker.as_ref(), self.popup.is_none(), + self.gpus.as_deref(), ) }) .collect(); @@ -1051,30 +1010,68 @@ impl cosmic::Application for CosmicAppList { .as_cosmic_icon() .size(self.core.applet.suggested_size().0) .into() - } else if let Some(( - _popup_id, - DockItem { + } else if let Some((_popup_id, id)) = self.popup.as_ref().filter(|p| id == p.0) { + let Some(DockItem { toplevels, desktop_info, .. - }, - )) = self.popup.as_ref().filter(|p| id == p.0) - { + }) = self + .favorite_list + .iter() + .chain(self.active_list.iter()) + .find(|i| i.id == *id) + else { + return iced::widget::text("").into(); + }; + let is_favorite = self.config.favorites.contains(&desktop_info.id) || desktop_info .wm_class .as_ref() .is_some_and(|wm_class| self.config.favorites.contains(wm_class)); - let mut content = column![ - iced::widget::text(&desktop_info.name).horizontal_alignment(Horizontal::Center), - cosmic::widget::button(iced::widget::text(fl!("new-window"))) - .style(Button::Text) - .on_press(Message::Exec(desktop_info.exec.clone())), - ] - .padding(8) - .spacing(4) + let mut content = column![container( + iced::widget::text(&desktop_info.name).horizontal_alignment(Horizontal::Center) + ) + .padding(menu_control_padding()),] + .padding([8, 0]) .align_items(Alignment::Center); + + if let Some(exec) = desktop_info.exec.clone() { + if !toplevels.is_empty() { + content = content.push( + menu_button(iced::widget::text(fl!("new-window"))) + .on_press(Message::Exec(exec, None)), + ); + } else if let Some(gpus) = self.gpus.as_ref() { + let default_idx = if desktop_info.prefers_dgpu { + gpus.iter().position(|gpu| !gpu.default).unwrap_or(0) + } else { + gpus.iter().position(|gpu| gpu.default).unwrap_or(0) + }; + for (i, gpu) in gpus.iter().enumerate() { + content = content.push( + menu_button(iced::widget::text(format!( + "{} {}", + fl!("run-on", gpu = gpu.name.clone()), + if i == default_idx { + fl!("run-on-default") + } else { + String::new() + } + ))) + .on_press(Message::Exec(exec.clone(), Some(i))), + ); + } + } else { + content = content.push( + menu_button(iced::widget::text(fl!("run"))) + .on_press(Message::Exec(exec, None)), + ); + } + content = content.push(divider::horizontal::default()); + } + if !toplevels.is_empty() { let mut list_col = column![]; for (handle, info) in toplevels { @@ -1084,35 +1081,29 @@ impl cosmic::Application for CosmicAppList { info.title.clone() }; list_col = list_col.push( - cosmic::widget::button(iced::widget::text(title)) - .style(Button::Text) + menu_button(iced::widget::text(title)) .on_press(Message::Activate(handle.clone())), ); } - content = content.push(divider::horizontal::default()); content = content.push(list_col); + content = content.push(divider::horizontal::default()); } - content = content.push(divider::horizontal::default()); content = content.push(if is_favorite { - cosmic::widget::button(iced::widget::text(fl!("unfavorite"))) - .style(Button::Text) + menu_button(iced::widget::text(fl!("unfavorite"))) .on_press(Message::UnFavorite(desktop_info.id.clone())) } else { - cosmic::widget::button(iced::widget::text(fl!("favorite"))) - .style(Button::Text) + menu_button(iced::widget::text(fl!("favorite"))) .on_press(Message::Favorite(desktop_info.id.clone())) }); content = match toplevels.len() { 0 => content, 1 => content.push( - cosmic::widget::button(iced::widget::text(fl!("quit"))) - .style(Button::Text) + menu_button(iced::widget::text(fl!("quit"))) .on_press(Message::Quit(desktop_info.id.clone())), ), _ => content.push( - cosmic::widget::button(iced::widget::text(&fl!("quit-all"))) - .style(Button::Text) + menu_button(iced::widget::text(&fl!("quit-all"))) .on_press(Message::Quit(desktop_info.id.clone())), ), }; diff --git a/cosmic-app-list/src/main.rs b/cosmic-app-list/src/main.rs index ad175a8e..0d17a245 100644 --- a/cosmic-app-list/src/main.rs +++ b/cosmic-app-list/src/main.rs @@ -2,7 +2,6 @@ mod app; mod config; mod localize; -mod process; mod wayland_handler; mod wayland_subscription; diff --git a/cosmic-app-list/src/process.rs b/cosmic-app-list/src/process.rs deleted file mode 100644 index 1f58531a..00000000 --- a/cosmic-app-list/src/process.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::process::{exit, Command, Stdio}; - -use nix::sys::wait::waitpid; -use nix::unistd::{fork, ForkResult}; - -/// Performs a double fork with setsid to spawn and detach a command. -pub fn spawn(mut command: Command) { - command - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); - - unsafe { - match fork() { - Ok(ForkResult::Parent { child }) => { - let _res = waitpid(Some(child), None); - } - - Ok(ForkResult::Child) => { - let _res = nix::unistd::setsid(); - let _res = command.spawn(); - - exit(0); - } - - Err(why) => { - println!("failed to fork and spawn command: {}", why.desc()); - } - } - } -} diff --git a/cosmic-app-list/src/wayland_handler.rs b/cosmic-app-list/src/wayland_handler.rs index 33a44d7a..5f80861d 100644 --- a/cosmic-app-list/src/wayland_handler.rs +++ b/cosmic-app-list/src/wayland_handler.rs @@ -52,6 +52,7 @@ impl ProvidesRegistryState for AppData { struct ExecRequestData { data: RequestData, exec: String, + gpu_idx: Option, } impl RequestDataExt for ExecRequestData { @@ -74,6 +75,7 @@ impl ActivationHandler for AppData { let _ = self.tx.unbounded_send(WaylandUpdate::ActivationToken { token: Some(token), exec: data.exec.clone(), + gpu_idx: data.gpu_idx, }); } } @@ -213,11 +215,12 @@ pub(crate) fn wayland_handler( let manager = &state.toplevel_manager_state.manager; manager.close(&handle); } - ToplevelRequest::Exit => { - state.exit = true; - } }, - WaylandRequest::TokenRequest { app_id, exec } => { + WaylandRequest::TokenRequest { + app_id, + exec, + gpu_idx, + } => { if let Some(activation_state) = state.activation_state.as_ref() { activation_state.request_token_with_data( &state.queue_handle, @@ -232,12 +235,15 @@ pub(crate) fn wayland_handler( surface: None, }, exec, + gpu_idx, }, ); } else { - let _ = state - .tx - .unbounded_send(WaylandUpdate::ActivationToken { token: None, exec }); + let _ = state.tx.unbounded_send(WaylandUpdate::ActivationToken { + token: None, + exec, + gpu_idx, + }); } } }, diff --git a/cosmic-app-list/src/wayland_subscription.rs b/cosmic-app-list/src/wayland_subscription.rs index a348573e..c9f13d79 100644 --- a/cosmic-app-list/src/wayland_subscription.rs +++ b/cosmic-app-list/src/wayland_subscription.rs @@ -12,7 +12,7 @@ use futures::{ SinkExt, StreamExt, }; use once_cell::sync::Lazy; -use std::{fmt::Debug, hash::Hash, thread::JoinHandle}; +use std::fmt::Debug; use tokio::sync::Mutex; use crate::wayland_handler::wayland_handler; @@ -79,7 +79,11 @@ pub enum WaylandUpdate { Init(calloop::channel::Sender), Finished, Toplevel(ToplevelUpdate), - ActivationToken { token: Option, exec: String }, + ActivationToken { + token: Option, + exec: String, + gpu_idx: Option, + }, } #[derive(Clone, Debug)] @@ -92,12 +96,15 @@ pub enum ToplevelUpdate { #[derive(Clone, Debug)] pub enum WaylandRequest { Toplevel(ToplevelRequest), - TokenRequest { app_id: String, exec: String }, + TokenRequest { + app_id: String, + exec: String, + gpu_idx: Option, + }, } #[derive(Debug, Clone)] pub enum ToplevelRequest { Activate(ZcosmicToplevelHandleV1), Quit(ZcosmicToplevelHandleV1), - Exit, } diff --git a/cosmic-applet-battery/Cargo.toml b/cosmic-applet-battery/Cargo.toml index b386d470..6ad00010 100644 --- a/cosmic-applet-battery/Cargo.toml +++ b/cosmic-applet-battery/Cargo.toml @@ -17,4 +17,6 @@ tracing-log.workspace = true i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] } i18n-embed-fl = "0.6.4" rust-embed = "6.3.0" -tokio = { version = "1.17.0", features = ["sync", "rt", "rt-multi-thread", "fs"] } +tokio = { version = "1.17.0", features = ["sync", "rt", "rt-multi-thread", "fs", "macros"] } +udev = "0.8" +drm = "0.11.1" \ No newline at end of file diff --git a/cosmic-applet-battery/i18n/de/cosmic_applet_battery.ftl b/cosmic-applet-battery/i18n/de/cosmic_applet_battery.ftl index 3b47d70d..a3f516a5 100644 --- a/cosmic-applet-battery/i18n/de/cosmic_applet_battery.ftl +++ b/cosmic-applet-battery/i18n/de/cosmic_applet_battery.ftl @@ -11,3 +11,5 @@ minutes = m hours = h until-empty = bis leer power-settings = Energie- und Batterieeinstellungen... +dgpu-running = Dedizierte GPU ist aktiv und kann die Batterielaufzeit reduzieren +dgpu-applications = Anwendungen, die die dedizierte GPU {$gpu_name} nutzen \ No newline at end of file diff --git a/cosmic-applet-battery/i18n/en/cosmic_applet_battery.ftl b/cosmic-applet-battery/i18n/en/cosmic_applet_battery.ftl index 57d327ed..3214e58b 100644 --- a/cosmic-applet-battery/i18n/en/cosmic_applet_battery.ftl +++ b/cosmic-applet-battery/i18n/en/cosmic_applet_battery.ftl @@ -11,3 +11,5 @@ minutes = m hours = h until-empty = until empty power-settings = Power and Battery Settings... +dgpu-running = Discrete GPU is active and can reduce battery life +dgpu-applications = Applications using {$gpu_name} discrete GPU \ No newline at end of file diff --git a/cosmic-applet-battery/src/app.rs b/cosmic-applet-battery/src/app.rs index 0e61f0c2..009aea24 100644 --- a/cosmic-applet-battery/src/app.rs +++ b/cosmic-applet-battery/src/app.rs @@ -2,6 +2,7 @@ use crate::backlight::{ screen_backlight_subscription, ScreenBacklightRequest, ScreenBacklightUpdate, }; use crate::config; +use crate::dgpu::{dgpu_subscription, Entry, GpuUpdate}; use crate::fl; use crate::power_daemon::{ power_profile_subscription, Power, PowerProfileRequest, PowerProfileUpdate, @@ -10,6 +11,7 @@ use crate::upower_device::{device_subscription, DeviceDbusEvent}; use crate::upower_kbdbacklight::{ kbd_backlight_subscription, KeyboardBacklightRequest, KeyboardBacklightUpdate, }; +use cosmic::applet::cosmic_panel_config::PanelAnchor; use cosmic::applet::token::subscription::{ activation_token_subscription, TokenRequest, TokenUpdate, }; @@ -21,14 +23,19 @@ use cosmic::iced::{ widget::{column, container, row, slider, text}, window, Alignment, Length, Subscription, }; +use cosmic::iced_core::alignment::Vertical; +use cosmic::iced_core::{Background, Color}; use cosmic::iced_runtime::core::layout::Limits; use cosmic::iced_style::application; -use cosmic::widget::{divider, horizontal_space, icon}; +use cosmic::iced_widget::{Column, Row}; +use cosmic::widget::{divider, horizontal_space, icon, scrollable, vertical_space}; use cosmic::Command; use cosmic::{Element, Theme}; use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline}; use log::error; +use std::collections::HashMap; +use std::path::PathBuf; use std::time::Duration; use tokio::sync::mpsc::UnboundedSender; @@ -49,11 +56,18 @@ fn format_duration(duration: Duration) -> String { } pub fn run() -> cosmic::iced::Result { - cosmic::applet::run::(false, ()) + cosmic::applet::run::(true, ()) } static MAX_CHARGE: Lazy = Lazy::new(id::Toggler::unique); +#[derive(Clone, Default)] +struct GPUData { + name: String, + toggled: bool, + app_list: Option>, +} + #[derive(Clone, Default)] struct CosmicBatteryApplet { core: cosmic::app::Core, @@ -62,6 +76,7 @@ struct CosmicBatteryApplet { charging_limit: bool, battery_percent: f64, on_battery: bool, + gpus: HashMap, time_remaining: Duration, kbd_brightness: f64, screen_brightness: f64, @@ -144,6 +159,9 @@ enum Message { UpdateScreenBrightness(f64), InitKbdBacklight(UnboundedSender, f64), InitScreenBacklight(UnboundedSender, f64), + GpuOn(PathBuf, String, Option>), + GpuOff(PathBuf), + ToggleGpuApps(PathBuf), Errored(String), InitProfile(UnboundedSender, Power), Profile(Power), @@ -319,16 +337,67 @@ impl cosmic::Application for CosmicBatteryApplet { cosmic::process::spawn(cmd); } }, + Message::GpuOn(path, name, app_list) => { + let toggled = self + .gpus + .get(&path) + .map(|data| data.toggled) + .unwrap_or_default(); + self.gpus.insert( + path, + GPUData { + name, + app_list, + toggled, + }, + ); + } + Message::GpuOff(path) => { + self.gpus.remove(&path); + } + Message::ToggleGpuApps(path) => { + if let Some(data) = self.gpus.get_mut(&path) { + data.toggled = !data.toggled; + } + } } Command::none() } fn view(&self) -> Element { - self.core + let btn = self + .core .applet .icon_button(&self.icon_name) .on_press(Message::TogglePopup) - .into() + .into(); + + if !self.gpus.is_empty() { + let dot = container(vertical_space(Length::Fixed(0.0))) + .padding(2.0) + .style(::Style::Custom(Box::new( + |theme| container::Appearance { + text_color: Some(Color::TRANSPARENT), + background: Some(Background::Color(theme.cosmic().accent_color().into())), + border_radius: 2.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + icon_color: Some(Color::TRANSPARENT), + }, + ))) + .into(); + + match self.core.applet.anchor { + PanelAnchor::Left | PanelAnchor::Right => Column::with_children(vec![btn, dot]) + .align_items(Alignment::Center) + .into(), + PanelAnchor::Top | PanelAnchor::Bottom => Row::with_children(vec![btn, dot]) + .align_items(Alignment::Center) + .into(), + } + } else { + btn + } } fn view_window(&self, _id: window::Id) -> Element { @@ -344,133 +413,241 @@ impl cosmic::Application for CosmicBatteryApplet { ) }) .size(10); - self.core - .applet - .popup_container( - column![ - padded_control( - row![ - icon::from_name(&*self.icon_name).size(24).symbolic(true), - column![name, description] - ] - .spacing(8) - .align_items(Alignment::Center) - ), - padded_control(divider::horizontal::default()), - menu_button( - row![ - column![ - text(fl!("battery")).size(14), - text(fl!("battery-desc")).size(10) - ] - .width(Length::Fill), - if matches!(self.power_profile, Power::Battery) { - container( - icon::from_name("emblem-ok-symbolic") - .size(12) - .symbolic(true), - ) - } else { - container(horizontal_space(1.0)) - } - ] - .align_items(Alignment::Center) - ) - .on_press(Message::SelectProfile(Power::Battery)), - menu_button( - row![ - column![ - text(fl!("balanced")).size(14), - text(fl!("balanced-desc")).size(10) - ] - .width(Length::Fill), - if matches!(self.power_profile, Power::Balanced) { - container( - icon::from_name("emblem-ok-symbolic") - .size(12) - .symbolic(true), - ) - } else { - container(horizontal_space(1.0)) - } - ] - .align_items(Alignment::Center) - ) - .on_press(Message::SelectProfile(Power::Balanced)), - menu_button( - row![ - column![ - text(fl!("performance")).size(14), - text(fl!("performance-desc")).size(10) - ] - .width(Length::Fill), - if matches!(self.power_profile, Power::Performance) { - container( - icon::from_name("emblem-ok-symbolic") - .size(12) - .symbolic(true), - ) - } else { - container(horizontal_space(1.0)) - } - ] - .align_items(Alignment::Center) - ) - .on_press(Message::SelectProfile(Power::Performance)), - padded_control(divider::horizontal::default()), - padded_control( - anim!( - //toggler - MAX_CHARGE, - &self.timeline, - fl!("max-charge"), - self.charging_limit, - Message::SetChargingLimit, + + let mut content = vec![ + padded_control( + row![ + icon::from_name(&*self.icon_name).size(24).symbolic(true), + column![name, description] + ] + .spacing(8) + .align_items(Alignment::Center), + ) + .into(), + padded_control(divider::horizontal::default()).into(), + menu_button( + row![ + column![ + text(fl!("battery")).size(14), + text(fl!("battery-desc")).size(10) + ] + .width(Length::Fill), + if matches!(self.power_profile, Power::Battery) { + container( + icon::from_name("emblem-ok-symbolic") + .size(12) + .symbolic(true), ) - .text_size(14) - .width(Length::Fill) - ), - padded_control(divider::horizontal::default()), - padded_control( - row![ - icon::from_name(self.display_icon_name.as_str()) - .size(24) + } else { + container(horizontal_space(1.0)) + } + ] + .align_items(Alignment::Center), + ) + .on_press(Message::SelectProfile(Power::Battery)) + .into(), + menu_button( + row![ + column![ + text(fl!("balanced")).size(14), + text(fl!("balanced-desc")).size(10) + ] + .width(Length::Fill), + if matches!(self.power_profile, Power::Balanced) { + container( + icon::from_name("emblem-ok-symbolic") + .size(12) .symbolic(true), - slider( - 1..=100, - (self.screen_brightness * 100.0) as i32, - Message::SetScreenBrightness - ), - text(format!("{:.0}%", self.screen_brightness * 100.0)) - .size(16) - .width(Length::Fixed(40.0)) - .horizontal_alignment(Horizontal::Right) - ] - .spacing(12) - ), - padded_control( - row![ - icon::from_name("keyboard-brightness-symbolic") - .size(24) + ) + } else { + container(horizontal_space(1.0)) + } + ] + .align_items(Alignment::Center), + ) + .on_press(Message::SelectProfile(Power::Balanced)) + .into(), + menu_button( + row![ + column![ + text(fl!("performance")).size(14), + text(fl!("performance-desc")).size(10) + ] + .width(Length::Fill), + if matches!(self.power_profile, Power::Performance) { + container( + icon::from_name("emblem-ok-symbolic") + .size(12) .symbolic(true), - slider( - 0..=100, - (self.kbd_brightness * 100.0) as i32, - Message::SetKbdBrightness - ), - text(format!("{:.0}%", self.kbd_brightness * 100.0)) - .size(16) - .width(Length::Fixed(40.0)) - .horizontal_alignment(Horizontal::Right) - ] - .spacing(12) + ) + } else { + container(horizontal_space(1.0)) + } + ] + .align_items(Alignment::Center), + ) + .on_press(Message::SelectProfile(Power::Performance)) + .into(), + padded_control(divider::horizontal::default()).into(), + padded_control( + anim!( + //toggler + MAX_CHARGE, + &self.timeline, + fl!("max-charge"), + self.charging_limit, + Message::SetChargingLimit, + ) + .text_size(14) + .width(Length::Fill), + ) + .into(), + padded_control(divider::horizontal::default()).into(), + padded_control( + row![ + icon::from_name(self.display_icon_name.as_str()) + .size(24) + .symbolic(true), + slider( + 1..=100, + (self.screen_brightness * 100.0) as i32, + Message::SetScreenBrightness + ), + text(format!("{:.0}%", self.screen_brightness * 100.0)) + .size(16) + .width(Length::Fixed(40.0)) + .horizontal_alignment(Horizontal::Right) + ] + .spacing(12), + ) + .into(), + padded_control( + row![ + icon::from_name("keyboard-brightness-symbolic") + .size(24) + .symbolic(true), + slider( + 0..=100, + (self.kbd_brightness * 100.0) as i32, + Message::SetKbdBrightness ), - padded_control(divider::horizontal::default()), - menu_button(text(fl!("power-settings")).size(14).width(Length::Fill)) - .on_press(Message::OpenSettings) + text(format!("{:.0}%", self.kbd_brightness * 100.0)) + .size(16) + .width(Length::Fixed(40.0)) + .horizontal_alignment(Horizontal::Right) ] - .padding([8, 0]), + .spacing(12), ) + .into(), + padded_control(divider::horizontal::default()).into(), + ]; + + if !self.gpus.is_empty() { + content.push( + padded_control( + row![ + text(fl!("dgpu-running")) + .size(16) + .width(Length::Fill) + .horizontal_alignment(Horizontal::Left), + container(vertical_space(Length::Fixed(0.0))) + .padding(4) + .style(::Style::Custom(Box::new( + |theme| container::Appearance { + text_color: Some(Color::TRANSPARENT), + background: Some(Background::Color( + theme.cosmic().accent_color().into(), + )), + border_radius: 4.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + icon_color: Some(Color::TRANSPARENT), + }, + ))), + ] + .align_items(Alignment::Center), + ) + .into(), + ); + content.push(padded_control(divider::horizontal::default()).into()); + } + + for (key, gpu) in &self.gpus { + if gpu.app_list.is_none() { + continue; + } + + content.push( + menu_button(row![ + text(fl!( + "dgpu-applications", + gpu_name = if self.gpus.len() == 1 { + String::new() + } else { + format!("\"{}\"", gpu.name) + } + )) + .size(14) + .width(Length::Fill) + .height(Length::Fixed(24.0)) + .vertical_alignment(Vertical::Center), + container( + icon::from_name(if gpu.toggled { + "go-down-symbolic" + } else { + "go-up-symbolic" + }) + .size(14) + .symbolic(true) + ) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .width(Length::Fixed(24.0)) + .height(Length::Fixed(24.0)), + ]) + .on_press(Message::ToggleGpuApps(key.clone())) + .into(), + ); + + if gpu.toggled { + let app_list = gpu.app_list.as_ref().unwrap(); + let mut list_apps = Vec::with_capacity(app_list.len()); + for app in app_list { + list_apps.push( + padded_control( + row![ + if let Some(icon) = &app.icon { + container(icon::from_name(&**icon).size(12).symbolic(true)) + } else { + container(horizontal_space(12.0)) + }, + column![text(&app.name).size(14), text(&app.secondary).size(10)] + .width(Length::Fill), + ] + .spacing(8) + .align_items(Alignment::Center), + ) + .into(), + ); + } + content.push( + scrollable(Column::with_children(list_apps)) + .height(Length::Fixed(300.0)) + .into(), + ); + } + content.push(padded_control(divider::horizontal::default()).into()); + } + + content.push( + menu_button(text(fl!("power-settings")).size(14).width(Length::Fill)) + .on_press(Message::OpenSettings) + .into(), + ); + + self.core + .applet + .popup_container(Column::with_children(content).padding([8, 0])) .into() } @@ -500,6 +677,10 @@ impl cosmic::Application for CosmicBatteryApplet { PowerProfileUpdate::Init(tx, p) => Message::InitProfile(p, tx), PowerProfileUpdate::Error(e) => Message::Errored(e), // TODO: handle error }), + dgpu_subscription(0).map(|event| match event { + GpuUpdate::On(path, name, list) => Message::GpuOn(path, name, list), + GpuUpdate::Off(path) => Message::GpuOff(path), + }), self.timeline .as_subscription() .map(|(_, now)| Message::Frame(now)), diff --git a/cosmic-applet-battery/src/dgpu.rs b/cosmic-applet-battery/src/dgpu.rs new file mode 100644 index 00000000..2fb5bf60 --- /dev/null +++ b/cosmic-applet-battery/src/dgpu.rs @@ -0,0 +1,518 @@ +use std::{ + ffi::{OsStr, OsString}, + fmt::{self, Debug}, + hash::Hash, + io, + os::fd::{AsFd, AsRawFd}, + path::{Path, PathBuf}, + time::Duration, +}; + +use cosmic::iced::{self, subscription}; +use drm::control::Device as ControlDevice; +use futures::{FutureExt, SinkExt}; +use tokio::{ + io::unix::AsyncFd, + task::spawn_blocking, + time::{self, Interval}, +}; +use tracing::{debug, info, trace}; +use udev::EventType; + +pub struct GpuMonitor { + primary_gpu: PathBuf, + gpus: Vec, + monitor: AsyncFd, + seat: String, +} + +struct WrappedSocket(udev::MonitorSocket); +impl AsRawFd for WrappedSocket { + fn as_raw_fd(&self) -> std::os::unix::io::RawFd { + self.0.as_raw_fd() + } +} +unsafe impl Send for WrappedSocket {} +unsafe impl Sync for WrappedSocket {} + +impl Debug for GpuMonitor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("GpuMonitor") + .field("primary_gpu", &self.primary_gpu) + .field("gpus", &self.gpus) + .field("monitor", &"...") + .field("seat", &self.seat) + .finish() + } +} + +#[derive(Debug)] +struct Gpu { + path: PathBuf, + name: String, + primary: bool, + enabled: bool, + driver: Option, + interval: Interval, +} + +async fn is_desktop() -> bool { + let chassis = tokio::fs::read_to_string("/sys/class/dmi/id/chassis_type") + .await + .unwrap_or_default(); + + chassis.trim() == "3" +} + +async fn powered_on(path: impl AsRef) -> bool { + let Some(component) = path.as_ref().components().last() else { + return true; + }; + let name_str = component.as_os_str(); + let Some(name) = name_str.to_str() else { + return true; + }; + let Ok(state) = + tokio::fs::read_to_string(format!("/sys/class/drm/{}/device/power_state", name)).await + else { + return true; + }; + + match state.trim() { + "D0" => true, + "D3cold" | "D3hot" => false, + x => { + debug!( + "Unknown power state {} for node {}", + x, + path.as_ref().display() + ); + true + } + } +} + +impl GpuMonitor { + async fn new() -> Option { + if is_desktop().await { + info!("Desktop, skipping dGPU code"); + return None; + } + + let seat = std::env::var("XDG_SEAT").unwrap_or_else(|_| String::from("seat0")); + let seat_clone = seat.clone(); + let gpus = spawn_blocking(move || all_gpus(seat)).await.ok()?.ok()?; + + let monitor = AsyncFd::new(WrappedSocket( + udev::MonitorBuilder::new() + .ok()? + .match_subsystem("drm") + .ok()? + .listen() + .ok()?, + )) + .ok()?; + + let primary_gpu = gpus + .iter() + .find_map(|gpu| gpu.primary.then(|| gpu.path.clone()))?; + + Some(GpuMonitor { + primary_gpu, + gpus, + monitor, + seat: seat_clone, + }) + } +} + +#[derive(Debug, Clone)] +pub struct Entry { + pub name: String, + pub icon: Option, + pub secondary: String, +} + +#[derive(Debug)] +pub struct RunningApp { + name: String, + icon: Option, + executable_name: String, +} + +impl Gpu { + async fn connected_outputs(&self) -> Option> { + let path = self.path.clone(); + spawn_blocking(move || { + struct Device(std::fs::File); + impl AsFd for Device { + fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_> { + self.0.as_fd() + } + } + impl drm::Device for Device {} + impl ControlDevice for Device {} + + let device = Device(std::fs::File::open(path).ok()?); + let resources = device.resource_handles().ok()?; + + let outputs = resources + .connectors + .into_iter() + .filter_map(|conn| device.get_connector(conn, false).ok()) + .filter(|info| info.state() == drm::control::connector::State::Connected) + .map(|info| Entry { + name: format!( + "Output @ {}:{}", + info.interface().as_str(), + info.interface_id() + ), + icon: Some("display-symbolic".to_string()), + secondary: String::new(), + }) + .collect(); + // TODO read and parse edid with libdisplay-info and display output manufacture/model + + Some(outputs) + }) + .await + .ok()? + } + + async fn app_list(&self, running_apps: &[RunningApp]) -> Option> { + match self.driver.as_ref().and_then(|s| s.to_str()) { + Some("nvidia") => { + // figure out bus path for calling nvidia-smi + let mut sys_path = PathBuf::from("/sys/class/drm"); + sys_path.push(self.path.components().last()?.as_os_str()); + let buslink = std::fs::read_link(sys_path) + .ok()? + .components() + .rev() + .nth(2)? + .as_os_str() + .to_string_lossy() + .into_owned(); + + let smi_output = match tokio::process::Command::new("nvidia-smi") + .args(["pmon", "--id", &buslink, "--count", "1"]) + .output() + .await + { + Ok(output) if output.status.success() => { + String::from_utf8_lossy(&output.stdout).into_owned() + } + Ok(output) => { + debug!( + "smi returned error code {}: {}", + output.status, + String::from_utf8_lossy(&output.stdout) + ); + return None; + } + Err(err) => { + debug!("smi returned error code: {}", err); + return None; + } + }; + + Some( + smi_output + .lines() + .filter(|line| !line.starts_with('#')) + .map(|line| { + let components = line.split_whitespace().collect::>(); + let pid = components[1].trim(); + let process_name = components.last().unwrap().trim(); + + if let Some(application) = running_apps + .iter() + .find(|running_app| running_app.executable_name == process_name) + { + Entry { + name: application.name.clone(), + icon: application.icon.clone(), + secondary: String::new(), + } + } else { + Entry { + name: process_name.to_string(), + icon: None, + secondary: pid.to_string(), + } + } + }) + .collect(), + ) + } + _ => { + let lsof_output = match tokio::process::Command::new("lsof") + .args([OsStr::new("-t"), self.path.as_os_str()]) + .output() + .await + { + Ok(output) => String::from_utf8_lossy(&output.stdout).into_owned(), + Err(err) => { + debug!("lsof returned error code: {}", err); + return None; + } + }; + + Some( + lsof_output + .lines() + .filter_map(|pid| { + let executable = std::fs::read_link(format!("/proc/{}/exe", pid)) + .ok()? + .components() + .last()? + .as_os_str() + .to_string_lossy() + .into_owned(); + + if let Some(application) = running_apps + .iter() + .find(|running_app| running_app.executable_name == executable) + { + Some(Entry { + name: application.name.clone(), + icon: application.icon.clone(), + secondary: String::new(), + }) + } else { + Some(Entry { + name: executable, + icon: None, + secondary: pid.to_string(), + }) + } + }) + .collect(), + ) + } + } + } +} + +fn all_gpus>(seat: S) -> io::Result> { + let mut enumerator = udev::Enumerator::new()?; + enumerator.match_subsystem("drm")?; + enumerator.match_sysname("card[0-9]*")?; + Ok(enumerator + .scan_devices()? + .filter(|device| { + device + .property_value("ID_SEAT") + .map(|x| x.to_os_string()) + .unwrap_or_else(|| OsString::from("seat0")) + == *seat.as_ref() + }) + .flat_map(|device| { + let path = device.devnode().map(PathBuf::from)?; + let boot_vga = if let Ok(Some(pci)) = device.parent_with_subsystem(Path::new("pci")) { + if let Some(value) = pci.attribute_value("boot_vga") { + value == "1" + } else { + false + } + } else { + false + }; + + let name = if let Some(parent) = device.parent() { + let vendor = parent + .property_value("SWITCHEROO_CONTROL_VENDOR_NAME") + .or_else(|| parent.property_value("ID_VENDOR_FROM_DATABASE")); + let name = parent + .property_value("SWITCHEROO_CONTROL_PRODUCT_NAME") + .or_else(|| parent.property_value("ID_MODEL_FROM_DATABASE")); + + if vendor.is_none() && name.is_none() { + String::from("Unknown GPU") + } else { + format!( + "{} {}", + vendor.map(|s| s.to_string_lossy()).unwrap_or_default(), + name.map(|s| s.to_string_lossy()).unwrap_or_default() + ) + } + } else { + String::from("Unknown GPU") + }; + + let mut device = Some(device); + let driver = loop { + if let Some(dev) = device { + if dev.driver().is_some() { + break dev.driver().map(std::ffi::OsStr::to_os_string); + } else { + device = dev.parent(); + } + } else { + break None; + } + }; + + let mut interval = time::interval(Duration::from_secs(3)); + interval.set_missed_tick_behavior(time::MissedTickBehavior::Delay); + + Some(Gpu { + path, + name, + primary: boot_vga, + enabled: false, + driver, + interval, + }) + }) + .collect()) +} + +pub fn dgpu_subscription( + id: I, +) -> iced::Subscription { + subscription::channel(id, 50, move |mut output| async move { + let mut state = State::Ready; + + loop { + state = start_listening(state, &mut output).await; + } + }) +} + +#[derive(Debug)] +pub enum State { + Ready, + Waiting(GpuMonitor), + Finished, +} + +#[derive(Debug)] +pub enum GpuUpdate { + Off(PathBuf), + On(PathBuf, String, Option>), +} + +async fn start_listening( + state: State, + output: &mut futures::channel::mpsc::Sender, +) -> State { + match state { + State::Ready => match GpuMonitor::new().await { + Some(monitor) => State::Waiting(monitor), + None => State::Finished, + }, + State::Waiting(mut monitor) => { + let select_all = futures::future::select_all( + monitor + .gpus + .iter_mut() + .map(|gpu| Box::pin(gpu.interval.tick())), + ) + .map(|(_, i, _)| i); + + tokio::select! { + guard = monitor.monitor.readable() => { + if let Ok(mut guard) = guard { + for event in monitor.monitor.get_ref().0.iter() { + match event.event_type() { + // New device + EventType::Add => { + if let Some(path) = event.devnode() { + let device = event.device(); + let name = if let Some(parent) = device.parent() { + let vendor = parent + .property_value("SWITCHEROO_CONTROL_VENDOR_NAME") + .or_else(|| parent.property_value("ID_VENDOR_FROM_DATABASE")); + let name = parent + .property_value("SWITCHEROO_CONTROL_PRODUCT_NAME") + .or_else(|| parent.property_value("ID_MODEL_FROM_DATABASE")); + + if vendor.is_none() && name.is_none() { + String::from("Unknown GPU") + } else { + format!( + "{} {}", + vendor.map(|s| s.to_string_lossy()).unwrap_or_default(), + name.map(|s| s.to_string_lossy()).unwrap_or_default() + ) + } + } else { + String::from("Unknown GPU") + }; + + let mut device = Some(device); + let driver = loop { + if let Some(dev) = device { + if dev.driver().is_some() { + break dev.driver().map(std::ffi::OsStr::to_os_string); + } else { + device = dev.parent(); + } + } else { + break None; + } + }; + + let mut interval = time::interval(Duration::from_secs(3)); + interval.set_missed_tick_behavior(time::MissedTickBehavior::Delay); + monitor.gpus.push(Gpu { + path: path.to_path_buf(), + name, + primary: false, + enabled: false, + driver, + interval, + }); + } + }, + EventType::Remove => { + if let Some(path) = event.devnode() { + monitor.gpus.retain(|gpu| gpu.path != path); + } + } + _ => {}, + } + } + + guard.clear_ready_matching(tokio::io::Ready::READABLE); + } else { + return State::Finished; + } + } + i = select_all => { + let gpu = &mut monitor.gpus[i]; + if gpu.path == monitor.primary_gpu { + return State::Waiting(monitor); + } + + trace!("Polling gpu {}", gpu.path.display()); + let enabled = powered_on(&gpu.path).await; + + if enabled != gpu.enabled { + let mut new_interval = time::interval(Duration::from_secs(if enabled { 30 } else { 3 })); + new_interval.set_missed_tick_behavior(time::MissedTickBehavior::Delay); + gpu.interval = new_interval; + gpu.enabled = enabled; + } + + if enabled { + let mut list = gpu.connected_outputs().await.unwrap_or_default(); + if let Some(mut apps) = gpu.app_list(&[]).await { + apps.retain(|app| app.name != "cosmic-comp" && app.name != "Xwayland"); + list.extend(apps); + } + if output.send(GpuUpdate::On(gpu.path.clone(), gpu.name.clone(), (!list.is_empty()).then_some(list))).await.is_err() { + return State::Finished; + } + } else if output.send(GpuUpdate::Off(gpu.path.clone())).await.is_err() { + return State::Finished; + } + } + }; + + State::Waiting(monitor) + } + State::Finished => iced::futures::future::pending().await, + } +} diff --git a/cosmic-applet-battery/src/main.rs b/cosmic-applet-battery/src/main.rs index 25e3ad04..f19df1de 100644 --- a/cosmic-applet-battery/src/main.rs +++ b/cosmic-applet-battery/src/main.rs @@ -2,10 +2,10 @@ mod backlight; mod app; mod config; +mod dgpu; mod localize; mod power_daemon; mod upower; - mod upower_device; mod upower_kbdbacklight; use config::APP_ID; diff --git a/debian/control b/debian/control index 99068127..388b59ca 100644 --- a/debian/control +++ b/debian/control @@ -10,6 +10,7 @@ Build-Depends: libdbus-1-dev, libegl-dev, libpulse-dev, + libudev-dev, libxkbcommon-dev, libwayland-dev, just,