159 lines
5.2 KiB
Python
Executable File
159 lines
5.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# description: Capture screenshots via KWin ScreenShot2 D-Bus (Wayland-safe). Args: [active|screen|area|window] [--output=path] [--screen=n]
|
|
"""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 argparse
|
|
import subprocess
|
|
import tempfile
|
|
|
|
SERVICE = "org.kde.KWin"
|
|
OBJ_PATH = "/org/kde/KWin/ScreenShot2"
|
|
IFACE = "org.kde.KWin.ScreenShot2"
|
|
|
|
|
|
def parse_args():
|
|
p = argparse.ArgumentParser(description="KDE screenshot via KWin D-Bus")
|
|
p.add_argument("mode", nargs="?", default="active",
|
|
choices=["active", "screen", "area", "window"],
|
|
help="Capture mode (default: active)")
|
|
p.add_argument("--output", "-o", default="",
|
|
help="Output file path (default: /tmp/ollie_screenshot_<ts>.png)")
|
|
p.add_argument("--screen", type=int, default=0,
|
|
help="Screen index for 'screen' mode")
|
|
p.add_argument("--title", default="",
|
|
help="Window title substring for 'window' mode")
|
|
p.add_argument("--include-cursor", action="store_true", default=False)
|
|
p.add_argument("--include-decoration", action="store_true", default=True)
|
|
return p.parse_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()
|