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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,18 @@ gettext-rs = { version = "0.7", features = ["gettext-system"]}
memchr = "2"
thiserror = "1"
xdg = "2.4.0"
udev = "0.6.3"
zbus = "2.2.0"
fork = "0.1.19"

[dev-dependencies]
speculoos = "0.9.0"

[[example]]
name = "de-launch"
path = "examples/de_launch.rs"


[[example]]
name = "de-list"
path = "examples/de_list.rs"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Freedesktop Desktop Entry Specification

This crate provides a library for efficiently parsing [Desktop Entry](https://specifications.freedesktop.org/desktop-entry-spec/latest/index.html) files.
This crate provides a library for efficiently parsing and launching [Desktop Entry](https://specifications.freedesktop.org/desktop-entry-spec/latest/index.html) files.

```rust
use std::fs;
Expand Down
12 changes: 12 additions & 0 deletions examples/de_launch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use freedesktop_desktop_entry::DesktopEntry;
use std::path::PathBuf;
use std::{env, fs};

fn main() {
let args: Vec<String> = env::args().collect();
let path = &args.get(1).expect("Not enough arguments");
let path = PathBuf::from(path);
let input = fs::read_to_string(&path).expect("Failed to read file");
let de = DesktopEntry::decode(path.as_path(), &input).expect("Error decoding desktop entry");
de.launch(&[]).expect("Failed to run desktop entry");
}
File renamed without changes.
90 changes: 90 additions & 0 deletions src/exec/dbus.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright 2021 System76 <[email protected]>
// SPDX-License-Identifier: MPL-2.0

use crate::exec::error::ExecError;
use crate::exec::graphics::Gpus;
use crate::DesktopEntry;
use std::collections::HashMap;
use zbus::blocking::Connection;
use zbus::dbus_proxy;
use zbus::names::OwnedBusName;
use zbus::zvariant::{OwnedValue, Str};

#[dbus_proxy(interface = "org.freedesktop.Application")]
trait Application {
fn activate(&self, platform_data: HashMap<String, OwnedValue>) -> zbus::Result<()>;
fn activate_action(
&self,
action_name: &str,
parameters: &[OwnedValue],
platform_data: HashMap<String, OwnedValue>,
) -> zbus::Result<()>;
fn open(&self, uris: &[&str], platform_data: HashMap<String, OwnedValue>) -> zbus::Result<()>;
}

impl DesktopEntry<'_> {
pub(crate) fn dbus_launch(
&self,
conn: &Connection,
uris: &[&str],
action: Option<String>,
) -> Result<(), ExecError> {
let dbus_path = self.appid.replace('.', "/");
let dbus_path = format!("/{dbus_path}");
let app_proxy = ApplicationProxyBlocking::builder(conn)
.destination(self.appid)?
.path(dbus_path.as_str())?
.build()?;

let mut platform_data = HashMap::new();
if self.prefers_non_default_gpu() {
let gpus = Gpus::load();
if let Some(gpu) = gpus.non_default() {
for (opt, value) in gpu.launch_options() {
platform_data.insert(opt, OwnedValue::from(Str::from(value.as_str())));
}
}
}

match action {
None => {
if !uris.is_empty() {
app_proxy.open(uris, platform_data)?;
} else {
app_proxy.activate(platform_data)?;
}
}
Some(action) => {
let parameters: Vec<OwnedValue> = uris
.iter()
.map(|uri| OwnedValue::from(Str::from(*uri)))
.collect();
app_proxy.activate_action(&action, parameters.as_slice(), platform_data)?
}
}

Ok(())
}

pub(crate) fn is_bus_actionable(&self, conn: &Connection) -> bool {
let dbus_proxy = zbus::blocking::fdo::DBusProxy::new(conn);

if dbus_proxy.is_err() {
return false;
}

let dbus_proxy = dbus_proxy.unwrap();
let dbus_names = dbus_proxy.list_activatable_names();

if dbus_names.is_err() {
return false;
}

let dbus_names = dbus_names.unwrap();

dbus_names
.into_iter()
.map(OwnedBusName::into_inner)
.any(|name| name.as_str() == self.appid)
}
}
46 changes: 46 additions & 0 deletions src/exec/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2021 System76 <[email protected]>
// SPDX-License-Identifier: MPL-2.0

use std::env::VarError;
use std::io;
use std::path::Path;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ExecError<'a> {
#[error("Exec string is empty")]
EmptyExecString,

#[error("$SHELL environment variable is not set")]
ShellNotFound(#[from] VarError),

#[error("Failed to run Exec command")]
IoError(#[from] io::Error),

#[error("Exec command '{exec}' exited with status code '{status:?}'")]
NonZeroStatusCode { status: Option<i32>, exec: String },

#[error("Unknown field code: '{0}'")]
UnknownFieldCode(String),

#[error("Deprecated field code: '{0}'")]
DeprecatedFieldCode(String),

#[error("Exec key not found in desktop entry '{0:?}'")]
MissingExecKey(&'a Path),

#[error("Action '{action}' not found for desktop entry '{desktop_entry:?}'")]
ActionNotFound {
action: String,
desktop_entry: &'a Path,
},

#[error("Exec key not found for action :'{action}' in desktop entry '{desktop_entry:?}'")]
ActionExecKeyNotFound {
action: String,
desktop_entry: &'a Path,
},

#[error("Failed to launch aplication via dbus: {0}")]
DBusError(#[from] zbus::Error),
}
210 changes: 210 additions & 0 deletions src/exec/graphics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// Copyright 2021 System76 <[email protected]>
// SPDX-License-Identifier: MPL-2.0

use std::collections::HashSet;
use std::hash::{Hash, Hasher};
use std::io;
use std::ops::Deref;
use std::path::{Path, PathBuf};

const VULKAN_ICD_PATH: &str = std::env!(
"VULKAN_ICD_PATH",
"must define system Vulkan ICD path (ex: `/usr/share/vulkan/icd.d`)"
);

#[derive(Debug, Default)]
pub struct Gpus {
devices: Vec<Dev>,
default: Option<Dev>,
}

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 {
/// 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![];

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<Vec<String>> {
let vulkan_icd_paths = dirs::data_dir()
.expect("local data dir does not exists")
.join("vulkan/icd.d");

let vulkan_icd_paths = &[Path::new(VULKAN_ICD_PATH), vulkan_icd_paths.as_path()];

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())
}
}
}
}
}
}

Ok(icd_paths)
}
}

// Ensure we filter out "render" devices having the same parent as the card
impl Hash for Dev {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write(self.parent_path.to_string_lossy().as_bytes());
state.finish();
}
}

impl PartialEq<Dev> for Dev {
fn eq(&self, other: &Self) -> bool {
self.parent_path == other.parent_path
}
}

impl Eq for Dev {}

#[derive(Debug)]
enum Driver {
Intel,
Amd(String),
Nvidia,
Unknown,
}

impl Driver {
fn from_udev<S: Deref<Target = str>>(driver: Option<S>) -> 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<HashSet<Dev>> {
let mut enumerator = udev::Enumerator::new()?;
let mut dev_map = HashSet::new();
let mut drivers: Vec<Dev> = 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)
}
Loading