sleep-stopper: A simple GTK4 app to stop Linux from sleeping.
Hello,
I came up with a simple GUI for stopping Linux from sleeping when idle using Python for fun. This project came to be because of my comment:
https://social.linux.pizza/@shanmukhateja/115843701361619857
You can find GitHub link for this project below:
https://github.com/shanmukhateja/sleep-stopper/
Theory
We use DBus interface org.freedesktop.ScreenSaver on Session Bus to tell Linux to disallow going to sleep on idle.
This feature is very useful when waiting on long-running tasks like system updates, media playback, etc. It is already part of all major applications like web browsers, system tools, media players, etc. and now, this project is also part of that list :)
I wrote this in Python as I’m very comfortable with it but it should be doable in any programming language.
The idea is to have 2 buttons - block sleep and unblock sleep shown to user by checking whether we have a valid cookie. This is an int value provided to us by Inhibit method from DBus interface when our app has successfully requested sleep inhibition.
In order to request inhibiting sleep, we need to provide 2 arguments:
App Title
Shows the app calling for sleep inhibition to userReason
Shows a label for reason behind sleep inhibition for user’s understanding.
We will need the following Python packages for this project:
GTK4 (for GUI)
dbus-fast (for communicating with DBus)
Implementation
Note: This code is hacked together in about 3 hours and it’s only goal is to work as expected. It is not meant to deliver a polished experience.
Let’s look at app.py which contains the GUI and the main logic:
from dbus import SleepStopperDBusClient
import gi
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk
class SleepStopperApplication(Gtk.ApplicationWindow):
def __init__(self, **kwargs):
super().__init__(**kwargs, title="Sleep Stopper")
self.set_size_request(300, 150)
self.vbox = Gtk.Box()
self.set_child(self.vbox)
self.dbus = SleepStopperDBusClient()
self.block_button = Gtk.Button.new_with_label("Block sleep")
self.unblock_button = Gtk.Button.new_with_label("Unblock Sleep")
self.block_button.set_hexpand(True)
self.unblock_button.set_hexpand(True)
self.vbox.append(self.block_button)
self.vbox.append(self.unblock_button)
self.block_button.connect("clicked", self.on_block_clicked)
self.unblock_button.connect("clicked", self.on_unblock_clicked)
self.update_ui_state()
def on_block_clicked(self, _btn):
result = self.dbus.inhibit()
if result:
# show toast
pass
else:
# show error toast
pass
self.update_ui_state()
def on_unblock_clicked(self, _btn):
result = self.dbus.uninhibit()
if result:
# show toast
pass
else:
# show error toast
pass
self.update_ui_state()
def update_ui_state(self):
is_active = self.dbus.is_active()
if is_active:
# Inhibit not on?
self.block_button.set_visible(True)
self.unblock_button.set_visible(False)
else:
# Inhibit IS SET!!
self.block_button.set_visible(False)
self.unblock_button.set_visible(True)
def on_activate(app):
win = SleepStopperApplication(application=app)
win.present()
app = Gtk.Application(application_id="in.suryatejak.sleepstopper")
app.connect("activate", on_activate)
app.run(None)
This is a pretty straight-forward Python code. We initialize the app, create a GtkWindow and show 2 buttons as discussed above.
The GUI logic is driven by a handy function update_ui_state which talks to our DBus client SleepStopperDBusClient (see dbus.py for its code).
I initially planned to show a Toast message of sorts but I realized that means more work which is not necessary for a simple tool.
Now, let’s look at dbus.py which sets up communication with DBus interface and provides handy functions for GUI to use.
dbus.py
import asyncio
from dbus_fast.aio import MessageBus
class SleepStopperDBusClient(object):
def __init__(self):
self.cookie = None
self.runner = asyncio.get_event_loop()
self.runner.run_until_complete(self.init())
async def init(self):
self.bus = await MessageBus().connect()
introspection = await self.bus.introspect('org.freedesktop.ScreenSaver', '/org/freedesktop/ScreenSaver')
self.obj = self.bus.get_proxy_object('org.freedesktop.ScreenSaver', '/org/freedesktop/ScreenSaver', introspection)
self.screensaver = self.obj.get_interface('org.freedesktop.ScreenSaver')
def is_active(self):
return self.cookie is None
def inhibit(self):
self.runner.run_until_complete(self._set_inhibit())
return True
def uninhibit(self):
if not self.cookie:
return False
self.runner.run_until_complete(self._set_uninhibit())
async def _set_inhibit(self):
self.cookie = await self.screensaver.call_inhibit("Sleep Stopper", "User requested inhibit!")
print(self.cookie)
async def _set_uninhibit(self):
if not self.cookie:
print("self.cookie not set, ignoring request..")
return False
await self.screensaver.call_un_inhibit(self.cookie)
self.cookie = None
return True
Here, we grab a reference to org.freedesktop.ScreenSaver from DBus so that we can call Inhibit and UnInhibit methods.
In order to make GTK work with async-await, I grabbed instance of asyncio’s Event loop in self.runner and use run_until_complete function to call the DBus methods.
You may have noticed that I have added logic to ignore UnInhibit requests if
self.cookiedoesn’t exist and I feel this is probably unnecessary.If an app requests to inhibit sleep BUT after sometime, it is either killed or the process exits without un-inhibiting sleep, the sleep inhibition request is gone.
Pretty cool, isn’t it?
Screenshots
Launch screen

Sleep inhibit is active

KDE Plasma shows inhibit request along with the app title and reason.

Conclusion
I hope this was informative :)
I built this tool for fun - to simply stretch my wings by building simple stuff. I wrote all this in around 3 hours (~2.5hrs) and I was fun project.
The challenge in here, was to code that dealt with separating synchronous code with async-await stuff. My current workaround works because I do not do anything complex with the return result.
Anyway, I hope you liked this post. Please give it a Like to show your appreciation. If you feel there’s something I missed or can do better, let me know in the comments.
Follow me on Mastodon.
Bye for now :)