170 lines
5.5 KiB
Python
Executable File
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()
|