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