Skip to main content

leenfetch_core/modules/linux/system/
distro.rs

1use std::fs;
2use std::path::Path;
3
4use crate::modules::enums::DistroDisplay;
5
6pub fn get_distro(format: DistroDisplay) -> String {
7    let release_files = [
8        "/etc/os-release",
9        "/usr/lib/os-release",
10        "/etc/lsb-release",
11        "/etc/openwrt_release",
12    ];
13
14    for path in release_files {
15        if Path::new(path).exists() {
16            if let Ok(contents) = fs::read_to_string(path) {
17                return parse_distro_info(&contents, format);
18            }
19        }
20    }
21
22    "Unknown".into()
23}
24
25fn parse_distro_info(contents: &str, format: DistroDisplay) -> String {
26    let mut name = None;
27    let mut version = None;
28    let mut pretty = None;
29    let mut description = None;
30    let mut codename = None;
31
32    for line in contents.lines() {
33        let line = line.trim();
34        if line.starts_with("NAME=") {
35            name = Some(trim_quotes(&line[5..]));
36        } else if line.starts_with("VERSION_ID=") {
37            version = Some(trim_quotes(&line[11..]));
38        } else if line.starts_with("PRETTY_NAME=") {
39            pretty = Some(trim_quotes(&line[12..]));
40        } else if line.starts_with("DISTRIB_DESCRIPTION=") {
41            description = Some(trim_quotes(&line[21..]));
42        } else if line.starts_with("VERSION_CODENAME=") {
43            codename = Some(trim_quotes(&line[17..]));
44        } else if line.starts_with("UBUNTU_CODENAME=") {
45            codename = Some(trim_quotes(&line[16..]));
46        } else if line.starts_with("TAILS_PRODUCT_NAME=") {
47            name = Some(trim_quotes(&line[20..]));
48        }
49    }
50
51    let name = name
52        .or_else(|| pretty.clone())
53        .or_else(|| description.clone())
54        .unwrap_or_else(|| "Unknown".to_string());
55
56    let version = version.or_else(|| {
57        description.as_ref().and_then(|desc| {
58            desc.split_whitespace()
59                .find(|s| {
60                    s.chars()
61                        .next()
62                        .map(|c| c.is_ascii_digit())
63                        .unwrap_or(false)
64                })
65                .map(|s| s.to_string())
66        })
67    });
68
69    let arch = std::env::consts::ARCH;
70    let model = infer_model(&name, &codename, &description);
71
72    match format {
73        DistroDisplay::Name => name,
74        DistroDisplay::NameVersion => format!("{name} {}", version.unwrap_or_default())
75            .trim()
76            .to_string(),
77        DistroDisplay::NameArch => format!("{name} {}", arch).to_string(),
78        DistroDisplay::NameModel => format!("{name} {}", model).trim().to_string(),
79        DistroDisplay::NameModelVersion => {
80            format!("{name} {} {}", model, version.unwrap_or_default())
81                .trim()
82                .to_string()
83        }
84        DistroDisplay::NameModelArch => format!("{name} {} {}", model, arch).trim().to_string(),
85        DistroDisplay::NameModelVersionArch => {
86            format!("{name} {} {} {}", model, version.unwrap_or_default(), arch)
87                .trim()
88                .to_string()
89        }
90    }
91}
92
93fn trim_quotes(s: &str) -> String {
94    s.trim_matches('"').to_string()
95}
96
97struct DistroModel {
98    keyword: &'static str,
99    model: &'static str,
100}
101
102static MODEL_HINTS: &[DistroModel] = &[
103    DistroModel {
104        keyword: "arch",
105        model: "Rolling",
106    },
107    DistroModel {
108        keyword: "artix",
109        model: "Rolling",
110    },
111    DistroModel {
112        keyword: "endeavouros",
113        model: "Rolling",
114    },
115    DistroModel {
116        keyword: "manjaro",
117        model: "Rolling",
118    },
119    DistroModel {
120        keyword: "void",
121        model: "Rolling",
122    },
123    DistroModel {
124        keyword: "nixos",
125        model: "Rolling",
126    },
127    DistroModel {
128        keyword: "tumbleweed",
129        model: "Rolling",
130    },
131    DistroModel {
132        keyword: "rawhide",
133        model: "Testing",
134    },
135    DistroModel {
136        keyword: "testing",
137        model: "Testing",
138    },
139    DistroModel {
140        keyword: "stable",
141        model: "Stable",
142    },
143    DistroModel {
144        keyword: "ubuntu",
145        model: "LTS",
146    }, // fallback for known Ubuntu LTS
147    DistroModel {
148        keyword: "lts",
149        model: "LTS",
150    },
151    DistroModel {
152        keyword: "tails",
153        model: "Stable",
154    },
155    DistroModel {
156        keyword: "alpine",
157        model: "Stable",
158    },
159    DistroModel {
160        keyword: "debian",
161        model: "Stable",
162    }, // default to Stable unless Testing is seen
163];
164
165fn infer_model(name: &str, codename: &Option<String>, description: &Option<String>) -> String {
166    let text = format!(
167        "{} {} {}",
168        name.to_lowercase(),
169        codename.as_deref().unwrap_or("").to_lowercase(),
170        description.as_deref().unwrap_or("").to_lowercase()
171    );
172
173    for entry in MODEL_HINTS {
174        if text.contains(entry.keyword) {
175            return entry.model.to_string();
176        }
177    }
178
179    "Unknown".into()
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use crate::modules::enums::DistroDisplay;
186
187    fn sample_release() -> &'static str {
188        r#"NAME="ExampleOS"
189VERSION_ID="42"
190PRETTY_NAME="ExampleOS 42"
191VERSION_CODENAME="Aurora"
192DISTRIB_DESCRIPTION="ExampleOS 42 Aurora"
193"#
194    }
195
196    #[test]
197    fn parses_name_variants() {
198        let data = sample_release();
199        assert_eq!(parse_distro_info(data, DistroDisplay::Name), "ExampleOS");
200        assert_eq!(
201            parse_distro_info(data, DistroDisplay::NameVersion),
202            "ExampleOS 42"
203        );
204        assert!(
205            parse_distro_info(data, DistroDisplay::NameArch).contains("ExampleOS"),
206            "NameArch should include distro name"
207        );
208    }
209
210    #[test]
211    fn infers_model_from_hints() {
212        let desc = Some("Arch Linux Rolling".to_string());
213        let model = infer_model("Arch Linux", &None, &desc);
214        assert_eq!(model, "Rolling");
215
216        let codename = Some("jammy".to_string());
217        let model = infer_model("Ubuntu", &codename, &None);
218        assert_eq!(model, "LTS");
219    }
220}