ollie/data/tools/gui_screenshot

170 lines
5.5 KiB
Python
Executable File

#!/usr/bin/env python3
# args_json: {"type":"object","properties":{"mode":{"type":"string","description":"active|screen|area|window (default: active)"},"output":{"type":"string","description":"Output file path"},"screen":{"type":"integer","description":"Screen index for screen mode"},"title":{"type":"string","description":"Window title substring for window mode"},"include_cursor":{"type":"boolean","description":"Include cursor in capture"},"include_decoration":{"type":"boolean","description":"Include window decoration (default: true)"}}}
# ollie:prompt
# ## gui_screenshot
#
# Capture screenshots via KWin (Wayland-safe).
#
# **Args**: `[mode]` or `[mode, "--output=path"]`
# - mode: `active`, `screen`, `area`, `window` (default: active)
#
# ```
# gui_screenshot(mode="active")
# gui_screenshot(mode="screen", output="--output=/tmp/shot.png")
# ```
# ollie:end
"""KDE screenshot tool using org.kde.KWin.ScreenShot2.
Modes:
active - capture the currently active window (default)
screen - capture entire screen (or specific with --screen=N)
area - capture a region (interactive picker)
window - capture window by title substring match
Output defaults to /tmp/ollie_screenshot_<timestamp>.png
"""
import sys
import os
import time
import subprocess
import tempfile
SERVICE = "org.kde.KWin"
OBJ_PATH = "/org/kde/KWin/ScreenShot2"
IFACE = "org.kde.KWin.ScreenShot2"
def parse_args():
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _lib.args import parse_args as _parse
_a = _parse()
class _Args:
mode = _a.get("mode", "active")
output = _a.get("output", "")
screen = _a.get_int("screen", 0)
title = _a.get("title", "")
include_cursor = _a.get_bool("include_cursor")
include_decoration = _a.get_bool("include_decoration", True)
return _Args()
def get_output_path(args):
if args.output:
return args.output
ts = int(time.time())
return f"/tmp/ollie_screenshot_{ts}.png"
def capture_with_spectacle(mode, output, title=""):
"""Fallback: use spectacle CLI if ScreenShot2 is unavailable."""
cmd = ["spectacle", "-b", "-n", "-o", output]
if mode == "active":
cmd.append("-a")
elif mode == "screen":
cmd.append("-f")
elif mode == "area":
cmd.append("-r")
elif mode == "window":
cmd.append("-a") # fall back to active window
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
if result.returncode != 0:
return None
return output
def find_window_id_by_title(title):
"""Use KWin scripting to find window UUID by title."""
script = f"""
const clients = workspace.windowList();
for (let i = 0; i < clients.length; i++) {{
if (clients[i].caption.toLowerCase().includes('{title.lower()}')) {{
print(clients[i].internalId);
break;
}}
}}
"""
# Load script via D-Bus
load_cmd = [
"gdbus", "call", "--session",
"--dest", "org.kde.KWin",
"--object-path", "/Scripting",
"--method", "org.kde.kwin.Scripting.loadScript", "", script
]
# This is complex; for now fall back to xdotool/wmctrl
try:
result = subprocess.run(
["xdotool", "search", "--name", title],
capture_output=True, text=True, timeout=5
)
if result.stdout.strip():
return result.stdout.strip().split("\n")[0]
except (FileNotFoundError, subprocess.TimeoutExpired):
pass
return None
def capture_via_dbus(mode, output, args):
"""Use KWin ScreenShot2 portal-style capture with pipe fd."""
hints = {
"include-cursor": args.include_cursor,
"include-decoration": args.include_decoration,
}
# ScreenShot2 methods use file descriptors which gdbus can't handle directly.
# Use the portal interface instead, or fall back to spectacle.
# Try the portal first:
portal_cmd = [
"gdbus", "call", "--session",
"--dest", "org.freedesktop.portal.Desktop",
"--object-path", "/org/freedesktop/portal/desktop",
"--method", "org.freedesktop.portal.Screenshot.Screenshot",
"", # parent_window
"{'interactive': <false>}",
]
# Spectacle is more reliable for non-interactive capture
result = capture_with_spectacle(mode, output, args.title)
if result and os.path.isfile(result):
return result
# Last resort: try import (ImageMagick) for X11
if os.environ.get("DISPLAY"):
if mode == "active":
wid = subprocess.run(
["xdotool", "getactivewindow"],
capture_output=True, text=True
).stdout.strip()
if wid:
r = subprocess.run(["import", "-window", wid, output],
capture_output=True, timeout=10)
if r.returncode == 0:
return output
elif mode == "screen":
r = subprocess.run(["import", "-window", "root", output],
capture_output=True, timeout=10)
if r.returncode == 0:
return output
return None
def main():
args = parse_args()
output = get_output_path(args)
result = capture_via_dbus(args.mode, output, args)
if result and os.path.isfile(result):
size = os.path.getsize(result)
print(f"{result} ({size} bytes)")
else:
print("ERROR: screenshot capture failed", file=sys.stderr)
print("Ensure spectacle or imagemagick is installed", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()