Skip to main content

leenfetch_core/modules/linux/desktop/
wm.rs

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