leenfetch_core/modules/linux/system/
distro.rs1use 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 }, 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 }, ];
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}