leenfetch_core/modules/linux/
title.rs

1use std::env;
2use std::process::Command;
3
4/// Return (username, hostname, combined length) as a tuple.
5///
6/// FQDN is passed to `get_hostname` to determine whether to include the domain.
7///
8/// The length is the sum of the lengths of the username and hostname,
9/// plus one for the '@' separator.
10pub fn get_titles(fqdn: bool) -> (String, String) {
11    let user = get_user();
12    let host = get_hostname(fqdn);
13
14    (user, host)
15}
16
17fn get_user() -> String {
18    // 1. Try $USER
19    if let Some(u) = env::var_os("USER") {
20        let s = u.to_string_lossy();
21        if !s.is_empty() {
22            return s.into();
23        }
24    }
25
26    if let Ok(out) = Command::new("id").arg("-un").output() {
27        if out.status.success() {
28            let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
29            if !s.is_empty() {
30                return s;
31            }
32        }
33    }
34
35    if let Ok(home) = env::var("HOME") {
36        if let Some(name) = home.rsplit('/').find(|s| !s.is_empty()) {
37            return name.to_string();
38        }
39    }
40
41    // 4. Worst-case
42    "unknown".into()
43}
44
45fn get_hostname(fqdn: bool) -> String {
46    if fqdn {
47        if let Ok(out) = Command::new("hostname").arg("-f").output() {
48            if out.status.success() {
49                let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
50                if !s.is_empty() {
51                    return s;
52                }
53            }
54        }
55    }
56
57    if let Some(h) = env::var_os("HOSTNAME") {
58        let s = h.to_string_lossy();
59        if !s.is_empty() {
60            return s.into();
61        }
62    }
63
64    if let Ok(out) = Command::new("hostname").output() {
65        if out.status.success() {
66            let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
67            if !s.is_empty() {
68                return s;
69            }
70        }
71    }
72
73    "localhost".into()
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::test_utils::EnvLock;
80
81    #[test]
82    fn test_get_user_from_env() {
83        let env_lock = EnvLock::acquire(&["USER"]);
84        env_lock.set_var("USER", "testuser");
85        assert_eq!(get_user(), "testuser");
86        drop(env_lock);
87    }
88
89    #[test]
90    fn test_hostname_from_env() {
91        let env_lock = EnvLock::acquire(&["HOSTNAME"]);
92        env_lock.set_var("HOSTNAME", "testhost");
93        assert_eq!(get_hostname(false), "testhost");
94        drop(env_lock);
95    }
96
97    #[test]
98    fn test_hostname_command_fallback() {
99        let env_lock = EnvLock::acquire(&["HOSTNAME"]);
100        env_lock.remove_var("HOSTNAME");
101        let result = get_hostname(false);
102        assert!(!result.is_empty(), "Hostname should not be empty");
103        drop(env_lock);
104    }
105
106    #[test]
107    fn test_hostname_final_fallback() {
108        // This test can't force full fallback easily since `hostname` command always exists,
109        // but we can at least ensure it's non-empty
110        let result = get_hostname(false);
111        assert!(!result.is_empty());
112    }
113}