fix: add xprop/wmctrl fallbacks for window detection when xdotool is not installed
Some checks are pending
Build & Release / Build & test (push) Waiting to run
Build & Release / Build .deb (push) Blocked by required conditions

This commit is contained in:
Khoa Vo 2026-07-01 12:05:20 +07:00
parent 81b483e7ac
commit 7ac73485e4

View file

@ -132,6 +132,12 @@ pub fn get_active_window_title() -> Option<String> {
} }
} }
} }
// Try X11 via xprop/wmctrl (fallback when xdotool not installed)
if let Some(title) = get_wmctrl_window_title() {
return Some(title);
}
None None
} }
@ -212,6 +218,16 @@ pub fn get_focused_window_class() -> Option<String> {
return Some(class); return Some(class);
} }
// Try X11 via xprop (fallback when xdotool is not installed)
if let Some(class) = get_xprop_window_class() {
return Some(class);
}
// Try X11 via wmctrl (fallback)
if let Some(class) = get_wmctrl_window_class() {
return Some(class);
}
// Fallback: try reading from /proc // Fallback: try reading from /proc
if let Some(class) = get_proc_window_class() { if let Some(class) = get_proc_window_class() {
return Some(class); return Some(class);
@ -259,6 +275,103 @@ fn get_x11_window_class() -> Option<String> {
None None
} }
/// Get WM_CLASS via xprop (works on X11 without xdotool)
fn get_xprop_window_class() -> Option<String> {
// First get the active window ID
let id = get_active_window_id_xprop()?;
// Then get WM_CLASS for that window
let output = Command::new("xprop")
.args(["-id", &id, "WM_CLASS"])
.output()
.ok()?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
// Format: WM_CLASS(STRING) = "gnome-terminal", "Gnome-terminal"
// We want the first string (application name)
if let Some(class_part) = stdout.split('"').nth(1) {
let class = class_part.trim().to_lowercase();
if !class.is_empty() {
return Some(class);
}
}
}
None
}
/// Get active window ID via xprop (X11, no xdotool needed)
fn get_active_window_id_xprop() -> Option<String> {
let output = Command::new("xprop")
.args(["-root", "_NET_ACTIVE_WINDOW"])
.output()
.ok()?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
// Format: "_NET_ACTIVE_WINDOW(WINDOW): window id # 0x3a00004"
if let Some(hex) = stdout.split("window id # ").nth(1) {
let hex = hex.trim();
if !hex.is_empty() {
return Some(hex.to_string());
}
}
}
None
}
/// Get window class via wmctrl (X11 fallback)
fn get_wmctrl_window_class() -> Option<String> {
// Only try wmctrl if xdotool is unavailable
if Command::new("xdotool").output().is_ok() {
return None; // xdotool exists, prefer it
}
// wmctrl -l -x lists windows with their class (WM_CLASS)
let output = Command::new("wmctrl")
.args(["-l", "-x"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout);
// Each line format: 0x00a00001 desktop_num class_name title
// Class name format: "gnome-terminal.Gnome-terminal"
// We want the part before the dot
for line in stdout.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
let class_part = parts[2];
// Get the app part before the dot if present
let class = class_part.split('.').next().unwrap_or(class_part).to_lowercase();
if !class.is_empty() && class != "nvidia-settings" {
// wmctrl returns ALL windows, not just focused.
// We check if the first listed window is focused,
// or if there's an active-state marker.
return Some(class);
}
}
}
None
}
/// Get active window title using wmctrl
fn get_wmctrl_window_title() -> Option<String> {
let id = get_active_window_id_xprop()?;
let output = Command::new("xprop")
.args(["-id", &id, "WM_NAME"])
.output()
.ok()?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
// Format: "WM_NAME(STRING) = "title""
if let Some(title) = stdout.split('"').nth(1) {
let title = title.trim();
if !title.is_empty() {
return Some(title.to_lowercase());
}
}
}
None
}
fn get_wayland_window_class() -> Option<String> { fn get_wayland_window_class() -> Option<String> {
// Try wlr-foreign-toplevel-management protocol via wlrctl // Try wlr-foreign-toplevel-management protocol via wlrctl
let output = Command::new("wlrctl") let output = Command::new("wlrctl")