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
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-c
cosmic-mime-apps = { git = "https://github.com/pop-os/cosmic-mime-apps.git", optional = true }
dirs = "6.0.0"
env_logger = "0.11"
freedesktop_entry_parser = "1.3"
freedesktop_entry_parser = "2.0"
futures = "0.3.31"
gio = { version = "0.21", optional = true }
glib = { version = "0.21", optional = true }
Expand Down
41 changes: 20 additions & 21 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -838,29 +838,28 @@ impl App {
fn launch_desktop_entries(paths: &[impl AsRef<Path>]) {
for path in paths.iter().map(AsRef::as_ref) {
match freedesktop_entry_parser::parse_entry(path) {
Ok(entry) => match entry.section("Desktop Entry").attr("Exec") {
Some(exec) => match mime_app::exec_to_command(exec, &[] as &[&str; 0]) {
Some(commands) => {
for mut command in commands {
if let Err(err) = spawn_detached(&mut command) {
log::warn!("failed to execute {}: {}", path.display(), err);
}
}
}
None => {
log::warn!(
"failed to parse {}: invalid Desktop Entry/Exec",
path.display()
);
Ok(entry) => {
let Some(exec_attr) = entry.get("Desktop Entry", "Exec") else {
log::warn!("failed to parse {}: missing Desktop Entry", path.display());
continue;
};

let Some(exec) = exec_attr.first() else {
log::warn!("failed to parse {}: missing Exec", path.display());
continue;
};

let Some(commands) = mime_app::exec_to_command(exec, &[] as &[&str]) else {
log::warn!("failed to parse {}: invalid Exec", path.display());
continue;
};

for mut command in commands {
if let Err(err) = spawn_detached(&mut command) {
log::warn!("failed to execute {}: {}", path.display(), err);
}
},
None => {
log::warn!(
"failed to parse {}: missing Desktop Entry/Exec",
path.display()
);
}
},
}
Err(err) => {
log::warn!("failed to parse {}: {}", path.display(), err);
}
Expand Down
135 changes: 61 additions & 74 deletions src/mime_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#[cfg(feature = "desktop")]
use cosmic::desktop;
use cosmic::widget;
use freedesktop_entry_parser::Section;
pub use mime_guess::Mime;
use rustc_hash::FxHashMap;
use std::{
Expand Down Expand Up @@ -48,35 +49,29 @@ pub fn exec_to_command(
let field_code_pos = args_vec
.iter()
.position(|arg| EXEC_HANDLERS.contains(&arg.as_str()));
let args_handler = field_code_pos.and_then(|i| args_vec.get(i));
// msrv
// .inspect(|handler| log::trace!("Found paths handler: {handler} for exec: {exec}"));
let args_handler = field_code_pos
.and_then(|i| args_vec.get(i))
.inspect(|&handler| log::trace!("Found paths handler: {handler} for exec: {exec}"));
// Number of args before the field code.
// This won't be an off by one err below because take is not zero indexed.
let field_code_pos = field_code_pos.unwrap_or_default();
let mut processes = match args_handler.map(String::as_str) {
Some("%f") => {
let mut processes = Vec::with_capacity(path_opt.len());

for path in path_opt.iter().map(AsRef::as_ref) {
// TODO: %f and %F need to handle non-file URLs (see spec)
if from_file_or_dir(path).is_none() {
log::warn!("Desktop file expects a file path instead of a URL: {path:?}");
}

// Passing multiple paths to %f should open an instance per path
let mut process = process::Command::new(program);
process.args(
args_vec
.iter()
.map(AsRef::as_ref)
.take(field_code_pos)
.chain(std::iter::once(path)),
);
processes.push(process);
}
path_opt
.iter()
.map(|path| {
let path = path.as_ref();
// TODO: %f and %F need to handle non-file URLs (see spec)
if from_file_or_dir(path).is_none() {
log::warn!("Desktop file expects a file path instead of a URL: {path:?}");
}

processes
// Passing multiple paths to %f should open an instance per path
let mut process = process::Command::new(program);
process.args(args_vec.iter().take(field_code_pos)).arg(path);
process
})
.collect()
}
Some("%F") => {
// TODO: %f and %F need to handle non-file URLs (see spec)
Expand All @@ -90,39 +85,25 @@ pub fn exec_to_command(

// Launch one instance with all args
let mut process = process::Command::new(program);
process.args(
args_vec
.iter()
.map(OsStr::new)
.take(field_code_pos)
.chain(path_opt.iter().map(AsRef::as_ref)),
);
process
.args(args_vec.iter().take(field_code_pos))
.args(path_opt);

vec![process]
}
Some("%u") => path_opt
.iter()
.map(|path| {
let mut process = process::Command::new(program);
process.args(
args_vec
.iter()
.map(OsStr::new)
.take(field_code_pos)
.chain(std::iter::once(path.as_ref())),
);
process.args(args_vec.iter().take(field_code_pos)).arg(path);
process
})
.collect(),
Some("%U") => {
let mut process = process::Command::new(program);
process.args(
args_vec
.iter()
.map(OsStr::new)
.take(field_code_pos)
.chain(path_opt.iter().map(AsRef::as_ref)),
);
process
.args(args_vec.iter().take(field_code_pos))
.args(path_opt);
vec![process]
}
Some(invalid) => unreachable!("All valid variants were checked; got: {invalid}"),
Expand Down Expand Up @@ -315,38 +296,42 @@ impl MimeAppCache {
}
};

for attr in entry
.section("Added Associations")
.attrs()
.chain(entry.section("Default Applications").attrs())
let added_iter = entry.section("Added Associations").map(Section::attrs);
let default_iter = entry.section("Default Applications").map(Section::attrs);
// TODO: replace with Option::into_flat_iter when stable and within MSRV
// https://github.com/rust-lang/rust/issues/148441
for (attr_key, attrs) in added_iter
.into_iter()
.flatten()
.chain(default_iter.into_iter().flatten())
{
if let Ok(mime) = attr.name.parse::<Mime>() {
if let Some(filenames) = attr.value {
for filename in filenames.split_terminator(';') {
log::trace!("add {mime}={filename}");
let apps = self
.cache
.entry(mime.clone())
.or_insert_with(|| Vec::with_capacity(1));
if !apps.iter().any(|x| filename_eq(&x.path, filename)) {
if let Some(app) =
all_apps.iter().find(|&x| filename_eq(&x.path, filename))
{
apps.push(MimeApp::from(app));
} else {
log::info!(
"failed to add association for {mime:?}: application {filename:?} not found"
);
}
if let Ok(mime) = attr_key.key.parse::<Mime>()
&& let Some(filenames) = attrs.first()
{
let mime_str = mime.as_ref();
for filename in filenames.split_terminator(';') {
log::trace!("add {mime_str}={filename}");
let apps = self.cache.entry(mime.clone()).or_default();
if !apps.iter().any(|x| filename_eq(&x.path, filename)) {
if let Some(app) =
all_apps.iter().find(|&x| filename_eq(&x.path, filename))
{
apps.push(MimeApp::from(app));
} else {
log::info!(
"failed to add association for {mime_str}: application {filename} not found"
);
}
}
}
}
}

for attr in entry.section("Removed Associations").attrs() {
if let Ok(mime) = attr.name.parse::<Mime>() {
if let Some(filenames) = attr.value {
if let Some(removed_associations) = entry.section("Removed Associations") {
for (attr_key, attrs) in removed_associations.attrs() {
if let Ok(mime) = attr_key.key.parse::<Mime>()
&& let Some(filenames) = attrs.first()
{
for filename in filenames.split_terminator(';') {
log::trace!("remove {mime}={filename}");
if let Some(apps) = self.cache.get_mut(&mime) {
Expand All @@ -357,14 +342,16 @@ impl MimeAppCache {
}
}

for attr in entry.section("Default Applications").attrs() {
if let Ok(mime) = attr.name.parse::<Mime>() {
if let Some(filenames) = attr.value {
if let Some(default_applications) = entry.section("Default Applications") {
for (attr_key, attrs) in default_applications.attrs() {
if let Ok(mime) = attr_key.key.parse::<Mime>()
&& let Some(filenames) = attrs.first()
{
for filename in filenames.split_terminator(';') {
log::trace!("default {mime}={filename}");
if let Some(apps) = self.cache.get_mut(&mime) {
let mut found = false;
for app in apps.iter_mut() {
for app in apps {
if filename_eq(&app.path, filename) {
app.is_default = true;
found = true;
Expand All @@ -376,7 +363,7 @@ impl MimeAppCache {
break;
}
log::debug!(
"failed to set default for {mime:?}: application {filename:?} not found"
"failed to set default for {mime}: application {filename} not found"
);
}
}
Expand Down
20 changes: 13 additions & 7 deletions src/tab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -608,9 +608,10 @@ fn get_desktop_file_display_name(path: &Path) -> Option<String> {
};

entry
.section("Desktop Entry")
.section("Desktop Entry")?
.attr("Name")
.map(str::to_string)
.first()
.cloned()
}

fn get_desktop_file_icon(path: &Path) -> Option<String> {
Expand All @@ -623,9 +624,10 @@ fn get_desktop_file_icon(path: &Path) -> Option<String> {
};

entry
.section("Desktop Entry")
.section("Desktop Entry")?
.attr("Icon")
.map(str::to_string)
.first()
.cloned()
}

pub fn parse_desktop_file(path: &Path) -> (Option<String>, Option<String>) {
Expand All @@ -636,10 +638,14 @@ pub fn parse_desktop_file(path: &Path) -> (Option<String>, Option<String>) {
return (None, None);
}
};
let section = entry.section("Desktop Entry");

let Some(section) = entry.section("Desktop Entry") else {
return (None, None);
};

(
section.attr("Name").map(str::to_string),
section.attr("Icon").map(str::to_string),
section.attr("Name").first().cloned(),
section.attr("Icon").first().cloned(),
)
}

Expand Down
23 changes: 13 additions & 10 deletions src/thumbnailer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,21 @@ impl ThumbnailerCache {
}
};

let Some(section) = entry.section("Thumbnailer Entry") else {
log::warn!(
"missing Thumbnailer Entry section for thumbnailer {}",
path.display()
);
continue;
};

//TODO: use TryExec?
let section = entry.section("Thumbnailer Entry");
let Some(exec) = section.attr("Exec") else {
let Some(exec) = section.attr("Exec").first() else {
log::warn!("missing Exec attribute for thumbnailer {}", path.display());
continue;
};
let Some(mime_types) = section.attr("MimeType") else {

let Some(mime_types) = section.attr("MimeType").first() else {
log::warn!(
"missing MimeType attribute for thumbnailer {}",
path.display()
Expand All @@ -140,13 +148,8 @@ impl ThumbnailerCache {
for mime_type in mime_types.split_terminator(';') {
if let Ok(mime) = mime_type.parse::<Mime>() {
log::trace!("thumbnailer {}={}", mime, path.display());
let apps = self
.cache
.entry(mime)
.or_insert_with(|| Vec::with_capacity(1));
apps.push(Thumbnailer {
exec: exec.to_string(),
});
let apps = self.cache.entry(mime).or_default();
apps.push(Thumbnailer { exec: exec.clone() });
}
}
}
Expand Down
Loading