#pragma once #include #include #include #include #include #include #include #include #include #include "desktopentrymonitor.hpp" #include "doc.hpp" #include "model.hpp" class DesktopAction; class DesktopEntryMonitor; struct DesktopActionData { QString id; QString name; QString icon; QString execString; QVector command; QHash entries; }; struct ParsedDesktopEntryData { QString id; QString name; QString genericName; QString startupClass; bool noDisplay = false; QString comment; QString icon; QString execString; QVector command; QString workingDirectory; bool terminal = false; QVector categories; QVector keywords; QHash entries; QHash actions; }; /// A desktop entry. See @@DesktopEntries for details. class DesktopEntry: public QObject { Q_OBJECT; Q_PROPERTY(QString id MEMBER mId CONSTANT); /// Name of the specific application, such as "Firefox". // clang-format off Q_PROPERTY(QString name READ default WRITE default NOTIFY nameChanged BINDABLE bindableName); /// Short description of the application, such as "Web Browser". May be empty. Q_PROPERTY(QString genericName READ default WRITE default NOTIFY genericNameChanged BINDABLE bindableGenericName); /// Initial class or app id the app intends to use. May be useful for matching running apps /// to desktop entries. Q_PROPERTY(QString startupClass READ default WRITE default NOTIFY startupClassChanged BINDABLE bindableStartupClass); /// If true, this application should not be displayed in menus and launchers. Q_PROPERTY(bool noDisplay READ default WRITE default NOTIFY noDisplayChanged BINDABLE bindableNoDisplay); /// Long description of the application, such as "View websites on the internet". May be empty. Q_PROPERTY(QString comment READ default WRITE default NOTIFY commentChanged BINDABLE bindableComment); /// Name of the icon associated with this application. May be empty. Q_PROPERTY(QString icon READ default WRITE default NOTIFY iconChanged BINDABLE bindableIcon); /// The raw `Exec` string from the desktop entry. /// /// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run. Q_PROPERTY(QString execString READ default WRITE default NOTIFY execStringChanged BINDABLE bindableExecString); /// The parsed `Exec` command in the desktop entry. /// /// The entry can be run with @@execute(), or by using this command in /// @@Quickshell.Quickshell.execDetached() or @@Quickshell.Io.Process. /// If used in `execDetached` or a `Process`, @@workingDirectory should also be passed to /// the invoked process. See @@execute() for details. /// /// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true. Q_PROPERTY(QVector command READ default WRITE default NOTIFY commandChanged BINDABLE bindableCommand); /// The working directory to execute from. Q_PROPERTY(QString workingDirectory READ default WRITE default NOTIFY workingDirectoryChanged BINDABLE bindableWorkingDirectory); /// If the application should run in a terminal. Q_PROPERTY(bool runInTerminal READ default WRITE default NOTIFY runInTerminalChanged BINDABLE bindableRunInTerminal); Q_PROPERTY(QVector categories READ default WRITE default NOTIFY categoriesChanged BINDABLE bindableCategories); Q_PROPERTY(QVector keywords READ default WRITE default NOTIFY keywordsChanged BINDABLE bindableKeywords); // clang-format on Q_PROPERTY(QVector 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)) {} static ParsedDesktopEntryData parseText(const QString& id, const QString& text); void updateState(const ParsedDesktopEntryData& newState); /// Run the application. Currently ignores @@runInTerminal and field codes. /// /// This is equivalent to calling @@Quickshell.Quickshell.execDetached() with @@command /// and @@DesktopEntry.workingDirectory as shown below: /// /// ```qml /// Quickshell.execDetached({ /// command: desktopEntry.command, /// workingDirectory: desktopEntry.workingDirectory, /// }); /// ``` Q_INVOKABLE void execute() const; [[nodiscard]] bool isValid() const; [[nodiscard]] QVector actions() const; [[nodiscard]] QBindable bindableName() const { return &this->bName; } [[nodiscard]] QBindable bindableGenericName() const { return &this->bGenericName; } [[nodiscard]] QBindable bindableStartupClass() const { return &this->bStartupClass; } [[nodiscard]] QBindable bindableNoDisplay() const { return &this->bNoDisplay; } [[nodiscard]] QBindable bindableComment() const { return &this->bComment; } [[nodiscard]] QBindable bindableIcon() const { return &this->bIcon; } [[nodiscard]] QBindable bindableExecString() const { return &this->bExecString; } [[nodiscard]] QBindable> bindableCommand() const { return &this->bCommand; } [[nodiscard]] QBindable bindableWorkingDirectory() const { return &this->bWorkingDirectory; } [[nodiscard]] QBindable bindableRunInTerminal() const { return &this->bRunInTerminal; } [[nodiscard]] QBindable> bindableCategories() const { return &this->bCategories; } [[nodiscard]] QBindable> bindableKeywords() const { return &this->bKeywords; } // currently ignores all field codes. static QVector parseExecString(const QString& execString); static void doExec(const QList& execString, const QString& workingDirectory); signals: void nameChanged(); void genericNameChanged(); void startupClassChanged(); void noDisplayChanged(); void commentChanged(); void iconChanged(); void execStringChanged(); void commandChanged(); void workingDirectoryChanged(); void runInTerminalChanged(); void categoriesChanged(); void keywordsChanged(); public: QString mId; // clang-format off Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bName, &DesktopEntry::nameChanged); 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, QString, bComment, &DesktopEntry::commentChanged); Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bIcon, &DesktopEntry::iconChanged); Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bExecString, &DesktopEntry::execStringChanged); Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector, bCommand, &DesktopEntry::commandChanged); Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bWorkingDirectory, &DesktopEntry::workingDirectoryChanged); Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, bool, bRunInTerminal, &DesktopEntry::runInTerminalChanged); Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector, bCategories, &DesktopEntry::categoriesChanged); Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector, bKeywords, &DesktopEntry::keywordsChanged); // clang-format on private: void updateActions(const QHash& newActions); ParsedDesktopEntryData state; QHash mActions; friend class DesktopAction; }; /// An action of a @@DesktopEntry$. class DesktopAction: public QObject { Q_OBJECT; Q_PROPERTY(QString id MEMBER mId CONSTANT); // clang-format off Q_PROPERTY(QString name READ default WRITE default NOTIFY nameChanged BINDABLE bindableName); Q_PROPERTY(QString icon READ default WRITE default NOTIFY iconChanged BINDABLE bindableIcon); /// The raw `Exec` string from the action. /// /// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run. Q_PROPERTY(QString execString READ default WRITE default NOTIFY execStringChanged BINDABLE bindableExecString); /// The parsed `Exec` command in the action. /// /// The entry can be run with @@execute(), or by using this command in /// @@Quickshell.Quickshell.execDetached() or @@Quickshell.Io.Process. /// If used in `execDetached` or a `Process`, @@DesktopEntry.workingDirectory should also be passed to /// the invoked process. /// /// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true. Q_PROPERTY(QVector command READ default WRITE default NOTIFY commandChanged BINDABLE bindableCommand); // clang-format on 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 @@DesktopEntry.runInTerminal and field codes. /// /// This is equivalent to calling @@Quickshell.Quickshell.execDetached() with @@command /// and @@DesktopEntry.workingDirectory. Q_INVOKABLE void execute() const; [[nodiscard]] QBindable bindableName() const { return &this->bName; } [[nodiscard]] QBindable bindableIcon() const { return &this->bIcon; } [[nodiscard]] QBindable bindableExecString() const { return &this->bExecString; } [[nodiscard]] QBindable> bindableCommand() const { return &this->bCommand; } signals: void nameChanged(); void iconChanged(); void execStringChanged(); void commandChanged(); private: DesktopEntry* entry; QString mId; QHash mEntries; // clang-format off Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bName, &DesktopAction::nameChanged); Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bIcon, &DesktopAction::iconChanged); Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bExecString, &DesktopAction::execStringChanged); Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QVector, bCommand, &DesktopAction::commandChanged); // clang-format on friend class DesktopEntry; }; class DesktopEntryManager; class DesktopEntryScanner: public QRunnable { public: explicit DesktopEntryScanner(DesktopEntryManager* manager); void run() override; // clang-format off void scanDirectory(const QDir& dir, const QString& idPrefix, QList& entries); // clang-format on private: DesktopEntryManager* manager; }; class DesktopEntryManager: public QObject { Q_OBJECT; public: void scanDesktopEntries(); [[nodiscard]] DesktopEntry* byId(const QString& id); [[nodiscard]] DesktopEntry* heuristicLookup(const QString& name); [[nodiscard]] ObjectModel* applications(); static DesktopEntryManager* instance(); static const QStringList& desktopPaths(); signals: void applicationsChanged(); private slots: void handleFileChanges(); void onScanCompleted(const QList& scanResults); private: explicit DesktopEntryManager(); QHash desktopEntries; QHash lowercaseDesktopEntries; ObjectModel mApplications {this}; DesktopEntryMonitor* monitor = nullptr; bool scanInProgress = false; bool scanQueued = false; friend class DesktopEntryScanner; }; ///! 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. QSDOC_TYPE_OVERRIDE(ObjectModel*); Q_PROPERTY(UntypedObjectModel* applications READ applications CONSTANT); QML_ELEMENT; QML_SINGLETON; public: explicit DesktopEntries(); /// Look up a desktop entry by name. Includes NoDisplay entries. May return null. /// /// While this function requires an exact match, @@heuristicLookup() will correctly /// find an entry more often and is generally more useful. Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id); /// Look up a desktop entry by name using heuristics. Unlike @@byId(), /// if no exact matches are found this function will try to guess - potentially incorrectly. /// May return null. Q_INVOKABLE [[nodiscard]] static DesktopEntry* heuristicLookup(const QString& name); [[nodiscard]] static ObjectModel* applications(); signals: void applicationsChanged(); };