Almost four years ago, I explained how I automated taking screenshots of video games on Windows. I integrated this code into a GUI two years later to simplify starting and stopping the capture. It was not a pretty application, but it did the job. Since I resurrected my Linux experiment a while ago, gaming on Linux and even the general day-to-day use of Linux have caught up to the point where it is a serious contender for daily driving. Therefore, I was looking for a way to automate taking a screenshot while I am gaming on Linux. My previous approach would not work since it relied on Windows’ infamous WinAPI, and Wine wasn’t something I wanted to dabble with for something so small.
My initial idea was to use a Wayland API to do something low-level. It seems like this is impossible by design in the name of security. I want to substantiate this with links to official statements or documentation. However, I could only find user messages in various forums and SO-like services saying precisely what I just did, but without providing a source.
The most viable solution I found was using DBus and calling into the XDG Desktop Portal to capture the screen. From my understanding, the desktop environment’s compositor implements this specification and serves the request, e.g., Gnome’s Mutter.
The solution I present here is based on this StackOverflow response. All of the credit goes to that user. I added a bit of context and explanation in this blog post. Note that this is not a DBus tutorial, although I implicitly tackle some core concepts when explaining the code. I would direct you to the Freedesktop tutorial on DBus for a high-level overview. I am not a DBus specialist, and some aspects still elude me.
The complete example code is in my GitHub repository. I only show the bare minimum here for the explanations.
Preparations
DBus requires an application loop for its event processing. There are several integrations available, of which my example uses GLib. This part is explained in the Python DBus tutorial.
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
loop = Glib.MainLoop()
To get everything running, you start the loop.
loop.run()
You do not want to do this immediately, however. First, more setup is required to prepare all the necessary infrastructure. The run() function is blocking, so you want to have everything ready before calling it.
The first thing required is a connection to the user’s session bus. See this SO answer for a very brief explanation of what it is if you have not read the tutorial.
bus = dbus.SessionBus()
The next step is to get a proxy for the remote Desktop portal. This proxy allows for invoking several functions, one of which is Screenshot().
desktop_proxy = bus.get_object(
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop"
)
Before invoking the Screenshot() function, you require a handler function to process the responses. For that, you create a unique response path based on the unique name of your application and a token. See the Request Interface documentation for more information.
Here’s how to create the response path. I used a randomly generated UUID, replacing the hyphens with underscores. Hyphens are not allowed by DBus.
my_name = bus.get_connection().get_unique_name()[1:].replace(".", "_")
response_path = f"/org/freedesktop/portal/desktop/request/{my_name}/{dbus_token}"
Now, you can register a signal receiver on the bus.
bus.add_signal_receiver(
self.response_handler,
dbus_interface = "org.freedesktop.portal.Request",
path = response_path,
)
There is also a way to register a handler on the exact proxy instead of the general bus. I have not figured out the correct parameters, though.
Taking The Shot
Now, you can invoke the Screenshot() function on the proxy. This method is a bit of voodoo. I am unsure where the first parameter, “Screenshot,” is defined. Maybe it is the “second Screenshot” in this interface description (“org.freedesktop.portal.Screenshot.Screenshot“)?
The object in the second parameter contains the unique token that is a part of the response path.
desktop_proxy.Screenshot(
"Screenshot",
{"handle_token": dbus_token},
dbus_interface="org.freedesktop.portal.Screenshot"
)
The most simple handler can be as follows. The documentation lists three potential values for “response”. The contents of “result” depend on the type of request. For a screenshot, only “uri” is returned. This is mentioned in the interface description of Screenshot.
def response_handler(response, result):
if response == 0:
print(f'Screenshot file: {result.get("uri")}')
else:
print("Failed to get screenshot")
Famous Last Words
I wrapped all this in a DBusScreenshotLoop class that executes a screenshot request every five minutes. I trigger the request with the GLib.timeout_add_seconds() function. Since my script is also a CLI-only implementation, I added a handler for CTRL-C that properly stops the main loop.
class DBusScreenshotLoop:
def __init__(self, screenshot_location):
self.screenshot_location = screenshot_location
self.dbus_token = "7fc46846_4d73_416e_95c6_5b4d6ed68492"
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
self.loop = GLib.MainLoop()
signal.signal(signal.SIGINT, self.sigint_handler)
def sigint_handler(self, sig, frame):
if signal.SIGINT == sig:
self.loop.quit()
As mentioned earlier, the complete example is available in my GitHub repository. It works well enough for me. The only improvements I could make now are the input parameters for the time interval and the target location.
The Gnome desktop will ask for permission once the script is invoked for the first time. The screen will also always flash white when a screenshot is taken. KDE Plasma did not ask for permission and showed no visual feedback.
The script does not expect any command line arguments. Instead, the two relevant values for manipulation are defined at the top of the script.
INTERVAL_SECONDS = 60 * 5
SCREENSHOT_LOCATION = "/home/rlo/Pictures/Screenshots"
Execute the script the following way.
python3 dbus-screenshot.py
I hope this was helpful.
Thank you for reading.