Skip to content

core/desktopentry: add file system watcher for desktop entry directories #114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ qt_add_library(quickshell-core STATIC
model.cpp
elapsedtimer.cpp
desktopentry.cpp
desktopentrymonitor.cpp
desktoputils.cpp
objectrepeater.cpp
platformmenu.cpp
qsmenu.cpp
Expand Down
117 changes: 92 additions & 25 deletions src/core/desktopentry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
#include <ranges>

#include "../io/processcore.hpp"
#include "desktopentrymonitor.hpp"
#include "desktoputils.hpp"
#include "logcat.hpp"
#include "model.hpp"
#include "qmlglobal.hpp"
Expand Down Expand Up @@ -268,40 +270,35 @@ void DesktopAction::execute() const {
}

DesktopEntryManager::DesktopEntryManager() {
// Create file watcher for desktop entries
this->monitor = new DesktopEntryMonitor(this);
connect(
this->monitor,
&DesktopEntryMonitor::desktopEntriesChanged,
this,
&DesktopEntryManager::handleFileChanges
);

// Initial scan
this->scanDesktopEntries();
this->populateApplications();
}

void DesktopEntryManager::scanDesktopEntries() {
QList<QString> dataPaths;

if (qEnvironmentVariableIsSet("XDG_DATA_HOME")) {
dataPaths.push_back(qEnvironmentVariable("XDG_DATA_HOME"));
} else if (qEnvironmentVariableIsSet("HOME")) {
dataPaths.push_back(qEnvironmentVariable("HOME") + "/.local/share");
}

if (qEnvironmentVariableIsSet("XDG_DATA_DIRS")) {
auto var = qEnvironmentVariable("XDG_DATA_DIRS");
dataPaths += var.split(u':', Qt::SkipEmptyParts);
} else {
dataPaths.push_back("/usr/local/share");
dataPaths.push_back("/usr/share");
}
auto desktopPaths = DesktopUtils::getDesktopDirectories();

qCDebug(logDesktopEntry) << "Creating desktop entry scanners";

for (auto& path: std::ranges::reverse_view(dataPaths)) {
auto p = QDir(path).filePath("applications");
auto file = QFileInfo(p);
for (auto& path: std::ranges::reverse_view(desktopPaths)) {
auto file = QFileInfo(path);

if (!file.isDir()) {
qCDebug(logDesktopEntry) << "Not scanning path" << p << "as it is not a directory";
qCDebug(logDesktopEntry) << "Not scanning path" << path << "as it is not a directory";
continue;
}

qCDebug(logDesktopEntry) << "Scanning path" << p;
this->scanPath(p);
qCDebug(logDesktopEntry) << "Scanning path" << path;
this->scanPath(path);
}
}

Expand All @@ -311,11 +308,11 @@ void DesktopEntryManager::populateApplications() {
}
}

void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) {
void DesktopEntryManager::scanPath(const QDir& dir) {
auto entries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);

for (auto& entry: entries) {
if (entry.isDir()) this->scanPath(entry.absoluteFilePath(), prefix + dir.dirName() + "-");
if (entry.isDir()) this->scanPath(entry.absoluteFilePath());
else if (entry.isFile()) {
auto path = entry.filePath();
if (!path.endsWith(".desktop")) {
Expand All @@ -329,7 +326,7 @@ void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) {
continue;
}

auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8);
auto id = this->extractIdFromPath(entry.absoluteFilePath());
auto lowerId = id.toLower();

auto text = QString::fromUtf8(file.readAll());
Expand Down Expand Up @@ -386,7 +383,77 @@ DesktopEntry* DesktopEntryManager::byId(const QString& id) {

ObjectModel<DesktopEntry>* DesktopEntryManager::applications() { return &this->mApplications; }

DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); }
void DesktopEntryManager::handleFileChanges(
const QHash<QString, DesktopEntryMonitor::ChangeEvent>&
) {
qCDebug(logDesktopEntry) << "Directory change detected, performing full rescan";

auto oldEntries = this->desktopEntries;

this->desktopEntries.clear();
this->lowercaseDesktopEntries.clear();

this->scanDesktopEntries();

QVector<DesktopEntry*> newApplications;
for (auto& entry: this->desktopEntries.values()) {
if (!entry->noDisplay()) {
newApplications.append(entry);
}
}

this->mApplications.diffUpdate(newApplications);

for (auto* e: oldEntries) {
if (!this->desktopEntries.contains(e->mId)) {
e->deleteLater();
}
}

emit applicationsChanged();
}

QString DesktopEntryManager::extractIdFromPath(const QString& path) {
// Extract ID from path following XDG spec
// e.g., /usr/share/applications/firefox.desktop -> firefox
// e.g., /usr/share/applications/kde4/kate.desktop -> kde4-kate

auto info = QFileInfo(path);
auto id = info.completeBaseName();

// Find the applications directory in the path
auto appIdx = path.lastIndexOf("/applications/");
if (appIdx != -1) {
auto relativePath = path.mid(appIdx + 14); // Skip "/applications/"
auto relInfo = QFileInfo(relativePath);

// Replace directory separators with dashes
auto dirPath = relInfo.path();
if (dirPath != ".") {
id = dirPath.replace('/', '-') + '-' + relInfo.completeBaseName();
}
}

return id;
}

void DesktopEntryManager::updateApplicationModel() {
QVector<DesktopEntry*> newApplications;
for (auto& entry: this->desktopEntries.values()) {
if (!entry->noDisplay()) newApplications.append(entry);
}
this->mApplications.diffUpdate(newApplications);
}

DesktopEntries::DesktopEntries() {
auto* mgr = DesktopEntryManager::instance();
QObject::connect(
mgr,
&DesktopEntryManager::applicationsChanged,
this,
&DesktopEntries::applicationsChanged
);
}

DesktopEntry* DesktopEntries::byId(const QString& id) {
return DesktopEntryManager::instance()->byId(id);
Expand Down
17 changes: 16 additions & 1 deletion src/core/desktopentry.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "model.hpp"

class DesktopAction;
class DesktopEntryMonitor;

/// A desktop entry. See @@DesktopEntries for details.
class DesktopEntry: public QObject {
Expand Down Expand Up @@ -144,6 +145,8 @@ class DesktopAction: public QObject {
friend class DesktopEntry;
};

#include "desktopentrymonitor.hpp"

class DesktopEntryManager: public QObject {
Q_OBJECT;

Expand All @@ -156,15 +159,24 @@ class DesktopEntryManager: public QObject {

static DesktopEntryManager* instance();

signals:
void applicationsChanged();

private slots:
void handleFileChanges(const QHash<QString, DesktopEntryMonitor::ChangeEvent>& changes);

private:
explicit DesktopEntryManager();

void populateApplications();
void scanPath(const QDir& dir, const QString& prefix = QString());
void scanPath(const QDir& dir);
QString extractIdFromPath(const QString& path);
void updateApplicationModel();

QHash<QString, DesktopEntry*> desktopEntries;
QHash<QString, DesktopEntry*> lowercaseDesktopEntries;
ObjectModel<DesktopEntry> mApplications {this};
DesktopEntryMonitor* monitor = nullptr;
};

///! Desktop entry index.
Expand All @@ -189,4 +201,7 @@ class DesktopEntries: public QObject {
Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id);

[[nodiscard]] static ObjectModel<DesktopEntry>* applications();

signals:
void applicationsChanged();
};
114 changes: 114 additions & 0 deletions src/core/desktopentrymonitor.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#include "desktopentrymonitor.hpp"

#include <qdir.h>
#include <qdiriterator.h>
#include <qfileinfo.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qtenvironmentvariables.h>

#include "desktoputils.hpp"
#include "logcat.hpp"

namespace {
QS_LOGGING_CATEGORY(logDesktopMonitor, "quickshell.desktopentrymonitor", QtWarningMsg);
}

DesktopEntryMonitor::DesktopEntryMonitor(QObject* parent): QObject(parent) {
this->watcher = new QFileSystemWatcher(this);
this->debounceTimer = new QTimer(this);
this->debounceTimer->setSingleShot(true);
this->debounceTimer->setInterval(50);

// Initialize XDG desktop paths
this->initializeDesktopPaths();

// Setup connections
QObject::connect(
this->watcher,
&QFileSystemWatcher::directoryChanged,
this,
&DesktopEntryMonitor::onDirectoryChanged
);
QObject::connect(
this->debounceTimer,
&QTimer::timeout,
this,
&DesktopEntryMonitor::processChanges
);

// Start monitoring
this->startMonitoring();
}

void DesktopEntryMonitor::initializeDesktopPaths() {
this->desktopPaths = DesktopUtils::getDesktopDirectories();
}

void DesktopEntryMonitor::startMonitoring() {
for (const QString& path: this->desktopPaths) {
if (QDir(path).exists()) {
qCDebug(logDesktopMonitor) << "Monitoring desktop entry path:" << path;
this->scanAndWatch(path);
}
}
}

void DesktopEntryMonitor::scanAndWatch(const QString& dirPath) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scanAndWatch + all the various change handlers share a lot of the code. consider initialization to be a diff from an empty set of entries and share the code.

auto dir = QDir(dirPath);
if (!dir.exists()) return;

// Add directory to watcher
if (this->watcher->addPath(dirPath)) {
qCDebug(logDesktopMonitor) << "Added directory to watcher:" << dirPath;
}

// Recurse into subdirs
for (const auto& sub: dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
this->scanAndWatch(dir.absoluteFilePath(sub));
}
}

void DesktopEntryMonitor::onDirectoryChanged(const QString& path) {
qCDebug(logDesktopMonitor) << "Directory changed:" << path;
auto dir = QDir(path);

// Check if directory still exists - handle removal/unmounting
if (!dir.exists()) {
qCDebug(logDesktopMonitor) << "Directory no longer exists, cleaning up:" << path;
this->watcher->removePath(path);
// Directory removal will be handled by full rescan
this->queueChange(ChangeEvent::Modified, path); // Trigger full rescan
return;
}

// Check for new subdirectories
auto subdirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const QString& subdir: subdirs) {
auto subdirPath = dir.absoluteFilePath(subdir);
if (!this->watcher->directories().contains(subdirPath)) {
this->scanAndWatch(subdirPath);
}
}

// Queue a change to trigger full rescan of all desktop paths
this->queueChange(ChangeEvent::Modified, path);
}

void DesktopEntryMonitor::queueChange(ChangeEvent event, const QString& path) {
this->pendingChanges.insert(path, event);
this->debounceTimer->start();
}

void DesktopEntryMonitor::processChanges() {
if (this->pendingChanges.isEmpty()) return;

qCDebug(logDesktopMonitor) << "Processing directory changes, triggering full rescan";

// Clear pending changes since we're doing a full rescan
this->pendingChanges.clear();

// Emit with empty hash to signal full rescan needed
auto emptyChanges = QHash<QString, ChangeEvent> {};
emit desktopEntriesChanged(emptyChanges);
}
35 changes: 35 additions & 0 deletions src/core/desktopentrymonitor.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#pragma once

#include <qfilesystemwatcher.h>
#include <qhash.h>
#include <qobject.h>
#include <qstringlist.h>
#include <qtimer.h>

class DesktopEntryMonitor: public QObject {
Q_OBJECT

public:
enum class ChangeEvent { Added, Modified, Removed };

explicit DesktopEntryMonitor(QObject* parent = nullptr);
~DesktopEntryMonitor() = default;

signals:
void desktopEntriesChanged(const QHash<QString, ChangeEvent>& changes);

private slots:
void onDirectoryChanged(const QString& path);
void processChanges();

private:
void initializeDesktopPaths();
void startMonitoring();
void scanAndWatch(const QString& dirPath);
void queueChange(ChangeEvent event, const QString& path);

QFileSystemWatcher* watcher;
QStringList desktopPaths;
QTimer* debounceTimer;
QHash<QString, ChangeEvent> pendingChanges;
};
Loading