#!/usr/bin/env python3 # args_json: {"type":"object","required":["action"],"properties":{"action":{"type":"string","description":"list|focus|close|minimize|maximize|move|resize|desktops|desktop"},"title":{"type":"string","description":"Window title substring"},"x":{"type":"string","description":"X position (for move)"},"y":{"type":"string","description":"Y position (for move)"},"width":{"type":"string","description":"Width (for resize)"},"height":{"type":"string","description":"Height (for resize)"},"number":{"type":"string","description":"Desktop number (for desktop)"}}} # ollie:prompt # ## gui_windows # # KWin window management via D-Bus. # # **Args**: `[action, ...]` # - Actions: `list`, `focus`, `close`, `minimize`, `maximize`, `move`, `resize`, `desktops` # # ``` # gui_windows(action="list") # gui_windows(action="focus", title="Firefox") # ``` # ollie:end """Manage KDE windows via KWin D-Bus scripting interface. Commands: list - list all windows (title, class, geometry, desktop, state) focus - raise and focus window matching title close - close window matching title minimize - minimize window maximize - toggle maximize on window move - move window to position resize - resize window desktops - list virtual desktops desktop - switch to virtual desktop tile - tile window to half-screen """ import sys import subprocess import json import tempfile import os import time def kwin_script(js_code): """Execute a KWin script and return output.""" # Write script to temp file with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False) as f: f.write(js_code) script_path = f.name try: # Load the script load_result = subprocess.run([ "gdbus", "call", "--session", "--dest", "org.kde.KWin", "--object-path", "/Scripting", "--method", "org.kde.kwin.Scripting.loadScript", script_path, "ollie_gui_windows" ], capture_output=True, text=True, timeout=5) if load_result.returncode != 0: print(f"ERROR: loadScript failed: {load_result.stderr}", file=sys.stderr) return None # Parse script ID from "(int32 N,)" script_id = load_result.stdout.strip().strip("()").split(",")[0].replace("int32 ", "") # Run the script run_result = subprocess.run([ "gdbus", "call", "--session", "--dest", "org.kde.KWin", "--object-path", f"/Scripting/Script{script_id}", "--method", "org.kde.kwin.Script.run" ], capture_output=True, text=True, timeout=5) # Give script time to execute (async) time.sleep(0.3) # Read output from KWin's journal/stdout - scripts use print() # Unfortunately KWin script output goes to systemd journal, not easily capturable. # For listing we'll use wmctrl/qdbus instead. # Unload subprocess.run([ "gdbus", "call", "--session", "--dest", "org.kde.KWin", "--object-path", "/Scripting", "--method", "org.kde.kwin.Scripting.unloadScript", "ollie_gui_windows" ], capture_output=True, text=True, timeout=5) return True finally: os.unlink(script_path) def list_windows_wmctrl(): """List windows using wmctrl (works on X11 and XWayland).""" try: result = subprocess.run( ["wmctrl", "-l", "-G", "-p"], capture_output=True, text=True, timeout=5 ) if result.returncode == 0: return result.stdout except FileNotFoundError: pass return None def list_windows_qdbus(): """List windows using KWin's D-Bus interface for Wayland.""" # Use KWin scripting to dump window list to a known file out_file = "/tmp/ollie_winlist.txt" js = f""" var clients = workspace.windowList(); var lines = []; for (var i = 0; i < clients.length; i++) {{ var c = clients[i]; if (c.skipTaskbar || c.specialWindow) continue; var geo = c.frameGeometry; var state = []; if (c.minimized) state.push('minimized'); if (c.maximized) state.push('maximized'); if (c.fullScreen) state.push('fullscreen'); if (c.active) state.push('active'); lines.push( c.caption + '\\t' + c.resourceClass + '\\t' + geo.x + ',' + geo.y + ',' + geo.width + ',' + geo.height + '\\t' + 'desktop:' + c.desktop + '\\t' + state.join(',') ); }} callDBus('org.ollie.SessionManager', '/org/ollie/SessionManager', 'org.ollie.SessionManager', 'SetConfig', 'winlist', lines.join('\\n')); """ # Alternative: write to file via script. But KWin scripts are sandboxed. # Use qdbus to get window list if available try: result = subprocess.run( ["qdbus6", "org.kde.KWin", "/KWin", "org.kde.KWin.queryWindowInfo"], capture_output=True, text=True, timeout=5 ) if result.returncode == 0: return result.stdout except FileNotFoundError: try: result = subprocess.run( ["qdbus", "org.kde.KWin", "/KWin", "org.kde.KWin.queryWindowInfo"], capture_output=True, text=True, timeout=5 ) if result.returncode == 0: return result.stdout except FileNotFoundError: pass return None def list_windows(): """List windows using best available method.""" # Try wmctrl first (most parseable) result = list_windows_wmctrl() if result: print("ID\tDESKTOP\tPID\tX\tY\tW\tH\tTITLE") for line in result.strip().split("\n"): parts = line.split(None, 7) if len(parts) >= 8: wid, desktop, pid, x, y, w, h = parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], parts[6] title = parts[7] if len(parts) > 7 else "" # Strip hostname prefix from title if " " in title: title = title.split(" ", 1)[1] if title.split(" ", 1)[0].isalpha() else title print(f"{wid}\t{desktop}\t{pid}\t{x}\t{y}\t{w}\t{h}\t{title}") return # Try xdotool try: result = subprocess.run( ["xdotool", "search", "--onlyvisible", "--name", ""], capture_output=True, text=True, timeout=5 ) if result.returncode == 0 and result.stdout.strip(): print("ID\tTITLE") for wid in result.stdout.strip().split("\n"): name_result = subprocess.run( ["xdotool", "getwindowname", wid], capture_output=True, text=True, timeout=2 ) name = name_result.stdout.strip() if name_result.returncode == 0 else "?" print(f"{wid}\t{name}") return except FileNotFoundError: pass print("ERROR: no window listing tool available (install wmctrl or xdotool)", file=sys.stderr) sys.exit(1) def find_window(title_substr): """Find window ID by title substring.""" try: result = subprocess.run( ["wmctrl", "-l"], capture_output=True, text=True, timeout=5 ) if result.returncode == 0: for line in result.stdout.strip().split("\n"): if title_substr.lower() in line.lower(): return line.split()[0] except FileNotFoundError: pass try: result = subprocess.run( ["xdotool", "search", "--name", title_substr], capture_output=True, text=True, timeout=5 ) if result.returncode == 0 and result.stdout.strip(): return result.stdout.strip().split("\n")[0] except FileNotFoundError: pass return None def focus_window(title_substr): wid = find_window(title_substr) if not wid: print(f"ERROR: no window matching '{title_substr}'", file=sys.stderr) sys.exit(1) try: subprocess.run(["wmctrl", "-i", "-a", wid], check=True, timeout=5) except (FileNotFoundError, subprocess.CalledProcessError): try: subprocess.run(["xdotool", "windowactivate", wid], check=True, timeout=5) except (FileNotFoundError, subprocess.CalledProcessError) as e: print(f"ERROR: {e}", file=sys.stderr) sys.exit(1) print(f"focused: {wid}") def close_window(title_substr): wid = find_window(title_substr) if not wid: print(f"ERROR: no window matching '{title_substr}'", file=sys.stderr) sys.exit(1) try: subprocess.run(["wmctrl", "-i", "-c", wid], check=True, timeout=5) except (FileNotFoundError, subprocess.CalledProcessError): try: subprocess.run(["xdotool", "windowclose", wid], check=True, timeout=5) except (FileNotFoundError, subprocess.CalledProcessError) as e: print(f"ERROR: {e}", file=sys.stderr) sys.exit(1) print(f"closed: {wid}") def minimize_window(title_substr): wid = find_window(title_substr) if not wid: print(f"ERROR: no window matching '{title_substr}'", file=sys.stderr) sys.exit(1) try: subprocess.run(["xdotool", "windowminimize", wid], check=True, timeout=5) except (FileNotFoundError, subprocess.CalledProcessError): # KWin script fallback kwin_script(f""" var clients = workspace.windowList(); for (var i = 0; i < clients.length; i++) {{ if (clients[i].caption.toLowerCase().includes('{title_substr.lower()}')) {{ clients[i].minimized = true; break; }} }} """) print(f"minimized: {wid}") def maximize_window(title_substr): # KWin script is the clean way kwin_script(f""" var clients = workspace.windowList(); for (var i = 0; i < clients.length; i++) {{ if (clients[i].caption.toLowerCase().includes('{title_substr.lower()}')) {{ clients[i].setMaximize(!clients[i].maximized, !clients[i].maximized); break; }} }} """) print(f"toggled maximize: {title_substr}") def move_window(title_substr, x, y): wid = find_window(title_substr) if not wid: print(f"ERROR: no window matching '{title_substr}'", file=sys.stderr) sys.exit(1) try: subprocess.run(["wmctrl", "-i", "-r", wid, "-e", f"0,{x},{y},-1,-1"], check=True, timeout=5) except (FileNotFoundError, subprocess.CalledProcessError): try: subprocess.run(["xdotool", "windowmove", wid, str(x), str(y)], check=True, timeout=5) except (FileNotFoundError, subprocess.CalledProcessError) as e: print(f"ERROR: {e}", file=sys.stderr) sys.exit(1) print(f"moved {wid} to {x},{y}") def resize_window(title_substr, w, h): wid = find_window(title_substr) if not wid: print(f"ERROR: no window matching '{title_substr}'", file=sys.stderr) sys.exit(1) try: subprocess.run(["wmctrl", "-i", "-r", wid, "-e", f"0,-1,-1,{w},{h}"], check=True, timeout=5) except (FileNotFoundError, subprocess.CalledProcessError): try: subprocess.run(["xdotool", "windowsize", wid, str(w), str(h)], check=True, timeout=5) except (FileNotFoundError, subprocess.CalledProcessError) as e: print(f"ERROR: {e}", file=sys.stderr) sys.exit(1) print(f"resized {wid} to {w}x{h}") def list_desktops(): try: result = subprocess.run( ["wmctrl", "-d"], capture_output=True, text=True, timeout=5 ) if result.returncode == 0: print(result.stdout.rstrip()) return except FileNotFoundError: pass # Fallback via KWin D-Bus try: result = subprocess.run([ "gdbus", "call", "--session", "--dest", "org.kde.KWin", "--object-path", "/VirtualDesktopManager", "--method", "org.freedesktop.DBus.Properties.Get", "org.kde.KWin.VirtualDesktopManager", "desktops" ], capture_output=True, text=True, timeout=5) if result.returncode == 0: print(result.stdout.rstrip()) return except FileNotFoundError: pass print("ERROR: cannot list desktops", file=sys.stderr) sys.exit(1) def switch_desktop(num): try: subprocess.run(["wmctrl", "-s", str(int(num) - 1)], check=True, timeout=5) print(f"switched to desktop {num}") except (FileNotFoundError, subprocess.CalledProcessError): try: subprocess.run([ "gdbus", "call", "--session", "--dest", "org.kde.KWin", "--object-path", "/VirtualDesktopManager", "--method", "org.kde.KWin.VirtualDesktopManager.setCurrent", str(num) ], check=True, capture_output=True, timeout=5) print(f"switched to desktop {num}") except Exception as e: print(f"ERROR: {e}", file=sys.stderr) sys.exit(1) def main(): sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _lib.args import parse_args as _parse args = _parse() cmd = args.require("action") if cmd == "list": list_windows() elif cmd == "focus": focus_window(args.require("title")) elif cmd == "close": close_window(args.require("title")) elif cmd == "minimize": minimize_window(args.require("title")) elif cmd == "maximize": maximize_window(args.require("title")) elif cmd == "move": move_window(args.require("title"), args.require("x"), args.require("y")) elif cmd == "resize": resize_window(args.require("title"), args.require("width"), args.require("height")) elif cmd == "desktops": list_desktops() elif cmd == "desktop": switch_desktop(args.require("number")) else: print(f"unknown command: {cmd}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()