diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index 637f758f..01c04087 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -123,13 +124,25 @@ ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString& else if (key == "StartupWMClass") data.startupClass = value; else if (key == "NoDisplay") data.noDisplay = value == "true"; else if (key == "Hidden") data.hidden = value == "true"; + else if (key == "OnlyShowIn") data.onlyShowIn = value.split(u';', Qt::SkipEmptyParts); + else if (key == "NotShowIn") data.notShowIn = value.split(u';', Qt::SkipEmptyParts); else if (key == "Comment") data.comment = value; else if (key == "Icon") data.icon = value; else if (key == "Exec") { data.execString = value; data.command = DesktopEntry::parseExecString(value); - } else if (key == "Path") data.workingDirectory = value; - else if (key == "Terminal") data.terminal = value == "true"; + } else if (key == "TryExec") data.tryExec = value; + else if (key == "Path") data.workingDirectory = value; + else if (key == "Terminal") data.runInTerminal = value == "true"; + else if (key == "X-TerminalArgExec" || key == "TerminalArgExec") + data.terminal.execArg = value; + else if (key == "X-TerminalArgAppId" || key == "TerminalArgAppId") + data.terminal.appIdArg = value; + else if (key == "X-TerminalArgTitle" || key == "TerminalArgTitle") + data.terminal.titleArg = value; + else if (key == "X-TerminalArgDir" || key == "TerminalArgDir") data.terminal.dirArg = value; + else if (key == "X-TerminalArgHold" || key == "TerminalArgHold") + data.terminal.holdArg = value; else if (key == "Categories") data.categories = value.split(u';', Qt::SkipEmptyParts); else if (key == "Keywords") data.keywords = value.split(u';', Qt::SkipEmptyParts); else if (key == "Actions") actionOrder = value.split(u';', Qt::SkipEmptyParts); @@ -213,16 +226,28 @@ void DesktopEntry::updateState(const ParsedDesktopEntryData& newState) { this->bGenericName = newState.genericName; this->bStartupClass = newState.startupClass; this->bNoDisplay = newState.noDisplay; + this->bOnlyShowIn = newState.onlyShowIn; + this->bNotShowIn = newState.notShowIn; this->bComment = newState.comment; this->bIcon = newState.icon; this->bExecString = newState.execString; this->bCommand = newState.command; this->bWorkingDirectory = newState.workingDirectory; - this->bRunInTerminal = newState.terminal; + this->bRunInTerminal = newState.runInTerminal; this->bCategories = newState.categories; this->bKeywords = newState.keywords; Qt::endPropertyUpdateGroup(); + this->tryExec = newState.tryExec; + this->sourcePriority = newState.sourcePriority; + this->terminal = { + .execArg = newState.terminal.execArg, + .appIdArg = newState.terminal.appIdArg, + .titleArg = newState.terminal.titleArg, + .dirArg = newState.terminal.dirArg, + .holdArg = newState.terminal.holdArg, + }; + this->state = newState; this->updateActions(newState.actions); } @@ -258,7 +283,15 @@ void DesktopEntry::updateActions(const QVector& newActions) { } void DesktopEntry::execute() const { - DesktopEntry::doExec(this->bCommand.value(), this->bWorkingDirectory.value()); + DesktopEntry::doExec( + this->bCommand.value(), + this->bWorkingDirectory.value(), + { + .enabled = this->bRunInTerminal.value(), + .appId = this->bStartupClass.value().isEmpty() ? this->mId : this->bStartupClass.value(), + .title = this->bName.value(), + } + ); } bool DesktopEntry::isValid() const { return !this->bName.value().isEmpty(); } @@ -335,15 +368,87 @@ QVector DesktopEntry::parseExecString(const QString& execString) { return arguments; } -void DesktopEntry::doExec(const QList& execString, const QString& workingDirectory) { +void DesktopEntry::doExec( + const QList& execString, + const QString& workingDirectory, + const DoExecTerminal& terminal +) { + auto command = execString; + + if (terminal.enabled) { + auto* manager = DesktopEntryManager::instance(); + auto found = false; + + for (const auto& term: manager->resolvedTerminals) { + if (!term.tryExec.isEmpty() && QStandardPaths::findExecutable(term.tryExec).isEmpty()) { + qCWarning(logDesktopEntry) << "Terminal" << term.command.first() << "TryExec" + << term.tryExec << "not found in PATH (skipping)"; + continue; + } + + if (term.command.isEmpty() || QStandardPaths::findExecutable(term.command.first()).isEmpty()) + { + qCWarning(logDesktopEntry) + << "Terminal executable" << (term.command.isEmpty() ? "(empty)" : term.command.first()) + << "not found in PATH (skipping)"; + continue; + } + + command = QList(); + command.append(term.command); + + auto appendTermArg = [&command](const QString& arg, const QString& value) { + if (arg.isEmpty() || value.isEmpty()) return; + if (arg.endsWith('=')) { + command.append(arg + value); + } else { + command.append(arg); + command.append(value); + } + }; + + appendTermArg(term.appIdArg, terminal.appId); + appendTermArg(term.titleArg, terminal.title); + appendTermArg(term.dirArg, workingDirectory); + + // Do not append the exec argumnet (the "-e" in ghostty -e bash) if it is empty. + // + // If we don't add a check & the exec argument doesn't exist, + // arguments would look like ["termemulator", "", "hx", "a.cpp", "a.hpp"], + // which is not desired. + if (!term.execArg.isEmpty()) command.append(term.execArg); + command.append( + execString + ); // This is a special(ly stupid) overload in QList. Think of it as Rust's `Vec::extend`. + + qCDebug(logDesktopEntry) << "Using terminal emulator:" << term.command.first(); + found = true; + break; + } + + if (!found) { + qCWarning(logDesktopEntry) << "No terminal emulator found; refusing to run without terminal."; + return; + } + } + qs::io::process::ProcessContext ctx; - ctx.setCommand(execString); + ctx.setCommand(command); ctx.setWorkingDirectory(workingDirectory); QuickshellGlobal::execDetached(ctx); } void DesktopAction::execute() const { - DesktopEntry::doExec(this->bCommand.value(), this->entry->bWorkingDirectory.value()); + auto* e = this->entry; + DesktopEntry::doExec( + this->bCommand.value(), + e->bWorkingDirectory.value(), + { + .enabled = e->bRunInTerminal.value(), + .appId = e->bStartupClass.value().isEmpty() ? e->mId : e->bStartupClass.value(), + .title = e->bName.value(), + } + ); } DesktopEntryScanner::DesktopEntryScanner(DesktopEntryManager* manager): manager(manager) { @@ -354,11 +459,14 @@ void DesktopEntryScanner::run() { const auto& desktopPaths = DesktopEntryManager::desktopPaths(); auto scanResults = QList(); + auto sourcePriority = desktopPaths.size(); for (const auto& path: desktopPaths | std::views::reverse) { + --sourcePriority; // Can't use std::views::enumerate, boo hoo. + auto file = QFileInfo(path); if (!file.isDir()) continue; - this->scanDirectory(QDir(path), QString(), scanResults); + this->scanDirectory(QDir(path), QString(), scanResults, static_cast(sourcePriority)); } QMetaObject::invokeMethod( @@ -372,14 +480,15 @@ void DesktopEntryScanner::run() { void DesktopEntryScanner::scanDirectory( const QDir& dir, const QString& idPrefix, - QList& entries + QList& entries, + int sourcePriority ) { auto dirEntries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); for (auto& entry: dirEntries) { if (entry.isDir()) { auto subdirPrefix = idPrefix.isEmpty() ? entry.fileName() : idPrefix + '-' + entry.fileName(); - this->scanDirectory(QDir(entry.absoluteFilePath()), subdirPrefix, entries); + this->scanDirectory(QDir(entry.absoluteFilePath()), subdirPrefix, entries, sourcePriority); } else if (entry.isFile()) { auto path = entry.filePath(); if (!path.endsWith(".desktop")) { @@ -398,6 +507,7 @@ void DesktopEntryScanner::scanDirectory( auto content = QString::fromUtf8(file.readAll()); auto data = DesktopEntry::parseText(id, content); + data.sourcePriority = sourcePriority; entries.append(std::move(data)); } } @@ -502,6 +612,39 @@ const QStringList& DesktopEntryManager::desktopPaths() { return paths; } +const QStringList& DesktopEntryManager::terminalConfigPaths() { + static const auto paths = []() { + auto configDirs = QStringList(); + + auto configHome = qEnvironmentVariable("XDG_CONFIG_HOME"); + if (configHome.isEmpty() && qEnvironmentVariableIsSet("HOME")) + configHome = qEnvironmentVariable("HOME") + "/.config"; + if (!configHome.isEmpty()) configDirs.append(configHome); + + auto configDirsStr = qEnvironmentVariable("XDG_CONFIG_DIRS"); + if (configDirsStr.isEmpty()) configDirsStr = "/etc/xdg"; + for (const auto& dir: configDirsStr.split(':', Qt::SkipEmptyParts)) configDirs.append(dir); + + auto dataDirs = qEnvironmentVariable("XDG_DATA_DIRS"); + if (dataDirs.isEmpty()) dataDirs = "/usr/local/share:/usr/share"; + for (const auto& dir: dataDirs.split(':', Qt::SkipEmptyParts)) + configDirs.append(dir + "/xdg-terminal-exec"); + + auto configPaths = QStringList(); + auto desktopNames = qEnvironmentVariable("XDG_CURRENT_DESKTOP").split(':', Qt::SkipEmptyParts); + for (const auto& dir: configDirs) { + for (const auto& name: desktopNames) { + configPaths.append(dir + "/" + name.toLower() + "-xdg-terminals.list"); + } + configPaths.append(dir + "/xdg-terminals.list"); + } + + return configPaths; + }(); + + return paths; +} + void DesktopEntryManager::onScanCompleted(const QList& scanResults) { auto guard = qScopeGuard([this] { this->scanInProgress = false; @@ -512,77 +655,301 @@ void DesktopEntryManager::onScanCompleted(const QList& s }); auto oldEntries = this->desktopEntries; - auto newEntries = QHash(); - auto newLowercaseEntries = QHash(); - for (const auto& data: scanResults) { - auto lowerId = data.id.toLower(); + { + auto newEntries = QHash(); + auto newEntriesLowercase = QHash(); + + for (const auto& data: scanResults) { + auto lowerId = data.id.toLower(); - if (data.hidden) { - if (auto* victim = newEntries.take(data.id)) victim->deleteLater(); - newLowercaseEntries.remove(lowerId); + if (data.hidden) { + if (auto* victim = newEntries.take(data.id)) victim->deleteLater(); + newEntriesLowercase.remove(lowerId); + + if (auto it = oldEntries.find(data.id); it != oldEntries.end()) { + it.value()->deleteLater(); + oldEntries.erase(it); + } + + qCDebug(logDesktopEntry) << "Masking hidden desktop entry" << data.id; + continue; + } + + DesktopEntry* dentry = nullptr; if (auto it = oldEntries.find(data.id); it != oldEntries.end()) { - it.value()->deleteLater(); + dentry = it.value(); oldEntries.erase(it); + dentry->updateState(data); + } else { + dentry = new DesktopEntry(data.id, this); + dentry->updateState(data); } - qCDebug(logDesktopEntry) << "Masking hidden desktop entry" << data.id; - continue; - } + if (!dentry->isValid()) { + qCDebug(logDesktopEntry) << "Skipping desktop entry" << data.id; + if (!oldEntries.contains(data.id)) { + dentry->deleteLater(); + } + continue; + } - DesktopEntry* dentry = nullptr; + qCDebug(logDesktopEntry) << "Found desktop entry" << data.id; - if (auto it = oldEntries.find(data.id); it != oldEntries.end()) { - dentry = it.value(); - oldEntries.erase(it); - dentry->updateState(data); - } else { - dentry = new DesktopEntry(data.id, this); - dentry->updateState(data); - } + auto conflictingId = newEntries.contains(data.id); - if (!dentry->isValid()) { - qCDebug(logDesktopEntry) << "Skipping desktop entry" << data.id; - if (!oldEntries.contains(data.id)) { - dentry->deleteLater(); + if (conflictingId) { + qCDebug(logDesktopEntry) << "Replacing old entry for" << data.id; + if (auto* victim = newEntries.take(data.id)) victim->deleteLater(); + newEntriesLowercase.remove(lowerId); } - continue; - } - qCDebug(logDesktopEntry) << "Found desktop entry" << data.id; + newEntries.insert(data.id, dentry); + + if (newEntriesLowercase.contains(lowerId)) { + qCInfo(logDesktopEntry).nospace() + << "Multiple desktop entries have the same lowercased id " << lowerId + << ". This can cause ambiguity when byId requests are not made with the correct case " + "already."; - auto conflictingId = newEntries.contains(data.id); + newEntriesLowercase.remove(lowerId); + } - if (conflictingId) { - qCDebug(logDesktopEntry) << "Replacing old entry for" << data.id; - if (auto* victim = newEntries.take(data.id)) victim->deleteLater(); - newLowercaseEntries.remove(lowerId); + newEntriesLowercase.insert(lowerId, dentry); } - newEntries.insert(data.id, dentry); + this->desktopEntries = newEntries; + this->lowercaseDesktopEntries = newEntriesLowercase; + } - if (newLowercaseEntries.contains(lowerId)) { - qCInfo(logDesktopEntry).nospace() - << "Multiple desktop entries have the same lowercased id " << lowerId - << ". This can cause ambiguity when byId requests are not made with the correct case " - "already."; + auto desktopNames = qEnvironmentVariable("XDG_CURRENT_DESKTOP").split(':', Qt::SkipEmptyParts); + auto showInCurrentDesktop = [&desktopNames](const DesktopEntry* entry) { + const auto& onlyShowIn = entry->bOnlyShowIn.value(); + const auto& notShowIn = entry->bNotShowIn.value(); - newLowercaseEntries.remove(lowerId); + for (const auto& name: desktopNames) { + if (onlyShowIn.has_value() && onlyShowIn->contains(name)) return true; + if (notShowIn.has_value() && notShowIn->contains(name)) return false; } - newLowercaseEntries.insert(lowerId, dentry); - } - - this->desktopEntries = newEntries; - this->lowercaseDesktopEntries = newLowercaseEntries; + return !onlyShowIn.has_value(); + }; auto newApplications = QVector(); - for (auto* entry: this->desktopEntries.values()) - if (!entry->bNoDisplay) newApplications.append(entry); + for (auto* entry: this->desktopEntries.values()) { + if (entry->bNoDisplay) continue; + if (!showInCurrentDesktop(entry)) continue; + + newApplications.append(entry); + } this->mApplications.diffUpdate(newApplications); + // Resolve terminal emulators via xdg-terminal-exec strict mode algorithm. + { + this->resolvedTerminals.clear(); + + struct ConfigEntry { + QString id; + QString action; + }; + enum class ConfigLineType { Explicit, Exclude, Protect }; + + auto configEntries = QVector(); + auto seenExplicitIds = QSet(); + auto excludedIds = QSet(); + auto seenFallbackDirectiveIds = QSet(); + + auto parseConfigFile = [&](const QString& path) { + auto isValidConfigEntryId = [](const QString& id) { + if (!id.endsWith(".desktop") || id == ".desktop") return false; + + return std::ranges::all_of(id, [](QChar c) { + auto u = c.unicode(); + return (u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z') || (u >= '0' && u <= '9') + || u == '_' || u == '.' || u == '-'; + }); + }; + auto isValidConfigActionId = [](const QString& id) { + return std::ranges::all_of(id, [](QChar c) { + auto u = c.unicode(); + return (u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z') || (u >= '0' && u <= '9') + || u == '-'; + }); + }; + + auto file = QFile(path); + if (!file.open(QFile::ReadOnly | QFile::Text)) return; + + qCDebug(logDesktopEntry) << "Reading terminal config:" << path; + auto content = QString::fromUtf8(file.readAll()); + + for (const auto& rawLine: content.split('\n', Qt::SkipEmptyParts)) { + auto line = rawLine.trimmed(); + if (line.isEmpty() || line.startsWith('#') || line.startsWith('/')) continue; + + auto type = ConfigLineType::Explicit; + auto target = line; + + if (target.startsWith('-')) { + type = ConfigLineType::Exclude; + target = target.mid(1); + } else if (target.startsWith('+')) { + type = ConfigLineType::Protect; + target = target.mid(1); + } + + auto desktopFileId = target; + auto actionId = QString(); + auto colonIdx = target.indexOf(':'); + if (colonIdx != -1) { + actionId = target.mid(colonIdx + 1); + desktopFileId = target.left(colonIdx); + } + + if (!isValidConfigEntryId(desktopFileId) || !isValidConfigActionId(actionId)) { + qCWarning(logDesktopEntry) << "Discarding invalid terminal config entry:" << line; + continue; + } + + auto entryId = desktopFileId.chopped(8); + + if (type == ConfigLineType::Exclude || type == ConfigLineType::Protect) { + if (seenFallbackDirectiveIds.contains(entryId)) continue; + seenFallbackDirectiveIds.insert(entryId); + if (type == ConfigLineType::Exclude) excludedIds.insert(entryId); + continue; + } + + if (seenExplicitIds.contains(entryId)) continue; + seenExplicitIds.insert(entryId); + + configEntries.append({ + .id = entryId, + .action = actionId, + }); + } + }; + + for (const auto& path: DesktopEntryManager::terminalConfigPaths()) parseConfigFile(path); + + // Expand escape sequences in X-TerminalArg* values (\s \n \t \r \\). + auto expandEscapes = [](const QString& value) { + QString result; + result.reserve(value.size()); + auto escape = false; + + for (auto c: value) { + if (escape) { + switch (c.unicode()) { + case 's': result += u' '; break; + case 'n': result += u'\n'; break; + case 't': result += u'\t'; break; + case 'r': result += u'\r'; break; + case '\\': result += u'\\'; break; + default: + qCWarning(logDesktopEntry).noquote() + << "Illegal escape sequence in desktop entry terminal arg:" << value; + result += c; + break; + } + escape = false; + } else if (c == u'\\') { + escape = true; + } else { + result += c; + } + } + + return result; + }; + + auto termWarn = [](const QString& id, const char* reason) { + qCWarning(logDesktopEntry) << "Terminal" << id << reason << "(skipping)"; + }; + auto termDebug = [](const QString& id, const char* reason) { + qCDebug(logDesktopEntry) << "Terminal" << id << reason << "(skipping)"; + }; + + // Scan-time validation: structural checks only, no PATH lookups. + auto isValidTerminal = [](const DesktopEntry* entry, auto&& log) -> bool { + if (!entry->bCategories.value().contains("TerminalEmulator")) { + log(entry->mId, "missing TerminalEmulator category"); + return false; + } + if (entry->bCommand.value().isEmpty()) { + log(entry->mId, "has empty Exec command"); + return false; + } + if (!entry->terminal.execArg.has_value()) { + log(entry->mId, "missing [X-]TerminalArgExec"); + return false; + } + return true; + }; + + auto addResolved = + [this, &expandEscapes](const DesktopEntry* entry, const QVector& command) { + this->resolvedTerminals.append({ + .command = command, + .tryExec = entry->tryExec, + .execArg = expandEscapes(entry->terminal.execArg.value()), + .appIdArg = expandEscapes(entry->terminal.appIdArg), + .titleArg = expandEscapes(entry->terminal.titleArg), + .dirArg = expandEscapes(entry->terminal.dirArg), + .holdArg = expandEscapes(entry->terminal.holdArg), + }); + }; + + // Explicit phase: config entries in priority order. + for (const auto& configEntry: configEntries) { + auto* dentry = this->desktopEntries.value(configEntry.id); + if (!dentry) { + qCWarning(logDesktopEntry) + << "Terminal" << configEntry.id + << "not found in desktop entries (instructed from xdg-terminals.list)"; + continue; + } + if (!isValidTerminal(dentry, termWarn)) continue; + + auto command = dentry->bCommand.value(); + if (!configEntry.action.isEmpty()) { + auto actions = dentry->actions(); + auto action = std::ranges::find(actions, configEntry.action, &DesktopAction::mId); + if (action == actions.end()) { + qCWarning(logDesktopEntry) + << "Terminal entry" << configEntry.id << "no action named" << configEntry.action; + continue; + } + command = (*action)->bCommand.value(); + } + + addResolved(dentry, command); + qCDebug(logDesktopEntry) << "Terminal candidate (explicit):" << dentry->mId; + } + + // Fallback phase: remaining TerminalEmulator entries. Preserve XDG data-dir + // priority, sort by ID only where the spec leaves order undefined. + auto fallbackEntries = this->desktopEntries.values(); + std::ranges::sort(fallbackEntries, [](const DesktopEntry* a, const DesktopEntry* b) { + if (a->sourcePriority != b->sourcePriority) return a->sourcePriority < b->sourcePriority; + return a->mId < b->mId; + }); + for (auto* entry: fallbackEntries) { + if (seenExplicitIds.contains(entry->mId)) continue; + if (!isValidTerminal(entry, termDebug)) continue; + if (!showInCurrentDesktop(entry)) continue; + if (excludedIds.contains(entry->mId)) continue; + + addResolved(entry, entry->bCommand.value()); + qCDebug(logDesktopEntry) << "Terminal candidate (fallback):" << entry->mId; + } + + qCDebug(logDesktopEntry) << "Resolved" << this->resolvedTerminals.size() + << "terminal candidates"; + } + emit this->applicationsChanged(); for (auto* e: oldEntries) e->deleteLater(); diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp index 0d1eff28..d204d3e6 100644 --- a/src/core/desktopentry.hpp +++ b/src/core/desktopentry.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -29,17 +30,28 @@ struct DesktopActionData { struct ParsedDesktopEntryData { QString id; + int sourcePriority = 0; QString name; QString genericName; QString startupClass; bool noDisplay = false; + std::optional> onlyShowIn; + std::optional> notShowIn; bool hidden = false; QString comment; QString icon; QString execString; QVector command; QString workingDirectory; - bool terminal = false; + bool runInTerminal = false; + QString tryExec; + struct { + std::optional execArg; + QString appIdArg; + QString titleArg; + QString dirArg; + QString holdArg; + } terminal; QVector categories; QVector keywords; QHash entries; @@ -94,9 +106,15 @@ class DesktopEntry: public QObject { static ParsedDesktopEntryData parseText(const QString& id, const QString& text); void updateState(const ParsedDesktopEntryData& newState); - /// Run the application. Currently ignores @@runInTerminal and field codes. + /// Run the application. Currently ignores field codes (%f, %u, %F, %U, etc encoded in @@command). /// - /// This is equivalent to calling @@Quickshell.Quickshell.execDetached() with @@command + /// When @@runInTerminal is true, a suitable terminal emulator resolved via + /// the [xdg-terminal-exec] strict mode algorithm is spawned, with @@command, + /// ID (@@startupClass, or if it is not set, @@id), @@name and @@workingDirectory + /// passed in as arguments. + /// + /// When @@runInTerminal is false, this is equivalent to calling + /// @@Quickshell.Quickshell.execDetached() with @@command /// and @@DesktopEntry.workingDirectory as shown below: /// /// ```qml @@ -105,6 +123,8 @@ class DesktopEntry: public QObject { /// workingDirectory: desktopEntry.workingDirectory, /// }); /// ``` + /// + /// [xdg-terminal-exec]: https://github.com/Vladimir-csp/xdg-terminal-exec Q_INVOKABLE void execute() const; [[nodiscard]] bool isValid() const; @@ -127,9 +147,19 @@ class DesktopEntry: public QObject { } [[nodiscard]] QBindable> bindableKeywords() const { return &this->bKeywords; } - // currently ignores all field codes. + struct DoExecTerminal { + bool enabled; + QString appId; + QString title; + }; + + // currently ignores all field codes (%f, %u, %F, %U, etc). static QVector parseExecString(const QString& execString); - static void doExec(const QList& execString, const QString& workingDirectory); + static void doExec( + const QList& execString, + const QString& workingDirectory, + const DoExecTerminal& terminal = {} + ); signals: void nameChanged(); @@ -144,6 +174,8 @@ class DesktopEntry: public QObject { void runInTerminalChanged(); void categoriesChanged(); void keywordsChanged(); + void onlyShowInChanged(); + void notShowInChanged(); public: QString mId; @@ -153,6 +185,8 @@ class DesktopEntry: public QObject { Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bGenericName, &DesktopEntry::genericNameChanged); Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bStartupClass, &DesktopEntry::startupClassChanged); Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, bool, bNoDisplay, &DesktopEntry::noDisplayChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, std::optional>, bOnlyShowIn, &DesktopEntry::onlyShowInChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, std::optional>, bNotShowIn, &DesktopEntry::notShowInChanged); Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bComment, &DesktopEntry::commentChanged); Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bIcon, &DesktopEntry::iconChanged); Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bExecString, &DesktopEntry::execStringChanged); @@ -163,6 +197,17 @@ class DesktopEntry: public QObject { Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector, bKeywords, &DesktopEntry::keywordsChanged); // clang-format on + // TODO: Expose as bindable. + QString tryExec; + int sourcePriority = 0; + struct { + std::optional execArg; + QString appIdArg; + QString titleArg; + QString dirArg; + QString holdArg; + } terminal; + private: void updateActions(const QVector& newActions); @@ -202,10 +247,7 @@ class DesktopAction: public QObject { , entry(entry) , mId(std::move(id)) {} - /// Run the application. Currently ignores @@DesktopEntry.runInTerminal and field codes. - /// - /// This is equivalent to calling @@Quickshell.Quickshell.execDetached() with @@command - /// and @@DesktopEntry.workingDirectory. + /// Run the action. See @@DesktopEntry.execute() for details on terminal handling. Q_INVOKABLE void execute() const; [[nodiscard]] QBindable bindableName() const { return &this->bName; } @@ -232,6 +274,7 @@ class DesktopAction: public QObject { // clang-format on friend class DesktopEntry; + friend class DesktopEntryManager; }; class DesktopEntryManager; @@ -242,7 +285,7 @@ class DesktopEntryScanner: public QRunnable { void run() override; // clang-format off - void scanDirectory(const QDir& dir, const QString& idPrefix, QList& entries); + void scanDirectory(const QDir& dir, const QString& idPrefix, QList& entries, int sourcePriority); // clang-format on private: @@ -263,6 +306,7 @@ class DesktopEntryManager: public QObject { static DesktopEntryManager* instance(); static const QStringList& desktopPaths(); + static const QStringList& terminalConfigPaths(); signals: void applicationsChanged(); @@ -274,13 +318,25 @@ private slots: private: explicit DesktopEntryManager(); + struct ResolvedTerminal { + QVector command; + QString tryExec; + QString execArg; + QString appIdArg; + QString titleArg; + QString dirArg; + QString holdArg; + }; + QHash desktopEntries; QHash lowercaseDesktopEntries; ObjectModel mApplications {this}; + QVector resolvedTerminals; DesktopEntryMonitor* monitor = nullptr; bool scanInProgress = false; bool scanQueued = false; + friend class DesktopEntry; friend class DesktopEntryScanner; }; diff --git a/src/core/desktopentrymonitor.cpp b/src/core/desktopentrymonitor.cpp index bed6ef15..d545c656 100644 --- a/src/core/desktopentrymonitor.cpp +++ b/src/core/desktopentrymonitor.cpp @@ -31,7 +31,13 @@ DesktopEntryMonitor::DesktopEntryMonitor(QObject* parent): QObject(parent) { &this->watcher, &QFileSystemWatcher::directoryChanged, this, - &DesktopEntryMonitor::onDirectoryChanged + &DesktopEntryMonitor::onWatchedPathChanged + ); + QObject::connect( + &this->watcher, + &QFileSystemWatcher::fileChanged, + this, + &DesktopEntryMonitor::onWatchedPathChanged ); QObject::connect( &this->debounceTimer, @@ -40,15 +46,18 @@ DesktopEntryMonitor::DesktopEntryMonitor(QObject* parent): QObject(parent) { &DesktopEntryMonitor::processChanges ); - this->startMonitoring(); + this->updateWatchedPaths(); } -void DesktopEntryMonitor::startMonitoring() { +void DesktopEntryMonitor::updateWatchedPaths() { for (const auto& path: DesktopEntryManager::desktopPaths()) { - if (!QDir(path).exists()) continue; addPathAndParents(this->watcher, path); this->scanAndWatch(path); } + + for (const auto& path: DesktopEntryManager::terminalConfigPaths()) { + addPathAndParents(this->watcher, path); + } } void DesktopEntryMonitor::scanAndWatch(const QString& dirPath) { @@ -61,8 +70,11 @@ void DesktopEntryMonitor::scanAndWatch(const QString& dirPath) { for (const auto& subdir: subdirs) this->watcher.addPath(subdir.absoluteFilePath()); } -void DesktopEntryMonitor::onDirectoryChanged(const QString& /*path*/) { +void DesktopEntryMonitor::onWatchedPathChanged(const QString& /*path*/) { this->debounceTimer.start(); } -void DesktopEntryMonitor::processChanges() { emit this->desktopEntriesChanged(); } \ No newline at end of file +void DesktopEntryMonitor::processChanges() { + this->updateWatchedPaths(); + emit this->desktopEntriesChanged(); +} diff --git a/src/core/desktopentrymonitor.hpp b/src/core/desktopentrymonitor.hpp index eb3251df..32cb7960 100644 --- a/src/core/desktopentrymonitor.hpp +++ b/src/core/desktopentrymonitor.hpp @@ -20,11 +20,11 @@ class DesktopEntryMonitor: public QObject { void desktopEntriesChanged(); private slots: - void onDirectoryChanged(const QString& path); + void onWatchedPathChanged(const QString& path); void processChanges(); private: - void startMonitoring(); + void updateWatchedPaths(); void scanAndWatch(const QString& dirPath); QFileSystemWatcher watcher; diff --git a/src/launch/launch.cpp b/src/launch/launch.cpp index 3a9a2a57..f0a5ef93 100644 --- a/src/launch/launch.cpp +++ b/src/launch/launch.cpp @@ -196,13 +196,10 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio // This seems to be controlled by the QPA and qt6ct does not provide it. { QList 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"); + { + auto dataDirs = qEnvironmentVariable("XDG_DATA_DIRS"); + if (dataDirs.isEmpty()) dataDirs = "/usr/local/share:/usr/share"; + dataPaths = dataDirs.split(u':', Qt::SkipEmptyParts); } auto fallbackPaths = QIcon::fallbackSearchPaths();