Skip to content

Commit 19f7923

Browse files
committed
feat: launch desktop entries via dbus
1 parent 63a932a commit 19f7923

File tree

6 files changed

+171
-43
lines changed

6 files changed

+171
-43
lines changed

Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ memchr = "2"
1818
thiserror = "1"
1919
xdg = "2.4.0"
2020
udev = "0.6.3"
21+
zbus = "2.2.0"
2122

2223
[dev-dependencies]
2324
speculoos = "0.9.0"
25+
26+
[[example]]
27+
name = "de-launch"
28+
path = "examples/de_launch.rs"
29+
30+
31+
[[example]]
32+
name = "de-list"
33+
path = "examples/de_list.rs"

examples/de_launch.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use freedesktop_desktop_entry::DesktopEntry;
2+
use std::path::PathBuf;
3+
use std::{env, fs};
4+
5+
fn main() {
6+
let args: Vec<String> = env::args().collect();
7+
let path = &args.get(1).expect("Not enough arguments");
8+
let path = PathBuf::from(path);
9+
let input = fs::read_to_string(&path).expect("Failed to read file");
10+
let de = DesktopEntry::decode(path.as_path(), &input).expect("Error decoding desktop entry");
11+
de.launch(&[], false)
12+
.expect("Failed to run desktop entry");
13+
}
File renamed without changes.

src/exec/dbus.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use crate::exec::error::ExecError;
2+
use crate::exec::graphics::Gpus;
3+
use crate::DesktopEntry;
4+
use std::collections::HashMap;
5+
use zbus::blocking::Connection;
6+
use zbus::dbus_proxy;
7+
use zbus::names::OwnedBusName;
8+
use zbus::zvariant::{OwnedValue, Str};
9+
10+
#[dbus_proxy(interface = "org.freedesktop.Application")]
11+
trait Application {
12+
fn activate(&self, platform_data: HashMap<String, OwnedValue>) -> zbus::Result<()>;
13+
fn activate_action(
14+
&self,
15+
action_name: &str,
16+
parameters: &[OwnedValue],
17+
platform_data: HashMap<String, OwnedValue>,
18+
) -> zbus::Result<()>;
19+
fn open(&self, uris: &[&str], platform_data: HashMap<String, OwnedValue>) -> zbus::Result<()>;
20+
}
21+
22+
impl DesktopEntry<'_> {
23+
pub(crate) fn dbus_launch(
24+
&self,
25+
conn: &Connection,
26+
uris: &[&str],
27+
prefer_non_default_gpu: bool,
28+
) -> Result<(), ExecError> {
29+
let dbus_path = self.appid.replace('.', "/");
30+
let dbus_path = format!("/{dbus_path}");
31+
let app_proxy = ApplicationProxyBlocking::builder(conn)
32+
.destination(self.appid)?
33+
.path(dbus_path.as_str())?
34+
.build()?;
35+
36+
let mut platform_data = HashMap::new();
37+
if prefer_non_default_gpu {
38+
let gpus = Gpus::load();
39+
if let Some(gpu) = gpus.non_default() {
40+
for (opt, value) in gpu.launch_options() {
41+
platform_data.insert(opt, OwnedValue::from(Str::from(value.as_str())));
42+
}
43+
}
44+
}
45+
46+
if !uris.is_empty() {
47+
app_proxy.open(uris, platform_data)?;
48+
} else {
49+
app_proxy.activate(platform_data)?;
50+
}
51+
52+
Ok(())
53+
}
54+
55+
pub(crate) fn is_bus_actionable(&self, conn: &Connection) -> bool {
56+
let dbus_proxy = zbus::blocking::fdo::DBusProxy::new(conn);
57+
58+
if dbus_proxy.is_err() {
59+
return false;
60+
}
61+
62+
let dbus_proxy = dbus_proxy.unwrap();
63+
let dbus_names = dbus_proxy.list_activatable_names().unwrap();
64+
65+
dbus_names
66+
.into_iter()
67+
.map(OwnedBusName::into_inner)
68+
.any(|name| name.as_str() == self.appid)
69+
}
70+
}

src/exec/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,7 @@ pub enum ExecError<'a> {
2828

2929
#[error("Exec key not found in desktop entry '{0:?}'")]
3030
MissingExecKey(&'a Path),
31+
32+
#[error("Failed to launch aplication via dbus: {0}")]
33+
DBusError(#[from] zbus::Error),
3134
}

src/exec/mod.rs

Lines changed: 75 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,29 @@ use crate::DesktopEntry;
44
use std::convert::TryFrom;
55
use std::path::PathBuf;
66
use std::process::Command;
7+
use zbus::blocking::Connection;
78

9+
mod dbus;
810
pub mod error;
911
mod graphics;
1012

1113
impl DesktopEntry<'_> {
1214
/// Execute the given desktop entry `Exec` key with either the default gpu or
1315
/// the alternative one if available.
14-
pub fn launch(
15-
&self,
16-
filename: Option<&str>,
17-
filenames: &[&str],
18-
url: Option<&str>,
19-
urls: &[&str],
20-
prefer_non_default_gpu: bool,
21-
) -> Result<(), ExecError> {
16+
pub fn launch(&self, uris: &[&str], prefer_non_default_gpu: bool) -> Result<(), ExecError> {
17+
match Connection::session() {
18+
Ok(conn) => {
19+
if self.is_bus_actionable(&conn) {
20+
self.dbus_launch(&conn, uris, prefer_non_default_gpu)
21+
} else {
22+
self.shell_launch(uris, prefer_non_default_gpu)
23+
}
24+
}
25+
Err(_) => self.shell_launch(uris, prefer_non_default_gpu),
26+
}
27+
}
28+
29+
fn shell_launch(&self, uris: &[&str], prefer_non_default_gpu: bool) -> Result<(), ExecError> {
2230
let exec = self.exec();
2331
if exec.is_none() {
2432
return Err(ExecError::MissingExecKey(self.path));
@@ -42,7 +50,7 @@ impl DesktopEntry<'_> {
4250
exec_args.push(arg);
4351
}
4452

45-
let exec_args = self.get_args(filename, filenames, url, urls, exec_args);
53+
let exec_args = self.get_args(uris, exec_args);
4654

4755
if exec_args.is_empty() {
4856
return Err(ExecError::EmptyExecString);
@@ -63,8 +71,8 @@ impl DesktopEntry<'_> {
6371
cmd
6472
}
6573
.args(args)
66-
.output()?
67-
.status
74+
.spawn()?
75+
.try_wait()?
6876
} else {
6977
let mut cmd = Command::new(shell);
7078

@@ -74,44 +82,33 @@ impl DesktopEntry<'_> {
7482
cmd
7583
}
7684
.args(&["-c", &exec_args])
77-
.output()?
78-
.status
85+
.spawn()?
86+
.try_wait()?
7987
};
8088

81-
if !status.success() {
82-
return Err(ExecError::NonZeroStatusCode {
83-
status: status.code(),
84-
exec: exec.to_string(),
85-
});
89+
if let Some(status) = status {
90+
if !status.success() {
91+
return Err(ExecError::NonZeroStatusCode {
92+
status: status.code(),
93+
exec: exec.to_string(),
94+
});
95+
}
8696
}
8797

8898
Ok(())
8999
}
90100

91101
// Replace field code with their values and ignore deprecated and unknown field codes
92-
fn get_args(
93-
&self,
94-
filename: Option<&str>,
95-
filenames: &[&str],
96-
url: Option<&str>,
97-
urls: &[&str],
98-
exec_args: Vec<ArgOrFieldCode>,
99-
) -> Vec<String> {
102+
fn get_args(&self, uris: &[&str], exec_args: Vec<ArgOrFieldCode>) -> Vec<String> {
100103
exec_args
101104
.iter()
102105
.filter_map(|arg| match arg {
103-
ArgOrFieldCode::SingleFileName => filename.map(|filename| filename.to_string()),
104-
ArgOrFieldCode::FileList => {
105-
if !filenames.is_empty() {
106-
Some(filenames.join(" "))
107-
} else {
108-
None
109-
}
106+
ArgOrFieldCode::SingleFileName | ArgOrFieldCode::SingleUrl => {
107+
uris.get(0).map(|filename| filename.to_string())
110108
}
111-
ArgOrFieldCode::SingleUrl => url.map(|url| url.to_string()),
112-
ArgOrFieldCode::UrlList => {
113-
if !urls.is_empty() {
114-
Some(urls.join(" "))
109+
ArgOrFieldCode::FileList | ArgOrFieldCode::UrlList => {
110+
if !uris.is_empty() {
111+
Some(uris.join(" "))
115112
} else {
116113
None
117114
}
@@ -129,7 +126,6 @@ impl DesktopEntry<'_> {
129126
ArgOrFieldCode::DesktopFileLocation => {
130127
Some(self.path.to_string_lossy().to_string())
131128
}
132-
// Ignore deprecated field-codes
133129
ArgOrFieldCode::Arg(arg) => Some(arg.to_string()),
134130
})
135131
.collect()
@@ -222,7 +218,7 @@ mod test {
222218
let path = PathBuf::from("tests/entries/unmatched-quotes.desktop");
223219
let input = fs::read_to_string(&path).unwrap();
224220
let de = DesktopEntry::decode(path.as_path(), &input).unwrap();
225-
let result = de.launch(None, &[], None, &[], false);
221+
let result = de.launch(&[], false);
226222

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

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

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

263259
assert_that!(result).is_ok();
264260
}
@@ -269,7 +265,43 @@ mod test {
269265
let path = PathBuf::from("tests/entries/non-terminal-cmd.desktop");
270266
let input = fs::read_to_string(&path).unwrap();
271267
let de = DesktopEntry::decode(path.as_path(), &input).unwrap();
272-
let result = de.launch(None, &[], None, &[], false);
268+
let result = de.launch(&[], false);
269+
270+
assert_that!(result).is_ok();
271+
}
272+
273+
#[test]
274+
#[ignore = "Needs a desktop environment with nvim installed, run locally only"]
275+
fn should_launch_with_field_codes() {
276+
let path = PathBuf::from("/usr/share/applications/nvim.desktop");
277+
let input = fs::read_to_string(&path).unwrap();
278+
let de = DesktopEntry::decode(path.as_path(), &input).unwrap();
279+
let result = de.launch(&["src/lib.rs"], false);
280+
281+
assert_that!(result).is_ok();
282+
}
283+
284+
#[test]
285+
#[ignore = "Needs a desktop environment with gnome Books installed, run locally only"]
286+
fn should_launch_with_dbus() {
287+
let path = PathBuf::from("/usr/share/applications/org.gnome.Books.desktop");
288+
let input = fs::read_to_string(&path).unwrap();
289+
let de = DesktopEntry::decode(path.as_path(), &input).unwrap();
290+
let result = de.launch(&[], false);
291+
292+
assert_that!(result).is_ok();
293+
}
294+
295+
#[test]
296+
#[ignore = "Needs a desktop environment with Nautilus installed, run locally only"]
297+
fn should_launch_with_dbus_and_field_codes() {
298+
let path = PathBuf::from("/usr/share/applications/org.gnome.Nautilus.desktop");
299+
let input = fs::read_to_string(&path).unwrap();
300+
let de = DesktopEntry::decode(path.as_path(), &input).unwrap();
301+
let path = std::env::current_dir().unwrap();
302+
let path = path.to_string_lossy();
303+
let path = format!("file:///{path}");
304+
let result = de.launch(&[path.as_str()], false);
273305

274306
assert_that!(result).is_ok();
275307
}

0 commit comments

Comments
 (0)