ollie/data/tools/gui_windows

406 lines
14 KiB
Python
Executable File

#!/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 <title_substr> - raise and focus window matching title
close <title_substr> - close window matching title
minimize <title_substr> - minimize window
maximize <title_substr> - toggle maximize on window
move <title_substr> <x> <y> - move window to position
resize <title_substr> <w> <h> - resize window
desktops - list virtual desktops
desktop <number> - switch to virtual desktop
tile <left|right|title_substr> - 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()