File indexing completed on 2024-04-21 07:43:48

0001 /*
0002     SPDX-FileCopyrightText: 2006 Aaron Seigo <aseigo@kde.org>
0003     SPDX-FileCopyrightText: 2007, 2009 Ryan P. Bitanga <ryan.bitanga@gmail.com>
0004     SPDX-FileCopyrightText: 2008 Jordi Polo <mumismo@gmail.com>
0005     SPDX-FileCopyrightText: 2023 Alexander Lohnau <alexander.lohnauŋmx.de>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include "runnermanager.h"
0011 
0012 #include <QCoreApplication>
0013 #include <QDir>
0014 #include <QElapsedTimer>
0015 #include <QMutableListIterator>
0016 #include <QPointer>
0017 #include <QRegularExpression>
0018 #include <QStandardPaths>
0019 #include <QThread>
0020 #include <QTimer>
0021 
0022 #include <KConfigWatcher>
0023 #include <KFileUtils>
0024 #include <KPluginMetaData>
0025 #include <KSharedConfig>
0026 
0027 #include "abstractrunner_p.h"
0028 #include "dbusrunner_p.h"
0029 #include "kpluginmetadata_utils_p.h"
0030 #include "krunner_debug.h"
0031 #include "querymatch.h"
0032 
0033 namespace KRunner
0034 {
0035 class RunnerManagerPrivate
0036 {
0037 public:
0038     RunnerManagerPrivate(const KConfigGroup &configurationGroup, KConfigGroup stateConfigGroup, RunnerManager *parent)
0039         : q(parent)
0040         , context(parent)
0041         , pluginConf(configurationGroup)
0042         , stateData(stateConfigGroup)
0043     {
0044         initializeKNotifyPluginWatcher();
0045         matchChangeTimer.setSingleShot(true);
0046         matchChangeTimer.setTimerType(Qt::TimerType::PreciseTimer); // Without this, autotest will fail due to imprecision of this timer
0047 
0048         QObject::connect(&matchChangeTimer, &QTimer::timeout, q, [this]() {
0049             matchesChanged();
0050         });
0051 
0052         // Set up tracking of the last time matchesChanged was signalled
0053         lastMatchChangeSignalled.start();
0054         QObject::connect(q, &RunnerManager::matchesChanged, q, [&] {
0055             lastMatchChangeSignalled.restart();
0056         });
0057         loadConfiguration();
0058     }
0059 
0060     void scheduleMatchesChanged()
0061     {
0062         // We avoid over-refreshing the client. We only refresh every this much milliseconds
0063         constexpr int refreshPeriod = 250;
0064         // This will tell us if we are reseting the matches to start a new search. RunnerContext::reset() clears its query string for its emission
0065         if (context.query().isEmpty()) {
0066             matchChangeTimer.stop();
0067             // This actually contains the query string for the new search that we're launching (if any):
0068             if (!this->untrimmedTerm.trimmed().isEmpty()) {
0069                 // We are starting a new search, we shall stall for some time before deciding to show an empty matches list.
0070                 // This stall should be enough for the engine to provide more meaningful result, so we avoid refreshing with
0071                 // an empty results list if possible.
0072                 matchChangeTimer.start(refreshPeriod);
0073                 // We "pretend" that we have refreshed it so the next call will be forced to wait the timeout:
0074                 lastMatchChangeSignalled.restart();
0075             } else {
0076                 // We have an empty input string, so it's not a real query. We don't expect any results to come, so no need to stall
0077                 Q_EMIT q->matchesChanged(context.matches());
0078             }
0079         } else if (lastMatchChangeSignalled.hasExpired(refreshPeriod)) {
0080             matchChangeTimer.stop();
0081             Q_EMIT q->matchesChanged(context.matches());
0082         } else {
0083             matchChangeTimer.start(refreshPeriod - lastMatchChangeSignalled.elapsed());
0084         }
0085     }
0086 
0087     void matchesChanged()
0088     {
0089         Q_EMIT q->matchesChanged(context.matches());
0090     }
0091 
0092     void loadConfiguration()
0093     {
0094         const KConfigGroup generalConfig = pluginConf.config()->group(QStringLiteral("General"));
0095         context.restore(stateData);
0096     }
0097 
0098     void loadSingleRunner()
0099     {
0100         // In case we are not in the single runner mode of we do not have an id
0101         if (!singleMode || singleModeRunnerId.isEmpty()) {
0102             currentSingleRunner = nullptr;
0103             return;
0104         }
0105 
0106         if (currentSingleRunner && currentSingleRunner->id() == singleModeRunnerId) {
0107             return;
0108         }
0109         currentSingleRunner = q->runner(singleModeRunnerId);
0110         // If there are no runners loaded or the single runner could no be loaded,
0111         // this is the case if it was disabled but gets queries using the singleRunnerMode, BUG: 435050
0112         if (runners.isEmpty() || !currentSingleRunner) {
0113             loadRunners(singleModeRunnerId);
0114             currentSingleRunner = q->runner(singleModeRunnerId);
0115         }
0116     }
0117 
0118     void deleteRunners(const QList<AbstractRunner *> &runners)
0119     {
0120         for (const auto runner : runners) {
0121             if (qobject_cast<DBusRunner *>(runner)) {
0122                 runner->deleteLater();
0123             } else {
0124                 Q_ASSERT(runner->thread() != q->thread());
0125                 runner->thread()->quit();
0126                 QObject::connect(runner->thread(), &QThread::finished, runner->thread(), &QObject::deleteLater);
0127                 QObject::connect(runner->thread(), &QThread::finished, runner, &QObject::deleteLater);
0128             }
0129         }
0130     }
0131 
0132     void loadRunners(const QString &singleRunnerId = QString())
0133     {
0134         QList<KPluginMetaData> offers = RunnerManager::runnerMetaDataList();
0135 
0136         const bool loadAll = stateData.readEntry("loadAll", false);
0137         const bool noWhiteList = whiteList.isEmpty();
0138 
0139         QList<AbstractRunner *> deadRunners;
0140         QMutableListIterator<KPluginMetaData> it(offers);
0141         while (it.hasNext()) {
0142             const KPluginMetaData &description = it.next();
0143             qCDebug(KRUNNER) << "Loading runner: " << description.pluginId();
0144 
0145             const QString runnerName = description.pluginId();
0146             const bool isPluginEnabled = description.isEnabled(pluginConf);
0147             const bool loaded = runners.contains(runnerName);
0148             bool selected = loadAll || disabledRunnerIds.contains(runnerName) || (isPluginEnabled && (noWhiteList || whiteList.contains(runnerName)));
0149             if (!selected && runnerName == singleRunnerId) {
0150                 selected = true;
0151                 disabledRunnerIds << runnerName;
0152             }
0153 
0154             if (selected) {
0155                 if (!loaded) {
0156                     if (auto runner = loadInstalledRunner(description)) {
0157                         qCDebug(KRUNNER) << "Loaded:" << runnerName;
0158                         runners.insert(runnerName, runner);
0159                     }
0160                 }
0161             } else if (loaded) {
0162                 // Remove runner
0163                 deadRunners.append(runners.take(runnerName));
0164                 qCDebug(KRUNNER) << "Plugin disabled. Removing runner: " << runnerName;
0165             }
0166         }
0167 
0168         deleteRunners(deadRunners);
0169         // in case we deleted it up above, just to be sure we do not have a dangeling pointer
0170         currentSingleRunner = nullptr;
0171         qCDebug(KRUNNER) << "All runners loaded, total:" << runners.count();
0172     }
0173 
0174     AbstractRunner *loadInstalledRunner(const KPluginMetaData &pluginMetaData)
0175     {
0176         if (!pluginMetaData.isValid()) {
0177             return nullptr;
0178         }
0179 
0180         AbstractRunner *runner = nullptr;
0181 
0182         const QString api = pluginMetaData.value(QStringLiteral("X-Plasma-API"));
0183         const bool isCppPlugin = api.isEmpty();
0184 
0185         if (isCppPlugin) {
0186             if (auto res = KPluginFactory::instantiatePlugin<AbstractRunner>(pluginMetaData, q)) {
0187                 runner = res.plugin;
0188             } else {
0189                 qCWarning(KRUNNER).nospace() << "Could not load runner " << pluginMetaData.name() << ":" << res.errorString
0190                                              << " (library path was:" << pluginMetaData.fileName() << ")";
0191             }
0192         } else if (api.startsWith(QLatin1String("DBus"))) {
0193             runner = new DBusRunner(q, pluginMetaData);
0194         } else {
0195             qCWarning(KRUNNER) << "Unknown X-Plasma-API requested for runner" << pluginMetaData.fileName();
0196             return nullptr;
0197         }
0198 
0199         if (runner) {
0200             QPointer<AbstractRunner> ptr(runner);
0201             q->connect(runner, &AbstractRunner::matchingResumed, q, [this, ptr]() {
0202                 if (ptr) {
0203                     runnerMatchingResumed(ptr.get());
0204                 }
0205             });
0206             if (isCppPlugin) {
0207                 auto thread = new QThread();
0208                 thread->setObjectName(pluginMetaData.pluginId());
0209                 thread->start();
0210                 runner->moveToThread(thread);
0211             }
0212             // The runner might outlive the manager due to us waiting for the thread to exit
0213             q->connect(runner, &AbstractRunner::matchInternalFinished, q, [this](const QString &jobId) {
0214                 onRunnerJobFinished(jobId);
0215             });
0216 
0217             if (prepped) {
0218                 Q_EMIT runner->prepare();
0219             }
0220         }
0221 
0222         return runner;
0223     }
0224 
0225     void onRunnerJobFinished(const QString &jobId)
0226     {
0227         if (currentJobs.remove(jobId) && currentJobs.isEmpty()) {
0228             // If there are any new matches scheduled to be notified, we should anticipate it and just refresh right now
0229             if (matchChangeTimer.isActive()) {
0230                 matchChangeTimer.stop();
0231                 matchesChanged();
0232             } else if (context.matches().isEmpty()) {
0233                 // we finished our run, and there are no valid matches, and so no
0234                 // signal will have been sent out, so we need to emit the signal ourselves here
0235                 matchesChanged();
0236             }
0237             Q_EMIT q->queryFinished(); // NOLINT(readability-misleading-indentation)
0238         }
0239         if (!currentJobs.isEmpty()) {
0240             qCDebug(KRUNNER) << "Current jobs are" << currentJobs;
0241         }
0242     }
0243 
0244     void teardown()
0245     {
0246         pendingJobsAfterSuspend.clear(); // Do not start old jobs when the match session is over
0247         if (allRunnersPrepped) {
0248             for (AbstractRunner *runner : std::as_const(runners)) {
0249                 Q_EMIT runner->teardown();
0250             }
0251             allRunnersPrepped = false;
0252         }
0253 
0254         if (singleRunnerPrepped) {
0255             if (currentSingleRunner) {
0256                 Q_EMIT currentSingleRunner->teardown();
0257             }
0258             singleRunnerPrepped = false;
0259         }
0260 
0261         prepped = false;
0262     }
0263 
0264     void runnerMatchingResumed(AbstractRunner *runner)
0265     {
0266         Q_ASSERT(runner);
0267         const QString jobId = pendingJobsAfterSuspend.value(runner);
0268         if (jobId.isEmpty()) {
0269             qCDebug(KRUNNER) << runner << "was not scheduled for current query";
0270             return;
0271         }
0272         // Ignore this runner
0273         if (singleMode && runner->id() != singleModeRunnerId) {
0274             qCDebug(KRUNNER) << runner << "did not match requested singlerunnermode ID";
0275             return;
0276         }
0277 
0278         const QString query = context.query();
0279         bool matchesCount = singleMode || runner->minLetterCount() <= query.size();
0280         bool matchesRegex = singleMode || !runner->hasMatchRegex() || runner->matchRegex().match(query).hasMatch();
0281 
0282         if (matchesCount && matchesRegex) {
0283             startJob(runner);
0284         } else {
0285             onRunnerJobFinished(jobId);
0286         }
0287     }
0288 
0289     void startJob(AbstractRunner *runner)
0290     {
0291         QMetaObject::invokeMethod(runner, "matchInternal", Qt::QueuedConnection, Q_ARG(KRunner::RunnerContext, context));
0292     }
0293 
0294     // Must only be called once
0295     void initializeKNotifyPluginWatcher()
0296     {
0297         Q_ASSERT(!watcher);
0298         watcher = KConfigWatcher::create(KSharedConfig::openConfig(pluginConf.config()->name()));
0299         q->connect(watcher.data(), &KConfigWatcher::configChanged, q, [this](const KConfigGroup &group, const QByteArrayList &changedNames) {
0300             const QString groupName = group.name();
0301             if (groupName == QLatin1String("Plugins")) {
0302                 q->reloadConfiguration();
0303             } else if (groupName == QLatin1String("Runners")) {
0304                 for (auto *runner : std::as_const(runners)) {
0305                     // Signals from the KCM contain the component name, which is the KRunner plugin's id
0306                     if (changedNames.contains(runner->metadata().pluginId().toUtf8())) {
0307                         QMetaObject::invokeMethod(runner, "reloadConfigurationInternal");
0308                     }
0309                 }
0310             } else if (group.parent().isValid() && group.parent().name() == QLatin1String("Runners")) {
0311                 for (auto *runner : std::as_const(runners)) {
0312                     // If the same config group has been modified which gets created in AbstractRunner::config()
0313                     if (groupName == runner->id()) {
0314                         QMetaObject::invokeMethod(runner, "reloadConfigurationInternal");
0315                     }
0316                 }
0317             }
0318         });
0319     }
0320 
0321     void addToHistory()
0322     {
0323         const QString term = context.query();
0324         // We want to imitate the shall behavior
0325         if (!historyEnabled || term.isEmpty() || untrimmedTerm.startsWith(QLatin1Char(' '))) {
0326             return;
0327         }
0328         QStringList historyEntries = readHistoryForCurrentEnv();
0329         // Avoid removing the same item from the front and prepending it again
0330         if (!historyEntries.isEmpty() && historyEntries.constFirst() == term) {
0331             return;
0332         }
0333 
0334         historyEntries.removeOne(term);
0335         historyEntries.prepend(term);
0336 
0337         while (historyEntries.count() > 50) { // we don't want to store more than 50 entries
0338             historyEntries.removeLast();
0339         }
0340         writeHistory(historyEntries);
0341     }
0342 
0343     void writeHistory(const QStringList &historyEntries)
0344     {
0345         stateData.group(QStringLiteral("History")).writeEntry(historyEnvironmentIdentifier, historyEntries, KConfig::Notify);
0346         stateData.sync();
0347     }
0348 
0349     inline QStringList readHistoryForCurrentEnv()
0350     {
0351         return stateData.group(QStringLiteral("History")).readEntry(historyEnvironmentIdentifier, QStringList());
0352     }
0353 
0354     QString historyEnvironmentIdentifier = QStringLiteral("default");
0355     RunnerManager *const q;
0356     RunnerContext context;
0357     QTimer matchChangeTimer;
0358     QElapsedTimer lastMatchChangeSignalled;
0359     QHash<QString, AbstractRunner *> runners;
0360     QHash<AbstractRunner *, QString> pendingJobsAfterSuspend;
0361     AbstractRunner *currentSingleRunner = nullptr;
0362     QSet<QString> currentJobs;
0363     QString singleModeRunnerId;
0364     bool prepped = false;
0365     bool allRunnersPrepped = false;
0366     bool singleRunnerPrepped = false;
0367     bool singleMode = false;
0368     bool historyEnabled = true;
0369     QStringList whiteList;
0370     KConfigWatcher::Ptr watcher;
0371     QString untrimmedTerm;
0372     KConfigGroup pluginConf;
0373     KConfigGroup stateData;
0374     QSet<QString> disabledRunnerIds; // Runners that are disabled but were loaded as single runners
0375 };
0376 
0377 RunnerManager::RunnerManager(const KConfigGroup &pluginConfigGroup, KConfigGroup stateConfigGroup, QObject *parent)
0378     : QObject(parent)
0379     , d(new RunnerManagerPrivate(pluginConfigGroup, stateConfigGroup, this))
0380 {
0381     Q_ASSERT(pluginConfigGroup.isValid());
0382     Q_ASSERT(stateConfigGroup.isValid());
0383 }
0384 
0385 RunnerManager::RunnerManager(QObject *parent)
0386     : QObject(parent)
0387 {
0388     auto defaultStatePtr = KSharedConfig::openConfig(QStringLiteral("krunnerstaterc"), KConfig::NoGlobals, QStandardPaths::GenericDataLocation);
0389     auto configPtr = KSharedConfig::openConfig(QStringLiteral("krunnerrc"), KConfig::NoGlobals);
0390     d.reset(new RunnerManagerPrivate(configPtr->group(QStringLiteral("Plugins")), defaultStatePtr->group(QStringLiteral("PlasmaRunnerManager")), this));
0391 }
0392 
0393 RunnerManager::~RunnerManager()
0394 {
0395     d->context.reset();
0396     d->deleteRunners(d->runners.values());
0397 }
0398 
0399 void RunnerManager::reloadConfiguration()
0400 {
0401     d->pluginConf.config()->reparseConfiguration();
0402     d->stateData.config()->reparseConfiguration();
0403     d->loadConfiguration();
0404     d->loadRunners();
0405 }
0406 
0407 void RunnerManager::setAllowedRunners(const QStringList &runners)
0408 {
0409     d->whiteList = runners;
0410     if (!d->runners.isEmpty()) {
0411         // this has been called with runners already created. so let's do an instant reload
0412         d->loadRunners();
0413     }
0414 }
0415 
0416 AbstractRunner *RunnerManager::loadRunner(const KPluginMetaData &pluginMetaData)
0417 {
0418     const QString runnerId = pluginMetaData.pluginId();
0419     if (auto loadedRunner = d->runners.value(runnerId)) {
0420         return loadedRunner;
0421     }
0422     if (!runnerId.isEmpty()) {
0423         if (AbstractRunner *runner = d->loadInstalledRunner(pluginMetaData)) {
0424             d->runners.insert(runnerId, runner);
0425             return runner;
0426         }
0427     }
0428     return nullptr;
0429 }
0430 
0431 AbstractRunner *RunnerManager::runner(const QString &pluginId) const
0432 {
0433     if (d->runners.isEmpty()) {
0434         d->loadRunners();
0435     }
0436 
0437     return d->runners.value(pluginId, nullptr);
0438 }
0439 
0440 QList<AbstractRunner *> RunnerManager::runners() const
0441 {
0442     if (d->runners.isEmpty()) {
0443         d->loadRunners();
0444     }
0445     return d->runners.values();
0446 }
0447 
0448 RunnerContext *RunnerManager::searchContext() const
0449 {
0450     return &d->context;
0451 }
0452 
0453 QList<QueryMatch> RunnerManager::matches() const
0454 {
0455     return d->context.matches();
0456 }
0457 
0458 bool RunnerManager::run(const QueryMatch &match, const KRunner::Action &selectedAction)
0459 {
0460     if (!match.isValid() || !match.isEnabled()) { // The model should prevent this
0461         return false;
0462     }
0463 
0464     // Modify the match and run it
0465     QueryMatch m = match;
0466     m.setSelectedAction(selectedAction);
0467     m.runner()->run(d->context, m);
0468     // To allow the RunnerContext to increase the relevance of often launched apps
0469     d->context.increaseLaunchCount(m);
0470 
0471     if (!d->context.shouldIgnoreCurrentMatchForHistory()) {
0472         d->addToHistory();
0473     }
0474     if (d->context.requestedQueryString().isEmpty()) {
0475         return true;
0476     } else {
0477         Q_EMIT requestUpdateQueryString(d->context.requestedQueryString(), d->context.requestedCursorPosition());
0478         return false;
0479     }
0480 }
0481 
0482 QMimeData *RunnerManager::mimeDataForMatch(const QueryMatch &match) const
0483 {
0484     return match.isValid() ? match.runner()->mimeDataForMatch(match) : nullptr;
0485 }
0486 
0487 QList<KPluginMetaData> RunnerManager::runnerMetaDataList()
0488 {
0489     QList<KPluginMetaData> pluginMetaDatas = KPluginMetaData::findPlugins(QStringLiteral("kf6/krunner"));
0490     QSet<QString> knownRunnerIds;
0491     knownRunnerIds.reserve(pluginMetaDatas.size());
0492     for (const KPluginMetaData &pluginMetaData : std::as_const(pluginMetaDatas)) {
0493         knownRunnerIds.insert(pluginMetaData.pluginId());
0494     }
0495 
0496     const QStringList dBusPlugindirs =
0497         QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("krunner/dbusplugins"), QStandardPaths::LocateDirectory);
0498     const QStringList dbusRunnerFiles = KFileUtils::findAllUniqueFiles(dBusPlugindirs, QStringList(QStringLiteral("*.desktop")));
0499     for (const QString &dbusRunnerFile : dbusRunnerFiles) {
0500         KPluginMetaData pluginMetaData = parseMetaDataFromDesktopFile(dbusRunnerFile);
0501         if (pluginMetaData.isValid() && !knownRunnerIds.contains(pluginMetaData.pluginId())) {
0502             pluginMetaDatas.append(pluginMetaData);
0503             knownRunnerIds.insert(pluginMetaData.pluginId());
0504         }
0505     }
0506 
0507     return pluginMetaDatas;
0508 }
0509 
0510 void RunnerManager::setupMatchSession()
0511 {
0512     if (d->prepped) {
0513         return;
0514     }
0515 
0516     d->prepped = true;
0517     if (d->singleMode) {
0518         if (d->currentSingleRunner) {
0519             Q_EMIT d->currentSingleRunner->prepare();
0520             d->singleRunnerPrepped = true;
0521         }
0522     } else {
0523         for (AbstractRunner *runner : std::as_const(d->runners)) {
0524             if (!d->disabledRunnerIds.contains(runner->name())) {
0525                 Q_EMIT runner->prepare();
0526             }
0527         }
0528 
0529         d->allRunnersPrepped = true;
0530     }
0531 }
0532 
0533 void RunnerManager::matchSessionComplete()
0534 {
0535     if (!d->prepped) {
0536         return;
0537     }
0538 
0539     d->teardown();
0540     // We save the context config after each session, just like the history entries
0541     // BUG: 424505
0542     d->context.save(d->stateData);
0543 }
0544 
0545 void RunnerManager::launchQuery(const QString &untrimmedTerm, const QString &runnerName)
0546 {
0547     d->pendingJobsAfterSuspend.clear(); // Do not start old jobs when we got a new query
0548     QString term = untrimmedTerm.trimmed();
0549     const QString prevSingleRunner = d->singleModeRunnerId;
0550     d->untrimmedTerm = untrimmedTerm;
0551 
0552     // Set the required values and load the runner
0553     d->singleModeRunnerId = runnerName;
0554     d->singleMode = !runnerName.isEmpty();
0555     d->loadSingleRunner();
0556     // If we could not load the single runner we reset
0557     if (!runnerName.isEmpty() && !d->currentSingleRunner) {
0558         reset();
0559         return;
0560     }
0561     if (term.isEmpty()) {
0562         QTimer::singleShot(0, this, &RunnerManager::queryFinished);
0563         reset();
0564         return;
0565     }
0566 
0567     if (d->context.query() == term && prevSingleRunner == runnerName) {
0568         // we already are searching for this!
0569         return;
0570     }
0571 
0572     if (!d->singleMode && d->runners.isEmpty()) {
0573         d->loadRunners();
0574     }
0575 
0576     reset();
0577     d->context.setQuery(term);
0578 
0579     QHash<QString, AbstractRunner *> runnable;
0580 
0581     // if the name is not empty we will launch only the specified runner
0582     if (d->singleMode) {
0583         runnable.insert(QString(), d->currentSingleRunner);
0584         d->context.setSingleRunnerQueryMode(true);
0585     } else {
0586         runnable = d->runners;
0587     }
0588 
0589     qint64 startTs = QDateTime::currentMSecsSinceEpoch();
0590     d->context.setJobStartTs(startTs);
0591     setupMatchSession();
0592     for (KRunner::AbstractRunner *r : std::as_const(runnable)) {
0593         const QString &jobId = d->context.runnerJobId(r);
0594         if (r->isMatchingSuspended()) {
0595             d->pendingJobsAfterSuspend.insert(r, jobId);
0596             d->currentJobs.insert(jobId);
0597             continue;
0598         }
0599         // If this runner is loaded but disabled
0600         if (!d->singleMode && d->disabledRunnerIds.contains(r->id())) {
0601             continue;
0602         }
0603         // The runners can set the min letter count as a property, this way we don't
0604         // have to spawn threads just for the runner to reject the query, because it is too short
0605         if (!d->singleMode && term.length() < r->minLetterCount()) {
0606             continue;
0607         }
0608         // If the runner has one ore more trigger words it can set the matchRegex to prevent
0609         // thread spawning if the pattern does not match
0610         if (!d->singleMode && r->hasMatchRegex() && !r->matchRegex().match(term).hasMatch()) {
0611             continue;
0612         }
0613 
0614         d->currentJobs.insert(jobId);
0615         d->startJob(r);
0616     }
0617     // In the unlikely case that no runner gets queried we have to emit the signals here
0618     if (d->currentJobs.isEmpty()) {
0619         QTimer::singleShot(0, this, [this]() {
0620             d->currentJobs.clear();
0621             Q_EMIT matchesChanged({});
0622             Q_EMIT queryFinished();
0623         });
0624     }
0625 }
0626 
0627 QString RunnerManager::query() const
0628 {
0629     return d->context.query();
0630 }
0631 
0632 QStringList RunnerManager::history() const
0633 {
0634     return d->readHistoryForCurrentEnv();
0635 }
0636 
0637 void RunnerManager::removeFromHistory(int index)
0638 {
0639     QStringList changedHistory = history();
0640     if (index < changedHistory.length()) {
0641         changedHistory.removeAt(index);
0642         d->writeHistory(changedHistory);
0643     }
0644 }
0645 
0646 QString RunnerManager::getHistorySuggestion(const QString &typedQuery) const
0647 {
0648     const QStringList historyList = history();
0649     for (const QString &entry : historyList) {
0650         if (entry.startsWith(typedQuery, Qt::CaseInsensitive)) {
0651             return entry;
0652         }
0653     }
0654     return QString();
0655 }
0656 
0657 void RunnerManager::reset()
0658 {
0659     if (!d->currentJobs.empty()) {
0660         Q_EMIT queryFinished();
0661         d->currentJobs.clear();
0662     }
0663     d->context.reset();
0664 }
0665 
0666 KPluginMetaData RunnerManager::convertDBusRunnerToJson(const QString &filename) const
0667 {
0668     return parseMetaDataFromDesktopFile(filename);
0669 }
0670 
0671 bool RunnerManager::historyEnabled()
0672 {
0673     return d->historyEnabled;
0674 }
0675 
0676 void RunnerManager::setHistoryEnabled(bool enabled)
0677 {
0678     d->historyEnabled = enabled;
0679     Q_EMIT historyEnabledChanged();
0680 }
0681 
0682 // Gets called by RunnerContext to inform that we got new matches
0683 void RunnerManager::onMatchesChanged()
0684 {
0685     d->scheduleMatchesChanged();
0686 }
0687 void RunnerManager::setHistoryEnvironmentIdentifier(const QString &identifier)
0688 {
0689     Q_ASSERT(!identifier.isEmpty());
0690     d->historyEnvironmentIdentifier = identifier;
0691 }
0692 
0693 } // KRunner namespace
0694 
0695 #include "moc_runnermanager.cpp"