File indexing completed on 2024-04-28 17:06:55

0001 /*
0002  * Copyright 2010-2012 Bart Kroon <bart@tarmack.eu>
0003  * Copyright 2012, 2013 Martin Sandsmark <martin.sandsmark@kde.org>
0004  * 
0005  * Redistribution and use in source and binary forms, with or without
0006  * modification, are permitted provided that the following conditions
0007  * are met:
0008  * 
0009  * 1. Redistributions of source code must retain the above copyright
0010  *   notice, this list of conditions and the following disclaimer.
0011  * 2. Redistributions in binary form must reproduce the above copyright
0012  *   notice, this list of conditions and the following disclaimer in the
0013  *   documentation and/or other materials provided with the distribution.
0014  * 
0015  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
0016  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
0017  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
0018  * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
0019  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
0020  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
0021  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
0022  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
0023  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
0024  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0025  */
0026 
0027 #include "Applications.h"
0028 
0029 #include <QDebug>
0030 #include <KLocalizedString>
0031 #include <QProcess>
0032 #include <QSettings>
0033 #include <QRegularExpression>
0034 #include <QFileInfo>
0035 #include <QDateTime>
0036 #include <QFileSystemWatcher>
0037 #include <QStandardPaths>
0038 #include <QDir>
0039 
0040 Applications::Applications(QObject *parent) :
0041     Provider(parent),
0042     m_fsWatcher(new QFileSystemWatcher(this))
0043 {
0044     for (const QString &dirPath : QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation)) {
0045         m_fsWatcher->addPath(dirPath);
0046         loadDir(dirPath);
0047     }
0048     for (const QString &dirPath : QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation)) {
0049         m_fsWatcher->addPath(dirPath + QStringLiteral("/kservices5/"));
0050         loadDir(dirPath + QStringLiteral("/kservices5/")); // kcm files
0051     }
0052 
0053     connect(m_fsWatcher, &QFileSystemWatcher::directoryChanged, this, &Applications::onDirectoryChanged);
0054 }
0055 
0056 Applications::~Applications()
0057 {
0058 }
0059 
0060 ProviderResult *Applications::createApp(const Application &service)
0061 {
0062     ProviderResult *app = new ProviderResult;
0063     app->name = service.name;
0064     app->completion = app->name;
0065     app->icon = service.icon;
0066     app->object = this;
0067 
0068     QString exec = service.exec;
0069     exec.remove(QRegularExpression("\\%[fFuUdDnNickvm]"));
0070 
0071     app->program = exec;
0072     app->priority = service.lastModified;
0073 
0074     if (service.exec.contains("kcmshell")) { // so sue me
0075         app->type = i18n("Open control module");
0076     } else {
0077         app->type = i18n("Run application");
0078     }
0079 
0080     return app;
0081 }
0082 
0083 QList<ProviderResult *> Applications::getResults(QString term)
0084 {
0085     QList<ProviderResult*> list;
0086     term = term.toLower(); // we lowercase the keywords when indexing
0087 
0088     qint64 currentSecsSinceEpoch = QDateTime::currentSecsSinceEpoch();
0089 
0090     for (const Application &application : m_applications) {
0091         if (!application.keywords.contains(term)) {
0092             continue;
0093         }
0094         ProviderResult *app = createApp(application);
0095         if (app->name.isEmpty()) {
0096             delete app;
0097             continue;
0098         }
0099         app->priority = currentSecsSinceEpoch - app->priority;
0100         //if (service->isApplication()) app->priority *= 1.1;
0101 
0102         list.append(app);
0103     }
0104     return list;
0105 }
0106 
0107 int Applications::launch(const QString &selected)
0108 {
0109 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
0110     if (QProcess::startDetached(selected)) {
0111         return 0;
0112     } else {
0113         return 1;
0114     }
0115 #else
0116     const QStringList commandAndArgs = QProcess::splitCommand(selected);
0117 
0118     if (QProcess::startDetached(commandAndArgs[0], commandAndArgs.mid(1))) {
0119         return 0;
0120     } else {
0121         return 1;
0122     }
0123 #endif
0124 }
0125 
0126 void Applications::onDirectoryChanged(const QString &path)
0127 {
0128     QMutableHashIterator<QString, Application> it(m_applications);
0129     while (it.hasNext()) {
0130         it.next();
0131         if (it.key().startsWith(path)) {
0132             it.remove();
0133         }
0134     }
0135     loadDir(path);
0136 }
0137 
0138 void Applications::loadDir(const QString &path)
0139 {
0140     for (const QFileInfo &file : QDir(path).entryInfoList(QStringList("*.desktop"))) {
0141         Application app = loadDesktopFile(file);
0142         if (!app.isValid()) {
0143             continue;
0144         }
0145         m_applications[file.absoluteFilePath()] = app;
0146     }
0147 }
0148 
0149 Applications::Application Applications::loadDesktopFile(const QFileInfo &fileInfo)
0150 {
0151     // Ugliest implementation of .desktop file reading ever
0152     // Don't remember why I didn't use QSettings
0153 
0154     QFile file(fileInfo.absoluteFilePath());
0155     if (!file.open(QIODevice::ReadOnly)) {
0156         qWarning() << "Failed to open" << fileInfo.fileName();
0157         return {};
0158     }
0159 
0160     bool inCorrectGroup = false;
0161 
0162     Application app;
0163     app.lastModified = fileInfo.lastModified().toSecsSinceEpoch();
0164 
0165     // Don't necessarily need to change this all the time, but the language can
0166     // in theory be changed by the user. And this function isn't called often
0167     // enough to be in a hot path.
0168     QSet<QString> wantedLanguages;
0169     { // Because Qt suddenly decided that being True C++™ was more important than ease of use
0170         const QStringList languages = QLocale::system().uiLanguages();
0171         wantedLanguages = QSet<QString>(languages.begin(), languages.end());
0172         wantedLanguages.insert(QLocale::system().bcp47Name());
0173         wantedLanguages.insert("en");
0174     }
0175 
0176     while (!file.atEnd()) {
0177         const QString line = QString::fromLocal8Bit(file.readLine()).simplified();
0178 
0179         if (line.startsWith('[')) {
0180             inCorrectGroup = (line == "[Desktop Entry]");
0181             continue;
0182         }
0183 
0184         if (!inCorrectGroup) {
0185             continue;
0186         }
0187 
0188         const int separatorPos = line.indexOf('=');
0189         if (separatorPos == -1) {
0190             continue;
0191         }
0192         const QStringRef value = line.midRef(separatorPos + 1);
0193 
0194         const int openBracketPos = line.indexOf('[');
0195 
0196         // There is a bracket in the Key part, e. g. Foo[lang]=asdf
0197         // These are translated versions of the entries.
0198         if (openBracketPos != -1 && openBracketPos < separatorPos) {
0199             const int closeBracketPos = line.indexOf(']');
0200             if (closeBracketPos < openBracketPos) {
0201                 qWarning() << "Invalid brackets in" << fileInfo.fileName() << line;
0202                 continue;
0203             }
0204             if (!wantedLanguages.contains(line.mid(openBracketPos + 1, closeBracketPos - openBracketPos - 1))) {
0205                 continue;
0206             }
0207         }
0208 
0209         if (line.startsWith("Name")) {
0210             app.name = value.toString();
0211             continue;
0212         }
0213         if (line.startsWith("Keywords")) {
0214             app.keywords = value.toString();
0215             continue;
0216         }
0217 
0218 
0219         if (line.startsWith("Icon")) {
0220             app.icon = value.toString();
0221             continue;
0222         }
0223 
0224         if (line.startsWith("Exec")) {
0225             app.exec = value.toString();
0226             continue;
0227         }
0228 
0229         if (line.startsWith("NoDisplay=") && line.contains("true", Qt::CaseInsensitive)) {
0230             return {};
0231         }
0232     }
0233 
0234     if (app.name.isEmpty()) {
0235         qWarning() << fileInfo.absoluteFilePath() << "missing name";
0236         app.name = fileInfo.baseName();
0237     }
0238 
0239     if (!app.isValid()) {
0240         return {};
0241     }
0242 
0243     if (!app.keywords.contains(app.name, Qt::CaseInsensitive)) {
0244         app.keywords += ';' + app.name;
0245     }
0246     if (!app.keywords.contains(app.exec, Qt::CaseInsensitive)) {
0247         app.keywords += ';' + app.exec;
0248     }
0249     app.keywords = app.keywords.toLower();
0250 
0251     if (app.icon.isEmpty()) {
0252         app.icon = "application-x-executable";
0253     }
0254 
0255     return app;
0256 }
0257 
0258 
0259 // kate: indent-mode cstyle; space-indent on; indent-width 4;