-
Notifications
You must be signed in to change notification settings - Fork 37
Adding a Desktop Environment
Logitune uses per-application profiles that switch automatically when window focus changes. This requires desktop environment integration for focus tracking. This guide explains how to add support for a new DE.
All desktop integrations implement IDesktopIntegration (defined in src/core/interfaces/IDesktopIntegration.h):
class IDesktopIntegration : public QObject {
Q_OBJECT
public:
virtual void start() = 0;
virtual bool available() const = 0;
virtual QString desktopName() const = 0;
virtual QStringList detectedCompositors() const = 0;
virtual void blockGlobalShortcuts(bool block) = 0;
virtual QVariantList runningApplications() const = 0;
signals:
void activeWindowChanged(const QString &wmClass, const QString &title);
};| Method | Purpose | When Called |
|---|---|---|
start() |
Initialize focus tracking (install scripts, connect signals, start polling) | Once, after AppRoot::init() |
available() |
Return true if this DE is detected and usable | Checked before relying on DE features |
desktopName() |
Human-readable name (e.g., "KDE", "GNOME") | Logging and UI |
detectedCompositors() |
List of detected compositor names | Diagnostics |
blockGlobalShortcuts(bool) |
Temporarily disable global shortcuts during keystroke capture | During KeystrokeCapture QML component |
runningApplications() |
Return list of installed GUI applications | App profile picker dialog |
void activeWindowChanged(const QString &wmClass, const QString &title);This signal drives the entire profile switching system. wmClass must be a stable, unique identifier for the application. On KDE, this is the .desktop file's completeBaseName (e.g., org.kde.dolphin). On other DEs, it might be the X11 WM_CLASS or a Wayland app_id.
The KDE implementation (src/core/desktop/KDeDesktop.h/cpp) uses:
-
KWin Script — a JavaScript snippet loaded into KWin via D-Bus that calls back on
workspace.windowActivated -
D-Bus callback — the script calls
com.logitune.app /FocusWatcher focusChanged(resourceClass, title, desktopFileName) -
Desktop file resolution — maps
resourceClassto canonical.desktopfile baseName - kglobalaccel D-Bus — blocks global shortcuts during keystroke capture
The generic fallback (src/core/desktop/GenericDesktop.h/cpp) provides a minimal implementation. It is used when no specific DE is detected.
Create src/core/desktop/GnomeDesktop.h:
#pragma once
#include "interfaces/IDesktopIntegration.h"
#include <QDBusInterface>
#include <QTimer>
namespace logitune {
class GnomeDesktop : public IDesktopIntegration {
Q_OBJECT
public:
explicit GnomeDesktop(QObject *parent = nullptr);
void start() override;
bool available() const override;
QString desktopName() const override;
QStringList detectedCompositors() const override;
void blockGlobalShortcuts(bool block) override;
QVariantList runningApplications() const override;
private:
bool m_available = false;
QString m_lastAppId;
QTimer *m_pollTimer = nullptr;
void pollActiveWindow();
QString resolveAppId(const QString &wmClass) const;
};
} // namespace logituneNote — GNOME Wayland only. The shipped
GnomeDesktoprefuses to start on X11 sessions (XDG_SESSION_TYPE != "wayland"). The pattern below assumes Wayland; if you want X11 support, fall back to theGenericDesktoppolling loop.
The real GnomeDesktop uses an event-driven approach: a small GNOME Shell extension watches global.display::notify::focus-window and calls back into the app via D-Bus. No polling. The app auto-installs and enables the extension on first run, then registers com.logitune.app / /FocusWatcher on the session bus for the extension to call.
Key pieces (all already in-tree):
-
src/core/desktop/GnomeDesktop.{h,cpp}— the C++ integration class. SeeGnomeDesktop::start()for session-type check,ensureExtensionInstalled()for the install flow (system dir → user dir copy),detectAppIndicatorStatus()for tray-icon support detection viaorg.kde.StatusNotifierWatcher. -
data/gnome-extension/— the Shell extension source, with v42 and v45 variants for the two GNOME Shell extension API generations.
Create src/core/desktop/<DE>Desktop.cpp using the same shape:
#include "desktop/<DE>Desktop.h"
#include "logging/LogManager.h"
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusMessage>
#include <QProcessEnvironment>
namespace logitune {
<DE>Desktop::<DE>Desktop(QObject *parent)
: LinuxDesktopBase(parent)
{
}
void <DE>Desktop::start()
{
// 1. Sanity-check the session — bail if this DE isn't actually running
// or if the session type is wrong for your focus API.
// 2. If you need a Shell extension / KWin script / wlr protocol,
// install or activate it here.
// 3. Register your D-Bus callback so the DE-side agent can call
// back into the app with focus events. focusChanged() is
// inherited from LinuxDesktopBase and does the desktop-file
// resolution + duplicate suppression.
//
// See GnomeDesktop::start() for a complete worked example.
m_available = true;
}
bool GnomeDesktop::available() const
{
return m_available;
}
QString GnomeDesktop::desktopName() const
{
return QStringLiteral("GNOME");
}
QStringList GnomeDesktop::detectedCompositors() const
{
QStringList compositors;
const QString desktop = QProcessEnvironment::systemEnvironment()
.value(QStringLiteral("XDG_CURRENT_DESKTOP"));
if (desktop.contains(QStringLiteral("GNOME"), Qt::CaseInsensitive))
compositors << QStringLiteral("Mutter");
return compositors;
}
// focusChanged() is the D-Bus entry point the DE-side agent calls.
// LinuxDesktopBase provides resolveDesktopFile() + duplicate-event
// suppression, so subclasses only implement the DE-specific glue.
void <DE>Desktop::focusChanged(const QString &appId, const QString &title)
{
QString resolved = appId;
if (!appId.contains('.'))
resolved = resolveDesktopFile(appId);
if (resolved == m_lastAppId) return;
m_lastAppId = resolved;
emit activeWindowChanged(resolved, title);
}
// runningApplications() is inherited from LinuxDesktopBase — do not
// reimplement unless your DE has a faster dedicated API.
} // namespace logituneDifferent DEs offer different APIs for tracking window focus:
graph TB
subgraph "KDE Plasma"
KWin[KWin Script API<br/>workspace.windowActivated]
KWinDBus[D-Bus callback<br/>org.kde.KWin /Scripting]
end
subgraph "GNOME"
Introspect[Shell.Introspect<br/>GetWindows]
Extension[GNOME Shell Extension<br/>global.display.connect<br/>'notify::focus-window']
end
subgraph "Hyprland"
IPC[Hyprland IPC<br/>hyprctl activewindow]
Socket[Unix socket events<br/>activewindow>>]
end
subgraph "Sway / wlroots"
WLR[wlr-foreign-toplevel<br/>management protocol]
SwayIPC[Sway IPC<br/>swaymsg -t subscribe]
end
subgraph "X11 Generic"
Xprop[_NET_ACTIVE_WINDOW<br/>property change notification]
XLib[XSelectInput on root<br/>PropertyChangeMask]
end
| DE | Recommended Approach | Latency | Reliability |
|---|---|---|---|
| KDE Plasma 6 | KWin script D-Bus callback | <10ms | High (event-driven) |
| GNOME 45+ | Shell.Introspect polling or Extension | ~500ms (poll) / <10ms (extension) | Medium / High |
| Hyprland | IPC socket subscription | <10ms | High |
| Sway | IPC subscription | <10ms | High |
| X11 (any) |
_NET_ACTIVE_WINDOW via XCB |
<10ms | High |
The trickiest part of desktop integration is resolving a window to a stable application ID. Different compositors report different identifiers:
| Compositor | Identifier | Example |
|---|---|---|
| KWin (Wayland) |
desktopFileName or resourceClass
|
org.kde.dolphin or dolphin
|
| Mutter (GNOME) |
app-id (from Wayland) |
org.gnome.Nautilus |
| Hyprland | class |
firefox |
| X11 |
WM_CLASS (instance, class) |
Navigator, firefox
|
Logitune normalizes all of these to a .desktop file baseName. The resolveDesktopFile() method on LinuxDesktopBase (inherited by both KDeDesktop and GnomeDesktop) does this by:
- Checking
desktopFileNameif the compositor provides it directly - Searching
.desktopfiles for a matching filename component - Searching
.desktopfiles for a matchingStartupWMClass - Falling back to the raw identifier
This logic lives in LinuxDesktopBase so every Linux DE implementation inherits it for free — runningApplications() and desktopDirs() are in the same base class.
Edit src/app/AppRoot.cpp to select the right desktop integration:
AppRoot::AppRoot(IDesktopIntegration *desktop, IInputInjector *injector, QObject *parent)
: QObject(parent)
, m_deviceManager(&m_registry)
, m_actionExecutor(nullptr)
{
if (desktop) {
m_desktop = desktop;
} else {
// Detect desktop environment and create appropriate integration
QString xdgDesktop = QProcessEnvironment::systemEnvironment()
.value("XDG_CURRENT_DESKTOP");
if (xdgDesktop.contains("KDE", Qt::CaseInsensitive)) {
m_ownedDesktop = std::make_unique<KDeDesktop>();
} else if (xdgDesktop.contains("GNOME", Qt::CaseInsensitive)) {
m_ownedDesktop = std::make_unique<GnomeDesktop>();
} else {
m_ownedDesktop = std::make_unique<GenericDesktop>();
}
m_desktop = m_ownedDesktop.get();
}
// ... rest unchanged
}Edit src/core/CMakeLists.txt:
target_sources(logitune-core PRIVATE
# ... existing files ...
desktop/KDeDesktop.cpp
desktop/GenericDesktop.cpp
desktop/GnomeDesktop.cpp # Add this
)The mock infrastructure is already DE-agnostic. MockDesktop implements IDesktopIntegration and provides simulateFocus() to trigger focus changes in tests. No DE-specific test infrastructure is needed.
However, you should add a test for the detection logic:
TEST(DesktopDetectionTest, GnomeDetected) {
// Set XDG_CURRENT_DESKTOP to GNOME and verify GnomeDesktop is created
// (This may require environment variable manipulation)
}If you are adding a second DE implementation, consider extracting these shared utilities:
-
resolveDesktopFile()—.desktopfile lookup by resourceClass/StartupWMClass -
runningApplications()— scanning.desktopfiles for GUI applications -
Desktop directory list —
/usr/share/applications,~/.local/share/applications, Flatpak/Snap paths, etc.
These could live in a DesktopUtils static class or be moved to the GenericDesktop base class.
Hyprland is a wlroots-based compositor with a powerful IPC system. Here is a brief outline:
-
Class:
HyprlandDesktopextendingIDesktopIntegration -
Focus tracking: Subscribe to Hyprland IPC socket (
$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock) foractivewindow>>events -
Window identity: Hyprland reports the
classproperty, equivalent to X11WM_CLASS. Run throughresolveDesktopFile(). -
blockGlobalShortcuts: Use
hyprctl keyword bindto temporarily unbind all shortcuts, or usehyprctl dispatch submapto switch to an empty submap -
Detection: Check for
HYPRLAND_INSTANCE_SIGNATUREenvironment variable
The Hyprland IPC approach would be event-driven (no polling), making it more efficient than the GNOME polling fallback.
Logitune — Linux configurator for Logitech peripherals · Source · GPL-3.0