Last active
June 26, 2026 11:22
-
-
Save 3v1n0/3140f9c21a31ce6b894e86eb3812af55 to your computer and use it in GitHub Desktop.
Non-interactive script to take screenshots using XDG desktop portals
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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