2024-01-25 15:02:31 +00:00
|
|
|
pub use freedesktop_desktop_entry::DesktopEntry;
|
2024-05-31 05:05:36 +02:00
|
|
|
use iced_widget::canvas::path::lyon_path::geom::euclid::approxord::min;
|
2024-03-01 11:53:26 -07:00
|
|
|
pub use mime::Mime;
|
2024-01-25 15:02:31 +00:00
|
|
|
use std::{
|
|
|
|
|
borrow::Cow,
|
2024-05-31 05:05:36 +02:00
|
|
|
cmp::max,
|
2024-01-25 15:02:31 +00:00
|
|
|
ffi::OsStr,
|
|
|
|
|
path::{Path, PathBuf},
|
|
|
|
|
};
|
2024-07-24 18:40:18 -07:00
|
|
|
use zbus::zvariant;
|
2024-01-25 15:02:31 +00:00
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub enum IconSource {
|
|
|
|
|
Name(String),
|
|
|
|
|
Path(PathBuf),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl IconSource {
|
|
|
|
|
pub fn from_unknown(icon: &str) -> Self {
|
|
|
|
|
let icon_path = Path::new(icon);
|
|
|
|
|
if icon_path.is_absolute() && icon_path.exists() {
|
|
|
|
|
Self::Path(icon_path.into())
|
|
|
|
|
} else {
|
|
|
|
|
Self::Name(icon.into())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn as_cosmic_icon(&self) -> crate::widget::icon::Icon {
|
|
|
|
|
match self {
|
|
|
|
|
Self::Name(name) => crate::widget::icon::from_name(name.as_str())
|
|
|
|
|
.size(128)
|
|
|
|
|
.fallback(Some(crate::widget::icon::IconFallback::Names(vec![
|
|
|
|
|
"application-default".into(),
|
|
|
|
|
"application-x-executable".into(),
|
|
|
|
|
])))
|
|
|
|
|
.into(),
|
|
|
|
|
Self::Path(path) => crate::widget::icon(crate::widget::icon::from_path(path.clone())),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for IconSource {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self::Name("application-default".to_string())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
|
|
|
pub struct DesktopAction {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub exec: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Default)]
|
|
|
|
|
pub struct DesktopEntryData {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub wm_class: Option<String>,
|
|
|
|
|
pub exec: Option<String>,
|
|
|
|
|
pub icon: IconSource,
|
|
|
|
|
pub path: Option<PathBuf>,
|
2024-03-04 09:07:16 -07:00
|
|
|
pub categories: Vec<String>,
|
2024-01-25 15:02:31 +00:00
|
|
|
pub desktop_actions: Vec<DesktopAction>,
|
2024-03-01 11:53:26 -07:00
|
|
|
pub mime_types: Vec<Mime>,
|
2024-01-25 15:02:31 +00:00
|
|
|
pub prefers_dgpu: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn load_applications<'a>(
|
|
|
|
|
locale: impl Into<Option<&'a str>>,
|
|
|
|
|
include_no_display: bool,
|
|
|
|
|
) -> Vec<DesktopEntryData> {
|
|
|
|
|
load_applications_filtered(locale, |de| include_no_display || !de.no_display())
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-11 23:05:45 -05:00
|
|
|
pub fn app_id_or_fallback_matches(app_id: &str, entry: &DesktopEntryData) -> bool {
|
|
|
|
|
let lowercase_wm_class = match entry.wm_class.as_ref() {
|
|
|
|
|
Some(s) => Some(s.to_lowercase()),
|
|
|
|
|
None => None,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
app_id == entry.id
|
|
|
|
|
|| Some(app_id.to_lowercase()) == lowercase_wm_class
|
|
|
|
|
|| app_id.to_lowercase() == entry.name.to_lowercase()
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-31 05:05:36 +02:00
|
|
|
/// From 0 to 1.
|
|
|
|
|
/// 1 is a perfect match.
|
|
|
|
|
fn match_entry(id: &str, de: &DesktopEntry) -> f32 {
|
|
|
|
|
let cmp = |id, de| {
|
|
|
|
|
let lcsstr = textdistance::str::lcsstr(id, de);
|
|
|
|
|
lcsstr as f32 / (max(id.len(), de.len())) as f32
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fn max_f32(a: f32, b: f32) -> f32 {
|
|
|
|
|
if a > b {
|
|
|
|
|
return a;
|
|
|
|
|
} else {
|
|
|
|
|
b
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let id = id.to_lowercase();
|
|
|
|
|
let de_id = de.appid.to_lowercase();
|
|
|
|
|
let de_wm_class = de.startup_wm_class().unwrap_or_default().to_lowercase();
|
|
|
|
|
let de_name = de.name(None).unwrap_or_default().to_lowercase();
|
|
|
|
|
|
|
|
|
|
return max_f32(
|
|
|
|
|
cmp(&id, &de_id),
|
|
|
|
|
max_f32(cmp(&id, &de_wm_class), cmp(&id, &de_name)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-25 15:02:31 +00:00
|
|
|
pub fn load_applications_for_app_ids<'a, 'b>(
|
|
|
|
|
locale: impl Into<Option<&'a str>>,
|
|
|
|
|
app_ids: impl Iterator<Item = &'b str>,
|
|
|
|
|
fill_missing_ones: bool,
|
2024-03-11 17:19:10 -04:00
|
|
|
include_no_display: bool,
|
2024-01-25 15:02:31 +00:00
|
|
|
) -> Vec<DesktopEntryData> {
|
2024-05-31 05:05:36 +02:00
|
|
|
// need to be owned
|
|
|
|
|
let all_desktop_entries_string =
|
|
|
|
|
freedesktop_desktop_entry::Iter::new(freedesktop_desktop_entry::default_paths())
|
|
|
|
|
.filter_map(|path| {
|
|
|
|
|
std::fs::read_to_string(&path)
|
|
|
|
|
.ok()
|
|
|
|
|
.map(|content| (path, content))
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
|
|
let all_desktop_entries = all_desktop_entries_string
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|(path, content)| DesktopEntry::decode(&path, &content).ok())
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
|
|
let mut applications = Vec::new();
|
|
|
|
|
let mut missing = Vec::new();
|
|
|
|
|
|
|
|
|
|
let locale = locale.into();
|
|
|
|
|
|
|
|
|
|
for id in app_ids {
|
|
|
|
|
let mut max_score = None;
|
|
|
|
|
let mut second_max_score = 0.;
|
|
|
|
|
|
|
|
|
|
for de in &all_desktop_entries {
|
|
|
|
|
if !include_no_display && de.no_display() {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let score = match_entry(id, de);
|
|
|
|
|
|
|
|
|
|
match max_score {
|
|
|
|
|
Some((prev_max_score, _)) => {
|
|
|
|
|
if prev_max_score < score {
|
|
|
|
|
second_max_score = prev_max_score;
|
|
|
|
|
max_score = Some((score, de));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
max_score = Some((score, de));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if score > 0.99 {
|
|
|
|
|
break;
|
|
|
|
|
}
|
2024-03-11 17:19:10 -04:00
|
|
|
}
|
2024-05-31 05:05:36 +02:00
|
|
|
|
|
|
|
|
let mut add_missing = false;
|
|
|
|
|
match max_score {
|
|
|
|
|
Some((max_score, de)) => {
|
|
|
|
|
let entropy = max_score - second_max_score;
|
|
|
|
|
|
|
|
|
|
if max_score > 0.7 || entropy > 0.2 && max_score > 0.2 {
|
|
|
|
|
applications.push(DesktopEntryData::from_desktop_entry(
|
|
|
|
|
locale,
|
|
|
|
|
Some(de.path.to_path_buf()),
|
|
|
|
|
de,
|
|
|
|
|
));
|
|
|
|
|
} else {
|
|
|
|
|
add_missing = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
add_missing = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if fill_missing_ones && add_missing {
|
|
|
|
|
missing.push(id);
|
2024-01-25 15:02:31 +00:00
|
|
|
}
|
2024-05-31 05:05:36 +02:00
|
|
|
}
|
|
|
|
|
|
2024-01-25 15:02:31 +00:00
|
|
|
if fill_missing_ones {
|
2024-05-31 05:05:36 +02:00
|
|
|
applications.extend(missing.into_iter().map(|app_id| DesktopEntryData {
|
2024-01-25 15:02:31 +00:00
|
|
|
id: app_id.to_string(),
|
|
|
|
|
name: app_id.to_string(),
|
|
|
|
|
icon: IconSource::default(),
|
|
|
|
|
..Default::default()
|
|
|
|
|
}));
|
|
|
|
|
}
|
2024-05-31 05:05:36 +02:00
|
|
|
|
2024-01-25 15:02:31 +00:00
|
|
|
applications
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn load_applications_filtered<'a, F: FnMut(&DesktopEntry) -> bool>(
|
|
|
|
|
locale: impl Into<Option<&'a str>>,
|
|
|
|
|
mut filter: F,
|
|
|
|
|
) -> Vec<DesktopEntryData> {
|
|
|
|
|
let locale = locale.into();
|
|
|
|
|
|
|
|
|
|
freedesktop_desktop_entry::Iter::new(freedesktop_desktop_entry::default_paths())
|
|
|
|
|
.filter_map(|path| {
|
|
|
|
|
std::fs::read_to_string(&path).ok().and_then(|input| {
|
|
|
|
|
DesktopEntry::decode(&path, &input).ok().and_then(|de| {
|
|
|
|
|
if !filter(&de) {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Some(DesktopEntryData::from_desktop_entry(
|
|
|
|
|
locale,
|
|
|
|
|
path.clone(),
|
2024-05-31 05:05:36 +02:00
|
|
|
&de,
|
2024-01-25 15:02:31 +00:00
|
|
|
))
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn load_desktop_file<'a>(
|
|
|
|
|
locale: impl Into<Option<&'a str>>,
|
|
|
|
|
path: impl AsRef<Path>,
|
|
|
|
|
) -> Option<DesktopEntryData> {
|
|
|
|
|
let path = path.as_ref();
|
|
|
|
|
std::fs::read_to_string(path).ok().and_then(|input| {
|
|
|
|
|
DesktopEntry::decode(path, &input)
|
|
|
|
|
.ok()
|
2024-05-31 05:05:36 +02:00
|
|
|
.map(|de| DesktopEntryData::from_desktop_entry(locale, PathBuf::from(path), &de))
|
2024-01-25 15:02:31 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DesktopEntryData {
|
|
|
|
|
fn from_desktop_entry<'a>(
|
|
|
|
|
locale: impl Into<Option<&'a str>>,
|
|
|
|
|
path: impl Into<Option<PathBuf>>,
|
2024-05-31 05:05:36 +02:00
|
|
|
de: &DesktopEntry,
|
2024-01-25 15:02:31 +00:00
|
|
|
) -> DesktopEntryData {
|
|
|
|
|
let locale = locale.into();
|
|
|
|
|
|
|
|
|
|
let name = de
|
|
|
|
|
.name(locale)
|
|
|
|
|
.unwrap_or(Cow::Borrowed(de.appid))
|
|
|
|
|
.to_string();
|
|
|
|
|
|
|
|
|
|
// check if absolute path exists and otherwise treat it as a name
|
|
|
|
|
let icon = de.icon().unwrap_or(de.appid);
|
|
|
|
|
let icon_path = Path::new(icon);
|
|
|
|
|
let icon = if icon_path.is_absolute() && icon_path.exists() {
|
|
|
|
|
IconSource::Path(icon_path.into())
|
|
|
|
|
} else {
|
|
|
|
|
IconSource::Name(icon.into())
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
DesktopEntryData {
|
|
|
|
|
id: de.appid.to_string(),
|
|
|
|
|
wm_class: de.startup_wm_class().map(ToString::to_string),
|
|
|
|
|
exec: de.exec().map(ToString::to_string),
|
|
|
|
|
name,
|
|
|
|
|
icon,
|
|
|
|
|
path: path.into(),
|
2024-03-04 09:07:16 -07:00
|
|
|
categories: de
|
|
|
|
|
.categories()
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
.split_terminator(';')
|
2024-03-11 10:37:30 -04:00
|
|
|
.map(std::string::ToString::to_string)
|
2024-03-04 09:07:16 -07:00
|
|
|
.collect(),
|
2024-01-25 15:02:31 +00:00
|
|
|
desktop_actions: de
|
|
|
|
|
.actions()
|
|
|
|
|
.map(|actions| {
|
|
|
|
|
actions
|
|
|
|
|
.split(';')
|
|
|
|
|
.filter_map(|action| {
|
|
|
|
|
let name = de.action_entry_localized(action, "Name", locale);
|
|
|
|
|
let exec = de.action_entry(action, "Exec");
|
|
|
|
|
if let (Some(name), Some(exec)) = (name, exec) {
|
|
|
|
|
Some(DesktopAction {
|
|
|
|
|
name: name.to_string(),
|
|
|
|
|
exec: exec.to_string(),
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_default(),
|
2024-03-01 11:53:26 -07:00
|
|
|
mime_types: de
|
|
|
|
|
.mime_type()
|
|
|
|
|
.map(|mime_types| {
|
|
|
|
|
mime_types
|
|
|
|
|
.split_terminator(';')
|
|
|
|
|
.filter_map(|mime_type| mime_type.parse::<Mime>().ok())
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_default(),
|
2024-01-25 15:02:31 +00:00
|
|
|
prefers_dgpu: de.prefers_non_default_gpu(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-24 18:40:18 -07:00
|
|
|
pub async fn spawn_desktop_exec<S, I, K, V>(exec: S, env_vars: I, app_id: Option<&str>)
|
2024-01-25 15:02:31 +00:00
|
|
|
where
|
|
|
|
|
S: AsRef<str>,
|
|
|
|
|
I: IntoIterator<Item = (K, V)>,
|
|
|
|
|
K: AsRef<OsStr>,
|
|
|
|
|
V: AsRef<OsStr>,
|
|
|
|
|
{
|
|
|
|
|
let mut exec = shlex::Shlex::new(exec.as_ref());
|
2024-07-24 18:40:18 -07:00
|
|
|
|
|
|
|
|
let executable = match exec.next() {
|
|
|
|
|
Some(executable) if !executable.contains('=') => executable,
|
2024-01-25 15:02:31 +00:00
|
|
|
_ => return,
|
|
|
|
|
};
|
|
|
|
|
|
2024-07-24 18:40:18 -07:00
|
|
|
let mut cmd = std::process::Command::new(&executable);
|
|
|
|
|
|
2024-01-25 15:02:31 +00:00
|
|
|
for arg in exec {
|
|
|
|
|
// TODO handle "%" args here if necessary?
|
|
|
|
|
if !arg.starts_with('%') {
|
|
|
|
|
cmd.arg(arg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd.envs(env_vars);
|
|
|
|
|
|
2024-07-24 18:40:18 -07:00
|
|
|
// https://systemd.io/DESKTOP_ENVIRONMENTS
|
|
|
|
|
//
|
|
|
|
|
// Similar to what Gnome sets, for now.
|
2024-07-25 09:26:54 -07:00
|
|
|
if let Some(pid) = crate::process::spawn(cmd).await {
|
2024-07-24 18:40:18 -07:00
|
|
|
if let Ok(session) = zbus::Connection::session().await {
|
|
|
|
|
if let Ok(systemd_manager) = SystemdMangerProxy::new(&session).await {
|
|
|
|
|
let _ = systemd_manager
|
|
|
|
|
.start_transient_unit(
|
|
|
|
|
&format!("app-cosmic-{}-{}.scope", app_id.unwrap_or(&executable), pid),
|
|
|
|
|
"fail",
|
|
|
|
|
&[
|
|
|
|
|
(
|
|
|
|
|
"Description".to_string(),
|
|
|
|
|
zvariant::Value::from("Application launched by COSMIC")
|
|
|
|
|
.try_to_owned()
|
|
|
|
|
.unwrap(),
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"PIDs".to_string(),
|
|
|
|
|
zvariant::Value::from(vec![pid]).try_to_owned().unwrap(),
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"CollectMode".to_string(),
|
|
|
|
|
zvariant::Value::from("inactive-or-failed")
|
|
|
|
|
.try_to_owned()
|
|
|
|
|
.unwrap(),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
&[],
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[zbus::proxy(
|
|
|
|
|
interface = "org.freedesktop.systemd1.Manager",
|
|
|
|
|
default_service = "org.freedesktop.systemd1",
|
|
|
|
|
default_path = "/org/freedesktop/systemd1"
|
|
|
|
|
)]
|
|
|
|
|
trait SystemdManger {
|
|
|
|
|
async fn start_transient_unit(
|
|
|
|
|
&self,
|
|
|
|
|
name: &str,
|
|
|
|
|
mode: &str,
|
|
|
|
|
properties: &[(String, zvariant::OwnedValue)],
|
|
|
|
|
aux: &[(String, Vec<(String, zvariant::OwnedValue)>)],
|
|
|
|
|
) -> zbus::Result<zvariant::OwnedObjectPath>;
|
2024-01-25 15:02:31 +00:00
|
|
|
}
|