free_launch/
desktop_item.rs1use egui::Image;
2use freedesktop_desktop_entry::DesktopEntry;
3use icon::{IconFile, Icons};
4use std::fs::OpenOptions;
5use std::io::Write;
6use std::path::{Path, PathBuf};
7use std::process::Command;
8use std::sync::LazyLock;
9
10use crate::free_launch;
11use crate::launch_entry::{LaunchAction, LaunchId, Launchable};
12
13static ICONS: LazyLock<Icons> = LazyLock::new(|| Icons::new());
15const ICON_SIZE: u32 = 64;
16const ICON_SCALE: u32 = 1;
17const ICON_THEME: &str = "Adwaita";
18
19#[derive(Debug, Clone)]
20pub struct DesktopItem {
21 pub name: String,
22 pub exec: String,
23 pub icon_file: Option<IconFile>,
24 pub comment: Option<String>,
25 pub selected: bool,
26 pub desktop_file_path: PathBuf,
27}
28
29impl DesktopItem {
30 pub(crate) fn from_desktop_file(path: &Path) -> Option<Self> {
31 let desktop_entry = DesktopEntry::from_path(path, Some(&free_launch::LOCALES)).ok()?;
32
33 let name = desktop_entry.name(&free_launch::LOCALES)?.to_string();
34 let exec = desktop_entry.exec()?.to_string();
35 let icon = desktop_entry
36 .icon()
37 .map(|s| ICONS.find_icon(s, ICON_SIZE, ICON_SCALE, ICON_THEME))
38 .flatten();
39 let comment = desktop_entry
40 .comment(&free_launch::LOCALES)
41 .map(|s| s.to_string());
42
43 Some(DesktopItem {
44 name,
45 exec,
46 icon_file: icon,
47 comment,
48 selected: false,
49 desktop_file_path: path.to_path_buf(),
50 })
51 }
52
53 pub(crate) fn name(&self) -> &str {
54 &self.name
55 }
56
57 fn log_directory_open_error(&self, directory: &Path, error: &std::io::Error) {
58 let log_path = "/var/log/free-launch.log";
59 let timestamp = std::time::SystemTime::now()
60 .duration_since(std::time::UNIX_EPOCH)
61 .unwrap_or_default()
62 .as_secs();
63
64 let log_entry = format!(
65 "[{}] ERROR: Failed to open directory '{}' for item '{}': {}\n",
66 timestamp,
67 directory.display(),
68 self.name,
69 error
70 );
71
72 match OpenOptions::new().create(true).append(true).open(log_path) {
74 Ok(mut file) => {
75 if let Err(write_err) = file.write_all(log_entry.as_bytes()) {
76 eprintln!("Failed to write to log file {}: {}", log_path, write_err);
77 eprintln!("{}", log_entry.trim());
78 }
79 }
80 Err(open_err) => {
81 eprintln!("Failed to open log file {}: {}", log_path, open_err);
82 eprintln!("{}", log_entry.trim());
83 }
84 }
85 }
86
87 fn log_launch_error(&self, program: &str, args: &[&str], error: &std::io::Error) {
88 let log_path = "/var/log/free-launch.log";
89 let timestamp = std::time::SystemTime::now()
90 .duration_since(std::time::UNIX_EPOCH)
91 .unwrap_or_default()
92 .as_secs();
93
94 let log_entry = format!(
95 "[{}] ERROR: Failed to launch '{}' with args {:?} from item '{}': {}\n",
96 timestamp, program, args, self.name, error
97 );
98
99 match OpenOptions::new().create(true).append(true).open(log_path) {
101 Ok(mut file) => {
102 if let Err(write_err) = file.write_all(log_entry.as_bytes()) {
103 eprintln!("Failed to write to log file {}: {}", log_path, write_err);
104 eprintln!("{}", log_entry.trim());
105 }
106 }
107 Err(open_err) => {
108 eprintln!("Failed to open log file {}: {}", log_path, open_err);
109 eprintln!("{}", log_entry.trim());
110 }
111 }
112 }
113}
114
115impl LaunchId for DesktopItem {
116 fn id(&self) -> &str {
117 let first_exec_part = self.exec.split_whitespace().next().unwrap_or("");
123
124 let id_string = format!("desktop-{}-{}", self.name, first_exec_part);
128 Box::leak(id_string.into_boxed_str())
129 }
130
131 fn file_path(&self) -> &Path {
132 &self.desktop_file_path
133 }
134
135 fn icon_file(&self) -> Option<&IconFile> {
136 self.icon_file.as_ref()
137 }
138
139 fn comment(&self) -> Option<&str> {
140 self.comment.as_deref()
141 }
142
143 fn icon(&self) -> Option<Image> {
144 match &self.icon_file {
145 Some(file) => match file {
146 IconFile { path, file_type: _ } => Some(
147 Image::from_uri(format!("file://{}", path.to_string_lossy())),
149 ),
150 },
152 None => None,
153 }
154 }
155}
156
157impl Launchable for DesktopItem {}
158
159impl LaunchAction for DesktopItem {
160 fn launch(&self) {
161 let sanitized = self
167 .exec
168 .replace("%u", "")
169 .replace("%U", "")
170 .replace("%f", "")
171 .replace("%F", "")
172 .trim()
173 .to_string();
174
175 let mut parts = sanitized.split_whitespace();
180 if let Some(program) = parts.next() {
181 let args: Vec<&str> = parts.collect();
182
183 match Command::new(program).args(&args).spawn() {
186 Ok(_) => {
187 }
189 Err(e) => {
190 self.log_launch_error(program, &args, &e);
192 }
193 }
194 };
196 }
197
198 fn open_directory(&self) {
199 if let Some(parent_dir) = &self.desktop_file_path.parent() {
200 let result = Command::new("xdg-open").arg(parent_dir).spawn();
202
203 if let Err(e) = result {
204 self.log_directory_open_error(parent_dir, &e);
205 }
206 }
207 }
208}