leenfetch_core/modules/linux/info/
os_age.rs

1use std::fmt::Write;
2use std::fs;
3use std::process::Command;
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use crate::modules::enums::OsAgeShorthand;
7
8/// Returns the OS "age" (time since root FS creation/install) formatted per shorthand.
9/// Mirrors the style of your `get_uptime` function.
10pub fn get_os_age(shorthand: OsAgeShorthand) -> Option<String> {
11    let install_epoch = read_install_epoch_seconds()?;
12    let now = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
13
14    // Guard against clock skew or unknown/invalid install time
15    let seconds = now.saturating_sub(install_epoch);
16
17    Some(format_age(seconds, shorthand))
18}
19
20/// Best-effort detection of install time (epoch seconds) of the root filesystem.
21/// Strategy:
22/// 1) Try `metadata("/").created()` (when supported by platform/fs)
23/// 2) Fallback to `stat -c %W /` (Linux) which returns birth time or 0/-1 if unknown
24fn read_install_epoch_seconds() -> Option<u64> {
25    // Try std first (portable when supported)
26    if let Ok(md) = fs::metadata("/") {
27        if let Ok(created) = md.created() {
28            if let Ok(dur) = created.duration_since(UNIX_EPOCH) {
29                let secs = dur.as_secs();
30                if secs > 0 {
31                    return Some(secs);
32                }
33            }
34        }
35    }
36
37    // Fallback: Linux `stat` birth time for `/`
38    let out = Command::new("stat").args(["-c", "%W", "/"]).output().ok()?;
39
40    if !out.status.success() {
41        return None;
42    }
43
44    let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
45    // %W might be "0" or "-1" if unknown
46    if s == "0" || s == "-1" {
47        return None;
48    }
49
50    // Handle potential negative parse safely
51    if let Ok(v) = s.parse::<i64>() {
52        if v > 0 {
53            return Some(v as u64);
54        }
55    }
56
57    None
58}
59
60fn format_age(seconds: u64, shorthand: OsAgeShorthand) -> String {
61    let days = seconds / 86_400;
62    let hours = (seconds / 3_600) % 24;
63    let minutes = (seconds / 60) % 60;
64
65    let mut buf = String::with_capacity(32);
66
67    match shorthand {
68        OsAgeShorthand::Full => {
69            if days > 0 {
70                let _ = write!(buf, "{} day{}, ", days, if days != 1 { "s" } else { "" });
71            }
72            if hours > 0 {
73                let _ = write!(buf, "{} hour{}, ", hours, if hours != 1 { "s" } else { "" });
74            }
75            if minutes > 0 {
76                let _ = write!(
77                    buf,
78                    "{} minute{}",
79                    minutes,
80                    if minutes != 1 { "s" } else { "" }
81                );
82            }
83            if buf.is_empty() {
84                let _ = write!(buf, "{} seconds", seconds);
85            }
86        }
87        OsAgeShorthand::Tiny => {
88            if days > 0 {
89                let _ = write!(buf, "{} days", days);
90            }
91        }
92        OsAgeShorthand::Seconds => {
93            let _ = write!(buf, "{}s", seconds);
94        }
95    }
96
97    buf.trim_end_matches([' ', ','].as_ref()).to_string()
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn formats_full_age() {
106        let result = format_age(86_400 * 2 + 3_600 + 120, OsAgeShorthand::Full);
107        assert!(
108            result.contains("2 days") && result.contains("1 hour") && result.contains("2 minutes"),
109            "unexpected format: {result}"
110        );
111    }
112
113    #[test]
114    fn formats_tiny_age() {
115        let result = format_age(86_400 * 5 + 300, OsAgeShorthand::Tiny);
116        assert_eq!(result, "5 days");
117    }
118
119    #[test]
120    fn formats_seconds_age() {
121        let result = format_age(42, OsAgeShorthand::Seconds);
122        assert_eq!(result, "42s");
123    }
124}