use freedesktop_desktop_entry::DesktopEntry;
use serde::ser::StdError;
use serde_yaml::to_string;
use std::collections::HashMap;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::LazyLock;
use std::sync::mpsc::Sender;
use tracing::{debug, error, info};
use crate::free_launch::free_launch::{self, LOCALES};
use crate::launch_entries::action_name::ActionName;
use crate::launch_entries::action_result::ActionResult;
use crate::launch_entries::launch_entry_trait::{LaunchAction, LaunchId, Launchable};
use crate::signaling::item_update::ItemUpdate;
use crate::signaling::request::Request;
use crate::util::reveal;
#[derive(Debug, Clone)]
pub struct DesktopItem {
pub name: String,
pub exec: String,
pub icon: Option<String>,
pub comment: Option<String>,
pub selected: bool,
pub desktop_file_path: PathBuf,
}
impl DesktopItem {
pub(crate) fn from_desktop_file(path: &Path) -> Option<Self> {
let desktop_entry = DesktopEntry::from_path(path, Some(&LOCALES)).ok()?;
if desktop_entry.hidden() {
return None;
}
if desktop_entry.no_display() {
return None;
}
let name = desktop_entry.name(&LOCALES)?.to_string();
let exec = desktop_entry.exec()?.to_string();
let icon = desktop_entry.icon().map(|i| to_string(i).ok()).flatten();
let comment = desktop_entry
.comment(&free_launch::LOCALES)
.map(|s| s.to_string());
Some(DesktopItem {
name,
exec,
icon,
comment,
selected: false,
desktop_file_path: path.to_path_buf(),
})
}
pub(crate) fn name(&self) -> &str {
&self.name
}
fn log_directory_open_error(&self, directory: &Path, error: &Box<dyn StdError>) {
let log_path = "/var/log/free-launch.log";
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let log_entry = format!(
"[{}] ERROR: Failed to open directory '{}' for item '{}': {}\n",
timestamp,
directory.display(),
self.name,
error
);
match OpenOptions::new().create(true).append(true).open(log_path) {
Ok(mut file) => {
if let Err(write_err) = file.write_all(log_entry.as_bytes()) {
error!(
"Failed to write to log file {}: {}: {}",
log_path,
write_err,
log_entry.trim()
);
}
}
Err(open_err) => {
error!(
"Failed to open log file {}: {}: {}",
log_path,
open_err,
log_entry.trim()
);
}
}
}
fn log_launch_error(&self, program: &str, args: &[&str], error: &std::io::Error) {
let log_path = "/var/log/free-launch.log";
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let log_entry = format!(
"[{}] ERROR: Failed to launch '{}' with args {:?} from item '{}': {}\n",
timestamp, program, args, self.name, error
);
match OpenOptions::new().create(true).append(true).open(log_path) {
Ok(mut file) => {
if let Err(write_err) = file.write_all(log_entry.as_bytes()) {
error!(
"Failed to write to log file {}: {}: {}",
log_path,
write_err,
log_entry.trim()
);
}
}
Err(open_err) => {
error!(
"Failed to open log file {}: {}: {}",
log_path,
open_err,
log_entry.trim()
);
}
}
}
fn exec_name(&self) -> Option<&str> {
self.exec
.split_whitespace()
.next()
.and_then(|e| e.rsplit('/').next())
}
fn path_name(&self) -> Option<&str> {
self.desktop_file_path
.iter()
.last()
.and_then(|f| f.to_str())
}
fn launch(&self) {
let sanitized = self
.exec
.replace("%u", "")
.replace("%U", "")
.replace("%f", "")
.replace("%F", "")
.trim()
.to_string();
info!("Launching item: {}", sanitized);
let mut parts = sanitized.split_whitespace();
if let Some(program) = parts.next() {
let args: Vec<&str> = parts.collect();
match Command::new(program).args(&args).spawn() {
Ok(_) => {
}
Err(e) => {
self.log_launch_error(program, &args, &e);
}
}
};
}
fn open_directory(&self) {
let path_buf = &self.desktop_file_path;
info!("Revealing item: {:?}", path_buf);
if let Err(e) = reveal(path_buf) {
self.log_directory_open_error(path_buf, &e);
}
}
}
impl LaunchId for DesktopItem {
fn id(&self) -> String {
let exec_name = self.path_name().unwrap_or("NONE");
format!("desktop-{}-{}", self.name, exec_name)
}
fn launchable(&self) -> bool {
self.desktop_file_path.exists()
}
fn file_path(&self) -> &Path {
&self.desktop_file_path
}
fn icon_name(&self) -> Option<&str> {
self.icon.as_deref()
}
fn comment(&self) -> Option<&str> {
self.comment.as_deref()
}
fn debug_ui(&self, ui: &mut egui::Ui, count: usize) {
ui.label(format!(" [{}] ID: {}", count, self.id()));
ui.label(format!(" Name: {}", self.name));
ui.label(format!(" Exec: {}", self.exec));
ui.label(format!(" Path: {}", self.file_path().display()));
if let Some(comment) = self.comment() {
ui.label(format!(" Comment: {}", comment));
}
ui.label(format!(" Icon: {:?}", self.icon_name()));
}
}
impl Launchable for DesktopItem {}
static DISPATCH_TABLE: LazyLock<HashMap<&'static str, fn(&DesktopItem)>> = LazyLock::new(|| {
let mut m: HashMap<&'static str, fn(&DesktopItem)> = HashMap::new();
m.insert("launch", DesktopItem::launch);
m.insert("reveal", DesktopItem::open_directory);
m
});
impl LaunchAction for DesktopItem {
fn actions(&self) -> Vec<&'static str> {
DISPATCH_TABLE.keys().copied().collect()
}
fn default_action(&self) -> &'static str {
"launch"
}
fn secondary_action(&self) -> &'static str {
"reveal"
}
fn invoke(&self, action_name: &ActionName) -> ActionResult {
let action = match action_name {
ActionName::Default => self.default_action(),
ActionName::Action(name) => name,
};
if let Some(func) = DISPATCH_TABLE.get(action) {
func(self);
ActionResult::Success
} else {
let error_message = format!(
"Unknown action '{}' for DesktopItem '{}'",
action, self.name
);
error!(error_message);
ActionResult::Error(error_message)
}
}
fn invoke_default(&self) -> ActionResult {
self.invoke(&ActionName::Default)
}
}
pub(crate) fn load_desktop_files(request_sender: Sender<Box<Request>>) {
let desktop_entries =
freedesktop_desktop_entry::desktop_entries(&free_launch::LOCALES.map(|l| l.to_owned()));
let total_desktop_entries = desktop_entries.len();
info!("Loading {} desktop entries", total_desktop_entries);
for entry in desktop_entries {
debug!("Loading desktop entry: {}", entry);
if let Some(desktop_item) = DesktopItem::from_desktop_file(&entry.path) {
request_sender
.send(Box::new(Request::UpdateIndexItem {
item: ItemUpdate::DesktopItem(desktop_item),
}))
.expect("should be able to send item update");
}
}
drop(request_sender);
}