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

source code of krunner/src/runnermanager.cpp