From ad7205c1a4266563f5e415a4377ab8d7578db776 Mon Sep 17 00:00:00 2001 From: Paul Delafosse Date: Sun, 29 May 2022 13:40:07 +0200 Subject: [PATCH 1/4] feat: add destkop entries actions --- Cargo.lock | 7 ++ plugins/Cargo.toml | 1 + plugins/src/calc/mod.rs | 2 + plugins/src/desktop_entries/mod.rs | 181 ++++++++++++++++++++++------- service/src/lib.rs | 11 -- src/lib.rs | 9 +- 6 files changed, 155 insertions(+), 56 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 64a2baa..99f57cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1103,6 +1103,7 @@ dependencies = [ "ron", "serde", "serde_json", + "shell-words", "slab", "strsim", "sysfs-class", @@ -1506,6 +1507,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "signal-hook-registry" version = "1.4.0" diff --git a/plugins/Cargo.toml b/plugins/Cargo.toml index 83f1f84..a4b1c7e 100644 --- a/plugins/Cargo.toml +++ b/plugins/Cargo.toml @@ -33,6 +33,7 @@ dirs = "4.0.0" futures = "0.3.21" bytes = "1.1.0" recently-used-xbel = "1.0.0" +shell-words = "1.1.0" [dependencies.reqwest] version = "0.11.10" diff --git a/plugins/src/calc/mod.rs b/plugins/src/calc/mod.rs index fe88c71..d46ebdf 100644 --- a/plugins/src/calc/mod.rs +++ b/plugins/src/calc/mod.rs @@ -70,6 +70,8 @@ impl App { let options = vec![ContextOption { id: 0, name: "Qalculate! Manual".into(), + description: "Browse Qalculate! user manual".to_string(), + exec: None, }]; crate::send(&mut self.out, PluginResponse::Context { id: 0, options }).await; diff --git a/plugins/src/desktop_entries/mod.rs b/plugins/src/desktop_entries/mod.rs index 9524d5c..56aff39 100644 --- a/plugins/src/desktop_entries/mod.rs +++ b/plugins/src/desktop_entries/mod.rs @@ -9,22 +9,86 @@ use futures::StreamExt; use pop_launcher::*; use std::borrow::Cow; use std::hash::{Hash, Hasher}; -use std::path::PathBuf; +use std::process::Command; use tokio::io::AsyncWrite; +use tracing::error; +use crate::desktop_entries::graphics::is_switchable; #[derive(Debug, Eq)] struct Item { appid: String, description: String, + context: Vec, + prefer_non_default_gpu: bool, exec: String, icon: Option, keywords: Option>, name: String, - path: PathBuf, - prefers_non_default_gpu: bool, src: PathSource, } +#[derive(Debug, PartialEq, Eq)] +pub enum ContextAction { + Action(Action), + GpuPreference(GpuPreference) +} + +impl Item { + fn run(&self, action_idx: Option) { + match action_idx { + // No action provided just run the desktop entry with the default gpu + None => run_exec_command(&self.exec, self.prefer_non_default_gpu), + // Run the provided action + Some(idx) => { + match self.context.get(idx as usize) { + None => error!("Could not find context action at index {idx}"), + Some(action) => match action { + ContextAction::Action(action) => run_exec_command(&action.exec, self.prefer_non_default_gpu), + ContextAction::GpuPreference(pref) => match pref { + GpuPreference::Default => run_exec_command(&self.exec, false), + GpuPreference::NonDefault => run_exec_command(&self.exec, true), + }, + }, + } + } + } + } +} + +fn run_exec_command(exec: &str, discrete_graphics: bool) { + let cmd = shell_words::split(exec); + let cmd: Vec = cmd.unwrap(); + + let args = cmd + .iter() + // Filter desktop entries field code. Is this needed ? + // see: https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html + .filter(|arg| !arg.starts_with('%')) + .collect::>(); + + let mut cmd = Command::new(&args[0]); + let cmd = cmd.args(&args[1..]); + + // FIXME: Will this work with Nvidia Gpu ? + // We probably want to look there : https://gitlab.freedesktop.org/hadess/switcheroo-control/-/blob/master/src/switcheroo-control.c + let cmd = if discrete_graphics { + cmd.env("DRI_PRIME", "1") + } else { + cmd + }; + + if let Err(err) = cmd.spawn() { + error!("Failed to run desktop entry: {err}"); + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Action { + pub name: String, + pub description: String, + pub exec: String, +} + impl Hash for Item { fn hash(&self, state: &mut H) { self.appid.hash(state); @@ -150,6 +214,44 @@ impl App { continue; } + let mut actions = vec![]; + + if let Some(entries) = entry.actions() { + for action in entries.split(';') { + let action = + entry.action_name(action, locale).and_then(|name| { + entry.action_exec(action).map(|exec| Action { + name: action.to_string(), + description: name.to_string(), + exec: exec.to_string(), + }) + }); + + if let Some(action) = action { + actions.push(action); + } + } + } + + let actions = actions + .into_iter() + .map(|action| ContextAction::Action(action)); + + let entry_prefers_non_default_gpu = entry.prefers_non_default_gpu(); + let prefer_non_default_gpu = entry_prefers_non_default_gpu && is_switchable(); + let prefer_default_gpu = !entry_prefers_non_default_gpu && is_switchable(); + + let context: Vec = if prefer_non_default_gpu { + vec![ContextAction::GpuPreference(GpuPreference::Default)] + } else if prefer_default_gpu { + vec![ContextAction::GpuPreference(GpuPreference::NonDefault)] + } else { + vec![] + } + .into_iter() + .chain(actions) + .collect(); + let item = Item { appid: entry.appid.to_owned(), name: name.to_string(), @@ -163,9 +265,9 @@ impl App { }), icon: entry.icon().map(|x| x.to_owned()), exec: exec.to_owned(), - path: path.clone(), - prefers_non_default_gpu: entry.prefers_non_default_gpu(), src, + context, + prefer_non_default_gpu: entry_prefers_non_default_gpu }; deduplicator.insert(item); @@ -179,35 +281,22 @@ impl App { } async fn activate(&mut self, id: u32) { + send(&mut self.tx, PluginResponse::Close).await; + if let Some(entry) = self.entries.get(id as usize) { - let response = PluginResponse::DesktopEntry { - path: entry.path.clone(), - gpu_preference: if entry.prefers_non_default_gpu { - GpuPreference::NonDefault - } else { - GpuPreference::Default - }, - }; - - send(&mut self.tx, response).await; + entry.run(None); + } else { + error!("Desktop entry not found at index {id}"); } + + std::process::exit(0); } async fn activate_context(&mut self, id: u32, context: u32) { - if let Some(entry) = self.entries.get(id as usize) { - let response = match context { - 0 => PluginResponse::DesktopEntry { - path: entry.path.clone(), - gpu_preference: if !entry.prefers_non_default_gpu { - GpuPreference::NonDefault - } else { - GpuPreference::Default - }, - }, - _ => return, - }; + send(&mut self.tx, PluginResponse::Close).await; - send(&mut self.tx, response).await; + if let Some(entry) = self.entries.get(id as usize) { + entry.run(Some(context)) } } @@ -215,21 +304,35 @@ impl App { if let Some(entry) = self.entries.get(id as usize) { let mut options = Vec::new(); - if graphics::is_switchable() { - options.push(ContextOption { - id: 0, - name: (if entry.prefers_non_default_gpu { - "Launch Using Integrated Graphics Card" - } else { - "Launch Using Discrete Graphics Card" - }) - .to_owned(), - }); + for (idx, action) in entry.context.iter().enumerate() { + match action { + ContextAction::Action(action) => options.push(ContextOption { + id: idx as u32, + name: action.name.to_owned(), + description: action.description.to_owned(), + exec: Some(action.exec.to_string()), + }), + ContextAction::GpuPreference(pref) => { + match pref { + GpuPreference::Default => options.push(ContextOption { + id: 0, + name: "Integrated Graphics".to_owned(), + description: "Launch Using Integrated Graphics Card".to_owned(), + exec: None, + }), + GpuPreference::NonDefault => options.push(ContextOption { + id: 0, + name: "Discrete Graphics".to_owned(), + description: "Launch Using Discrete Graphics Card".to_owned(), + exec: None, + }), + } + } + } } if !options.is_empty() { let response = PluginResponse::Context { id, options }; - send(&mut self.tx, response).await; } } diff --git a/service/src/lib.rs b/service/src/lib.rs index 6d7d6f9..495b127 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -166,17 +166,6 @@ impl + Unpin> Service { } PluginResponse::Fill(text) => self.fill(text).await, PluginResponse::Finished => self.finished(plugin).await, - PluginResponse::DesktopEntry { - path, - gpu_preference, - } => { - self.respond(Response::DesktopEntry { - path, - gpu_preference, - }) - .await; - } - // Report the plugin as finished and remove it from future polling PluginResponse::Deactivate => { self.finished(plugin).await; diff --git a/src/lib.rs b/src/lib.rs index 169562f..9798830 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,9 +48,11 @@ pub type Indice = u32; pub struct ContextOption { pub id: Indice, pub name: String, + pub description: String, + pub exec: Option, } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] pub enum GpuPreference { Default, NonDefault, @@ -80,11 +82,6 @@ pub enum PluginResponse { }, /// Instruct the launcher service to deactivate this plugin. Deactivate, - // Notifies that a .desktop entry should be launched by the frontend. - DesktopEntry { - path: PathBuf, - gpu_preference: GpuPreference, - }, /// Update the text in the launcher. Fill(String), /// Indicoates that a plugin is finished with its queries. From d0622a473c110aedc5b6d9726592992424eabd62 Mon Sep 17 00:00:00 2001 From: Paul Delafosse Date: Sun, 29 May 2022 16:13:25 +0200 Subject: [PATCH 2/4] chore: cargo fmt --- plugins/src/desktop_entries/mod.rs | 76 +++++++++++++++--------------- plugins/src/web/mod.rs | 2 +- service/src/plugins/help.rs | 2 +- service/src/plugins/mod.rs | 2 +- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/plugins/src/desktop_entries/mod.rs b/plugins/src/desktop_entries/mod.rs index 56aff39..2f3f9df 100644 --- a/plugins/src/desktop_entries/mod.rs +++ b/plugins/src/desktop_entries/mod.rs @@ -3,6 +3,7 @@ mod graphics; +use crate::desktop_entries::graphics::is_switchable; use crate::*; use freedesktop_desktop_entry::{default_paths, DesktopEntry, Iter as DesktopIter, PathSource}; use futures::StreamExt; @@ -12,7 +13,6 @@ use std::hash::{Hash, Hasher}; use std::process::Command; use tokio::io::AsyncWrite; use tracing::error; -use crate::desktop_entries::graphics::is_switchable; #[derive(Debug, Eq)] struct Item { @@ -30,7 +30,7 @@ struct Item { #[derive(Debug, PartialEq, Eq)] pub enum ContextAction { Action(Action), - GpuPreference(GpuPreference) + GpuPreference(GpuPreference), } impl Item { @@ -39,18 +39,18 @@ impl Item { // No action provided just run the desktop entry with the default gpu None => run_exec_command(&self.exec, self.prefer_non_default_gpu), // Run the provided action - Some(idx) => { - match self.context.get(idx as usize) { - None => error!("Could not find context action at index {idx}"), - Some(action) => match action { - ContextAction::Action(action) => run_exec_command(&action.exec, self.prefer_non_default_gpu), - ContextAction::GpuPreference(pref) => match pref { - GpuPreference::Default => run_exec_command(&self.exec, false), - GpuPreference::NonDefault => run_exec_command(&self.exec, true), - }, + Some(idx) => match self.context.get(idx as usize) { + None => error!("Could not find context action at index {idx}"), + Some(action) => match action { + ContextAction::Action(action) => { + run_exec_command(&action.exec, self.prefer_non_default_gpu) + } + ContextAction::GpuPreference(pref) => match pref { + GpuPreference::Default => run_exec_command(&self.exec, false), + GpuPreference::NonDefault => run_exec_command(&self.exec, true), }, - } - } + }, + }, } } } @@ -221,8 +221,8 @@ impl App { let action = entry.action_name(action, locale).and_then(|name| { entry.action_exec(action).map(|exec| Action { - name: action.to_string(), - description: name.to_string(), + name: name.to_string(), + description: action.to_string(), exec: exec.to_string(), }) }); @@ -238,19 +238,21 @@ impl App { .map(|action| ContextAction::Action(action)); let entry_prefers_non_default_gpu = entry.prefers_non_default_gpu(); - let prefer_non_default_gpu = entry_prefers_non_default_gpu && is_switchable(); - let prefer_default_gpu = !entry_prefers_non_default_gpu && is_switchable(); + let prefer_non_default_gpu = + entry_prefers_non_default_gpu && is_switchable(); + let prefer_default_gpu = + !entry_prefers_non_default_gpu && is_switchable(); - let context: Vec = if prefer_non_default_gpu { + let context: Vec = if prefer_non_default_gpu { vec![ContextAction::GpuPreference(GpuPreference::Default)] } else if prefer_default_gpu { vec![ContextAction::GpuPreference(GpuPreference::NonDefault)] } else { vec![] } - .into_iter() - .chain(actions) - .collect(); + .into_iter() + .chain(actions) + .collect(); let item = Item { appid: entry.appid.to_owned(), @@ -267,7 +269,7 @@ impl App { exec: exec.to_owned(), src, context, - prefer_non_default_gpu: entry_prefers_non_default_gpu + prefer_non_default_gpu: entry_prefers_non_default_gpu, }; deduplicator.insert(item); @@ -312,22 +314,20 @@ impl App { description: action.description.to_owned(), exec: Some(action.exec.to_string()), }), - ContextAction::GpuPreference(pref) => { - match pref { - GpuPreference::Default => options.push(ContextOption { - id: 0, - name: "Integrated Graphics".to_owned(), - description: "Launch Using Integrated Graphics Card".to_owned(), - exec: None, - }), - GpuPreference::NonDefault => options.push(ContextOption { - id: 0, - name: "Discrete Graphics".to_owned(), - description: "Launch Using Discrete Graphics Card".to_owned(), - exec: None, - }), - } - } + ContextAction::GpuPreference(pref) => match pref { + GpuPreference::Default => options.push(ContextOption { + id: 0, + name: "Integrated Graphics".to_owned(), + description: "Launch Using Integrated Graphics Card".to_owned(), + exec: None, + }), + GpuPreference::NonDefault => options.push(ContextOption { + id: 0, + name: "Discrete Graphics".to_owned(), + description: "Launch Using Discrete Graphics Card".to_owned(), + exec: None, + }), + }, } } diff --git a/plugins/src/web/mod.rs b/plugins/src/web/mod.rs index 8ca427b..d42f8f9 100644 --- a/plugins/src/web/mod.rs +++ b/plugins/src/web/mod.rs @@ -12,7 +12,7 @@ use url::Url; use pop_launcher::*; -pub use config::{Config, Definition, load}; +pub use config::{load, Config, Definition}; use regex::Regex; mod config; diff --git a/service/src/plugins/help.rs b/service/src/plugins/help.rs index ddfbb0d..2abff6e 100644 --- a/service/src/plugins/help.rs +++ b/service/src/plugins/help.rs @@ -23,7 +23,7 @@ pub const CONFIG: PluginConfig = PluginConfig { regex: None, }, icon: Some(IconSource::Name(Cow::Borrowed("system-help-symbolic"))), - history: false + history: false, }; pub struct HelpPlugin { pub id: usize, diff --git a/service/src/plugins/mod.rs b/service/src/plugins/mod.rs index 656c189..ed2f6b3 100644 --- a/service/src/plugins/mod.rs +++ b/service/src/plugins/mod.rs @@ -1,8 +1,8 @@ // Copyright 2021 System76 // SPDX-License-Identifier: MPL-2.0 -pub(crate) mod external; pub mod config; +pub(crate) mod external; pub mod help; pub use external::load; From 78af46cc5e833346bdae41d04d2e18fb5b429bbd Mon Sep 17 00:00:00 2001 From: Paul Delafosse Date: Tue, 31 May 2022 10:09:25 +0200 Subject: [PATCH 3/4] feat: add switchable graphics --- Cargo.lock | 33 +++- plugins/Cargo.toml | 2 + plugins/src/desktop_entries/graphics.rs | 222 ++++++++++++++++++++---- plugins/src/desktop_entries/mod.rs | 26 ++- src/lib.rs | 10 +- 5 files changed, 242 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 99f57cb..3401c55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -768,6 +768,16 @@ dependencies = [ "cc", ] +[[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 = "locale_config" version = "0.3.0" @@ -964,9 +974,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" +checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" [[package]] name = "ordered-stream" @@ -1042,6 +1052,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + [[package]] name = "polling" version = "2.2.0" @@ -1096,6 +1112,7 @@ dependencies = [ "human-sort", "human_format", "new_mime_guess", + "once_cell", "pop-launcher", "recently-used-xbel", "regex", @@ -1109,6 +1126,7 @@ dependencies = [ "sysfs-class", "tokio", "tracing", + "udev", "url", "urlencoding", "ward", @@ -1769,6 +1787,17 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "udev" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c960764f7e816eed851a96c364745d37f9fe71a2e7dba79fbd40104530b5dd0" +dependencies = [ + "libc", + "libudev-sys", + "pkg-config", +] + [[package]] name = "unicase" version = "2.6.0" diff --git a/plugins/Cargo.toml b/plugins/Cargo.toml index a4b1c7e..27bf033 100644 --- a/plugins/Cargo.toml +++ b/plugins/Cargo.toml @@ -34,6 +34,8 @@ futures = "0.3.21" bytes = "1.1.0" recently-used-xbel = "1.0.0" shell-words = "1.1.0" +udev = "0.6.3" +once_cell = "1.12.0" [dependencies.reqwest] version = "0.11.10" diff --git a/plugins/src/desktop_entries/graphics.rs b/plugins/src/desktop_entries/graphics.rs index ff0ec54..7a6d5a3 100644 --- a/plugins/src/desktop_entries/graphics.rs +++ b/plugins/src/desktop_entries/graphics.rs @@ -1,43 +1,203 @@ // Copyright 2022 System76 // SPDX-License-Identifier: GPL-3.0-only -use anyhow::Context; -use sysfs_class::{PciDevice, SysClass}; - -/// Checks if the system has switchable graphics. -/// -/// A system is considered switchable if multiple graphics card devices are found. -pub fn is_switchable() -> bool { - let main = || -> anyhow::Result { - let devices = PciDevice::all().context("cannot get PCI devices")?; - - let mut amd_graphics = 0; - let mut intel_graphics = 0; - let mut nvidia_graphics = 0; - - for dev in devices { - let c = dev.class().context("cannot get class of device")?; - if let 0x03 = (c >> 16) & 0xFF { - match dev.vendor().context("cannot get vendor of device")? { - 0x1002 => amd_graphics += 1, - 0x10DE => nvidia_graphics += 1, - 0x8086 => intel_graphics += 1, - _ => (), +use std::collections::HashSet; +use std::hash::{Hash, Hasher}; +use std::io; +use std::ops::Deref; +use std::path::PathBuf; + +#[derive(Debug, Default)] +pub struct Gpus { + devices: Vec, + default: Option, +} + +impl Gpus { + // Get gpus via udev + pub fn load() -> Self { + let drivers = get_gpus(); + + let mut gpus = Gpus::default(); + for dev in drivers.unwrap() { + if dev.is_default { + gpus.default = Some(dev) + } else { + gpus.devices.push(dev) + } + } + + gpus + } + + /// `true` if there is at least one non default gpu + pub fn is_switchable(&self) -> bool { + self.default.is_some() && !self.devices.is_empty() + } + + /// Return the default gpu + pub fn get_default(&self) -> Option<&Dev> { + self.default.as_ref() + } + + /// Get the first non-default gpu, the current `PreferNonDefaultGpu` specification + /// Does not tell us which one should be used. Anyway most machine out there should have + /// only one discrete graphic card. + /// see: https://gitlab.freedesktop.org/xdg/xdg-specs/-/issues/59 + pub fn non_default(&self) -> Option<&Dev> { + self.devices.first() + } +} + +#[derive(Debug)] +pub struct Dev { + id: usize, + driver: Driver, + is_default: bool, + parent_path: PathBuf, +} + +impl Dev { + pub fn launch_options(&self) -> Vec<(String, String)> { + let dev_num = self.id.to_string(); + let mut options = vec![]; + + match self.driver { + Driver::Unknown | Driver::Amd(_) | Driver::Intel => { + options.push(("DRI_PRIME".into(), dev_num)) + } + Driver::Nvidia => { + options.push(("__GLX_VENDOR_LIBRARY_NAME".into(), "nvidia".into())); + options.push(("__NV_PRIME_RENDER_OFFLOAD".into(), "1".into())); + options.push((" __VK_LAYER_NV_optimus".into(), "NVIDIA_only".into())); + } + } + + match self.get_vulkan_icd_paths() { + Ok(vulkan_icd_paths) if !vulkan_icd_paths.is_empty() => { + options.push(("VK_ICD_FILENAMES".into(), vulkan_icd_paths.join(":"))) + } + Err(err) => eprintln!("Failed to open vulkan icd paths: {err}"), + _ => {} + } + + options + } + + // Lookup vulkan icd files and return the ones matching the driver in use + fn get_vulkan_icd_paths(&self) -> io::Result> { + let vulkan_icd_paths = dirs::data_dir() + .expect("local data dir does not exists") + .join("vulkan/icd.d"); + let vulkan_icd_paths = &[PathBuf::from("/usr/share/vulkan/icd.d"), vulkan_icd_paths]; + + let mut icd_paths = vec![]; + if let Some(driver) = self.driver.as_str() { + for path in vulkan_icd_paths { + if path.exists() { + for entry in path.read_dir()? { + let entry = entry?; + let path = entry.path(); + if path.is_file() { + let path_str = path.to_string_lossy(); + if path_str.contains(driver) { + icd_paths.push(path_str.to_string()) + } + } + } } } } - let switchable = (nvidia_graphics > 0 && (intel_graphics > 0 || amd_graphics > 0)) - || (intel_graphics > 0 && amd_graphics > 0); + Ok(icd_paths) + } +} - Ok(switchable) - }; +// Ensure we filter out "render" devices having the same parent as the card +impl Hash for Dev { + fn hash(&self, state: &mut H) { + state.write(self.parent_path.to_string_lossy().as_bytes()); + state.finish(); + } +} + +impl PartialEq for Dev { + fn eq(&self, other: &Self) -> bool { + self.parent_path == other.parent_path + } +} + +impl Eq for Dev {} - match main() { - Ok(value) => value, - Err(why) => { - tracing::error!("{}", why); - false +#[derive(Debug)] +enum Driver { + Intel, + Amd(String), + Nvidia, + Unknown, +} + +impl Driver { + fn from_udev>(driver: Option) -> Driver { + match driver.as_deref() { + // For amd devices we need the name of the driver to get vulkan icd files + Some("radeon") => Driver::Amd("radeon".to_string()), + Some("amdgpu") => Driver::Amd("amdgpu".to_string()), + Some("nvidia") => Driver::Nvidia, + Some("iris") | Some("i915") | Some("i965") => Driver::Intel, + _ => Driver::Unknown, + } + } + + fn as_str(&self) -> Option<&str> { + match self { + Driver::Intel => Some("intel"), + Driver::Amd(driver) => Some(driver.as_str()), + Driver::Nvidia => Some("nvidia"), + Driver::Unknown => None, } } } + +fn get_gpus() -> io::Result> { + let mut enumerator = udev::Enumerator::new()?; + let mut dev_map = HashSet::new(); + let mut drivers: Vec = enumerator + .scan_devices()? + .into_iter() + .filter(|dev| { + dev.devnode() + .map(|path| path.starts_with("/dev/dri")) + .unwrap_or(false) + }) + .filter_map(|dev| { + dev.parent().and_then(|parent| { + let id = dev.sysnum(); + let parent_path = parent.syspath().to_path_buf(); + let driver = parent.driver().map(|d| d.to_string_lossy().to_string()); + let driver = Driver::from_udev(driver); + + let is_default = parent + .attribute_value("boot_vga") + .map(|v| v == "1") + .unwrap_or(false); + + id.map(|id| Dev { + id, + driver, + is_default, + parent_path, + }) + }) + }) + .collect(); + + // Sort the devices by sysnum so we get card0, card1 first and ignore the other 3D devices + drivers.sort_by(|a, b| a.id.cmp(&b.id)); + + for dev in drivers { + dev_map.insert(dev); + } + + Ok(dev_map) +} diff --git a/plugins/src/desktop_entries/mod.rs b/plugins/src/desktop_entries/mod.rs index 2f3f9df..c2692a9 100644 --- a/plugins/src/desktop_entries/mod.rs +++ b/plugins/src/desktop_entries/mod.rs @@ -3,10 +3,11 @@ mod graphics; -use crate::desktop_entries::graphics::is_switchable; +use crate::desktop_entries::graphics::Gpus; use crate::*; use freedesktop_desktop_entry::{default_paths, DesktopEntry, Iter as DesktopIter, PathSource}; use futures::StreamExt; +use once_cell::sync::Lazy; use pop_launcher::*; use std::borrow::Cow; use std::hash::{Hash, Hasher}; @@ -14,6 +15,8 @@ use std::process::Command; use tokio::io::AsyncWrite; use tracing::error; +static GPUS: Lazy = Lazy::new(Gpus::load); + #[derive(Debug, Eq)] struct Item { appid: String, @@ -67,16 +70,20 @@ fn run_exec_command(exec: &str, discrete_graphics: bool) { .collect::>(); let mut cmd = Command::new(&args[0]); - let cmd = cmd.args(&args[1..]); + let mut cmd = cmd.args(&args[1..]); - // FIXME: Will this work with Nvidia Gpu ? - // We probably want to look there : https://gitlab.freedesktop.org/hadess/switcheroo-control/-/blob/master/src/switcheroo-control.c - let cmd = if discrete_graphics { - cmd.env("DRI_PRIME", "1") + let gpu = if discrete_graphics { + GPUS.non_default() } else { - cmd + GPUS.get_default() }; + if let Some(gpu) = gpu { + for (opt, value) in gpu.launch_options() { + cmd = cmd.env(opt, value); + } + } + if let Err(err) = cmd.spawn() { error!("Failed to run desktop entry: {err}"); } @@ -237,11 +244,12 @@ impl App { .into_iter() .map(|action| ContextAction::Action(action)); + let is_switchable = GPUS.is_switchable(); let entry_prefers_non_default_gpu = entry.prefers_non_default_gpu(); let prefer_non_default_gpu = - entry_prefers_non_default_gpu && is_switchable(); + entry_prefers_non_default_gpu && is_switchable; let prefer_default_gpu = - !entry_prefers_non_default_gpu && is_switchable(); + !entry_prefers_non_default_gpu && is_switchable; let context: Vec = if prefer_non_default_gpu { vec![ContextAction::GpuPreference(GpuPreference::Default)] diff --git a/src/lib.rs b/src/lib.rs index 9798830..df47c67 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,10 +8,7 @@ pub use self::codec::*; use const_format::concatcp; use serde::{Deserialize, Serialize}; -use std::{ - borrow::Cow, - path::{Path, PathBuf}, -}; +use std::{borrow::Cow, path::Path}; pub const LOCAL: &str = "~/.local/share/pop-launcher"; pub const LOCAL_PLUGINS: &str = concatcp!(LOCAL, "/plugins"); @@ -138,11 +135,6 @@ pub enum Response { id: Indice, options: Vec, }, - // Notifies that a .desktop entry should be launched by the frontend. - DesktopEntry { - path: PathBuf, - gpu_preference: GpuPreference, - }, // The frontend should clear its search results and display a new list. Update(Vec), // An item was selected that resulted in a need to autofill the launcher. From db622816d669c8e5e0ff7fcd77e2dd1aa2461557 Mon Sep 17 00:00:00 2001 From: Paul Delafosse Date: Tue, 31 May 2022 15:20:38 +0200 Subject: [PATCH 4/4] WIP --- plugins/src/desktop_entries/graphics.rs | 3 +- plugins/src/desktop_entries/mod.rs | 181 ++++++++++++++---------- plugins/src/lib.rs | 24 ++++ plugins/src/terminal/mod.rs | 16 +-- toolkit/examples/man-pages-plugin.rs | 16 +-- 5 files changed, 133 insertions(+), 107 deletions(-) diff --git a/plugins/src/desktop_entries/graphics.rs b/plugins/src/desktop_entries/graphics.rs index 7a6d5a3..0c82103 100644 --- a/plugins/src/desktop_entries/graphics.rs +++ b/plugins/src/desktop_entries/graphics.rs @@ -58,6 +58,7 @@ pub struct Dev { } impl Dev { + /// Get the environment variable to launch a program with the correct gpu settings pub fn launch_options(&self) -> Vec<(String, String)> { let dev_num = self.id.to_string(); let mut options = vec![]; @@ -138,7 +139,7 @@ enum Driver { } impl Driver { - fn from_udev>(driver: Option) -> Driver { + fn from_udev>(driver: Option) -> Driver { match driver.as_deref() { // For amd devices we need the name of the driver to get vulkan icd files Some("radeon") => Driver::Amd("radeon".to_string()), diff --git a/plugins/src/desktop_entries/mod.rs b/plugins/src/desktop_entries/mod.rs index c2692a9..8bbf196 100644 --- a/plugins/src/desktop_entries/mod.rs +++ b/plugins/src/desktop_entries/mod.rs @@ -12,6 +12,7 @@ use pop_launcher::*; use std::borrow::Cow; use std::hash::{Hash, Hasher}; use std::process::Command; +use fork::{daemon, Fork}; use tokio::io::AsyncWrite; use tracing::error; @@ -24,6 +25,7 @@ struct Item { context: Vec, prefer_non_default_gpu: bool, exec: String, + terminal: bool, icon: Option, keywords: Option>, name: String, @@ -40,17 +42,17 @@ impl Item { fn run(&self, action_idx: Option) { match action_idx { // No action provided just run the desktop entry with the default gpu - None => run_exec_command(&self.exec, self.prefer_non_default_gpu), + None => run_exec_command(&self.exec, self.prefer_non_default_gpu, self.terminal), // Run the provided action Some(idx) => match self.context.get(idx as usize) { None => error!("Could not find context action at index {idx}"), Some(action) => match action { ContextAction::Action(action) => { - run_exec_command(&action.exec, self.prefer_non_default_gpu) + run_exec_command(&action.exec, self.prefer_non_default_gpu, self.terminal) } ContextAction::GpuPreference(pref) => match pref { - GpuPreference::Default => run_exec_command(&self.exec, false), - GpuPreference::NonDefault => run_exec_command(&self.exec, true), + GpuPreference::Default => run_exec_command(&self.exec, false, self.terminal), + GpuPreference::NonDefault => run_exec_command(&self.exec, true, self.terminal), }, }, }, @@ -58,19 +60,43 @@ impl Item { } } -fn run_exec_command(exec: &str, discrete_graphics: bool) { - let cmd = shell_words::split(exec); - let cmd: Vec = cmd.unwrap(); +fn run_exec_command(exec: &str, discrete_graphics: bool, terminal: bool) { + if terminal { + let args = get_args(exec); + let (terminal, targ) = detect_terminal(); + let mut cmd = Command::new(terminal); + let cmd = cmd.arg(targ); + let mut cmd = cmd.args(&args); + run_with_gpu(discrete_graphics, &mut cmd) - let args = cmd - .iter() - // Filter desktop entries field code. Is this needed ? - // see: https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html - .filter(|arg| !arg.starts_with('%')) - .collect::>(); - let mut cmd = Command::new(&args[0]); - let mut cmd = cmd.args(&args[1..]); + } else { + let args = get_args(exec); + let mut cmd = Command::new(&args[0]); + let mut cmd = cmd.args(&args[1..]); + run_with_gpu(discrete_graphics, &mut cmd) + }; +} + +fn get_args(exec: &str) -> Vec { + let args = shell_words::split(exec); + match args { + Err(err) => { + error!("Failed to parse args for {args:?}, aborting: {err}"); + panic!(); + } + Ok(args) => { + args + .into_iter() + // Filter desktop entries field code. + // see: https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html + .filter(|arg| !arg.starts_with('%')) + .collect::>() + } + } +} + +fn run_with_gpu(discrete_graphics: bool, cmd: &mut &mut Command) { let gpu = if discrete_graphics { GPUS.non_default() @@ -80,12 +106,12 @@ fn run_exec_command(exec: &str, discrete_graphics: bool) { if let Some(gpu) = gpu { for (opt, value) in gpu.launch_options() { - cmd = cmd.env(opt, value); + cmd.env(opt, value); } } if let Err(err) = cmd.spawn() { - error!("Failed to run desktop entry: {err}"); + error!("Failed to launch desktop entry: {err}"); } } @@ -216,72 +242,73 @@ impl App { } if let Some((name, exec)) = entry.name(locale).zip(entry.exec()) { - if let Some(exec) = exec.split_ascii_whitespace().next() { - if exec == "false" { - continue; - } + if exec == "false" { + continue; + } - let mut actions = vec![]; - - if let Some(entries) = entry.actions() { - for action in entries.split(';') { - let action = - entry.action_name(action, locale).and_then(|name| { - entry.action_exec(action).map(|exec| Action { - name: name.to_string(), - description: action.to_string(), - exec: exec.to_string(), - }) - }); - - if let Some(action) = action { - actions.push(action); - } + let mut actions = vec![]; + + if let Some(entries) = entry.actions() { + for action in entries.split(';') { + let action = + entry.action_name(action, locale).and_then(|name| { + entry.action_exec(action).map(|exec| Action { + name: name.to_string(), + description: action.to_string(), + exec: exec.to_string(), + }) + }); + + if let Some(action) = action { + actions.push(action); } } + } - let actions = actions - .into_iter() - .map(|action| ContextAction::Action(action)); - - let is_switchable = GPUS.is_switchable(); - let entry_prefers_non_default_gpu = entry.prefers_non_default_gpu(); - let prefer_non_default_gpu = - entry_prefers_non_default_gpu && is_switchable; - let prefer_default_gpu = - !entry_prefers_non_default_gpu && is_switchable; - - let context: Vec = if prefer_non_default_gpu { - vec![ContextAction::GpuPreference(GpuPreference::Default)] - } else if prefer_default_gpu { - vec![ContextAction::GpuPreference(GpuPreference::NonDefault)] - } else { - vec![] - } + let actions = actions + .into_iter() + .map(|action| ContextAction::Action(action)); + + let is_switchable = GPUS.is_switchable(); + let entry_prefers_non_default_gpu = entry.prefers_non_default_gpu(); + let prefer_non_default_gpu = + entry_prefers_non_default_gpu && is_switchable; + let prefer_default_gpu = + !entry_prefers_non_default_gpu && is_switchable; + + let context: Vec = if prefer_non_default_gpu { + vec![ContextAction::GpuPreference(GpuPreference::Default)] + } else if prefer_default_gpu { + vec![ContextAction::GpuPreference(GpuPreference::NonDefault)] + } else { + vec![] + } .into_iter() .chain(actions) .collect(); - let item = Item { - appid: entry.appid.to_owned(), - name: name.to_string(), - description: entry - .comment(locale) - .as_deref() - .unwrap_or("") - .to_owned(), - keywords: entry.keywords().map(|keywords| { - keywords.split(';').map(String::from).collect() - }), - icon: entry.icon().map(|x| x.to_owned()), - exec: exec.to_owned(), - src, - context, - prefer_non_default_gpu: entry_prefers_non_default_gpu, - }; - - deduplicator.insert(item); - } + let terminal = entry.terminal(); + + let item = Item { + appid: entry.appid.to_owned(), + name: name.to_string(), + description: entry + .comment(locale) + .as_deref() + .unwrap_or("") + .to_owned(), + keywords: entry.keywords().map(|keywords| { + keywords.split(';').map(String::from).collect() + }), + icon: entry.icon().map(|x| x.to_owned()), + exec: exec.to_owned(), + src, + context, + prefer_non_default_gpu: entry_prefers_non_default_gpu, + terminal, + }; + + deduplicator.insert(item); } } } @@ -370,8 +397,8 @@ impl App { let search_interest = search_interest.to_ascii_lowercase(); let append = search_interest.starts_with(&*query) || query - .split_ascii_whitespace() - .any(|query| search_interest.contains(&*query)) + .split_ascii_whitespace() + .any(|query| search_interest.contains(&*query)) || strsim::jaro_winkler(&*query, &*search_interest) > 0.6; if append { diff --git a/plugins/src/lib.rs b/plugins/src/lib.rs index a32d515..c5a350f 100644 --- a/plugins/src/lib.rs +++ b/plugins/src/lib.rs @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only // Copyright © 2021 System76 +extern crate core; + pub mod calc; pub mod desktop_entries; pub mod files; @@ -14,6 +16,7 @@ pub mod web; use pop_launcher::PluginResponse; use std::{borrow::Cow, ffi::OsStr, future::Future, path::Path}; +use std::path::PathBuf; use tokio::io::{AsyncWrite, AsyncWriteExt}; pub async fn send(tx: &mut W, response: PluginResponse) { @@ -49,3 +52,24 @@ pub fn mime_from_path(path: &Path) -> Cow<'static, str> { pub fn xdg_open>(file: S) { let _ = tokio::process::Command::new("xdg-open").arg(file).spawn(); } + +/// Returns the default terminal emulator linked to `/usr/bin/x-terminal-emulator` +/// or fallback to gnome terminal +pub fn detect_terminal() -> (PathBuf, &'static str) { + use std::fs::read_link; + + const SYMLINK: &str = "/usr/bin/x-terminal-emulator"; + + if let Ok(found) = read_link(SYMLINK) { + let arg = if found.to_string_lossy().contains("gnome-terminal") { + "--" + } else { + "-e" + }; + + return (read_link(&found).unwrap_or(found), arg); + } + + (PathBuf::from("/usr/bin/gnome-terminal"), "--") +} + diff --git a/plugins/src/terminal/mod.rs b/plugins/src/terminal/mod.rs index 15df6ec..8f1b65d 100644 --- a/plugins/src/terminal/mod.rs +++ b/plugins/src/terminal/mod.rs @@ -3,7 +3,7 @@ use futures::prelude::*; use pop_launcher::*; -use std::path::PathBuf; +use crate::detect_terminal; pub struct App { last_query: Option, @@ -113,16 +113,4 @@ impl App { ) .await; } -} - -fn detect_terminal() -> (PathBuf, &'static str) { - use std::fs::read_link; - - const SYMLINK: &str = "/usr/bin/x-terminal-emulator"; - - if let Ok(found) = read_link(SYMLINK) { - return (read_link(&found).unwrap_or(found), "-e"); - } - - (PathBuf::from("/usr/bin/gnome-terminal"), "--") -} +} \ No newline at end of file diff --git a/toolkit/examples/man-pages-plugin.rs b/toolkit/examples/man-pages-plugin.rs index f348cf7..f6567af 100644 --- a/toolkit/examples/man-pages-plugin.rs +++ b/toolkit/examples/man-pages-plugin.rs @@ -8,6 +8,7 @@ use std::io; use std::os::unix::process::CommandExt; use std::path::PathBuf; use std::process::{exit, Command}; +use pop_launcher_plugins::detect_terminal; // This example demonstrate how to write a pop-launcher plugin using the `PluginExt` helper trait. // We are going to build a plugin to display man pages descriptions and open them on activation. @@ -34,7 +35,6 @@ fn run_whatis(arg: &str) -> io::Result> { // Open a new terminal and run `man` with the provided man page name fn open_man_page(arg: &str) -> io::Result<()> { - // let (terminal, targ) = detect_terminal(); if let Ok(Fork::Child) = daemon(true, false) { @@ -44,20 +44,6 @@ fn open_man_page(arg: &str) -> io::Result<()> { exit(0); } -// A helper function to detect the user default terminal. -// If the terminal is not found, fallback to `gnome-termninal -fn detect_terminal() -> (PathBuf, &'static str) { - use std::fs::read_link; - - const SYMLINK: &str = "/usr/bin/x-terminal-emulator"; - - if let Ok(found) = read_link(SYMLINK) { - return (read_link(&found).unwrap_or(found), "-e"); - } - - (PathBuf::from("/usr/bin/gnome-terminal"), "--") -} - // Our plugin struct, holding the search results. #[derive(Default)] pub struct WhatIsPlugin {