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

source code of krunner/src/runnermanager.cpp