Skip to main content

k580_ui/
install_mode.rs

1use serde::{Deserialize, Serialize};
2use std::path::{Path, PathBuf};
3
4pub const MANIFEST_FILENAME: &str = "install.json";
5
6#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub enum InstallMode {
9    System,
10    Portable,
11}
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub enum InstallScope {
16    User,
17    Machine,
18}
19
20#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct InstallManifest {
23    pub manifest_version: u8,
24    pub mode: InstallMode,
25    pub scope: InstallScope,
26    #[serde(default)]
27    pub file_association: bool,
28}
29
30impl InstallManifest {
31    pub fn new(mode: InstallMode, scope: InstallScope) -> Self {
32        Self {
33            manifest_version: 1,
34            mode,
35            scope,
36            file_association: false,
37        }
38    }
39
40    pub fn with_file_association(mut self, file_association: bool) -> Self {
41        self.file_association = file_association;
42        self
43    }
44}
45
46pub fn write_manifest(root: &Path, manifest: &InstallManifest) -> Result<(), String> {
47    let json = serde_json::to_string_pretty(manifest).map_err(|e| format!("manifest json: {e}"))?;
48    std::fs::write(root.join(MANIFEST_FILENAME), json).map_err(|e| format!("write manifest: {e}"))
49}
50
51pub fn manifest_for_executable(exe: &Path) -> Result<Option<(PathBuf, InstallManifest)>, String> {
52    let Some(root) = install_root_from_executable(exe) else {
53        return Ok(None);
54    };
55    let json = std::fs::read_to_string(root.join(MANIFEST_FILENAME))
56        .map_err(|e| format!("read manifest: {e}"))?;
57    let manifest = serde_json::from_str(&json).map_err(|e| format!("parse manifest: {e}"))?;
58    Ok(Some((root, manifest)))
59}
60
61pub fn install_root_from_executable(exe: &Path) -> Option<PathBuf> {
62    let dir = exe.parent()?;
63    if dir.join(MANIFEST_FILENAME).is_file() {
64        return Some(dir.to_path_buf());
65    }
66    let parent = dir.parent()?;
67    let leaf = dir.file_name()?.to_string_lossy();
68    if matches_ci(&leaf, "app") || matches_ci(&leaf, "bin") {
69        let manifest = parent.join(MANIFEST_FILENAME);
70        if manifest.is_file() {
71            return Some(parent.to_path_buf());
72        }
73    }
74    None
75}
76
77fn matches_ci(left: &str, right: &str) -> bool {
78    left.eq_ignore_ascii_case(right)
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn detects_split_app_layout() {
87        let root = unique_temp_dir("split-app");
88        std::fs::create_dir_all(root.join("app")).unwrap();
89        std::fs::write(root.join(MANIFEST_FILENAME), "{}").unwrap();
90
91        assert_eq!(
92            install_root_from_executable(&root.join("app").join(binary_name("k580"))),
93            Some(root.clone())
94        );
95
96        let _ = std::fs::remove_dir_all(root);
97    }
98
99    #[test]
100    fn detects_split_bin_layout() {
101        let root = unique_temp_dir("split-bin");
102        std::fs::create_dir_all(root.join("bin")).unwrap();
103        std::fs::write(root.join(MANIFEST_FILENAME), "{}").unwrap();
104
105        assert_eq!(
106            install_root_from_executable(&root.join("bin").join(binary_name("kr"))),
107            Some(root.clone())
108        );
109
110        let _ = std::fs::remove_dir_all(root);
111    }
112
113    fn unique_temp_dir(name: &str) -> PathBuf {
114        let nanos = std::time::SystemTime::now()
115            .duration_since(std::time::UNIX_EPOCH)
116            .unwrap()
117            .as_nanos();
118        std::env::temp_dir().join(format!("k580-install-mode-{nanos}-{name}"))
119    }
120
121    #[cfg(windows)]
122    fn binary_name(name: &str) -> String {
123        format!("{name}.exe")
124    }
125
126    #[cfg(not(windows))]
127    fn binary_name(name: &str) -> String {
128        name.to_owned()
129    }
130}