Skip to content

Instantly share code, notes, and snippets.

@3v1n0
Last active June 26, 2026 11:22
Show Gist options
  • Select an option

  • Save 3v1n0/3140f9c21a31ce6b894e86eb3812af55 to your computer and use it in GitHub Desktop.

Select an option

Save 3v1n0/3140f9c21a31ce6b894e86eb3812af55 to your computer and use it in GitHub Desktop.
Non-interactive script to take screenshots using XDG desktop portals
#!/usr/bin/env python3
#
# SPDX-License-Identifier: GPL-3.0-or-later
import sys
import os
import argparse
from gi.repository import Gio, GioUnix, GLib
APP_ID = "me.trevi.portal.screenshotter"
def _desktop_file_path():
data_home = os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
return os.path.join(data_home, "applications", f"{APP_ID}.desktop")
def _desktop_entry_content():
script_path = os.path.abspath(sys.argv[0])
return (
"[Desktop Entry]\n"
"Name=Screenshotter\n"
f"Exec=python3 {script_path}\n"
"Type=Application\n"
)
def ensure_desktop_file():
path = _desktop_file_path()
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as f:
f.write(_desktop_entry_content())
app_info = GioUnix.DesktopAppInfo.new(f"{APP_ID}.desktop")
if app_info is None:
print(f"Warning: desktop file at {path} not found by GLib", file=sys.stderr)
return path
def _delete_permission(conn):
params = GLib.Variant.new_tuple(
GLib.Variant.new_string("screenshot"),
GLib.Variant.new_string("screenshot"),
GLib.Variant.new_string(APP_ID),
)
try:
conn.call_sync(
"org.freedesktop.impl.portal.PermissionStore",
"/org/freedesktop/impl/portal/PermissionStore",
"org.freedesktop.impl.portal.PermissionStore",
"DeletePermission",
params,
None,
Gio.DBusCallFlags.NONE,
-1,
None,
)
except Exception:
pass
def _remove_desktop_file(desktop_path):
try:
os.remove(desktop_path)
except OSError:
pass
class ScreenshotterApp(Gio.Application):
def __init__(self):
super().__init__(
application_id=APP_ID,
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
)
self._created_desktop_file = None
def do_command_line(self, command_line):
parser = argparse.ArgumentParser(
description="Take a screenshot via xdg-desktop-portal",
)
parser.add_argument(
"-o", "--output", default="portal-screenshot.png",
help="Output file path",
)
args = parser.parse_args(command_line.get_arguments()[1:])
self.output_path = args.output
self._created_desktop_file = ensure_desktop_file()
self.activate()
return 0
def do_activate(self):
self.hold()
conn = self.get_dbus_connection()
try:
self._register(conn)
self._pre_authorize(conn)
self._take_screenshot(conn)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
self.release()
def do_shutdown(self):
conn = self.get_dbus_connection()
_delete_permission(conn)
_remove_desktop_file(self._created_desktop_file)
Gio.Application.do_shutdown(self)
def _register(self, conn):
empty_opts = GLib.Variant("a{sv}", {})
params = GLib.Variant.new_tuple(
GLib.Variant.new_string(APP_ID),
empty_opts,
)
conn.call_sync(
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.host.portal.Registry",
"Register",
params,
None,
Gio.DBusCallFlags.NONE,
-1,
None,
)
def _pre_authorize(self, conn):
perms = GLib.Variant.new_array(
GLib.VariantType.new("s"),
[GLib.Variant.new_string("yes")],
)
params = GLib.Variant.new_tuple(
GLib.Variant.new_string("screenshot"),
GLib.Variant.new_boolean(True),
GLib.Variant.new_string("screenshot"),
GLib.Variant.new_string(APP_ID),
perms,
)
conn.call_sync(
"org.freedesktop.impl.portal.PermissionStore",
"/org/freedesktop/impl/portal/PermissionStore",
"org.freedesktop.impl.portal.PermissionStore",
"SetPermission",
params,
None,
Gio.DBusCallFlags.NONE,
-1,
None,
)
def _take_screenshot(self, conn):
options = GLib.Variant("a{sv}", {
"interactive": GLib.Variant("b", False),
"target": GLib.Variant("u", 1),
})
params = GLib.Variant.new_tuple(
GLib.Variant.new_string(""),
options,
)
result = conn.call_sync(
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.Screenshot",
"Screenshot",
params,
GLib.VariantType.new("(o)"),
Gio.DBusCallFlags.NONE,
-1,
None,
)
handle = result[0]
def on_response(connection, sender, path, iface, signal, params):
response, results = params
if response == 0:
uri = results.get("uri")
if uri is None:
print("Error: no URI in response", file=sys.stderr)
else:
src = Gio.File.new_for_uri(uri)
dst = Gio.File.new_for_path(self.output_path)
src.copy(dst, Gio.FileCopyFlags.OVERWRITE, None, None, None)
print(self.output_path)
elif response == 1:
print("Screenshot cancelled", file=sys.stderr)
else:
print(f"Screenshot failed (response={response})", file=sys.stderr)
self.release()
conn.signal_subscribe(
None,
"org.freedesktop.portal.Request",
"Response",
handle,
None,
Gio.DBusSignalFlags.NONE,
on_response,
)
def main():
app = ScreenshotterApp()
app.run(sys.argv)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment