core/desktopentry: add limited desktop entry api
This commit is contained in:
parent
ce5ddbf8ba
commit
f655875547
|
@ -28,6 +28,7 @@ qt_add_library(quickshell-core STATIC
|
||||||
boundcomponent.cpp
|
boundcomponent.cpp
|
||||||
model.cpp
|
model.cpp
|
||||||
elapsedtimer.cpp
|
elapsedtimer.cpp
|
||||||
|
desktopentry.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}")
|
set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}")
|
||||||
|
|
367
src/core/desktopentry.cpp
Normal file
367
src/core/desktopentry.cpp
Normal file
|
@ -0,0 +1,367 @@
|
||||||
|
#include "desktopentry.hpp"
|
||||||
|
|
||||||
|
#include <qcontainerfwd.h>
|
||||||
|
#include <qdebug.h>
|
||||||
|
#include <qdir.h>
|
||||||
|
#include <qfileinfo.h>
|
||||||
|
#include <qfilesystemwatcher.h>
|
||||||
|
#include <qhash.h>
|
||||||
|
#include <qlist.h>
|
||||||
|
#include <qlocale.h>
|
||||||
|
#include <qlogging.h>
|
||||||
|
#include <qloggingcategory.h>
|
||||||
|
#include <qnamespace.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qpair.h>
|
||||||
|
#include <qprocess.h>
|
||||||
|
#include <qstringview.h>
|
||||||
|
#include <qtenvironmentvariables.h>
|
||||||
|
#include <ranges>
|
||||||
|
|
||||||
|
#include "model.hpp"
|
||||||
|
|
||||||
|
Q_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg);
|
||||||
|
|
||||||
|
struct Locale {
|
||||||
|
explicit Locale() = default;
|
||||||
|
|
||||||
|
explicit Locale(const QString& string) {
|
||||||
|
auto territoryIdx = string.indexOf('_');
|
||||||
|
auto codesetIdx = string.indexOf('.');
|
||||||
|
auto modifierIdx = string.indexOf('@');
|
||||||
|
|
||||||
|
auto parseEnd = string.length();
|
||||||
|
|
||||||
|
if (modifierIdx != -1) {
|
||||||
|
this->modifier = string.sliced(modifierIdx + 1, parseEnd - modifierIdx - 1);
|
||||||
|
parseEnd = modifierIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codesetIdx != -1) {
|
||||||
|
parseEnd = codesetIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (territoryIdx != -1) {
|
||||||
|
this->territory = string.sliced(territoryIdx + 1, parseEnd - territoryIdx - 1);
|
||||||
|
parseEnd = territoryIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->language = string.sliced(0, parseEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool isValid() const { return !this->language.isEmpty(); }
|
||||||
|
|
||||||
|
[[nodiscard]] int matchScore(const Locale& other) const {
|
||||||
|
if (this->language != other.language) return 0;
|
||||||
|
auto territoryMatches = !this->territory.isEmpty() && this->territory == other.territory;
|
||||||
|
auto modifierMatches = !this->modifier.isEmpty() && this->modifier == other.modifier;
|
||||||
|
|
||||||
|
auto score = 1;
|
||||||
|
if (territoryMatches) score += 2;
|
||||||
|
if (modifierMatches) score += 1;
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Locale& system() {
|
||||||
|
static Locale* locale = nullptr; // NOLINT
|
||||||
|
|
||||||
|
if (locale == nullptr) {
|
||||||
|
auto lstr = qEnvironmentVariable("LC_MESSAGES");
|
||||||
|
if (lstr.isEmpty()) lstr = qEnvironmentVariable("LANG");
|
||||||
|
locale = new Locale(lstr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return *locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString language;
|
||||||
|
QString territory;
|
||||||
|
QString modifier;
|
||||||
|
};
|
||||||
|
|
||||||
|
QDebug operator<<(QDebug debug, const Locale& locale) {
|
||||||
|
auto saver = QDebugStateSaver(debug);
|
||||||
|
debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory
|
||||||
|
<< ", modifier" << locale.modifier << ')';
|
||||||
|
|
||||||
|
return debug;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopEntry::parseEntry(const QString& text) {
|
||||||
|
const auto& system = Locale::system();
|
||||||
|
|
||||||
|
auto groupName = QString();
|
||||||
|
auto entries = QHash<QString, QPair<Locale, QString>>();
|
||||||
|
|
||||||
|
auto finishCategory = [&]() {
|
||||||
|
if (groupName == "Desktop Entry") {
|
||||||
|
if (entries["Type"].second != "Application") return;
|
||||||
|
if (entries.contains("Hidden") && entries["Hidden"].second == "true") return;
|
||||||
|
|
||||||
|
for (const auto& [key, pair]: entries.asKeyValueRange()) {
|
||||||
|
auto& [_, value] = pair;
|
||||||
|
this->mEntries.insert(key, value);
|
||||||
|
|
||||||
|
if (key == "Name") this->mName = value;
|
||||||
|
else if (key == "GenericName") this->mGenericName = value;
|
||||||
|
else if (key == "NoDisplay") this->mNoDisplay = value == "true";
|
||||||
|
else if (key == "Comment") this->mComment = value;
|
||||||
|
else if (key == "Icon") this->mIcon = value;
|
||||||
|
else if (key == "Exec") this->mExecString = value;
|
||||||
|
else if (key == "Path") this->mWorkingDirectory = value;
|
||||||
|
else if (key == "Terminal") this->mTerminal = value == "true";
|
||||||
|
else if (key == "Categories") this->mCategories = value.split(u';', Qt::SkipEmptyParts);
|
||||||
|
else if (key == "Keywords") this->mKeywords = value.split(u';', Qt::SkipEmptyParts);
|
||||||
|
}
|
||||||
|
} else if (groupName.startsWith("Desktop Action ")) {
|
||||||
|
auto actionName = groupName.sliced(16);
|
||||||
|
auto* action = new DesktopAction(actionName, this);
|
||||||
|
|
||||||
|
for (const auto& [key, pair]: entries.asKeyValueRange()) {
|
||||||
|
const auto& [_, value] = pair;
|
||||||
|
action->mEntries.insert(key, value);
|
||||||
|
|
||||||
|
if (key == "Name") action->mName = value;
|
||||||
|
else if (key == "Icon") action->mIcon = value;
|
||||||
|
else if (key == "Exec") action->mExecString = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->mActions.insert(actionName, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
for (auto& line: text.split(u'\n', Qt::SkipEmptyParts)) {
|
||||||
|
if (line.startsWith(u'#')) continue;
|
||||||
|
|
||||||
|
if (line.startsWith(u'[') && line.endsWith(u']')) {
|
||||||
|
finishCategory();
|
||||||
|
groupName = line.sliced(1, line.length() - 2);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto splitIdx = line.indexOf(u'=');
|
||||||
|
if (splitIdx == -1) {
|
||||||
|
qCDebug(logDesktopEntry) << "Encountered invalid line in desktop entry (no =)" << line;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto key = line.sliced(0, splitIdx);
|
||||||
|
const auto& value = line.sliced(splitIdx + 1);
|
||||||
|
|
||||||
|
auto localeIdx = key.indexOf('[');
|
||||||
|
Locale locale;
|
||||||
|
if (localeIdx != -1 && localeIdx != key.length() - 1) {
|
||||||
|
locale = Locale(key.sliced(localeIdx + 1, key.length() - localeIdx - 2));
|
||||||
|
key = key.sliced(0, localeIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries.contains(key)) {
|
||||||
|
const auto& old = entries.value(key);
|
||||||
|
|
||||||
|
if (system.matchScore(locale) > system.matchScore(old.first)) {
|
||||||
|
entries.insert(key, qMakePair(locale, value));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entries.insert(key, qMakePair(locale, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finishCategory();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopEntry::execute() const {
|
||||||
|
DesktopEntry::doExec(this->mExecString, this->mWorkingDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DesktopEntry::isValid() const { return !this->mName.isEmpty(); }
|
||||||
|
bool DesktopEntry::noDisplay() const { return this->mNoDisplay; }
|
||||||
|
|
||||||
|
QVector<DesktopAction*> DesktopEntry::actions() const { return this->mActions.values(); }
|
||||||
|
|
||||||
|
QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
|
||||||
|
QVector<QString> arguments;
|
||||||
|
QString currentArgument;
|
||||||
|
auto parsingString = false;
|
||||||
|
auto escape = 0;
|
||||||
|
auto percent = false;
|
||||||
|
|
||||||
|
for (auto c: execString) {
|
||||||
|
if (escape == 0 && c == u'\\') {
|
||||||
|
escape = 1;
|
||||||
|
} else if (parsingString) {
|
||||||
|
if (c == '\\') {
|
||||||
|
escape++;
|
||||||
|
if (escape == 4) {
|
||||||
|
currentArgument += '\\';
|
||||||
|
escape = 0;
|
||||||
|
}
|
||||||
|
} else if (escape != 0) {
|
||||||
|
if (escape != 2) {
|
||||||
|
// Technically this is an illegal state, but the spec has a terrible double escape
|
||||||
|
// rule in strings for no discernable reason. Assuming someone might understandably
|
||||||
|
// misunderstand it, treat it as a normal escape and log it.
|
||||||
|
qCWarning(logDesktopEntry).noquote()
|
||||||
|
<< "Illegal escape sequence in desktop entry exec string:" << execString;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentArgument += c;
|
||||||
|
escape = 0;
|
||||||
|
} else if (c == u'"') {
|
||||||
|
parsingString = false;
|
||||||
|
} else {
|
||||||
|
currentArgument += c;
|
||||||
|
}
|
||||||
|
} else if (escape != 0) {
|
||||||
|
currentArgument += c;
|
||||||
|
escape = 0;
|
||||||
|
} else if (percent) {
|
||||||
|
if (c == '%') {
|
||||||
|
currentArgument += '%';
|
||||||
|
} // else discard
|
||||||
|
|
||||||
|
percent = false;
|
||||||
|
} else if (c == '%') {
|
||||||
|
percent = true;
|
||||||
|
} else if (c == u'"') {
|
||||||
|
parsingString = true;
|
||||||
|
} else if (c == u' ') {
|
||||||
|
if (!currentArgument.isEmpty()) {
|
||||||
|
arguments.push_back(currentArgument);
|
||||||
|
currentArgument.clear();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentArgument += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentArgument.isEmpty()) {
|
||||||
|
arguments.push_back(currentArgument);
|
||||||
|
currentArgument.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
return arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopEntry::doExec(const QString& execString, const QString& workingDirectory) {
|
||||||
|
auto args = DesktopEntry::parseExecString(execString);
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
qCWarning(logDesktopEntry) << "Tried to exec string" << execString << "which parsed as empty.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto process = QProcess();
|
||||||
|
process.setProgram(args.at(0));
|
||||||
|
process.setArguments(args.sliced(1));
|
||||||
|
if (!workingDirectory.isEmpty()) process.setWorkingDirectory(workingDirectory);
|
||||||
|
process.startDetached();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopAction::execute() const {
|
||||||
|
DesktopEntry::doExec(this->mExecString, this->entry->mWorkingDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopEntryManager::DesktopEntryManager() {
|
||||||
|
this->scanDesktopEntries();
|
||||||
|
this->populateApplications();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopEntryManager::scanDesktopEntries() {
|
||||||
|
QList<QString> dataPaths;
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
qCDebug(logDesktopEntry) << "Creating desktop entry scanners";
|
||||||
|
|
||||||
|
for (auto& path: std::ranges::reverse_view(dataPaths)) {
|
||||||
|
auto p = QDir(path).filePath("applications");
|
||||||
|
auto file = QFileInfo(p);
|
||||||
|
|
||||||
|
if (!file.isDir()) {
|
||||||
|
qCDebug(logDesktopEntry) << "Not scanning path" << p << "as it is not a directory";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
qCDebug(logDesktopEntry) << "Scanning path" << p;
|
||||||
|
this->scanPath(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopEntryManager::populateApplications() {
|
||||||
|
for (auto& entry: this->desktopEntries.values()) {
|
||||||
|
if (!entry->noDisplay()) this->mApplications.insertObject(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) {
|
||||||
|
auto entries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
|
||||||
|
|
||||||
|
for (auto& entry: entries) {
|
||||||
|
if (entry.isDir()) this->scanPath(entry.path(), prefix + dir.dirName() + "-");
|
||||||
|
else if (entry.isFile()) {
|
||||||
|
auto path = entry.filePath();
|
||||||
|
if (!path.endsWith(".desktop")) {
|
||||||
|
qCDebug(logDesktopEntry) << "Skipping file" << path << "as it has no .desktop extension";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* file = new QFile(path);
|
||||||
|
|
||||||
|
if (!file->open(QFile::ReadOnly)) {
|
||||||
|
qCDebug(logDesktopEntry) << "Could not open file" << path;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8);
|
||||||
|
|
||||||
|
auto text = QString::fromUtf8(file->readAll());
|
||||||
|
auto* dentry = new DesktopEntry(id, this);
|
||||||
|
dentry->parseEntry(text);
|
||||||
|
|
||||||
|
if (!dentry->isValid()) {
|
||||||
|
qCDebug(logDesktopEntry) << "Skipping desktop entry" << path;
|
||||||
|
delete dentry;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
qCDebug(logDesktopEntry) << "Found desktop entry" << id << "at" << path;
|
||||||
|
|
||||||
|
if (desktopEntries.contains(id)) {
|
||||||
|
qCDebug(logDesktopEntry) << "Replacing old entry for" << id;
|
||||||
|
delete desktopEntries.value(id);
|
||||||
|
desktopEntries.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
desktopEntries.insert(id, dentry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopEntryManager* DesktopEntryManager::instance() {
|
||||||
|
static auto* instance = new DesktopEntryManager(); // NOLINT
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopEntry* DesktopEntryManager::byId(const QString& id) {
|
||||||
|
return this->desktopEntries.value(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectModel<DesktopEntry>* DesktopEntryManager::applications() { return &this->mApplications; }
|
||||||
|
|
||||||
|
DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); }
|
||||||
|
|
||||||
|
DesktopEntry* DesktopEntries::byId(const QString& id) {
|
||||||
|
return DesktopEntryManager::instance()->byId(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectModel<DesktopEntry>* DesktopEntries::applications() {
|
||||||
|
return DesktopEntryManager::instance()->applications();
|
||||||
|
}
|
151
src/core/desktopentry.hpp
Normal file
151
src/core/desktopentry.hpp
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <qcontainerfwd.h>
|
||||||
|
#include <qdir.h>
|
||||||
|
#include <qhash.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qqmlintegration.h>
|
||||||
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
|
#include "model.hpp"
|
||||||
|
|
||||||
|
class DesktopAction;
|
||||||
|
|
||||||
|
/// A desktop entry. See [DesktopEntries](../desktopentries) for details.
|
||||||
|
class DesktopEntry: public QObject {
|
||||||
|
Q_OBJECT;
|
||||||
|
Q_PROPERTY(QString id MEMBER mId CONSTANT);
|
||||||
|
/// Name of the specific application, such as "Firefox".
|
||||||
|
Q_PROPERTY(QString name MEMBER mName CONSTANT);
|
||||||
|
/// Short description of the application, such as "Web Browser". May be empty.
|
||||||
|
Q_PROPERTY(QString genericName MEMBER mGenericName CONSTANT);
|
||||||
|
/// If true, this application should not be displayed in menus and launchers.
|
||||||
|
Q_PROPERTY(bool noDisplay MEMBER mNoDisplay CONSTANT);
|
||||||
|
/// Long description of the application, such as "View websites on the internet". May be empty.
|
||||||
|
Q_PROPERTY(QString comment MEMBER mComment CONSTANT);
|
||||||
|
/// Name of the icon associated with this application. May be empty.
|
||||||
|
Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
|
||||||
|
/// The raw `Exec` string from the desktop entry. You probably want `execute()`.
|
||||||
|
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
|
||||||
|
/// The working directory to execute from.
|
||||||
|
Q_PROPERTY(QString workingDirectory MEMBER mWorkingDirectory CONSTANT);
|
||||||
|
/// If the application should run in a terminal.
|
||||||
|
Q_PROPERTY(bool runInTerminal MEMBER mTerminal CONSTANT);
|
||||||
|
Q_PROPERTY(QVector<QString> categories MEMBER mCategories CONSTANT);
|
||||||
|
Q_PROPERTY(QVector<QString> keywords MEMBER mKeywords CONSTANT);
|
||||||
|
Q_PROPERTY(QVector<DesktopAction*> actions READ actions CONSTANT);
|
||||||
|
QML_ELEMENT;
|
||||||
|
QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries");
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DesktopEntry(QString id, QObject* parent): QObject(parent), mId(std::move(id)) {}
|
||||||
|
|
||||||
|
void parseEntry(const QString& text);
|
||||||
|
|
||||||
|
/// Run the application. Currently ignores `runInTerminal` and field codes.
|
||||||
|
Q_INVOKABLE void execute() const;
|
||||||
|
|
||||||
|
[[nodiscard]] bool isValid() const;
|
||||||
|
[[nodiscard]] bool noDisplay() const;
|
||||||
|
[[nodiscard]] QVector<DesktopAction*> actions() const;
|
||||||
|
|
||||||
|
// currently ignores all field codes.
|
||||||
|
static QVector<QString> parseExecString(const QString& execString);
|
||||||
|
static void doExec(const QString& execString, const QString& workingDirectory);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QHash<QString, QString> mEntries;
|
||||||
|
QHash<QString, DesktopAction*> mActions;
|
||||||
|
|
||||||
|
QString mId;
|
||||||
|
QString mName;
|
||||||
|
QString mGenericName;
|
||||||
|
bool mNoDisplay = false;
|
||||||
|
QString mComment;
|
||||||
|
QString mIcon;
|
||||||
|
QString mExecString;
|
||||||
|
QString mWorkingDirectory;
|
||||||
|
bool mTerminal = false;
|
||||||
|
QVector<QString> mCategories;
|
||||||
|
QVector<QString> mKeywords;
|
||||||
|
|
||||||
|
friend class DesktopAction;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// An action of a [DesktopEntry](../desktopentry).
|
||||||
|
class DesktopAction: public QObject {
|
||||||
|
Q_OBJECT;
|
||||||
|
Q_PROPERTY(QString id MEMBER mId CONSTANT);
|
||||||
|
Q_PROPERTY(QString name MEMBER mName CONSTANT);
|
||||||
|
Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
|
||||||
|
/// The raw `Exec` string from the desktop entry. You probably want `execute()`.
|
||||||
|
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
|
||||||
|
QML_ELEMENT;
|
||||||
|
QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry");
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DesktopAction(QString id, DesktopEntry* entry)
|
||||||
|
: QObject(entry)
|
||||||
|
, entry(entry)
|
||||||
|
, mId(std::move(id)) {}
|
||||||
|
|
||||||
|
/// Run the application. Currently ignores `runInTerminal` and field codes.
|
||||||
|
Q_INVOKABLE void execute() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
DesktopEntry* entry;
|
||||||
|
QString mId;
|
||||||
|
QString mName;
|
||||||
|
QString mIcon;
|
||||||
|
QString mExecString;
|
||||||
|
QHash<QString, QString> mEntries;
|
||||||
|
|
||||||
|
friend class DesktopEntry;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DesktopEntryManager: public QObject {
|
||||||
|
Q_OBJECT;
|
||||||
|
|
||||||
|
public:
|
||||||
|
void scanDesktopEntries();
|
||||||
|
|
||||||
|
[[nodiscard]] DesktopEntry* byId(const QString& id);
|
||||||
|
|
||||||
|
[[nodiscard]] ObjectModel<DesktopEntry>* applications();
|
||||||
|
|
||||||
|
static DesktopEntryManager* instance();
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit DesktopEntryManager();
|
||||||
|
|
||||||
|
void populateApplications();
|
||||||
|
void scanPath(const QDir& dir, const QString& prefix = QString());
|
||||||
|
|
||||||
|
QHash<QString, DesktopEntry*> desktopEntries;
|
||||||
|
ObjectModel<DesktopEntry> mApplications {this};
|
||||||
|
};
|
||||||
|
|
||||||
|
///! Desktop entry index.
|
||||||
|
/// Index of desktop entries according to the [desktop entry specification].
|
||||||
|
///
|
||||||
|
/// Primarily useful for looking up icons and metadata from an id, as there is
|
||||||
|
/// currently no mechanism for usage based sorting of entries and other launcher niceties.
|
||||||
|
///
|
||||||
|
/// [desktop entry specification]: https://specifications.freedesktop.org/desktop-entry-spec/latest/
|
||||||
|
class DesktopEntries: public QObject {
|
||||||
|
Q_OBJECT;
|
||||||
|
/// All desktop entries of type Application that are not Hidden or NoDisplay.
|
||||||
|
Q_PROPERTY(ObjectModel<DesktopEntry>* applications READ applications CONSTANT);
|
||||||
|
QML_ELEMENT;
|
||||||
|
QML_SINGLETON;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DesktopEntries();
|
||||||
|
|
||||||
|
/// Look up a desktop entry by name. Includes NoDisplay entries. May return null.
|
||||||
|
Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id);
|
||||||
|
|
||||||
|
[[nodiscard]] static ObjectModel<DesktopEntry>* applications();
|
||||||
|
};
|
|
@ -20,5 +20,6 @@ headers = [
|
||||||
"boundcomponent.hpp",
|
"boundcomponent.hpp",
|
||||||
"model.hpp",
|
"model.hpp",
|
||||||
"elapsedtimer.hpp",
|
"elapsedtimer.hpp",
|
||||||
|
"desktopentry.hpp",
|
||||||
]
|
]
|
||||||
-----
|
-----
|
||||||
|
|
Loading…
Reference in a new issue