Skip to content

Commit 68e33fb

Browse files
devthejoclaudeavifenesh
authored
feat(windowing): generic X11/EWMH window backend (#41)
* feat(windowing): add generic X11/EWMH window backend Generic wmctrl/xprop backend for list/focus/activate/move/resize on any EWMH-compliant X11 window manager without a dedicated backend (Cinnamon, MATE, Xfce, Openbox, …). Registered last in the backend order so a session-native backend always wins first. Also dispatch move_window/resize_window by the resolved window's backend instead of hardcoding the GNOME Shell extension, so geometry ops work on X11 too. doctor now reports window listing/focus available on plain X11 sessions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(windowing): harden X11 backend per review - list_windows: bail on non-X11 sessions so it never returns XWayland-only windows when a Wayland session falls through to this backend. - move_window: map a -1 x/y target to -2, since wmctrl -e reads -1 as "preserve current value" and would otherwise drop the move. - clean: compare "N/A" case-insensitively. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(windowing): gate X11 focus caps on xprop; list x11 in capability map probe(): the focused flag (and thus focused_window/activate verification) comes from _NET_ACTIVE_WINDOW via xprop, so advertise can_focus_apps/can_focus_windows only when xprop is on PATH. Listing still works with wmctrl alone. diagnostics: capabilities.window_control hard-coded the named backend fields and omitted i3 and x11, so it was empty on X11-only sessions even though the registry uses the x11 backend. Read i3/x11 from windowing.backends (tried last). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(windowing): reject non-positive X11 resize; fix doctor note when focus unavailable resize_window: reject width<=0 || height<=0 before wmctrl -e, where a non-positive value is the "preserve current value" sentinel and would report success while leaving a dimension unchanged. windowing note: when a backend can list windows but cannot verify focus (e.g. wmctrl present but xprop missing on X11, so can_focus_windows=false), say listing-only instead of claiming focused_window and targeted-input verification are available. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * style: format X11 backend changes --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Avi Fenesh <aviarchi1994@gmail.com>
1 parent 9f2f4f1 commit 68e33fb

6 files changed

Lines changed: 485 additions & 25 deletions

File tree

src/diagnostics.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::windowing::registry::{
22
self, COSMIC_WAYLAND_BACKEND, GNOME_SHELL_EXTENSION_BACKEND, GNOME_SHELL_INTROSPECT_BACKEND,
3-
HYPRLAND_BACKEND, KWIN_BACKEND,
3+
HYPRLAND_BACKEND, I3_BACKEND, KWIN_BACKEND, X11_BACKEND,
44
};
55
use schemars::JsonSchema;
66
use serde::Serialize;
@@ -240,6 +240,23 @@ fn capability_map(
240240
if windowing.cosmic_helper.ok {
241241
window_backends.push("cosmic".to_string());
242242
}
243+
// i3 and the generic X11/EWMH backend have no dedicated WindowingReport
244+
// field; read them from the probe map (tried last) so the capability list
245+
// matches the backends the registry will actually use.
246+
if windowing
247+
.backends
248+
.get(I3_BACKEND)
249+
.is_some_and(|check| check.ok)
250+
{
251+
window_backends.push(I3_BACKEND.to_string());
252+
}
253+
if windowing
254+
.backends
255+
.get(X11_BACKEND)
256+
.is_some_and(|check| check.ok)
257+
{
258+
window_backends.push(X11_BACKEND.to_string());
259+
}
243260

244261
let mut accessibility_backends = Vec::new();
245262
if accessibility.at_spi_enabled.ok || accessibility.toolkit_accessibility.ok {
@@ -578,14 +595,16 @@ fn windowing_report(platform: &PlatformReport) -> WindowingReport {
578595
let can_focus_apps = probes.iter().any(|probe| probe.can_focus_apps);
579596
let can_focus_windows = probes.iter().any(|probe| probe.can_focus_windows);
580597
let note = if can_list_windows {
581-
if cosmic_helper.ok && is_cosmic_wayland_platform(platform) {
598+
if !can_focus_windows {
599+
"A window listing backend is available for list_windows, but focused-window and targeted-input verification are unavailable (for example wmctrl is present but xprop is missing on X11)."
600+
} else if cosmic_helper.ok && is_cosmic_wayland_platform(platform) {
582601
"A COSMIC Wayland window backend is available for list_windows, focused_window, and targeted input verification."
583602
} else if kwin.ok {
584603
"A KWin/Plasma window backend is available for list_windows, focused_window, and targeted input verification."
585604
} else if hyprland.ok {
586605
"A Hyprland window backend is available for list_windows, focused_window, and targeted input verification."
587606
} else {
588-
"A GNOME window listing backend is available for list_windows, focused_window, and targeted input verification."
607+
"A window listing backend is available for list_windows, focused_window, and targeted input verification."
589608
}
590609
} else {
591610
"Window listing is unavailable or denied. Computer Use can still use screenshots, AT-SPI, and global ydotool input, but targeted window input cannot be verified. On GNOME, run setup_window_targeting to install the optional GNOME Shell extension backend. On COSMIC, ensure the bundled COSMIC helper is present and can connect to the session. On KDE/Plasma, ensure KWin exposes org.kde.KWin scripting on the session bus. On Hyprland, ensure hyprctl is available in the session."

src/server.rs

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,7 +1243,7 @@ impl ComputerUseLinux {
12431243

12441244
#[tool(
12451245
name = "move_window",
1246-
description = "Move a window to a new desktop position (frame top-left in desktop coordinates). Useful to recover windows that are partially off-screen. Requires the computer-use-linux GNOME Shell extension.",
1246+
description = "Move a window to a new desktop position (frame top-left in desktop coordinates). Useful to recover windows that are partially off-screen. Works through the computer-use-linux GNOME Shell extension or a generic X11/EWMH window manager (wmctrl).",
12471247
annotations(
12481248
read_only_hint = false,
12491249
destructive_hint = false,
@@ -1257,16 +1257,15 @@ impl ComputerUseLinux {
12571257
) -> Json<WindowGeometryOutput> {
12581258
let received = Some(serde_json::json!(params.clone()));
12591259
let target = params.target.clone().into_target();
1260-
self.window_geometry_op(received, &target, |window_id| async move {
1261-
crate::windowing::backends::gnome::move_extension_window(window_id, params.x, params.y)
1262-
.await
1260+
self.window_geometry_op(received, &target, |window| async move {
1261+
registry::move_window(&window, params.x, params.y).await
12631262
})
12641263
.await
12651264
}
12661265

12671266
#[tool(
12681267
name = "resize_window",
1269-
description = "Resize a window to a new frame width/height in desktop pixels, unmaximizing it first if needed. Useful to fit a window fully on-screen. Requires the computer-use-linux GNOME Shell extension.",
1268+
description = "Resize a window to a new frame width/height in desktop pixels, unmaximizing it first if needed. Useful to fit a window fully on-screen. Works through the computer-use-linux GNOME Shell extension or a generic X11/EWMH window manager (wmctrl).",
12701269
annotations(
12711270
read_only_hint = false,
12721271
destructive_hint = false,
@@ -1280,13 +1279,8 @@ impl ComputerUseLinux {
12801279
) -> Json<WindowGeometryOutput> {
12811280
let received = Some(serde_json::json!(params.clone()));
12821281
let target = params.target.clone().into_target();
1283-
self.window_geometry_op(received, &target, |window_id| async move {
1284-
crate::windowing::backends::gnome::resize_extension_window(
1285-
window_id,
1286-
params.width,
1287-
params.height,
1288-
)
1289-
.await
1282+
self.window_geometry_op(received, &target, |window| async move {
1283+
registry::resize_window(&window, params.width, params.height).await
12901284
})
12911285
.await
12921286
}
@@ -2219,7 +2213,7 @@ impl ComputerUseLinux {
22192213
op: F,
22202214
) -> Json<WindowGeometryOutput>
22212215
where
2222-
F: FnOnce(u64) -> Fut,
2216+
F: FnOnce(crate::windowing::WindowInfo) -> Fut,
22232217
Fut: Future<Output = Result<String>>,
22242218
{
22252219
let windows = match list_windows().await {
@@ -2229,29 +2223,31 @@ impl ComputerUseLinux {
22292223
return Json(WindowGeometryOutput {
22302224
ok: false,
22312225
implemented: true,
2232-
backend: crate::windowing::GNOME_SHELL_EXTENSION_BACKEND.to_string(),
2226+
backend: "unknown".to_string(),
22332227
window: None,
22342228
message: format!("Window listing failed: {error}"),
22352229
permissions_hint: window_permission_hint(&error),
22362230
received,
22372231
});
22382232
}
22392233
};
2240-
let window_id = match resolve_window_target(&windows, target) {
2241-
Ok(window) => window.window_id,
2234+
let window = match resolve_window_target(&windows, target) {
2235+
Ok(window) => window.clone(),
22422236
Err(error) => {
22432237
return Json(WindowGeometryOutput {
22442238
ok: false,
22452239
implemented: true,
2246-
backend: crate::windowing::GNOME_SHELL_EXTENSION_BACKEND.to_string(),
2240+
backend: "unknown".to_string(),
22472241
window: None,
22482242
message: format!("{error:#}"),
22492243
permissions_hint: None,
22502244
received,
22512245
});
22522246
}
22532247
};
2254-
match op(window_id).await {
2248+
let backend = window.backend.clone();
2249+
let window_id = window.window_id;
2250+
match op(window).await {
22552251
Ok(message) => {
22562252
// Re-query so the caller sees the compositor-final geometry
22572253
// (tiling constraints, minimum sizes, etc. may adjust it).
@@ -2269,7 +2265,7 @@ impl ComputerUseLinux {
22692265
Json(WindowGeometryOutput {
22702266
ok: true,
22712267
implemented: true,
2272-
backend: crate::windowing::GNOME_SHELL_EXTENSION_BACKEND.to_string(),
2268+
backend,
22732269
window,
22742270
message,
22752271
permissions_hint: None,
@@ -2281,7 +2277,7 @@ impl ComputerUseLinux {
22812277
Json(WindowGeometryOutput {
22822278
ok: false,
22832279
implemented: true,
2284-
backend: crate::windowing::GNOME_SHELL_EXTENSION_BACKEND.to_string(),
2280+
backend,
22852281
window: None,
22862282
permissions_hint: window_permission_hint(&error),
22872283
message: error,

src/windowing/backends/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ pub mod gnome;
33
pub mod hyprland;
44
pub mod i3;
55
pub mod kwin;
6+
pub mod x11;

0 commit comments

Comments
 (0)