1/*
2 SPDX-FileCopyrightText: 2006 Aaron Seigo <aseigo@kde.org>
3 SPDX-FileCopyrightText: 2007, 2009 Ryan P. Bitanga <ryan.bitanga@gmail.com>
4 SPDX-FileCopyrightText: 2008 Jordi Polo <mumismo@gmail.com>
5 SPDX-FileCopyrightText: 2023 Alexander Lohnau <alexander.lohnauŋmx.de>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8*/
9
10#include "runnermanager.h"
11
12#include <QCoreApplication>
13#include <QDir>
14#include <QElapsedTimer>
15#include <QMutableListIterator>
16#include <QPointer>
17#include <QRegularExpression>
18#include <QStandardPaths>
19#include <QThread>
20#include <QTimer>
21
22#include <KConfigWatcher>
23#include <KFileUtils>
24#include <KPluginMetaData>
25#include <KSharedConfig>
26
27#include "config.h"
28#if HAVE_KACTIVITIES
29#include <KActivities/Consumer>
30#endif
31
32#include "abstractrunner_p.h"
33#include "dbusrunner_p.h"
34#include "kpluginmetadata_utils_p.h"
35#include "krunner_debug.h"
36#include "querymatch.h"
37
38namespace KRunner
39{
40class RunnerManagerPrivate
41{
42public:
43 RunnerManagerPrivate(const KConfigGroup &configurationGroup, KConfigGroup stateConfigGroup, RunnerManager *parent)
44 : q(parent)
45 , context(parent)
46 , pluginConf(configurationGroup)
47 , stateData(stateConfigGroup)
48 {
49 initializeKNotifyPluginWatcher();
50 matchChangeTimer.setSingleShot(true);
51 matchChangeTimer.setTimerType(Qt::TimerType::PreciseTimer); // Without this, autotest will fail due to imprecision of this timer
52
53 QObject::connect(&matchChangeTimer, &QTimer::timeout, q, [this]() {
54 matchesChanged();
55 });
56
57 // Set up tracking of the last time matchesChanged was signalled
58 lastMatchChangeSignalled.start();
59 QObject::connect(q, &RunnerManager::matchesChanged, q, [&] {
60 lastMatchChangeSignalled.restart();
61 });
62 loadConfiguration();
63 }
64
65 void scheduleMatchesChanged()
66 {
67 // We avoid over-refreshing the client. We only refresh every this much milliseconds
68 constexpr int refreshPeriod = 250;
69 // This will tell us if we are reseting the matches to start a new search. RunnerContext::reset() clears its query string for its emission
70 if (context.query().isEmpty()) {
71 matchChangeTimer.stop();
72 // This actually contains the query string for the new search that we're launching (if any):
73 if (!this->untrimmedTerm.trimmed().isEmpty()) {
74 // We are starting a new search, we shall stall for some time before deciding to show an empty matches list.
75 // This stall should be enough for the engine to provide more meaningful result, so we avoid refreshing with
76 // an empty results list if possible.
77 matchChangeTimer.start(refreshPeriod);
78 // We "pretend" that we have refreshed it so the next call will be forced to wait the timeout:
79 lastMatchChangeSignalled.restart();
80 } else {
81 // 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
82 Q_EMIT q->matchesChanged(context.matches());
83 }
84 } else if (lastMatchChangeSignalled.hasExpired(refreshPeriod)) {
85 matchChangeTimer.stop();
86 Q_EMIT q->matchesChanged(context.matches());
87 } else {
88 matchChangeTimer.start(refreshPeriod - lastMatchChangeSignalled.elapsed());
89 }
90 }
91
92 void matchesChanged()
93 {
94 Q_EMIT q->matchesChanged(context.matches());
95 }
96
97 void loadConfiguration()
98 {
99#if HAVE_KACTIVITIES
100 // Wait for consumer to be ready
101 QObject::connect(&activitiesConsumer,
102 &KActivities::Consumer::serviceStatusChanged,
103 &activitiesConsumer,
104 [this](KActivities::Consumer::ServiceStatus status) {
105 if (status == KActivities::Consumer::Running) {
106 deleteHistoryOfDeletedActivities();
107 }
108 });
109#endif
110 const KConfigGroup generalConfig = pluginConf.config()->group(QStringLiteral("General"));
111 activityAware = generalConfig.readEntry(key: "ActivityAware", defaultValue: true);
112 context.restore(stateData);
113 }
114
115 void loadSingleRunner()
116 {
117 // In case we are not in the single runner mode of we do not have an id
118 if (!singleMode || singleModeRunnerId.isEmpty()) {
119 currentSingleRunner = nullptr;
120 return;
121 }
122
123 if (currentSingleRunner && currentSingleRunner->id() == singleModeRunnerId) {
124 return;
125 }
126 currentSingleRunner = q->runner(singleModeRunnerId);
127 // If there are no runners loaded or the single runner could no be loaded,
128 // this is the case if it was disabled but gets queries using the singleRunnerMode, BUG: 435050
129 if (runners.isEmpty() || !currentSingleRunner) {
130 loadRunners(singleModeRunnerId);
131 currentSingleRunner = q->runner(singleModeRunnerId);
132 }
133 }
134
135 void deleteRunners(const QList<AbstractRunner *> &runners)
136 {
137 for (const auto runner : runners) {
138 if (qobject_cast<DBusRunner *>(runner)) {
139 runner->deleteLater();
140 } else {
141 Q_ASSERT(runner->thread() != q->thread());
142 runner->thread()->quit();
143 QObject::connect(runner->thread(), &QThread::finished, runner->thread(), &QObject::deleteLater);
144 QObject::connect(runner->thread(), &QThread::finished, runner, &QObject::deleteLater);
145 }
146 }
147 }
148
149 void loadRunners(const QString &singleRunnerId = QString())
150 {
151 QList<KPluginMetaData> offers = RunnerManager::runnerMetaDataList();
152
153 const bool loadAll = stateData.readEntry("loadAll", false);
154 const bool noWhiteList = whiteList.isEmpty();
155
156 QList<AbstractRunner *> deadRunners;
157 QMutableListIterator<KPluginMetaData> it(offers);
158 while (it.hasNext()) {
159 const KPluginMetaData &description = it.next();
160 qCDebug(KRUNNER) << "Loading runner: " << description.pluginId();
161
162 const QString runnerName = description.pluginId();
163 const bool isPluginEnabled = description.isEnabled(pluginConf);
164 const bool loaded = runners.contains(runnerName);
165 bool selected = loadAll || disabledRunnerIds.contains(runnerName) || (isPluginEnabled && (noWhiteList || whiteList.contains(runnerName)));
166 if (!selected && runnerName == singleRunnerId) {
167 selected = true;
168 disabledRunnerIds << runnerName;
169 }
170
171 if (selected) {
172 if (!loaded) {
173 if (auto runner = loadInstalledRunner(pluginMetaData: description)) {
174 qCDebug(KRUNNER) << "Loaded:" << runnerName;
175 runners.insert(runnerName, runner);
176 }
177 }
178 } else if (loaded) {
179 // Remove runner
180 deadRunners.append(runners.take(runnerName));
181 qCDebug(KRUNNER) << "Plugin disabled. Removing runner: " << runnerName;
182 }
183 }
184
185 deleteRunners(deadRunners);
186 // in case we deleted it up above, just to be sure we do not have a dangeling pointer
187 currentSingleRunner = nullptr;
188 qCDebug(KRUNNER) << "All runners loaded, total:" << runners.count();
189 }
190
191 AbstractRunner *loadInstalledRunner(const KPluginMetaData &pluginMetaData)
192 {
193 if (!pluginMetaData.isValid()) {
194 return nullptr;
195 }
196
197 AbstractRunner *runner = nullptr;
198
199 const QString api = pluginMetaData.value(QStringLiteral("X-Plasma-API"));
200 const bool isCppPlugin = api.isEmpty();
201
202 if (isCppPlugin) {
203 if (auto res = KPluginFactory::instantiatePlugin<AbstractRunner>(pluginMetaData, q)) {
204 runner = res.plugin;
205 } else {
206 qCWarning(KRUNNER).nospace() << "Could not load runner " << pluginMetaData.name() << ":" << res.errorString
207 << " (library path was:" << pluginMetaData.fileName() << ")";
208 }
209 } else if (api.startsWith(QLatin1String("DBus"))) {
210 runner = new DBusRunner(q, pluginMetaData);
211 } else {
212 qCWarning(KRUNNER) << "Unknown X-Plasma-API requested for runner" << pluginMetaData.fileName();
213 return nullptr;
214 }
215
216 if (runner) {
217 QPointer<AbstractRunner> ptr(runner);
218 q->connect(runner, &AbstractRunner::matchingResumed, q, [this, ptr]() {
219 if (ptr) {
220 runnerMatchingResumed(ptr.get());
221 }
222 });
223 if (isCppPlugin) {
224 auto thread = new QThread();
225 thread->setObjectName(pluginMetaData.pluginId());
226 thread->start();
227 runner->moveToThread(thread);
228 }
229 // The runner might outlive the manager due to us waiting for the thread to exit
230 q->connect(runner, &AbstractRunner::matchInternalFinished, q, [this](const QString &jobId) {
231 onRunnerJobFinished(jobId);
232 });
233
234 if (prepped) {
235 Q_EMIT runner->prepare();
236 }
237 }
238
239 return runner;
240 }
241
242 void onRunnerJobFinished(const QString &jobId)
243 {
244 if (currentJobs.remove(jobId) && currentJobs.isEmpty()) {
245 // If there are any new matches scheduled to be notified, we should anticipate it and just refresh right now
246 if (matchChangeTimer.isActive()) {
247 matchChangeTimer.stop();
248 matchesChanged();
249 } else if (context.matches().isEmpty()) {
250 // we finished our run, and there are no valid matches, and so no
251 // signal will have been sent out, so we need to emit the signal ourselves here
252 matchesChanged();
253 }
254 Q_EMIT q->queryFinished();
255 }
256 if (!currentJobs.isEmpty()) {
257 qCDebug(KRUNNER) << "Current jobs are" << currentJobs;
258 }
259 }
260
261 void teardown()
262 {
263 pendingJobsAfterSuspend.clear(); // Do not start old jobs when the match session is over
264 if (allRunnersPrepped) {
265 for (AbstractRunner *runner : std::as_const(runners)) {
266 Q_EMIT runner->teardown();
267 }
268 allRunnersPrepped = false;
269 }
270
271 if (singleRunnerPrepped) {
272 if (currentSingleRunner) {
273 Q_EMIT currentSingleRunner->teardown();
274 }
275 singleRunnerPrepped = false;
276 }
277
278 prepped = false;
279 }
280
281 void runnerMatchingResumed(AbstractRunner *runner)
282 {
283 Q_ASSERT(runner);
284 const QString jobId = pendingJobsAfterSuspend.value(runner);
285 if (jobId.isEmpty()) {
286 qCDebug(KRUNNER) << runner << "was not scheduled for current query";
287 return;
288 }
289 // Ignore this runner
290 if (singleMode && runner->id() != singleModeRunnerId) {
291 qCDebug(KRUNNER) << runner << "did not match requested singlerunnermode ID";
292 return;
293 }
294
295 const QString query = context.query();
296 bool matchesCount = singleMode || runner->minLetterCount() <= query.size();
297 bool matchesRegex = singleMode || !runner->hasMatchRegex() || runner->matchRegex().match(query).hasMatch();
298
299 if (matchesCount && matchesRegex) {
300 if (allRunnersPrepped) {
301 Q_EMIT runner->prepare();
302 }
303 startJob(runner);
304 } else {
305 onRunnerJobFinished(jobId);
306 }
307 }
308
309 void startJob(AbstractRunner *runner)
310 {
311 QMetaObject::invokeMethod(runner, "matchInternal", Qt::QueuedConnection, Q_ARG(KRunner::RunnerContext, context));
312 }
313
314 // Must only be called once
315 void initializeKNotifyPluginWatcher()
316 {
317 Q_ASSERT(!watcher);
318 watcher = KConfigWatcher::create(KSharedConfig::openConfig(pluginConf.config()->name()));
319 q->connect(watcher.data(), &KConfigWatcher::configChanged, q, [this](const KConfigGroup &group, const QByteArrayList &changedNames) {
320 const QString groupName = group.name();
321 if (groupName == QLatin1String("Plugins")) {
322 q->reloadConfiguration();
323 } else if (groupName == QLatin1String("Runners")) {
324 for (auto *runner : std::as_const(runners)) {
325 // Signals from the KCM contain the component name, which is the KRunner plugin's id
326 if (changedNames.contains(runner->metadata().pluginId().toUtf8())) {
327 QMetaObject::invokeMethod(runner, "reloadConfigurationInternal");
328 }
329 }
330 } else if (group.parent().isValid() && group.parent().name() == QLatin1String("Runners")) {
331 for (auto *runner : std::as_const(runners)) {
332 // If the same config group has been modified which gets created in AbstractRunner::config()
333 if (groupName == runner->id()) {
334 QMetaObject::invokeMethod(runner, "reloadConfigurationInternal");
335 }
336 }
337 }
338 });
339 }
340
341 inline QString getActivityKey()
342 {
343#if HAVE_KACTIVITIES
344 if (activityAware) {
345 const QString currentActivity = activitiesConsumer.currentActivity();
346 return currentActivity.isEmpty() ? nulluuid : currentActivity;
347 }
348#endif
349 return nulluuid;
350 }
351
352 void addToHistory()
353 {
354 const QString term = context.query();
355 // We want to imitate the shall behavior
356 if (!historyEnabled || term.isEmpty() || untrimmedTerm.startsWith(QLatin1Char(' '))) {
357 return;
358 }
359 QStringList historyEntries = readHistoryForCurrentActivity();
360 // Avoid removing the same item from the front and prepending it again
361 if (!historyEntries.isEmpty() && historyEntries.constFirst() == term) {
362 return;
363 }
364
365 historyEntries.removeOne(term);
366 historyEntries.prepend(term);
367
368 while (historyEntries.count() > 50) { // we don't want to store more than 50 entries
369 historyEntries.removeLast();
370 }
371 writeActivityHistory(historyEntries);
372 }
373
374 void writeActivityHistory(const QStringList &historyEntries)
375 {
376 stateData.group(QStringLiteral("History")).writeEntry(getActivityKey(), historyEntries, KConfig::Notify);
377 stateData.sync();
378 }
379
380#if HAVE_KACTIVITIES
381 void deleteHistoryOfDeletedActivities()
382 {
383 KConfigGroup historyGroup = stateData.group(QStringLiteral("History"));
384 QStringList historyEntries = historyGroup.keyList();
385 historyEntries.removeOne(nulluuid);
386
387 // Check if history still exists
388 const QStringList activities = activitiesConsumer.activities();
389 for (const auto &a : activities) {
390 historyEntries.removeOne(a);
391 }
392
393 for (const QString &deletedActivity : std::as_const(historyEntries)) {
394 historyGroup.deleteEntry(deletedActivity);
395 }
396 historyGroup.sync();
397 }
398#endif
399
400 inline QStringList readHistoryForCurrentActivity()
401 {
402 return stateData.group(QStringLiteral("History")).readEntry(getActivityKey(), QStringList());
403 }
404
405 RunnerManager *const q;
406 RunnerContext context;
407 QTimer matchChangeTimer;
408 QElapsedTimer lastMatchChangeSignalled;
409 QHash<QString, AbstractRunner *> runners;
410 QHash<AbstractRunner *, QString> pendingJobsAfterSuspend;
411 AbstractRunner *currentSingleRunner = nullptr;
412 QSet<QString> currentJobs;
413 QString singleModeRunnerId;
414 bool prepped = false;
415 bool allRunnersPrepped = false;
416 bool singleRunnerPrepped = false;
417 bool singleMode = false;
418 bool activityAware = false;
419 bool historyEnabled = true;
420 QStringList whiteList;
421 KConfigWatcher::Ptr watcher;
422 QString untrimmedTerm;
423 const QString nulluuid = QStringLiteral("00000000-0000-0000-0000-000000000000");
424 KConfigGroup pluginConf;
425 KConfigGroup stateData;
426 QSet<QString> disabledRunnerIds; // Runners that are disabled but were loaded as single runners
427#if HAVE_KACTIVITIES
428 const KActivities::Consumer activitiesConsumer;
429#endif
430};
431
432RunnerManager::RunnerManager(const KConfigGroup &pluginConfigGroup, KConfigGroup stateConfigGroup, QObject *parent)
433 : QObject(parent)
434 , d(new RunnerManagerPrivate(pluginConfigGroup, stateConfigGroup, this))
435{
436 Q_ASSERT(pluginConfigGroup.isValid());
437 Q_ASSERT(stateConfigGroup.isValid());
438}
439
440RunnerManager::RunnerManager(QObject *parent)
441 : QObject(parent)
442{
443 auto defaultStatePtr = KSharedConfig::openConfig(QStringLiteral("krunnerstaterc"), KConfig::NoGlobals, QStandardPaths::GenericDataLocation);
444 auto configPtr = KSharedConfig::openConfig(QStringLiteral("krunnerrc"), KConfig::NoGlobals);
445 d.reset(p: new RunnerManagerPrivate(configPtr->group(QStringLiteral("Plugins")), defaultStatePtr->group(QStringLiteral("PlasmaRunnerManager")), this));
446}
447
448RunnerManager::~RunnerManager()
449{
450 d->context.reset();
451 d->deleteRunners(d->runners.values());
452}
453
454void RunnerManager::reloadConfiguration()
455{
456 d->pluginConf.config()->reparseConfiguration();
457 d->stateData.config()->reparseConfiguration();
458 d->loadConfiguration();
459 d->loadRunners();
460}
461
462void RunnerManager::setAllowedRunners(const QStringList &runners)
463{
464 d->whiteList = runners;
465 if (!d->runners.isEmpty()) {
466 // this has been called with runners already created. so let's do an instant reload
467 d->loadRunners();
468 }
469}
470
471AbstractRunner *RunnerManager::loadRunner(const KPluginMetaData &pluginMetaData)
472{
473 const QString runnerId = pluginMetaData.pluginId();
474 if (auto loadedRunner = d->runners.value(runnerId)) {
475 return loadedRunner;
476 }
477 if (!runnerId.isEmpty()) {
478 if (AbstractRunner *runner = d->loadInstalledRunner(pluginMetaData)) {
479 d->runners.insert(runnerId, runner);
480 return runner;
481 }
482 }
483 return nullptr;
484}
485
486AbstractRunner *RunnerManager::runner(const QString &pluginId) const
487{
488 if (d->runners.isEmpty()) {
489 d->loadRunners();
490 }
491
492 return d->runners.value(pluginId, nullptr);
493}
494
495QList<AbstractRunner *> RunnerManager::runners() const
496{
497 if (d->runners.isEmpty()) {
498 d->loadRunners();
499 }
500 return d->runners.values();
501}
502
503RunnerContext *RunnerManager::searchContext() const
504{
505 return &d->context;
506}
507
508QList<QueryMatch> RunnerManager::matches() const
509{
510 return d->context.matches();
511}
512
513bool RunnerManager::run(const QueryMatch &match, const KRunner::Action &selectedAction)
514{
515 if (!match.isValid() || !match.isEnabled()) { // The model should prevent this
516 return false;
517 }
518
519 // Modify the match and run it
520 QueryMatch m = match;
521 m.setSelectedAction(selectedAction);
522 m.runner()->run(d->context, m);
523 // To allow the RunnerContext to increase the relevance of often launched apps
524 d->context.increaseLaunchCount(m);
525
526 if (!d->context.shouldIgnoreCurrentMatchForHistory()) {
527 d->addToHistory();
528 }
529 if (d->context.requestedQueryString().isEmpty()) {
530 return true;
531 } else {
532 Q_EMIT requestUpdateQueryString(d->context.requestedQueryString(), d->context.requestedCursorPosition());
533 return false;
534 }
535}
536
537QMimeData *RunnerManager::mimeDataForMatch(const QueryMatch &match) const
538{
539 return match.isValid() ? match.runner()->mimeDataForMatch(match) : nullptr;
540}
541
542QList<KPluginMetaData> RunnerManager::runnerMetaDataList()
543{
544 QList<KPluginMetaData> pluginMetaDatas = KPluginMetaData::findPlugins(QStringLiteral("kf6/krunner"));
545 QSet<QString> knownRunnerIds;
546 knownRunnerIds.reserve(pluginMetaDatas.size());
547 for (const KPluginMetaData &pluginMetaData : std::as_const(pluginMetaDatas)) {
548 knownRunnerIds.insert(pluginMetaData.pluginId());
549 }
550
551 const QStringList dBusPlugindirs =
552 QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("krunner/dbusplugins"), QStandardPaths::LocateDirectory);
553 const QStringList dbusRunnerFiles = KFileUtils::findAllUniqueFiles(dBusPlugindirs, QStringList(QStringLiteral("*.desktop")));
554 for (const QString &dbusRunnerFile : dbusRunnerFiles) {
555 KPluginMetaData pluginMetaData = parseMetaDataFromDesktopFile(dbusRunnerFile);
556 if (pluginMetaData.isValid() && !knownRunnerIds.contains(pluginMetaData.pluginId())) {
557 pluginMetaDatas.append(pluginMetaData);
558 knownRunnerIds.insert(pluginMetaData.pluginId());
559 }
560 }
561
562 return pluginMetaDatas;
563}
564
565void RunnerManager::setupMatchSession()
566{
567 if (d->prepped) {
568 return;
569 }
570
571 d->prepped = true;
572 if (d->singleMode) {
573 if (d->currentSingleRunner) {
574 Q_EMIT d->currentSingleRunner->prepare();
575 d->singleRunnerPrepped = true;
576 }
577 } else {
578 for (AbstractRunner *runner : std::as_const(d->runners)) {
579 // In case it is suspended, we will emit prepare when it resumed
580 if (!d->disabledRunnerIds.contains(runner->name()) && !runner->isMatchingSuspended()) {
581 Q_EMIT runner->prepare();
582 }
583 }
584
585 d->allRunnersPrepped = true;
586 }
587}
588
589void RunnerManager::matchSessionComplete()
590{
591 if (!d->prepped) {
592 return;
593 }
594
595 d->teardown();
596 // We save the context config after each session, just like the history entries
597 // BUG: 424505
598 d->context.save(d->stateData);
599}
600
601void RunnerManager::launchQuery(const QString &untrimmedTerm, const QString &runnerName)
602{
603 d->pendingJobsAfterSuspend.clear(); // Do not start old jobs when we got a new query
604 setupMatchSession();
605 QString term = untrimmedTerm.trimmed();
606 const QString prevSingleRunner = d->singleModeRunnerId;
607 d->untrimmedTerm = untrimmedTerm;
608
609 // Set the required values and load the runner
610 d->singleModeRunnerId = runnerName;
611 d->singleMode = !runnerName.isEmpty();
612 d->loadSingleRunner();
613 // If we could not load the single runner we reset
614 if (!runnerName.isEmpty() && !d->currentSingleRunner) {
615 reset();
616 return;
617 }
618 if (term.isEmpty()) {
619 QTimer::singleShot(0, this, &RunnerManager::queryFinished);
620 reset();
621 return;
622 }
623
624 if (d->context.query() == term && prevSingleRunner == runnerName) {
625 // we already are searching for this!
626 return;
627 }
628
629 if (!d->singleMode && d->runners.isEmpty()) {
630 d->loadRunners();
631 }
632
633 reset();
634 d->context.setQuery(term);
635
636 QHash<QString, AbstractRunner *> runnable;
637
638 // if the name is not empty we will launch only the specified runner
639 if (d->singleMode) {
640 runnable.insert(QString(), d->currentSingleRunner);
641 d->context.setSingleRunnerQueryMode(true);
642 } else {
643 runnable = d->runners;
644 }
645
646 qint64 startTs = QDateTime::currentMSecsSinceEpoch();
647 d->context.setJobStartTs(startTs);
648 for (KRunner::AbstractRunner *r : std::as_const(runnable)) {
649 const QString &jobId = d->context.runnerJobId(r);
650 if (r->isMatchingSuspended()) {
651 d->pendingJobsAfterSuspend.insert(r, jobId);
652 d->currentJobs.insert(jobId);
653 continue;
654 }
655 // If this runner is loaded but disabled
656 if (!d->singleMode && d->disabledRunnerIds.contains(r->id())) {
657 continue;
658 }
659 // The runners can set the min letter count as a property, this way we don't
660 // have to spawn threads just for the runner to reject the query, because it is too short
661 if (!d->singleMode && term.length() < r->minLetterCount()) {
662 continue;
663 }
664 // If the runner has one ore more trigger words it can set the matchRegex to prevent
665 // thread spawning if the pattern does not match
666 if (!d->singleMode && r->hasMatchRegex() && !r->matchRegex().match(term).hasMatch()) {
667 continue;
668 }
669
670 d->currentJobs.insert(jobId);
671 d->startJob(r);
672 }
673 // In the unlikely case that no runner gets queried we have to emit the signals here
674 if (d->currentJobs.isEmpty()) {
675 QTimer::singleShot(0, this, [this]() {
676 d->currentJobs.clear();
677 Q_EMIT matchesChanged({});
678 Q_EMIT queryFinished();
679 });
680 }
681}
682
683QString RunnerManager::query() const
684{
685 return d->context.query();
686}
687
688QStringList RunnerManager::history() const
689{
690 return d->readHistoryForCurrentActivity();
691}
692
693void RunnerManager::removeFromHistory(int index)
694{
695 QStringList changedHistory = history();
696 if (index < changedHistory.length()) {
697 changedHistory.removeAt(index);
698 d->writeActivityHistory(changedHistory);
699 }
700}
701
702QString RunnerManager::getHistorySuggestion(const QString &typedQuery) const
703{
704 const QStringList historyList = history();
705 for (const QString &entry : historyList) {
706 if (entry.startsWith(typedQuery, Qt::CaseInsensitive)) {
707 return entry;
708 }
709 }
710 return QString();
711}
712
713void RunnerManager::reset()
714{
715 if (!d->currentJobs.empty()) {
716 Q_EMIT queryFinished();
717 d->currentJobs.clear();
718 }
719 d->context.reset();
720}
721
722KPluginMetaData RunnerManager::convertDBusRunnerToJson(const QString &filename) const
723{
724 return parseMetaDataFromDesktopFile(filename);
725}
726
727bool RunnerManager::historyEnabled()
728{
729 return d->historyEnabled;
730}
731
732void RunnerManager::setHistoryEnabled(bool enabled)
733{
734 d->historyEnabled = enabled;
735 Q_EMIT historyEnabledChanged();
736}
737
738// Gets called by RunnerContext to inform that we got new matches
739void RunnerManager::onMatchesChanged()
740{
741 d->scheduleMatchesChanged();
742}
743
744} // KRunner namespace
745
746#include "moc_runnermanager.cpp"
747

source code of krunner/src/runnermanager.cpp