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