use std::env;
use std::fs;
use std::process::Command;
pub fn get_wm() -> Option<String> {
if let Ok(runtime) = env::var("XDG_RUNTIME_DIR") {
let socket = env::var("WAYLAND_DISPLAY").unwrap_or_else(|_| "wayland-0".to_string());
let path = format!("{}/{}", runtime, socket);
if fs::metadata(&path).is_ok() {
if let Some(wm) = scan_proc(WAYLAND_WMS) {
return Some(wm);
}
}
}
if env::var("DISPLAY").is_ok() {
if let Some(wm) = get_wm_from_xprop() {
return Some(wm);
}
if let Some(wm) = scan_proc(X11_WMS) {
return Some(wm);
}
}
scan_proc(ALL_WMS)
}
fn scan_proc(wm_names: &[&str]) -> Option<String> {
let output = Command::new("ps")
.args(["-eo", "comm="])
.output()
.ok()?
.stdout;
let ps_text = String::from_utf8_lossy(&output);
for line in ps_text.lines() {
let name = line.trim();
for &wm in wm_names {
if name.eq_ignore_ascii_case(wm) || name.contains(wm) {
return Some(normalize_wm(wm));
}
}
}
None
}
fn get_wm_from_xprop() -> Option<String> {
let root = Command::new("xprop")
.args(["-root", "_NET_SUPPORTING_WM_CHECK"])
.output()
.ok()?;
let root_out = String::from_utf8_lossy(&root.stdout);
let win_id = root_out.rsplit(' ').next()?.trim();
let wm = Command::new("xprop")
.args(["-id", win_id, "-notype", "_NET_WM_NAME"])
.output()
.ok()?;
let name = String::from_utf8_lossy(&wm.stdout);
name.split('=')
.nth(1)
.map(|s| normalize_wm(s.trim().trim_matches('"')))
}
fn normalize_wm(wm: &str) -> String {
match wm {
"gnome-shell" | "GNOME Shell" => "Mutter",
"kwin_x11" | "kwin_wayland" | "kwin" => "KWin",
"WINDOWMAKER" => "wmaker",
other => other,
}
.to_string()
}
const WAYLAND_WMS: &[&str] = &[
"sway",
"wayfire",
"weston",
"gnome-shell",
"kwin_wayland",
"hikari",
"river",
"wlr-randr",
];
const X11_WMS: &[&str] = &[
"dwm",
"xmonad",
"openbox",
"fluxbox",
"i3",
"herbstluftwm",
"awesome",
"bspwm",
"kwin_x11",
];
const ALL_WMS: &[&str] = &[
"sway",
"wayfire",
"weston",
"gnome-shell",
"kwin_wayland",
"kwin_x11",
"dwm",
"xmonad",
"openbox",
"fluxbox",
"i3",
"herbstluftwm",
"awesome",
"bspwm",
"hikari",
"river",
"WINDOWMAKER",
];
#[cfg(test)]
mod tests {
use super::*;
use std::env;
fn clear_env() {
for var in ["XDG_RUNTIME_DIR", "WAYLAND_DISPLAY", "DISPLAY"] {
env::remove_var(var);
}
}
#[test]
fn test_normalize_wm_variants() {
assert_eq!(normalize_wm("gnome-shell"), "Mutter");
assert_eq!(normalize_wm("GNOME Shell"), "Mutter");
assert_eq!(normalize_wm("kwin_x11"), "KWin");
assert_eq!(normalize_wm("kwin_wayland"), "KWin");
assert_eq!(normalize_wm("kwin"), "KWin");
assert_eq!(normalize_wm("WINDOWMAKER"), "wmaker");
assert_eq!(normalize_wm("i3"), "i3"); }
#[test]
fn test_scan_proc_finds_known_wm() {
let sample_ps = "i3\nbash\nXorg\n";
for wm in ALL_WMS {
let fake_output = sample_ps.replace("i3", wm);
let found = parse_ps_for_wm(&fake_output, &[wm]);
assert_eq!(found, Some(normalize_wm(wm)));
}
}
fn parse_ps_for_wm(ps_output: &str, targets: &[&str]) -> Option<String> {
for line in ps_output.lines() {
let name = line.trim();
for &wm in targets {
if name.eq_ignore_ascii_case(wm) || name.contains(wm) {
return Some(normalize_wm(wm));
}
}
}
None
}
#[test]
fn test_get_wm_fallback_to_none_without_env() {
clear_env();
let _ = get_wm(); }
}