In this post let’s see how we can utilize GTK to create our own custom systray utility in linux.
Prerequisite #1 - libappindicator-gtk
libappindicator
is a library introduced to help with application side changes; it does register icons and menus and internally usesdbusmenu
to publish context menus overdbus
. It is based on a protocol that was designed in theKDE
project to renovate the systray protocol.libappindicator
adopts and extends this protocol, and connects it with dbusmenu to provide the full set of service that an application will require to migrate its code
In Arch Linux the gtk version of this library can be installed by the following command:
sudo pacman -S libappindicator-gtk
Prerequisite #2 - Python & GTK
Make sure you have python3 and GTK installed for creating GTK applications in python. Follow along these docs to set it up.
Prerequisite #3 - Bluez package
The utility which we will be building in this post will list the connected bluetooth devices and the corresponding battery levels in a little menu list. Querying the battery levels is achieved by using the experimental features provided by the bluez
library.
Additionally we will be querying our bluetooth devices using bluetoothctl which is part of the bluez-utils
package.
Speaking for Arch linux here, these packages are listed on the Arch Wiki for bluetooth and can be installed by the following command:
sudo pacman -S bluez bluez-utils
If you are using some other distro, find the correct names for all these packages accordingly.
Prerequisite #4 - Enable bluez experimental features
The experimental features which are necessary for querying battery levels of the bluetooth devices can be enabled by setting the Experimental = true
flag in the /etc/bluetooth/main.conf
file.
Prerequisite #5 - Dbus
In favorable likelihoods, dbus would be already present on your system if you use an init system like systemd. If this isn’t the case, feel free to swap dbus in the code below with something else. All we want to use dbus for is to communicate with the bluez daemon.
Python code for the utility
Now, finally we can code up our systray utility in python. A sample hello world python application to show battery levels of the connected bluetooth devices, by no means production grade, would look like this:
#!/usr/bin/python3
import os
import gi
import subprocess
import time
from threading import Thread
#required to avoid conflicts with other gtk versions on the system
gi.require_version('Gtk', '3.0')
gi.require_version('AppIndicator3', '0.1')
from gi.repository import Gtk as gtk, AppIndicator3 as appindicator
def get_connected_devices():
devices = subprocess.check_output("bluetoothctl devices Connected", shell=True).strip().decode()
devices = devices.split("\n")
connected_devices = []
for d in devices:
d = d.split(' ')
devId = d[1].replace(':','_')
connected_devices.append((devId, 0, ''.join(d[2:])))
return connected_devices
# utilizes dbus to query bluez for battery level
def get_battery_level(devId):
level = subprocess.check_output(f'dbus-send --print-reply=literal --system --dest=org.bluez /org/bluez/hci0/dev_{devId} org.freedesktop.DBus.Properties.Get string:"org.bluez.Battery1" string:"Percentage" | awk \'{{print $3}}\'', shell=True).strip().decode()
return level
# creates the tray icon
def create_indicator():
indicator = appindicator.Indicator.new("bluetooth-active", "bluetooth", appindicator.IndicatorCategory.APPLICATION_STATUS)
indicator.set_status(appindicator.IndicatorStatus.ACTIVE)
return indicator
def refresh_connected_devices():
indicator = create_indicator()
while True:
connected_devices = get_connected_devices()
for i, (devId, battery, devName) in enumerate(connected_devices):
battery_level = get_battery_level(devId)
connected_devices[i] = (devId, battery_level, devName)
indicator.set_menu(create_bluetooth_menu(connected_devices))
time.sleep(10)
# we are going to run the menu subroutine in a separate thread to refresh the list when devices connect/disconnect
# TODO: find other means to refresh the menu
# TODO: handle scenario when there is no bluetooth devices found
def main():
thread = Thread(target=refresh_connected_devices)
thread.start()
gtk.main()
def create_bluetooth_menu(devices):
menu = gtk.Menu()
for devId, battery, devName in devices:
command_one = gtk.MenuItem()
command_one.set_label(f'{devName} ({battery}%)')
menu.append(command_one)
menu.show_all()
return menu
if __name__ == "__main__":
main()
Possibilities
The possibilities are endless, to list a few:
- Hook up playerctl and MPRIS to control your browser media from the systray
- Create your own systray utility to control your VPN solution in place directly from the systray if you can’t find an existing solution for it
- Have better access to control your sound sinks, microphones, cameras etc.
- Pick the xdotool automation task to perform from a favorites list if you have plentiful of xdotool automation in your setup
- Control your home automation right from your taskbar
Long story short, your user flows for any app that you use on a regular basis can be optimized with a condensed set of actions present in the systray. Powerful.