Skip to main content

leenfetch_core/modules/linux/desktop/
wm.rs

1use std::env;
2use std::fs;
3
4pub fn get_wm() -> Option<String> {
5    // Check environment variables first (fastest)
6    if let Ok(swaysock) = env::var("SWAYSOCK") {
7        if !swaysock.is_empty() {
8            return Some("sway".to_string());
9        }
10    }
11
12    if let Ok(hyprland) = env::var("HYPRLAND_INSTANCE_SIGNATURE") {
13        if !hyprland.is_empty() {
14            return Some("Hyprland".to_string());
15        }
16    }
17
18    // Wayland detection via XDG_RUNTIME_DIR
19    if let Ok(runtime) = env::var("XDG_RUNTIME_DIR") {
20        let socket = env::var("WAYLAND_DISPLAY").unwrap_or_else(|_| "wayland-0".to_string());
21        let path = format!("{}/{}", runtime, socket);
22        if fs::metadata(&path).is_ok() {
23            if let Some(wm) = scan_proc(WAYLAND_WMS) {
24                return Some(wm);
25            }
26        }
27    }
28
29    // X11 (DISPLAY is set)
30    if env::var("DISPLAY").is_ok() {
31        // Scan known X11 WM processes
32        if let Some(wm) = scan_proc(X11_WMS) {
33            return Some(wm);
34        }
35    }
36
37    // Fallback: scan all known WM processes
38    scan_proc(ALL_WMS)
39}
40
41// Uses /proc to scan processes for known WMs (faster than spawning ps)
42fn scan_proc(wm_names: &[&str]) -> Option<String> {
43    let proc_path = match fs::read_dir("/proc") {
44        Ok(p) => p,
45        Err(_) => return None,
46    };
47
48    for entry in proc_path.flatten() {
49        let name = entry.file_name();
50        let name_str = name.to_string_lossy();
51
52        // Only look at numeric PIDs
53        if !name_str.chars().all(|c| c.is_ascii_digit()) {
54            continue;
55        }
56
57        // Read the comm file (process name)
58        let comm_path = entry.path().join("comm");
59        if let Ok(comm) = fs::read_to_string(&comm_path) {
60            let process_name = comm.trim();
61            for &wm in wm_names {
62                if process_name.eq_ignore_ascii_case(wm) || process_name.contains(wm) {
63                    return Some(normalize_wm(wm));
64                }
65            }
66        }
67    }
68
69    None
70}
71
72fn normalize_wm(wm: &str) -> String {
73    match wm {
74        "gnome-shell" | "GNOME Shell" => "Mutter",
75        "kwin_x11" | "kwin_wayland" | "kwin" => "KWin",
76        "WINDOWMAKER" => "wmaker",
77        other => other,
78    }
79    .to_string()
80}
81
82// Common known Wayland WMs and compositors
83const WAYLAND_WMS: &[&str] = &[
84    "sway",
85    "wayfire",
86    "weston",
87    "gnome-shell",
88    "kwin_wayland",
89    "hikari",
90    "river",
91    "wlr-randr",
92];
93
94// Common X11 WMs
95const X11_WMS: &[&str] = &[
96    "dwm",
97    "xmonad",
98    "openbox",
99    "fluxbox",
100    "i3",
101    "herbstluftwm",
102    "awesome",
103    "bspwm",
104    "kwin_x11",
105];
106
107// Combo of all known WMs
108const ALL_WMS: &[&str] = &[
109    "sway",
110    "wayfire",
111    "weston",
112    "gnome-shell",
113    "kwin_wayland",
114    "kwin_x11",
115    "dwm",
116    "xmonad",
117    "openbox",
118    "fluxbox",
119    "i3",
120    "herbstluftwm",
121    "awesome",
122    "bspwm",
123    "hikari",
124    "river",
125    "WINDOWMAKER",
126];
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use crate::test_utils::EnvLock;
132
133    fn clear_env() -> EnvLock {
134        let vars = ["XDG_RUNTIME_DIR", "WAYLAND_DISPLAY", "DISPLAY"];
135        let env_lock = EnvLock::acquire(&vars);
136        for var in vars {
137            env_lock.remove_var(var);
138        }
139        env_lock
140    }
141
142    #[test]
143    fn test_normalize_wm_variants() {
144        assert_eq!(normalize_wm("gnome-shell"), "Mutter");
145        assert_eq!(normalize_wm("GNOME Shell"), "Mutter");
146        assert_eq!(normalize_wm("kwin_x11"), "KWin");
147        assert_eq!(normalize_wm("kwin_wayland"), "KWin");
148        assert_eq!(normalize_wm("kwin"), "KWin");
149        assert_eq!(normalize_wm("WINDOWMAKER"), "wmaker");
150        assert_eq!(normalize_wm("i3"), "i3"); // fallback to default
151    }
152
153    #[test]
154    fn test_get_wm_fallback_to_none_without_env() {
155        let env_lock = clear_env();
156
157        // Note: we can't test full get_wm() without actually scanning the system,
158        // but we ensure it doesn't crash in a null environment
159        let _ = get_wm(); // just call it and ensure no panic
160        drop(env_lock);
161    }
162}