free_launch/
desktop_item.rs1use egui::Image;
2use freedesktop_desktop_entry::DesktopEntry;
3use serde_yaml::to_string;
4use std::fs::OpenOptions;
5use std::io::Write;
6use std::path::{Path, PathBuf};
7use std::process::Command;
8use tracing::{error, info};
9
10use crate::free_launch;
11use crate::launch_entry::{LaunchAction, LaunchId, Launchable};
12
13#[derive(Debug, Clone)]
14pub struct DesktopItem {
15 pub name: String,
16 pub exec: String,
17 pub icon: Option<String>,
18 pub comment: Option<String>,
19 pub selected: bool,
20 pub desktop_file_path: PathBuf,
21}
22
23impl DesktopItem {
24 pub(crate) fn from_desktop_file(path: &Path) -> Option<Self> {
26 let desktop_entry = DesktopEntry::from_path(path, Some(&free_launch::LOCALES)).ok()?;
27
28 if desktop_entry.hidden() {
33 return None;
34 }
35
36 if desktop_entry.no_display() {
38 return None;
39 }
40
41 let name = desktop_entry.name(&free_launch::LOCALES)?.to_string();
45 let exec = desktop_entry.exec()?.to_string();
46 let icon = desktop_entry.icon().map(|i| to_string(i).ok()).flatten();
48 let comment = desktop_entry
49 .comment(&free_launch::LOCALES)
50 .map(|s| s.to_string());
51
52 Some(DesktopItem {
53 name,
54 exec,
55 icon,
56 comment,
57 selected: false,
58 desktop_file_path: path.to_path_buf(),
59 })
60 }
61
62 pub(crate) fn name(&self) -> &str {
63 &self.name
64 }
65
66 fn log_directory_open_error(&self, directory: &Path, error: &std::io::Error) {
68 let log_path = "/var/log/free-launch.log";
69 let timestamp = std::time::SystemTime::now()
70 .duration_since(std::time::UNIX_EPOCH)
71 .unwrap_or_default()
72 .as_secs();
73
74 let log_entry = format!(
75 "[{}] ERROR: Failed to open directory '{}' for item '{}': {}\n",
76 timestamp,
77 directory.display(),
78 self.name,
79 error
80 );
81
82 match OpenOptions::new().create(true).append(true).open(log_path) {
84 Ok(mut file) => {
85 if let Err(write_err) = file.write_all(log_entry.as_bytes()) {
86 error!(
87 "Failed to write to log file {}: {}: {}",
88 log_path,
89 write_err,
90 log_entry.trim()
91 );
92 }
93 }
94 Err(open_err) => {
95 error!(
96 "Failed to open log file {}: {}: {}",
97 log_path,
98 open_err,
99 log_entry.trim()
100 );
101 }
102 }
103 }
104
105 fn log_launch_error(&self, program: &str, args: &[&str], error: &std::io::Error) {
107 let log_path = "/var/log/free-launch.log";
108 let timestamp = std::time::SystemTime::now()
109 .duration_since(std::time::UNIX_EPOCH)
110 .unwrap_or_default()
111 .as_secs();
112
113 let log_entry = format!(
114 "[{}] ERROR: Failed to launch '{}' with args {:?} from item '{}': {}\n",
115 timestamp, program, args, self.name, error
116 );
117
118 match OpenOptions::new().create(true).append(true).open(log_path) {
120 Ok(mut file) => {
121 if let Err(write_err) = file.write_all(log_entry.as_bytes()) {
122 error!(
123 "Failed to write to log file {}: {}: {}",
124 log_path,
125 write_err,
126 log_entry.trim()
127 );
128 }
129 }
130 Err(open_err) => {
131 error!(
132 "Failed to open log file {}: {}: {}",
133 log_path,
134 open_err,
135 log_entry.trim()
136 );
137 }
138 }
139 }
140
141 fn exec_name(&self) -> Option<&str> {
142 self.exec
143 .split_whitespace()
144 .next()
145 .and_then(|e| e.rsplit('/').next())
146 }
147
148 fn path_name(&self) -> Option<&str> {
149 self.desktop_file_path
150 .iter()
151 .last()
152 .and_then(|f| f.to_str())
153 }
154}
155
156impl LaunchId for DesktopItem {
157 fn id(&self) -> String {
159 let exec_name = self.path_name().unwrap_or("NONE");
161
162 format!("desktop-{}-{}", self.name, exec_name)
163 }
164
165 fn file_path(&self) -> &Path {
166 &self.desktop_file_path
167 }
168
169 fn icon_name(&self) -> Option<&str> {
170 self.icon.as_deref()
171 }
172
173 fn comment(&self) -> Option<&str> {
174 self.comment.as_deref()
175 }
176
177 fn debug_ui(&self, ui: &mut egui::Ui, count: usize) {
178 ui.label(format!(" [{}] ID: {}", count, self.id()));
179 ui.label(format!(" Name: {}", self.name));
180 ui.label(format!(" Exec: {}", self.exec));
181 ui.label(format!(" Path: {}", self.file_path().display()));
182 if let Some(comment) = self.comment() {
183 ui.label(format!(" Comment: {}", comment));
184 }
185 ui.label(format!(" Icon: {:?}", self.icon_name()));
186 }
187}
188
189impl Launchable for DesktopItem {}
190
191impl LaunchAction for DesktopItem {
192 fn launch(&self) {
193 let sanitized = self
199 .exec
200 .replace("%u", "")
201 .replace("%U", "")
202 .replace("%f", "")
203 .replace("%F", "")
204 .trim()
205 .to_string();
206
207 info!("Launching item: {}", sanitized);
211 let mut parts = sanitized.split_whitespace();
213 if let Some(program) = parts.next() {
214 let args: Vec<&str> = parts.collect();
215
216 match Command::new(program).args(&args).spawn() {
219 Ok(_) => {
220 }
222 Err(e) => {
223 self.log_launch_error(program, &args, &e);
225 }
226 }
227 };
229 }
230
231 fn open_directory(&self) {
232 if let Some(parent_dir) = &self.desktop_file_path.parent() {
233 let result = Command::new("xdg-open").arg(parent_dir).spawn();
235
236 if let Err(e) = result {
237 self.log_directory_open_error(parent_dir, &e);
238 }
239 }
240 }
241}