Skip to main content

upstream_rs/application/operations/
init_operation.rs

1use crate::utils::static_paths::UpstreamPaths;
2#[cfg(windows)]
3use anyhow::Context;
4use anyhow::Result;
5use std::collections::BTreeSet;
6use std::fs;
7use std::io::{self, Write};
8use std::path::Path;
9
10// Unix shell source lines
11const SOURCE_LINE_BASH: &str =
12    "[ -f $HOME/.upstream/metadata/paths.sh ] && source $HOME/.upstream/metadata/paths.sh";
13const SOURCE_LINE_FISH: &str = "source $HOME/.upstream/metadata/paths.sh";
14
15pub struct InitCheckReport {
16    pub ok: bool,
17    pub messages: Vec<String>,
18}
19
20#[cfg(windows)]
21fn normalize_windows_path(path: &str) -> String {
22    let mut normalized = path.replace('/', "\\").trim().to_ascii_lowercase();
23    while normalized.ends_with('\\') {
24        normalized.pop();
25    }
26    normalized
27}
28
29pub fn initialize(paths: &UpstreamPaths) -> Result<()> {
30    create_package_dirs(paths)?;
31    create_metadata_files(paths)?;
32
33    #[cfg(windows)]
34    add_to_windows_path(paths)?;
35
36    #[cfg(unix)]
37    update_shell_profiles(paths)?;
38
39    Ok(())
40}
41
42pub fn check(paths: &UpstreamPaths) -> Result<InitCheckReport> {
43    let mut report = InitCheckReport {
44        ok: true,
45        messages: Vec::new(),
46    };
47
48    for (label, path) in [
49        ("config directory", &paths.dirs.config_dir),
50        ("data directory", &paths.dirs.data_dir),
51        ("metadata directory", &paths.dirs.metadata_dir),
52        ("symlinks directory", &paths.integration.symlinks_dir),
53        ("appimages directory", &paths.install.appimages_dir),
54        ("binaries directory", &paths.install.binaries_dir),
55        ("archives directory", &paths.install.archives_dir),
56    ] {
57        if path.exists() {
58            report
59                .messages
60                .push(format!("[OK] {} exists: {}", label, path.display()));
61        } else {
62            report.ok = false;
63            report
64                .messages
65                .push(format!("[FAIL] {} missing: {}", label, path.display()));
66        }
67    }
68
69    #[cfg(unix)]
70    check_unix_integration(paths, &mut report)?;
71
72    #[cfg(windows)]
73    check_windows_integration(paths, &mut report)?;
74
75    Ok(report)
76}
77
78#[cfg(unix)]
79fn get_installed_shells() -> io::Result<Vec<String>> {
80    const SHELLS_FILE: &str = "/etc/shells";
81    if !Path::new(SHELLS_FILE).exists() {
82        return Ok(Vec::new());
83    }
84    let content = fs::read_to_string(SHELLS_FILE)?;
85    let shells = content
86        .lines()
87        .map(|l| l.trim())
88        .filter(|l| !l.is_empty() && !l.starts_with('#'))
89        .map(|l| l.to_string())
90        .collect();
91    Ok(shells)
92}
93
94#[cfg(windows)]
95fn add_to_windows_path(paths: &UpstreamPaths) -> Result<()> {
96    use winreg::RegKey;
97    use winreg::enums::*;
98
99    let hkcu = RegKey::predef(HKEY_CURRENT_USER);
100    let env_key = hkcu
101        .open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)
102        .context("Failed to open registry key")?;
103
104    let symlinks_path = paths.integration.symlinks_dir.display().to_string();
105    let symlinks_norm = normalize_windows_path(&symlinks_path);
106
107    // Get current PATH
108    let current_path: String = env_key.get_value("Path").unwrap_or_else(|_| String::new());
109
110    // Check if our path is already in PATH
111    let path_entries: Vec<&str> = current_path.split(';').collect();
112    if path_entries
113        .iter()
114        .any(|&p| normalize_windows_path(p) == symlinks_norm)
115    {
116        return Ok(()); // Already in PATH
117    }
118
119    // Add our path to the beginning
120    let new_path = if current_path.is_empty() {
121        symlinks_path
122    } else {
123        format!("{};{}", symlinks_path, current_path)
124    };
125
126    env_key
127        .set_value("Path", &new_path)
128        .context("Failed to set PATH")?;
129
130    // Broadcast WM_SETTINGCHANGE to notify other applications
131    broadcast_environment_change();
132
133    Ok(())
134}
135
136#[cfg(windows)]
137fn broadcast_environment_change() {
138    use std::ptr;
139    use winapi::shared::minwindef::LPARAM;
140    use winapi::um::winuser::{
141        HWND_BROADCAST, SMTO_ABORTIFHUNG, SendMessageTimeoutW, WM_SETTINGCHANGE,
142    };
143
144    unsafe {
145        let env_string: Vec<u16> = "Environment\0".encode_utf16().collect();
146        SendMessageTimeoutW(
147            HWND_BROADCAST,
148            WM_SETTINGCHANGE,
149            0,
150            env_string.as_ptr() as LPARAM,
151            SMTO_ABORTIFHUNG,
152            5000,
153            ptr::null_mut(),
154        );
155    }
156}
157
158fn create_package_dirs(paths: &UpstreamPaths) -> io::Result<()> {
159    fs::create_dir_all(&paths.dirs.config_dir)?;
160    fs::create_dir_all(&paths.dirs.data_dir)?;
161    fs::create_dir_all(&paths.dirs.metadata_dir)?;
162    fs::create_dir_all(&paths.install.appimages_dir)?;
163    fs::create_dir_all(&paths.install.binaries_dir)?;
164    fs::create_dir_all(&paths.install.archives_dir)?;
165    fs::create_dir_all(&paths.integration.icons_dir)?;
166    fs::create_dir_all(&paths.integration.symlinks_dir)?;
167    Ok(())
168}
169
170#[cfg(unix)]
171fn create_metadata_files(paths: &UpstreamPaths) -> io::Result<()> {
172    if !paths.config.paths_file.exists() {
173        let export_line = format!(
174            r#"export PATH="{}:$PATH""#,
175            paths.integration.symlinks_dir.display()
176        );
177        fs::write(
178            &paths.config.paths_file,
179            format!(
180                "#!/bin/bash\n# Upstream managed PATH additions\n{}\n",
181                export_line
182            ),
183        )?;
184    }
185    Ok(())
186}
187
188#[cfg(windows)]
189fn create_metadata_files(_paths: &UpstreamPaths) -> io::Result<()> {
190    // On Windows, we use registry-based PATH, so no metadata files needed
191    Ok(())
192}
193
194#[cfg(unix)]
195fn update_shell_profiles(paths: &UpstreamPaths) -> io::Result<()> {
196    let shells = get_installed_shells()?;
197    for shell_path in shells {
198        let shell_name = Path::new(&shell_path)
199            .file_name()
200            .and_then(|s| s.to_str())
201            .unwrap_or("");
202        match shell_name.to_lowercase().as_str() {
203            "bash" | "sh" => {
204                add_line_to_profile(paths, ".bashrc", SOURCE_LINE_BASH)?;
205            }
206            "zsh" => {
207                add_line_to_profile(paths, ".zshrc", SOURCE_LINE_BASH)?;
208            }
209            "fish" => {
210                let fish_config = Path::new(".config").join("fish").join("config.fish");
211                add_line_to_profile(paths, &fish_config.to_string_lossy(), SOURCE_LINE_FISH)?;
212            }
213            _ => {}
214        }
215    }
216    Ok(())
217}
218
219#[cfg(unix)]
220fn check_unix_integration(paths: &UpstreamPaths, report: &mut InitCheckReport) -> io::Result<()> {
221    let expected_line = format!(
222        r#"export PATH="{}:$PATH""#,
223        paths.integration.symlinks_dir.display()
224    );
225
226    if !paths.config.paths_file.exists() {
227        report.ok = false;
228        report.messages.push(format!(
229            "[FAIL] PATH metadata file missing: {}",
230            paths.config.paths_file.display()
231        ));
232    } else {
233        let content = fs::read_to_string(&paths.config.paths_file)?;
234        if content.contains(&expected_line) {
235            report.messages.push(format!(
236                "[OK] PATH metadata file contains symlink export: {}",
237                paths.config.paths_file.display()
238            ));
239        } else {
240            report.ok = false;
241            report.messages.push(format!(
242                "[FAIL] PATH metadata file missing expected export line: {}",
243                paths.config.paths_file.display()
244            ));
245        }
246    }
247
248    let mut profiles_to_check: BTreeSet<(String, String)> = BTreeSet::new();
249    for shell_path in get_installed_shells()? {
250        let shell_name = Path::new(&shell_path)
251            .file_name()
252            .and_then(|s| s.to_str())
253            .unwrap_or("")
254            .to_ascii_lowercase();
255        match shell_name.as_str() {
256            "bash" | "sh" => {
257                profiles_to_check.insert((".bashrc".to_string(), SOURCE_LINE_BASH.to_string()));
258            }
259            "zsh" => {
260                profiles_to_check.insert((".zshrc".to_string(), SOURCE_LINE_BASH.to_string()));
261            }
262            "fish" => {
263                profiles_to_check.insert((
264                    ".config/fish/config.fish".to_string(),
265                    SOURCE_LINE_FISH.to_string(),
266                ));
267            }
268            _ => {}
269        }
270    }
271
272    for (profile_rel, expected_line) in profiles_to_check {
273        let profile_path = paths.dirs.user_dir.join(&profile_rel);
274        if !profile_path.exists() {
275            report.ok = false;
276            report.messages.push(format!(
277                "[FAIL] Shell profile missing: {}",
278                profile_path.display()
279            ));
280            continue;
281        }
282
283        let content = fs::read_to_string(&profile_path)?;
284        if content.contains(&expected_line) {
285            report.messages.push(format!(
286                "[OK] Shell profile contains upstream hook: {}",
287                profile_path.display()
288            ));
289        } else {
290            report.ok = false;
291            report.messages.push(format!(
292                "[FAIL] Shell profile missing upstream hook: {}",
293                profile_path.display()
294            ));
295        }
296    }
297
298    Ok(())
299}
300
301#[cfg(unix)]
302fn add_line_to_profile(paths: &UpstreamPaths, relative_path: &str, line: &str) -> io::Result<()> {
303    let profile_path = paths.dirs.user_dir.join(relative_path);
304
305    // Ensure parent directory exists
306    if let Some(parent) = profile_path.parent() {
307        fs::create_dir_all(parent)?;
308    }
309
310    // Backup original file
311    if profile_path.exists() {
312        let backup_path = profile_path.with_extension("bak");
313        if !backup_path.exists() {
314            fs::copy(&profile_path, &backup_path)?;
315        }
316    }
317
318    if !profile_path.exists() {
319        fs::write(&profile_path, format!("{}\n", line))?;
320        return Ok(());
321    }
322
323    let content = fs::read_to_string(&profile_path)?;
324    if !content.contains(line) {
325        let mut file = fs::OpenOptions::new().append(true).open(&profile_path)?;
326        writeln!(file, "\n{}", line)?;
327    }
328
329    Ok(())
330}
331
332#[cfg(unix)]
333pub fn cleanup(paths: &UpstreamPaths) -> Result<()> {
334    let shells = get_installed_shells()?;
335    for shell_path in shells {
336        let shell_name = Path::new(&shell_path)
337            .file_name()
338            .and_then(|s| s.to_str())
339            .unwrap_or("");
340        let profile = match shell_name.to_lowercase().as_str() {
341            "bash" | "sh" => Some(".bashrc"),
342            "zsh" => Some(".zshrc"),
343            "fish" => Some(".config/fish/config.fish"),
344            _ => None,
345        };
346        if let Some(profile_rel) = profile {
347            let profile_path = paths.dirs.user_dir.join(profile_rel);
348            if !profile_path.exists() {
349                continue;
350            }
351            let mut content = fs::read_to_string(&profile_path)?;
352            content = content
353                .replace(&format!("{}\n", SOURCE_LINE_BASH), "")
354                .replace(SOURCE_LINE_BASH, "")
355                .replace(&format!("{}\n", SOURCE_LINE_FISH), "")
356                .replace(SOURCE_LINE_FISH, "");
357            fs::write(&profile_path, content)?;
358        }
359    }
360    Ok(())
361}
362
363#[cfg(windows)]
364pub fn cleanup(paths: &UpstreamPaths) -> Result<()> {
365    remove_from_windows_path(paths)
366}
367
368#[cfg(windows)]
369fn remove_from_windows_path(paths: &UpstreamPaths) -> Result<()> {
370    use winreg::RegKey;
371    use winreg::enums::*;
372
373    let hkcu = RegKey::predef(HKEY_CURRENT_USER);
374    let env_key = hkcu
375        .open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)
376        .context("Failed to open registry key")?;
377
378    let symlinks_path = paths.integration.symlinks_dir.display().to_string();
379    let symlinks_norm = normalize_windows_path(&symlinks_path);
380
381    // Get current PATH
382    let current_path: String = env_key.get_value("Path").unwrap_or_else(|_| String::new());
383
384    // Remove our path from PATH
385    let path_entries: Vec<&str> = current_path
386        .split(';')
387        .filter(|&p| normalize_windows_path(p) != symlinks_norm)
388        .collect();
389
390    let new_path = path_entries.join(";");
391
392    env_key
393        .set_value("Path", &new_path)
394        .context("Failed to set PATH")?;
395
396    // Broadcast WM_SETTINGCHANGE to notify other applications
397    broadcast_environment_change();
398
399    Ok(())
400}
401
402#[cfg(windows)]
403fn check_windows_integration(paths: &UpstreamPaths, report: &mut InitCheckReport) -> Result<()> {
404    use winreg::RegKey;
405    use winreg::enums::*;
406
407    let hkcu = RegKey::predef(HKEY_CURRENT_USER);
408    let env_key = hkcu
409        .open_subkey_with_flags("Environment", KEY_READ)
410        .context("Failed to open PATH")?;
411
412    let symlinks_path = paths.integration.symlinks_dir.display().to_string();
413    let symlinks_norm = normalize_windows_path(&symlinks_path);
414    let current_path: String = env_key.get_value("Path").unwrap_or_else(|_| String::new());
415
416    let in_path = current_path
417        .split(';')
418        .any(|p| normalize_windows_path(p) == symlinks_norm);
419
420    if in_path {
421        report
422            .messages
423            .push("[OK] Windows PATH contains upstream symlinks directory".to_string());
424    } else {
425        report.ok = false;
426        report
427            .messages
428            .push("[FAIL] Windows PATH missing upstream symlinks directory".to_string());
429    }
430
431    Ok(())
432}