-
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.
Note
For how desktop integration fits into the overall system, see the Desktop Integration section of the Architecture page.
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 AppController::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);Important
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 logituneCreate src/core/desktop/GnomeDesktop.cpp:
#include "desktop/GnomeDesktop.h"
#include "logging/LogManager.h"
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusReply>
#include <QProcessEnvironment>
#include <QDir>
#include <QFileInfo>
#include <QSettings>
namespace logitune {
GnomeDesktop::GnomeDesktop(QObject *parent)
: IDesktopIntegration(parent)
{
}
void GnomeDesktop::start()
{
// Check if GNOME Shell is running
QDBusInterface shell(
QStringLiteral("org.gnome.Shell"),
QStringLiteral("/org/gnome/Shell"),
QStringLiteral("org.gnome.Shell"),
QDBusConnection::sessionBus());
m_available = shell.isValid();
if (!m_available) return;
// GNOME Shell Extension approach:
// Install a GNOME Shell extension that sends D-Bus signals on focus change.
// Alternatively, use the GNOME Shell Eval API (deprecated) or
// the org.gnome.Shell.Introspect API for window tracking.
// For now, use polling via org.gnome.Shell.Introspect
m_pollTimer = new QTimer(this);
m_pollTimer->setInterval(500);
connect(m_pollTimer, &QTimer::timeout, this, &GnomeDesktop::pollActiveWindow);
m_pollTimer->start();
}
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;
}
void GnomeDesktop::pollActiveWindow()
{
// Option 1: org.gnome.Shell.Introspect.GetWindows
// Option 2: GNOME Shell extension with D-Bus callback
// Option 3: wlr-foreign-toplevel-management (wlroots-based compositors)
// This is the scaffold — the actual implementation depends on
// which GNOME API is available and stable.
// Example using Shell.Introspect:
QDBusMessage msg = QDBusMessage::createMethodCall(
QStringLiteral("org.gnome.Shell"),
QStringLiteral("/org/gnome/Shell/Introspect"),
QStringLiteral("org.gnome.Shell.Introspect"),
QStringLiteral("GetWindows"));
QDBusReply<QVariantMap> reply = QDBusConnection::sessionBus().call(msg);
if (!reply.isValid()) return;
// Find the focused window and extract its app-id
// ... parse reply.value() ...
// QString appId = ...;
// if (appId != m_lastAppId) {
// m_lastAppId = appId;
// emit activeWindowChanged(appId, title);
// }
}
void GnomeDesktop::blockGlobalShortcuts(bool block)
{
// GNOME doesn't have a direct D-Bus API for this.
// Options:
// 1. Use a GNOME Shell extension
// 2. Temporarily grab the keyboard via libinput
// 3. Leave unimplemented (keystroke capture will work but may trigger shortcuts)
Q_UNUSED(block)
}
QVariantList GnomeDesktop::runningApplications() const
{
// Same approach as KDeDesktop — scan .desktop files
// The KDeDesktop implementation is desktop-agnostic for this method
// Consider extracting it to a shared utility
QVariantList result;
// ... scan /usr/share/applications/*.desktop ...
return result;
}
} // 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) |
| 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 |
Warning
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 in KDeDesktop 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
Tip
This logic is currently in KDeDesktop but is DE-agnostic. Consider extracting it to a shared utility if adding multiple DEs.
Edit src/app/AppController.cpp to select the right desktop integration:
AppController::AppController(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)
}Tip
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, Flatpak 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:
| Step | Details |
|---|---|
| 📝 Class |
HyprlandDesktop extending IDesktopIntegration
|
| 🔍 Focus tracking | Subscribe to Hyprland IPC socket ($XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock) for activewindow>> events |
| 🪪 Window identity | Hyprland reports the class property, equivalent to X11 WM_CLASS. Run through resolveDesktopFile(). |
| 🔒 blockGlobalShortcuts | Use hyprctl keyword bind to temporarily unbind all shortcuts, or use hyprctl dispatch submap to switch to an empty submap |
| ✅ Detection | Check for HYPRLAND_INSTANCE_SIGNATURE environment variable |
Note
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