free_launch/
launch_item.rs

1use freedesktop_desktop_entry::DesktopEntry;
2use std::fs::OpenOptions;
3use std::io::Write;
4use std::path::Path;
5use std::process::Command;
6
7use crate::free_launch;
8
9#[derive(Debug, Clone)]
10pub struct LaunchItem {
11    pub name: String,
12    pub exec: String,
13    pub icon: Option<String>,
14    pub comment: Option<String>,
15    pub selected: bool,
16    pub desktop_file_path: Option<String>,
17}
18
19impl LaunchItem {
20    pub(crate) fn from_desktop_file(path: &Path) -> Option<Self> {
21        let desktop_entry = DesktopEntry::from_path(path, Some(&free_launch::LOCALES)).ok()?;
22
23        let name = desktop_entry.name(&free_launch::LOCALES)?.to_string();
24        let exec = desktop_entry.exec()?.to_string();
25        let icon = desktop_entry.icon().map(|s| s.to_string());
26        let comment = desktop_entry
27            .comment(&free_launch::LOCALES)
28            .map(|s| s.to_string());
29
30        Some(LaunchItem {
31            name,
32            exec,
33            icon,
34            comment,
35            selected: false,
36            desktop_file_path: Some(path.to_string_lossy().to_string()),
37        })
38    }
39
40    pub(crate) fn launch(&self) {
41        // check for field codes: https://specifications.freedesktop.org/desktop-entry-spec/latest/exec-variables.html
42
43        // NOTE: A real implementation would need to handle desktop file field codes.
44        // For a simple case, we will just remove any common field codes.
45        // TODO properly handle field codes
46        let sanitized = self
47            .exec
48            .replace("%u", "")
49            .replace("%U", "")
50            .replace("%f", "")
51            .replace("%F", "")
52            .trim()
53            .to_string();
54
55        // Split the command string into the program and its arguments.
56        // This simple splitting won't work if there are quoted spaces or escaped characters.
57        // Consider using a shell-like parser (for example, the [`shell-words`](https://crates.io/crates/shell-words) crate) if needed.
58        // TODO use proper shell splitting
59        let mut parts = sanitized.split_whitespace();
60        if let Some(program) = parts.next() {
61            let args: Vec<&str> = parts.collect();
62
63            // Launch the process.
64            // You might want to use .spawn() as above to run it concurrently, or .output() if you need to wait for it to finish.
65            match Command::new(program).args(&args).spawn() {
66                Ok(_) => {
67                    // Command launched successfully
68                }
69                Err(e) => {
70                    // Log the error to the standard NixOS log path
71                    self.log_launch_error(program, &args, &e);
72                }
73            }
74            // TODO ensure that we wait for the command or properly detach it if we are running in daemon mode
75        };
76    }
77
78    pub(crate) fn toggle_selected(&mut self) {
79        self.selected = !self.selected
80    }
81
82    pub(crate) fn open_directory(&self) {
83        if let Some(desktop_file_path) = &self.desktop_file_path {
84            let path = Path::new(desktop_file_path);
85            if let Some(parent_dir) = path.parent() {
86                // Try to open the directory with the default file manager
87                let result = Command::new("xdg-open").arg(parent_dir).spawn();
88
89                if let Err(e) = result {
90                    self.log_directory_open_error(parent_dir, &e);
91                }
92            }
93        }
94    }
95
96    fn log_directory_open_error(&self, directory: &Path, error: &std::io::Error) {
97        let log_path = "/var/log/free-launch.log";
98        let timestamp = std::time::SystemTime::now()
99            .duration_since(std::time::UNIX_EPOCH)
100            .unwrap_or_default()
101            .as_secs();
102
103        let log_entry = format!(
104            "[{}] ERROR: Failed to open directory '{}' for item '{}': {}\n",
105            timestamp,
106            directory.display(),
107            self.name,
108            error
109        );
110
111        // Try to write to the log file, fall back to stderr if that fails
112        match OpenOptions::new().create(true).append(true).open(log_path) {
113            Ok(mut file) => {
114                if let Err(write_err) = file.write_all(log_entry.as_bytes()) {
115                    eprintln!("Failed to write to log file {}: {}", log_path, write_err);
116                    eprintln!("{}", log_entry.trim());
117                }
118            }
119            Err(open_err) => {
120                eprintln!("Failed to open log file {}: {}", log_path, open_err);
121                eprintln!("{}", log_entry.trim());
122            }
123        }
124    }
125
126    fn log_launch_error(&self, program: &str, args: &[&str], error: &std::io::Error) {
127        let log_path = "/var/log/free-launch.log";
128        let timestamp = std::time::SystemTime::now()
129            .duration_since(std::time::UNIX_EPOCH)
130            .unwrap_or_default()
131            .as_secs();
132
133        let log_entry = format!(
134            "[{}] ERROR: Failed to launch '{}' with args {:?} from item '{}': {}\n",
135            timestamp, program, args, self.name, error
136        );
137
138        // Try to write to the log file, fall back to stderr if that fails
139        match OpenOptions::new().create(true).append(true).open(log_path) {
140            Ok(mut file) => {
141                if let Err(write_err) = file.write_all(log_entry.as_bytes()) {
142                    eprintln!("Failed to write to log file {}: {}", log_path, write_err);
143                    eprintln!("{}", log_entry.trim());
144                }
145            }
146            Err(open_err) => {
147                eprintln!("Failed to open log file {}: {}", log_path, open_err);
148                eprintln!("{}", log_entry.trim());
149            }
150        }
151    }
152}