Skip to content

Commit

Permalink
feat: launch desktop entries via dbus
Browse files Browse the repository at this point in the history
  • Loading branch information
oknozor committed Jun 2, 2022
1 parent 63a932a commit 19f7923
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 43 deletions.
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ memchr = "2"
thiserror = "1"
xdg = "2.4.0"
udev = "0.6.3"
zbus = "2.2.0"

[dev-dependencies]
speculoos = "0.9.0"

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


[[example]]
name = "de-list"
path = "examples/de_list.rs"
13 changes: 13 additions & 0 deletions examples/de_launch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
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(&[], false)
.expect("Failed to run desktop entry");
}
File renamed without changes.
70 changes: 70 additions & 0 deletions src/exec/dbus.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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],
prefer_non_default_gpu: bool,
) -> 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 prefer_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())));
}
}
}

if !uris.is_empty() {
app_proxy.open(uris, platform_data)?;
} else {
app_proxy.activate(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().unwrap();

dbus_names
.into_iter()
.map(OwnedBusName::into_inner)
.any(|name| name.as_str() == self.appid)
}
}
3 changes: 3 additions & 0 deletions src/exec/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@ pub enum ExecError<'a> {

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

#[error("Failed to launch aplication via dbus: {0}")]
DBusError(#[from] zbus::Error),
}
118 changes: 75 additions & 43 deletions src/exec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,29 @@ use crate::DesktopEntry;
use std::convert::TryFrom;
use std::path::PathBuf;
use std::process::Command;
use zbus::blocking::Connection;

mod dbus;
pub mod error;
mod graphics;

impl DesktopEntry<'_> {
/// Execute the given desktop entry `Exec` key with either the default gpu or
/// the alternative one if available.
pub fn launch(
&self,
filename: Option<&str>,
filenames: &[&str],
url: Option<&str>,
urls: &[&str],
prefer_non_default_gpu: bool,
) -> Result<(), ExecError> {
pub fn launch(&self, uris: &[&str], prefer_non_default_gpu: bool) -> Result<(), ExecError> {
match Connection::session() {
Ok(conn) => {
if self.is_bus_actionable(&conn) {
self.dbus_launch(&conn, uris, prefer_non_default_gpu)
} else {
self.shell_launch(uris, prefer_non_default_gpu)
}
}
Err(_) => self.shell_launch(uris, prefer_non_default_gpu),
}
}

fn shell_launch(&self, uris: &[&str], prefer_non_default_gpu: bool) -> Result<(), ExecError> {
let exec = self.exec();
if exec.is_none() {
return Err(ExecError::MissingExecKey(self.path));
Expand All @@ -42,7 +50,7 @@ impl DesktopEntry<'_> {
exec_args.push(arg);
}

let exec_args = self.get_args(filename, filenames, url, urls, exec_args);
let exec_args = self.get_args(uris, exec_args);

if exec_args.is_empty() {
return Err(ExecError::EmptyExecString);
Expand All @@ -63,8 +71,8 @@ impl DesktopEntry<'_> {
cmd
}
.args(args)
.output()?
.status
.spawn()?
.try_wait()?
} else {
let mut cmd = Command::new(shell);

Expand All @@ -74,44 +82,33 @@ impl DesktopEntry<'_> {
cmd
}
.args(&["-c", &exec_args])
.output()?
.status
.spawn()?
.try_wait()?
};

if !status.success() {
return Err(ExecError::NonZeroStatusCode {
status: status.code(),
exec: exec.to_string(),
});
if let Some(status) = status {
if !status.success() {
return Err(ExecError::NonZeroStatusCode {
status: status.code(),
exec: exec.to_string(),
});
}
}

Ok(())
}

// Replace field code with their values and ignore deprecated and unknown field codes
fn get_args(
&self,
filename: Option<&str>,
filenames: &[&str],
url: Option<&str>,
urls: &[&str],
exec_args: Vec<ArgOrFieldCode>,
) -> Vec<String> {
fn get_args(&self, uris: &[&str], exec_args: Vec<ArgOrFieldCode>) -> Vec<String> {
exec_args
.iter()
.filter_map(|arg| match arg {
ArgOrFieldCode::SingleFileName => filename.map(|filename| filename.to_string()),
ArgOrFieldCode::FileList => {
if !filenames.is_empty() {
Some(filenames.join(" "))
} else {
None
}
ArgOrFieldCode::SingleFileName | ArgOrFieldCode::SingleUrl => {
uris.get(0).map(|filename| filename.to_string())
}
ArgOrFieldCode::SingleUrl => url.map(|url| url.to_string()),
ArgOrFieldCode::UrlList => {
if !urls.is_empty() {
Some(urls.join(" "))
ArgOrFieldCode::FileList | ArgOrFieldCode::UrlList => {
if !uris.is_empty() {
Some(uris.join(" "))
} else {
None
}
Expand All @@ -129,7 +126,6 @@ impl DesktopEntry<'_> {
ArgOrFieldCode::DesktopFileLocation => {
Some(self.path.to_string_lossy().to_string())
}
// Ignore deprecated field-codes
ArgOrFieldCode::Arg(arg) => Some(arg.to_string()),
})
.collect()
Expand Down Expand Up @@ -222,7 +218,7 @@ mod test {
let path = PathBuf::from("tests/entries/unmatched-quotes.desktop");
let input = fs::read_to_string(&path).unwrap();
let de = DesktopEntry::decode(path.as_path(), &input).unwrap();
let result = de.launch(None, &[], None, &[], false);
let result = de.launch(&[], false);

assert_that!(result)
.is_err()
Expand All @@ -234,7 +230,7 @@ mod test {
let path = PathBuf::from("tests/entries/empty-exec.desktop");
let input = fs::read_to_string(&path).unwrap();
let de = DesktopEntry::decode(Path::new(path.as_path()), &input).unwrap();
let result = de.launch(None, &[], None, &[], false);
let result = de.launch(&[], false);

assert_that!(result)
.is_err()
Expand All @@ -247,7 +243,7 @@ mod test {
let path = PathBuf::from("tests/entries/alacritty-simple.desktop");
let input = fs::read_to_string(&path).unwrap();
let de = DesktopEntry::decode(path.as_path(), &input).unwrap();
let result = de.launch(None, &[], None, &[], false);
let result = de.launch(&[], false);

assert_that!(result).is_ok();
}
Expand All @@ -258,7 +254,7 @@ mod test {
let path = PathBuf::from("tests/entries/non-terminal-cmd.desktop");
let input = fs::read_to_string(&path).unwrap();
let de = DesktopEntry::decode(path.as_path(), &input).unwrap();
let result = de.launch(None, &[], None, &[], false);
let result = de.launch(&[], false);

assert_that!(result).is_ok();
}
Expand All @@ -269,7 +265,43 @@ mod test {
let path = PathBuf::from("tests/entries/non-terminal-cmd.desktop");
let input = fs::read_to_string(&path).unwrap();
let de = DesktopEntry::decode(path.as_path(), &input).unwrap();
let result = de.launch(None, &[], None, &[], false);
let result = de.launch(&[], false);

assert_that!(result).is_ok();
}

#[test]
#[ignore = "Needs a desktop environment with nvim installed, run locally only"]
fn should_launch_with_field_codes() {
let path = PathBuf::from("/usr/share/applications/nvim.desktop");
let input = fs::read_to_string(&path).unwrap();
let de = DesktopEntry::decode(path.as_path(), &input).unwrap();
let result = de.launch(&["src/lib.rs"], false);

assert_that!(result).is_ok();
}

#[test]
#[ignore = "Needs a desktop environment with gnome Books installed, run locally only"]
fn should_launch_with_dbus() {
let path = PathBuf::from("/usr/share/applications/org.gnome.Books.desktop");
let input = fs::read_to_string(&path).unwrap();
let de = DesktopEntry::decode(path.as_path(), &input).unwrap();
let result = de.launch(&[], false);

assert_that!(result).is_ok();
}

#[test]
#[ignore = "Needs a desktop environment with Nautilus installed, run locally only"]
fn should_launch_with_dbus_and_field_codes() {
let path = PathBuf::from("/usr/share/applications/org.gnome.Nautilus.desktop");
let input = fs::read_to_string(&path).unwrap();
let de = DesktopEntry::decode(path.as_path(), &input).unwrap();
let path = std::env::current_dir().unwrap();
let path = path.to_string_lossy();
let path = format!("file:///{path}");
let result = de.launch(&[path.as_str()], false);

assert_that!(result).is_ok();
}
Expand Down

0 comments on commit 19f7923

Please sign in to comment.