File indexing completed on 2024-04-28 15:29:44
0001 /* 0002 SPDX-FileCopyrightText: 2006-2007 Aaron Seigo <aseigo@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "runnercontext.h" 0008 0009 #include <cmath> 0010 0011 #include <QDir> 0012 #include <QFile> 0013 #include <QFileInfo> 0014 #include <QMimeDatabase> 0015 #include <QReadWriteLock> 0016 #include <QRegularExpression> 0017 #include <QSharedData> 0018 #include <QStandardPaths> 0019 #include <QUrl> 0020 0021 #include <KConfigGroup> 0022 #include <KShell> 0023 0024 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76) 0025 #include <KProtocolInfo> 0026 #endif 0027 0028 #include "abstractrunner.h" 0029 #include "krunner_debug.h" 0030 #include "querymatch.h" 0031 0032 #define LOCK_FOR_READ(d) d->lock.lockForRead(); 0033 #define LOCK_FOR_WRITE(d) d->lock.lockForWrite(); 0034 #define UNLOCK(d) d->lock.unlock(); 0035 0036 namespace Plasma 0037 { 0038 class RunnerContextPrivate : public QSharedData 0039 { 0040 public: 0041 explicit RunnerContextPrivate(RunnerContext *context) 0042 : QSharedData() 0043 , type(RunnerContext::UnknownType) 0044 , q(context) 0045 { 0046 } 0047 0048 explicit RunnerContextPrivate(const RunnerContextPrivate &p) 0049 : QSharedData() 0050 , launchCounts(p.launchCounts) 0051 , type(RunnerContext::None) 0052 , q(p.q) 0053 { 0054 } 0055 0056 ~RunnerContextPrivate() 0057 { 0058 } 0059 0060 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76) 0061 /** 0062 * Determines type of query 0063 && 0064 */ 0065 void determineType() 0066 { 0067 // NOTE! this method must NEVER be called from 0068 // code that may be running in multiple threads 0069 // with the same data. 0070 type = RunnerContext::UnknownType; 0071 QString path = QDir::cleanPath(KShell::tildeExpand(term)); 0072 0073 int space = path.indexOf(QLatin1Char(' ')); 0074 if (!QStandardPaths::findExecutable(path.left(space)).isEmpty()) { 0075 // it's a shell command if there's a space because that implies 0076 // that it has arguments! 0077 type = (space > 0) ? RunnerContext::ShellCommand : RunnerContext::Executable; 0078 } else { 0079 QUrl url = QUrl::fromUserInput(term); 0080 0081 // QUrl::fromUserInput assigns http to everything if it cannot match it to 0082 // anything else. We do not want that. 0083 if (url.scheme() == QLatin1String("http")) { 0084 if (!term.startsWith(QLatin1String("http"))) { 0085 url.setScheme(QString()); 0086 } 0087 } 0088 0089 const bool hasProtocol = !url.scheme().isEmpty(); 0090 const bool isLocalProtocol = !hasProtocol || KProtocolInfo::protocolClass(url.scheme()) == QLatin1String(":local"); 0091 if ((hasProtocol && ((!isLocalProtocol && !url.host().isEmpty()) || (isLocalProtocol && url.scheme() != QLatin1String("file")))) 0092 || term.startsWith(QLatin1String("\\\\"))) { 0093 // we either have a network protocol with a host, so we can show matches for it 0094 // or we have a non-file url that may be local so a host isn't required 0095 // or we have an UNC path (\\foo\bar) 0096 type = RunnerContext::NetworkLocation; 0097 } else if (isLocalProtocol) { 0098 // at this point in the game, we assume we have a path, 0099 // but if a path doesn't have any slashes 0100 // it's too ambiguous to be sure we're in a filesystem context 0101 path = !url.scheme().isEmpty() ? QDir::cleanPath(url.toLocalFile()) : path; 0102 if ((path.indexOf(QLatin1Char('/')) != -1 || path.indexOf(QLatin1Char('\\')) != -1)) { 0103 QFileInfo info(path); 0104 if (info.isSymLink()) { 0105 path = info.canonicalFilePath(); 0106 info = QFileInfo(path); 0107 } 0108 if (info.isDir()) { 0109 type = RunnerContext::Directory; 0110 mimeType = QStringLiteral("inode/folder"); 0111 } else if (info.isFile()) { 0112 type = RunnerContext::File; 0113 QMimeDatabase db; 0114 QMimeType mime = db.mimeTypeForFile(path); 0115 if (!mime.isDefault()) { 0116 mimeType = mime.name(); 0117 } 0118 } 0119 } 0120 } 0121 } 0122 } 0123 #endif 0124 0125 void invalidate() 0126 { 0127 q = &s_dummyContext; 0128 } 0129 0130 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 81) 0131 QueryMatch *findMatchById(const QString &id) 0132 { 0133 QueryMatch *match = nullptr; 0134 lock.lockForRead(); 0135 for (auto &matchEntry : matches) { 0136 if (matchEntry.id() == id) { 0137 match = &matchEntry; 0138 break; 0139 } 0140 } 0141 lock.unlock(); 0142 return match; 0143 } 0144 #endif 0145 0146 void addMatch(const QueryMatch &match) 0147 { 0148 if (match.runner() && match.runner()->hasUniqueResults()) { 0149 if (uniqueIds.contains(match.id())) { 0150 const QueryMatch &existentMatch = uniqueIds.value(match.id()); 0151 if (existentMatch.runner() && existentMatch.runner()->hasWeakResults()) { 0152 // There is an existing match with the same ID and we are allowed to replace it 0153 matches.removeOne(existentMatch); 0154 matches.append(match); 0155 } 0156 } else { 0157 // There is no existing match with the same id 0158 uniqueIds.insert(match.id(), match); 0159 matches.append(match); 0160 } 0161 } else { 0162 // Runner has the unique results property not set 0163 matches.append(match); 0164 } 0165 } 0166 0167 QReadWriteLock lock; 0168 QList<QueryMatch> matches; 0169 QHash<QString, int> launchCounts; 0170 QString term; 0171 QString mimeType; 0172 QStringList enabledCategories; 0173 RunnerContext::Type type; 0174 RunnerContext *q; 0175 static RunnerContext s_dummyContext; 0176 bool singleRunnerQueryMode = false; 0177 bool shouldIgnoreCurrentMatchForHistory = false; 0178 QMap<QString, QueryMatch> uniqueIds; 0179 QString requestedText; 0180 int requestedCursorPosition = 0; 0181 }; 0182 0183 RunnerContext RunnerContextPrivate::s_dummyContext; 0184 0185 RunnerContext::RunnerContext(QObject *parent) 0186 : QObject(parent) 0187 , d(new RunnerContextPrivate(this)) 0188 { 0189 } 0190 0191 // copy ctor 0192 RunnerContext::RunnerContext(RunnerContext &other, QObject *parent) 0193 : QObject(parent) 0194 { 0195 LOCK_FOR_READ(other.d) 0196 d = other.d; 0197 UNLOCK(other.d) 0198 } 0199 0200 RunnerContext::~RunnerContext() 0201 { 0202 } 0203 0204 RunnerContext &RunnerContext::operator=(const RunnerContext &other) 0205 { 0206 if (this->d == other.d) { 0207 return *this; 0208 } 0209 0210 QExplicitlySharedDataPointer<Plasma::RunnerContextPrivate> oldD = d; 0211 LOCK_FOR_WRITE(d) 0212 LOCK_FOR_READ(other.d) 0213 d = other.d; 0214 UNLOCK(other.d) 0215 UNLOCK(oldD) 0216 return *this; 0217 } 0218 0219 void RunnerContext::reset() 0220 { 0221 LOCK_FOR_WRITE(d); 0222 // We will detach if we are a copy of someone. But we will reset 0223 // if we are the 'main' context others copied from. Resetting 0224 // one RunnerContext makes all the copies obsolete. 0225 0226 // We need to mark the q pointer of the detached RunnerContextPrivate 0227 // as dirty on detach to avoid receiving results for old queries 0228 d->invalidate(); 0229 UNLOCK(d); 0230 0231 d.detach(); 0232 0233 // Now that we detached the d pointer we need to reset its q pointer 0234 0235 d->q = this; 0236 0237 // we still have to remove all the matches, since if the 0238 // ref count was 1 (e.g. only the RunnerContext is using 0239 // the dptr) then we won't get a copy made 0240 d->matches.clear(); 0241 d->term.clear(); 0242 Q_EMIT matchesChanged(); 0243 0244 d->mimeType.clear(); 0245 d->uniqueIds.clear(); 0246 d->type = UnknownType; 0247 d->singleRunnerQueryMode = false; 0248 d->shouldIgnoreCurrentMatchForHistory = false; 0249 } 0250 0251 void RunnerContext::setQuery(const QString &term) 0252 { 0253 if (!this->query().isEmpty()) { 0254 reset(); 0255 } 0256 0257 if (term.isEmpty()) { 0258 return; 0259 } 0260 0261 d->requestedText.clear(); // Invalidate this field whenever the query changes 0262 d->term = term; 0263 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76) 0264 d->determineType(); 0265 #endif 0266 } 0267 0268 QString RunnerContext::query() const 0269 { 0270 // the query term should never be set after 0271 // a search starts. in fact, reset() ensures this 0272 // and setQuery(QString) calls reset() 0273 return d->term; 0274 } 0275 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76) 0276 0277 void RunnerContext::setEnabledCategories(const QStringList &categories) 0278 { 0279 d->enabledCategories = categories; 0280 } 0281 #endif 0282 0283 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76) 0284 QStringList RunnerContext::enabledCategories() const 0285 { 0286 return d->enabledCategories; 0287 } 0288 #endif 0289 0290 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76) 0291 RunnerContext::Type RunnerContext::type() const 0292 { 0293 return d->type; 0294 } 0295 #endif 0296 0297 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76) 0298 QString RunnerContext::mimeType() const 0299 { 0300 return d->mimeType; 0301 } 0302 #endif 0303 0304 bool RunnerContext::isValid() const 0305 { 0306 // if our qptr is dirty, we aren't useful anymore 0307 LOCK_FOR_READ(d) 0308 const bool valid = (d->q != &(d->s_dummyContext)); 0309 UNLOCK(d) 0310 return valid; 0311 } 0312 0313 bool RunnerContext::addMatches(const QList<QueryMatch> &matches) 0314 { 0315 if (matches.isEmpty() || !isValid()) { 0316 // Bail out if the query is empty or the qptr is dirty 0317 return false; 0318 } 0319 0320 LOCK_FOR_WRITE(d) 0321 for (QueryMatch match : matches) { 0322 // Give previously launched matches a slight boost in relevance 0323 // The boost smoothly saturates to 0.5; 0324 if (int count = d->launchCounts.value(match.id())) { 0325 match.setRelevance(match.relevance() + 0.5 * (1 - exp(-count * 0.3))); 0326 } 0327 d->addMatch(match); 0328 } 0329 UNLOCK(d); 0330 // A copied searchContext may share the d pointer, 0331 // we always want to sent the signal of the object that created 0332 // the d pointer 0333 Q_EMIT d->q->matchesChanged(); 0334 0335 return true; 0336 } 0337 0338 bool RunnerContext::addMatch(const QueryMatch &match) 0339 { 0340 return addMatches({match}); 0341 } 0342 0343 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 81) 0344 bool RunnerContext::removeMatches(const QStringList matchIdList) 0345 { 0346 bool success = false; 0347 for (const QString &id : matchIdList) { 0348 if (removeMatch(id)) { 0349 success = true; 0350 } 0351 } 0352 0353 return success; 0354 } 0355 #endif 0356 0357 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 81) 0358 bool RunnerContext::removeMatch(const QString matchId) 0359 { 0360 if (!isValid()) { 0361 return false; 0362 } 0363 LOCK_FOR_READ(d) 0364 const QueryMatch *match = d->findMatchById(matchId); 0365 UNLOCK(d) 0366 if (!match) { 0367 return false; 0368 } 0369 LOCK_FOR_WRITE(d) 0370 d->matches.removeAll(*match); 0371 UNLOCK(d) 0372 Q_EMIT d->q->matchesChanged(); 0373 0374 return true; 0375 } 0376 #endif 0377 0378 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 81) 0379 bool RunnerContext::removeMatches(Plasma::AbstractRunner *runner) 0380 { 0381 if (!isValid()) { 0382 return false; 0383 } 0384 0385 QList<QueryMatch> presentMatchList; 0386 0387 LOCK_FOR_READ(d) 0388 for (const QueryMatch &match : std::as_const(d->matches)) { 0389 if (match.runner() == runner) { 0390 presentMatchList << match; 0391 } 0392 } 0393 UNLOCK(d) 0394 0395 if (presentMatchList.isEmpty()) { 0396 return false; 0397 } 0398 0399 LOCK_FOR_WRITE(d) 0400 for (const QueryMatch &match : std::as_const(presentMatchList)) { 0401 d->matches.removeAll(match); 0402 } 0403 UNLOCK(d) 0404 0405 Q_EMIT d->q->matchesChanged(); 0406 return true; 0407 } 0408 #endif 0409 0410 QList<QueryMatch> RunnerContext::matches() const 0411 { 0412 LOCK_FOR_READ(d) 0413 QList<QueryMatch> matches = d->matches; 0414 UNLOCK(d); 0415 return matches; 0416 } 0417 0418 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 79) 0419 QueryMatch RunnerContext::match(const QString &id) const 0420 { 0421 LOCK_FOR_READ(d) 0422 const QueryMatch *match = d->findMatchById(id); 0423 UNLOCK(d) 0424 return match ? *match : QueryMatch(nullptr); 0425 } 0426 #endif 0427 0428 void RunnerContext::requestQueryStringUpdate(const QString &text, int cursorPosition) const 0429 { 0430 d->requestedText = text; 0431 d->requestedCursorPosition = cursorPosition; 0432 } 0433 0434 void RunnerContext::setSingleRunnerQueryMode(bool enabled) 0435 { 0436 d->singleRunnerQueryMode = enabled; 0437 } 0438 0439 bool RunnerContext::singleRunnerQueryMode() const 0440 { 0441 return d->singleRunnerQueryMode; 0442 } 0443 0444 void RunnerContext::ignoreCurrentMatchForHistory() const 0445 { 0446 d->shouldIgnoreCurrentMatchForHistory = true; 0447 } 0448 0449 bool RunnerContext::shouldIgnoreCurrentMatchForHistory() const 0450 { 0451 return d->shouldIgnoreCurrentMatchForHistory; 0452 } 0453 0454 void RunnerContext::restore(const KConfigGroup &config) 0455 { 0456 const QStringList cfgList = config.readEntry("LaunchCounts", QStringList()); 0457 0458 static const QRegularExpression re(QStringLiteral("(\\d*) (.+)")); 0459 for (const QString &entry : cfgList) { 0460 const QRegularExpressionMatch match = re.match(entry); 0461 if (!match.hasMatch()) { 0462 continue; 0463 } 0464 const int count = match.captured(1).toInt(); 0465 const QString id = match.captured(2); 0466 d->launchCounts[id] = count; 0467 } 0468 } 0469 0470 void RunnerContext::save(KConfigGroup &config) 0471 { 0472 QStringList countList; 0473 0474 typedef QHash<QString, int>::const_iterator Iterator; 0475 Iterator end = d->launchCounts.constEnd(); 0476 for (Iterator i = d->launchCounts.constBegin(); i != end; ++i) { 0477 countList << QStringLiteral("%2 %1").arg(i.key()).arg(i.value()); 0478 } 0479 0480 config.writeEntry("LaunchCounts", countList); 0481 config.sync(); 0482 } 0483 0484 void RunnerContext::run(const QueryMatch &match) 0485 { 0486 ++d->launchCounts[match.id()]; 0487 match.run(*this); 0488 } 0489 0490 QString RunnerContext::requestedQueryString() const 0491 { 0492 return d->requestedText; 0493 } 0494 int RunnerContext::requestedCursorPosition() const 0495 { 0496 return d->requestedCursorPosition; 0497 } 0498 0499 } // Plasma namespace 0500 0501 #include "moc_runnercontext.cpp"