Skip to main content

Command Palette

Search for a command to run...

sleep-stopper: A simple GTK4 app to stop Linux from sleeping.

Updated
4 min read

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:

  1. App Title
    Shows the app calling for sleep inhibition to user

  2. Reason
    Shows a label for reason behind sleep inhibition for user’s understanding.

We will need the following Python packages for this project:

  1. GTK4 (for GUI)

  2. 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.cookie doesn’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-stopper startup UI

Sleep inhibit is active

sleep-stopper "Unblock Sleep" button is shown as sleep inhibit is active.

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

KDE Plasma power management UI showing sleep inhibit request by sleep-stopper app

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 :)