HeadsetControl GUI

June 6, 2026 · View on GitHub

A Qt6 GUI for the HeadsetControl CLI, with a control window and a system-tray icon.

This is a personal project: I wanted to control my headset from a GUI window and the system tray instead of the terminal. The scope is intentionally small - it only implements the features my headset (a Corsair VOID Elite Wireless) supports: sidetone, lights, battery, and notification sounds. Each control is shown only if the connected device reports that capability, so a headset missing one of these just won't display it - but the app doesn't cover HeadsetControl's other features (equalizer, inactive time, chat-mix, etc.). I only set out to cover what I needed.

Built with Python/PySide6, made for KDE Plasma (Wayland), though the control window runs on any desktop.

Screenshots

Control window and tray menu with a headset connected Window and tray menu when the headset is disconnected
Connected: controls and battery, with the minimal tray menu Disconnected: controls grey out automatically

Requirements

  • headsetcontrol installed and on your PATH (install guide) - this app is a front-end and shells out to it. The udev rules HeadsetControl ships are needed for USB access.
  • For running from source: Python 3.10+ with PySide6 (pip install PySide6).
  • The prebuilt AppImage bundles Python and Qt, so it only needs headsetcontrol present.

Install / Run

From source:

python -m headsetcontrol_gui
# or
./run.sh

AppImage (no Python/Qt needed, just headsetcontrol):

Download the AppImage for your CPU architecture from the Releases page - x86_64 for most PCs, aarch64 for ARM64 (e.g. a Raspberry Pi or ARM laptop):

chmod +x HeadsetControl-GUI-x86_64.AppImage
./HeadsetControl-GUI-x86_64.AppImage

(Running an AppImage may require fuse2; if it won't start, run it with --appimage-extract-and-run.)

Only one instance runs at a time - launching it again just raises the existing window instead of starting a second copy (multiple copies would fight over the USB device and make controls flaky).

The window and the tray

Left-click the tray icon (or right-click -> Open) to open the control window. The tray menu itself is intentionally minimal - battery, Open, Quit - because Plasma's tray menu (DBusMenu) can't host widgets like sliders. All the real controls live in the window:

  • Sidetone slider, shown as 0-100%
  • Lights on/off
  • Notification sound buttons
  • Show tray icon toggle
  • Re-apply saved settings on startup toggle

When does it quit vs. stay running?

The app only lives in the background when the system tray is active (the "Show tray icon" toggle is on and your desktop has a tray):

  • Tray active: closing the window hides it to the tray; the app keeps running. --hidden starts straight in the tray with no window.
  • Tray not active: closing the window quits the app, and launching with --hidden simply exits (there's nothing to show and nowhere to hide).

Settings persistence & autostart

Your sidetone level, lights state, and the two toggles are saved with QSettings (in ~/.config/headsetcontrol-gui/) and persist across restarts and reboots.

Headsets don't keep their sidetone/lights settings when powered off, and the CLI can't read the current values back - so the app re-applies your saved sidetone and lights whenever the headset connects (at startup and on every reconnect). So if you autostart it, your configuration is restored to the headset automatically on boot, and again each time you power the headset on.

Autostart on KDE Plasma: add it in System Settings -> Autostart -> Add -> Application, pointing at the AppImage (or run.sh). Pass the --hidden flag so it starts quietly in the tray instead of opening the window on every login.

Other desktops / generic XDG autostart: copy the desktop entry (it already includes --hidden):

cp headsetcontrol-gui.desktop ~/.config/autostart/

Autostart-with---hidden relies on the tray to keep running. If you disable the tray (or are on a desktop without one - see below), use a normal launch instead.

Desktop compatibility

  • Control window: works on any desktop - Wayland or X11, KDE, GNOME, XFCE, etc. (the build bundles both the Wayland and X11 Qt platform plugins).
  • Tray icon: uses StatusNotifierItem.
    • KDE Plasma: native. ✅
    • X11 desktops (XFCE, MATE, Cinnamon, ...): works via the system tray. ✅
    • GNOME: GNOME removed tray support, so you need the AppIndicator extension. Without it there's no tray icon - the app still works as a normal window, and the "Show tray icon" toggle is greyed out.

How it works

The CLI is the backend. State is read with headsetcontrol -o json (battery, capabilities, connection); settings are applied with -s (sidetone), -l (lights), and -n (notification sound). The UI is built dynamically from the device's reported capabilities, so headsets without a given feature simply don't show that control.

Building the AppImage yourself

bash packaging/build_appimage.sh
# -> dist/HeadsetControl-GUI-<arch>.AppImage  (matches your machine)

It uses PyInstaller to bundle the app + Python + Qt, lays out an AppDir, and packs it with appimagetool. The script builds for the host architecture; CI builds both x86_64 and aarch64 on native runners.

Releases / CI

.github/workflows/build.yml builds the x86_64 and aarch64 AppImages on every push and pull request (uploaded as run artifacts). Pushing a version tag also publishes a GitHub Release with both AppImages attached:

git tag v0.1.0
git push origin v0.1.0

Contributing

This is a small personal project, but feel free to fork it and adapt it to your needs - or open a PR to add better support for other HeadsetControl-supported headsets and their features. It's released into the public domain (see below), so do whatever you like with it.

Layout

  • headsetcontrol_gui/backend.py - CLI wrapper (read state, set sidetone/lights/notification)
  • headsetcontrol_gui/window.py - the control window (sliders, toggles, battery icon)
  • headsetcontrol_gui/tray.py - tray icon + minimal menu, app controller
  • headsetcontrol_gui/__main__.py - entry point + single-instance guard
  • packaging/ - AppImage build script, launcher, desktop entry, icon
  • .github/workflows/build.yml - CI: build on push/PR, release on tags

License

Released into the public domain under the Unlicense - no attribution required, do whatever you want with it.