feat(desktop_entries): Allow selecting gpu/secondary actions on COSMIC
This commit is contained in:
parent
0205007463
commit
091581c362
6 changed files with 692 additions and 229 deletions
735
Cargo.lock
generated
735
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -22,7 +22,7 @@ slab = "0.4.7"
|
||||||
strsim = "0.10.0"
|
strsim = "0.10.0"
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
urlencoding = "2.1.2"
|
urlencoding = "2.1.2"
|
||||||
zbus = "3.4.0"
|
zbus = "3.14.0"
|
||||||
zvariant = "3.7.1"
|
zvariant = "3.7.1"
|
||||||
ward = "2.1.0"
|
ward = "2.1.0"
|
||||||
url = "2.3.1"
|
url = "2.3.1"
|
||||||
|
|
@ -36,6 +36,8 @@ recently-used-xbel = "1.0.0"
|
||||||
|
|
||||||
# dependencies cosmic toplevel
|
# dependencies cosmic toplevel
|
||||||
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit" }
|
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit" }
|
||||||
|
# dependencies desktop entries
|
||||||
|
switcheroo-control = { git = "https://github.com/pop-os/dbus-settings-bindings" }
|
||||||
|
|
||||||
[dependencies.reqwest]
|
[dependencies.reqwest]
|
||||||
version = "0.11.12"
|
version = "0.11.12"
|
||||||
|
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
// Copyright 2022 System76 <info@system76.com>
|
|
||||||
// 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<bool> {
|
|
||||||
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,
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let switchable = (nvidia_graphics > 0 && (intel_graphics > 0 || amd_graphics > 0))
|
|
||||||
|| (intel_graphics > 0 && amd_graphics > 0);
|
|
||||||
|
|
||||||
Ok(switchable)
|
|
||||||
};
|
|
||||||
|
|
||||||
match main() {
|
|
||||||
Ok(value) => value,
|
|
||||||
Err(why) => {
|
|
||||||
tracing::error!("{}", why);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
// Copyright © 2021 System76
|
// Copyright © 2021 System76
|
||||||
|
|
||||||
mod graphics;
|
|
||||||
|
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use freedesktop_desktop_entry::{default_paths, DesktopEntry, Iter as DesktopIter, PathSource};
|
use freedesktop_desktop_entry::{default_paths, DesktopEntry, Iter as DesktopIter, PathSource};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
|
|
@ -23,6 +21,7 @@ struct Item {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
prefers_non_default_gpu: bool,
|
prefers_non_default_gpu: bool,
|
||||||
src: PathSource,
|
src: PathSource,
|
||||||
|
actions: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hash for Item {
|
impl Hash for Item {
|
||||||
|
|
@ -68,6 +67,7 @@ struct App<W> {
|
||||||
entries: Vec<Item>,
|
entries: Vec<Item>,
|
||||||
locale: Option<String>,
|
locale: Option<String>,
|
||||||
tx: W,
|
tx: W,
|
||||||
|
gpus: Option<Vec<switcheroo_control::Gpu>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<W: AsyncWrite + Unpin> App<W> {
|
impl<W: AsyncWrite + Unpin> App<W> {
|
||||||
|
|
@ -81,6 +81,7 @@ impl<W: AsyncWrite + Unpin> App<W> {
|
||||||
.and_then(|l| l.split('.').next())
|
.and_then(|l| l.split('.').next())
|
||||||
.map(String::from),
|
.map(String::from),
|
||||||
tx,
|
tx,
|
||||||
|
gpus: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,6 +172,18 @@ impl<W: AsyncWrite + Unpin> App<W> {
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
prefers_non_default_gpu: entry.prefers_non_default_gpu(),
|
prefers_non_default_gpu: entry.prefers_non_default_gpu(),
|
||||||
src,
|
src,
|
||||||
|
actions: entry
|
||||||
|
.actions()
|
||||||
|
.map(|actions| {
|
||||||
|
actions
|
||||||
|
.split(';')
|
||||||
|
.filter_map(|action| {
|
||||||
|
entry.action_entry_localized(action, "Name", None)
|
||||||
|
})
|
||||||
|
.map(Cow::into_owned)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
deduplicator.insert(item);
|
deduplicator.insert(item);
|
||||||
|
|
@ -180,7 +193,9 @@ impl<W: AsyncWrite + Unpin> App<W> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.entries.extend(deduplicator)
|
self.entries.extend(deduplicator);
|
||||||
|
|
||||||
|
self.gpus = try_get_gpus().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn activate(&mut self, id: u32) {
|
async fn activate(&mut self, id: u32) {
|
||||||
|
|
@ -192,6 +207,7 @@ impl<W: AsyncWrite + Unpin> App<W> {
|
||||||
} else {
|
} else {
|
||||||
GpuPreference::Default
|
GpuPreference::Default
|
||||||
},
|
},
|
||||||
|
action_name: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
send(&mut self.tx, response).await;
|
send(&mut self.tx, response).await;
|
||||||
|
|
@ -200,14 +216,33 @@ impl<W: AsyncWrite + Unpin> App<W> {
|
||||||
|
|
||||||
async fn activate_context(&mut self, id: u32, context: u32) {
|
async fn activate_context(&mut self, id: u32, context: u32) {
|
||||||
if let Some(entry) = self.entries.get(id as usize) {
|
if let Some(entry) = self.entries.get(id as usize) {
|
||||||
let response = match context {
|
let is_cosmic = matches!(current_desktop().as_deref(), Some("cosmic"));
|
||||||
0 => PluginResponse::DesktopEntry {
|
let gpu_len = self.gpus.as_ref().map(Vec::len).unwrap_or(0) as u32;
|
||||||
path: entry.path.clone(),
|
|
||||||
gpu_preference: if !entry.prefers_non_default_gpu {
|
let gpu_preference = if is_cosmic {
|
||||||
|
if context < gpu_len {
|
||||||
|
GpuPreference::SpecificIdx(context)
|
||||||
|
} else {
|
||||||
|
if entry.prefers_non_default_gpu {
|
||||||
GpuPreference::NonDefault
|
GpuPreference::NonDefault
|
||||||
} else {
|
} else {
|
||||||
GpuPreference::Default
|
GpuPreference::Default
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !entry.prefers_non_default_gpu {
|
||||||
|
GpuPreference::NonDefault
|
||||||
|
} else {
|
||||||
|
GpuPreference::Default
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = match context {
|
||||||
|
0 => PluginResponse::DesktopEntry {
|
||||||
|
path: entry.path.clone(),
|
||||||
|
gpu_preference,
|
||||||
|
action_name: (is_cosmic && context >= gpu_len)
|
||||||
|
.then(|| entry.actions[(context - gpu_len) as usize].clone()),
|
||||||
},
|
},
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
|
|
@ -218,19 +253,10 @@ impl<W: AsyncWrite + Unpin> App<W> {
|
||||||
|
|
||||||
async fn context(&mut self, id: u32) {
|
async fn context(&mut self, id: u32) {
|
||||||
if let Some(entry) = self.entries.get(id as usize) {
|
if let Some(entry) = self.entries.get(id as usize) {
|
||||||
let mut options = Vec::new();
|
let options = match current_desktop().as_deref() {
|
||||||
|
Some("cosmic") => self.cosmic_context(entry).await,
|
||||||
if graphics::is_switchable() {
|
_ => self.gnome_context(entry).await,
|
||||||
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(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if !options.is_empty() {
|
if !options.is_empty() {
|
||||||
let response = PluginResponse::Context { id, options };
|
let response = PluginResponse::Context { id, options };
|
||||||
|
|
@ -294,6 +320,54 @@ impl<W: AsyncWrite + Unpin> App<W> {
|
||||||
|
|
||||||
send(tx, PluginResponse::Finished).await;
|
send(tx, PluginResponse::Finished).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn gnome_context(&self, entry: &Item) -> Vec<ContextOption> {
|
||||||
|
if self.gpus.is_some() {
|
||||||
|
vec![ContextOption {
|
||||||
|
id: 0,
|
||||||
|
name: (if entry.prefers_non_default_gpu {
|
||||||
|
"Launch Using Integrated Graphics Card"
|
||||||
|
} else {
|
||||||
|
"Launch Using Discrete Graphics Card"
|
||||||
|
})
|
||||||
|
.to_owned(),
|
||||||
|
}]
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cosmic_context(&self, entry: &Item) -> Vec<ContextOption> {
|
||||||
|
let mut options = Vec::new();
|
||||||
|
|
||||||
|
if let Some(gpus) = self.gpus.as_ref() {
|
||||||
|
let default_idx = if entry.prefers_non_default_gpu {
|
||||||
|
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() {
|
||||||
|
options.push(ContextOption {
|
||||||
|
id: i as u32,
|
||||||
|
name: format!(
|
||||||
|
"Launch using {}{}",
|
||||||
|
gpu.name,
|
||||||
|
(i == default_idx).then_some(" (default)").unwrap_or("")
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let options_offset = self.gpus.as_ref().map(|gpus| gpus.len()).unwrap_or(0);
|
||||||
|
for (i, action) in entry.actions.iter().enumerate() {
|
||||||
|
options.push(ContextOption {
|
||||||
|
id: (i + options_offset) as u32,
|
||||||
|
name: action.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
options
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_desktop() -> Option<String> {
|
fn current_desktop() -> Option<String> {
|
||||||
|
|
@ -320,3 +394,20 @@ fn path_string(source: &PathSource) -> Cow<'static, str> {
|
||||||
PathSource::Other(other) => Cow::Owned(other.clone()),
|
PathSource::Other(other) => Cow::Owned(other.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn try_get_gpus() -> Option<Vec<switcheroo_control::Gpu>> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -216,10 +216,12 @@ impl<O: futures::Sink<Response> + Unpin> Service<O> {
|
||||||
PluginResponse::DesktopEntry {
|
PluginResponse::DesktopEntry {
|
||||||
path,
|
path,
|
||||||
gpu_preference,
|
gpu_preference,
|
||||||
|
action_name,
|
||||||
} => {
|
} => {
|
||||||
self.respond(Response::DesktopEntry {
|
self.respond(Response::DesktopEntry {
|
||||||
path,
|
path,
|
||||||
gpu_preference,
|
gpu_preference,
|
||||||
|
action_name,
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ pub struct ContextOption {
|
||||||
pub enum GpuPreference {
|
pub enum GpuPreference {
|
||||||
Default,
|
Default,
|
||||||
NonDefault,
|
NonDefault,
|
||||||
|
SpecificIdx(u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
|
@ -84,6 +85,8 @@ pub enum PluginResponse {
|
||||||
DesktopEntry {
|
DesktopEntry {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
gpu_preference: GpuPreference,
|
gpu_preference: GpuPreference,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||||
|
action_name: Option<String>,
|
||||||
},
|
},
|
||||||
/// Update the text in the launcher.
|
/// Update the text in the launcher.
|
||||||
Fill(String),
|
Fill(String),
|
||||||
|
|
@ -156,6 +159,7 @@ pub enum Response {
|
||||||
DesktopEntry {
|
DesktopEntry {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
gpu_preference: GpuPreference,
|
gpu_preference: GpuPreference,
|
||||||
|
action_name: Option<String>,
|
||||||
},
|
},
|
||||||
// The frontend should clear its search results and display a new list.
|
// The frontend should clear its search results and display a new list.
|
||||||
Update(Vec<SearchResult>),
|
Update(Vec<SearchResult>),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue