Creating a Custom Bluetooth Utility for Linux Systray
Creating a custom bluetooth utility for linux systray
Suyash Singh
Posted by Suyash Singh
on November 24, 2022
Photo by Onur on Unsplash

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 uses dbusmenu to publish context menus over dbus. It is based on a protocol that was designed in the KDE 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.