Skip to main content

retch_sysinfo/
wm.rs

1// SPDX-FileCopyrightText: 2026 Ken Tobias
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4//! Window manager detection.
5
6/// Returns the active window manager name, or `None` if it cannot be determined.
7///
8/// Detection order:
9/// 1. `$WINDOW_MANAGER` env var (user-set override)
10/// 2. Wayland compositors: check `$WAYLAND_DISPLAY` and `$XDG_SESSION_TYPE=wayland`,
11///    then probe well-known compositor process names.
12/// 3. X11: scan running processes for known WM names.
13/// 4. `wmctrl -m` (X11 only, may not be installed).
14pub fn detect_wm() -> Option<String> {
15    // Explicit override
16    if let Ok(wm) = std::env::var("WINDOW_MANAGER") {
17        let wm = wm.trim().to_string();
18        if !wm.is_empty() {
19            return Some(wm);
20        }
21    }
22
23    #[cfg(target_os = "linux")]
24    {
25        let is_wayland = std::env::var("WAYLAND_DISPLAY").is_ok()
26            || std::env::var("XDG_SESSION_TYPE")
27                .map(|v| v.to_lowercase() == "wayland")
28                .unwrap_or(false);
29
30        if is_wayland {
31            if let Some(wm) = probe_wayland_compositor() {
32                return Some(wm);
33            }
34        }
35
36        // X11 process scan (also catches Wayland compositors running XWayland)
37        if let Some(wm) = probe_wm_process() {
38            return Some(wm);
39        }
40
41        // wmctrl fallback (X11 only)
42        if let Ok(output) = std::process::Command::new("wmctrl").arg("-m").output() {
43            if let Ok(stdout) = String::from_utf8(output.stdout) {
44                for line in stdout.lines() {
45                    if let Some(rest) = line.trim().strip_prefix("Name:") {
46                        let name = rest.trim();
47                        if !name.is_empty() && name != "N/A" {
48                            return Some(name.to_string());
49                        }
50                    }
51                }
52            }
53        }
54    }
55
56    #[cfg(target_os = "macos")]
57    {
58        // macOS uses Quartz Compositor / Quartz Window Manager — no choice
59        return Some("Quartz Compositor".to_string());
60    }
61
62    #[cfg(target_os = "windows")]
63    {
64        return Some("Desktop Window Manager".to_string());
65    }
66
67    #[allow(unreachable_code)]
68    None
69}
70
71#[cfg(target_os = "linux")]
72fn probe_wayland_compositor() -> Option<String> {
73    // Ordered by prevalence
74    const COMPOSITORS: &[(&str, &str)] = &[
75        ("sway", "Sway"),
76        ("hyprland", "Hyprland"),
77        ("kwin_wayland", "KWin"),
78        ("mutter", "Mutter"),
79        ("wayfire", "Wayfire"),
80        ("river", "River"),
81        ("niri", "Niri"),
82        ("labwc", "labwc"),
83        ("weston", "Weston"),
84        ("cage", "Cage"),
85        ("gamescope", "gamescope"),
86        ("mir", "Mir"),
87    ];
88    probe_from_process_list(COMPOSITORS)
89}
90
91#[cfg(target_os = "linux")]
92fn probe_wm_process() -> Option<String> {
93    const WMS: &[(&str, &str)] = &[
94        ("openbox", "Openbox"),
95        ("i3", "i3"),
96        ("bspwm", "bspwm"),
97        ("xmonad", "XMonad"),
98        ("awesome", "Awesome"),
99        ("dwm", "dwm"),
100        ("fluxbox", "Fluxbox"),
101        ("icewm", "IceWM"),
102        ("jwm", "JWM"),
103        ("kwin_x11", "KWin"),
104        ("mutter", "Mutter"),
105        ("marco", "Marco"),
106        ("xfwm4", "Xfwm4"),
107        ("compiz", "Compiz"),
108        ("metacity", "Metacity"),
109        ("enlightenment", "Enlightenment"),
110        ("herbstluftwm", "herbstluftwm"),
111        ("qtile", "Qtile"),
112        ("spectrwm", "spectrwm"),
113        ("cwm", "cwm"),
114        ("2bwm", "2bwm"),
115        ("leftwm", "LeftWM"),
116        ("wmderland", "wmderland"),
117        ("penrose", "Penrose"),
118    ];
119    probe_from_process_list(WMS)
120}
121
122#[cfg(target_os = "linux")]
123fn probe_from_process_list(candidates: &[(&str, &str)]) -> Option<String> {
124    let Ok(entries) = std::fs::read_dir("/proc") else {
125        return None;
126    };
127    for entry in entries.filter_map(|e| e.ok()) {
128        let path = entry.path();
129        if !path.is_dir() {
130            continue;
131        }
132        let comm_path = path.join("comm");
133        let Ok(comm) = std::fs::read_to_string(&comm_path) else {
134            continue;
135        };
136        let comm = comm.trim().to_lowercase();
137        for (proc_name, display_name) in candidates {
138            if comm == *proc_name || comm.starts_with(proc_name) {
139                return Some(display_name.to_string());
140            }
141        }
142    }
143    None
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_detect_wm_does_not_panic() {
152        // Just verify it doesn't panic; actual result depends on the environment.
153        let _ = detect_wm();
154    }
155}