From c3dfba902237366d06deb83843c102a6ab852f4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Pereira?= Date: Mon, 25 Oct 2021 18:21:40 +0100 Subject: [PATCH] Add default terminal (#265) * XdgDesktopFile: Add a public tryExec method It should be public. The functionality already existed in the private checkTryExec() function. Code updated to use QStandardPaths::findExecutable(). It does a better job. * Adds a default terminal implementation xdg-utils doesn't provide a utility script to open the registered terminal emulator. xdg-terminal exists since 2006 but it didn't make to the first league. A Desktop environment should give it's user the power of choice. So we implemented it. The default terminal emulator is stored in qtxdg.conf files, which can be stored in several locations. QSettings own search order is used. Additionally, it is possible to define desktop environment-specific default terminal in a file named desktop-qtxdg.conf where desktop is the name of the desktop environment (taken from XDG_CURRENT_DESKTOP environment variable). For example, ~/.config/lxqt-qtxdg.conf defines user specific default terminal override for LXQt. These desktop-specific overrides take precedence over the corresponding non-desktop-specific file. If the desktop-specific file isn't found the non-specific will *not* be used. --- CMakeLists.txt | 7 + config/lxqt-qtxdg.conf | 1 + config/qtxdg.conf | 1 + src/qtxdg/xdgdefaultapps.cpp | 70 +++++++++ src/qtxdg/xdgdefaultapps.h | 19 +++ src/qtxdg/xdgdesktopfile.cpp | 64 ++++---- src/qtxdg/xdgdesktopfile.h | 9 ++ src/tools/mat/CMakeLists.txt | 1 + src/tools/mat/defterminalmatcommand.cpp | 187 ++++++++++++++++++++++++ src/tools/mat/defterminalmatcommand.h | 34 +++++ src/tools/mat/qtxdg-mat.cpp | 4 + 11 files changed, 369 insertions(+), 28 deletions(-) create mode 100644 config/lxqt-qtxdg.conf create mode 100644 config/qtxdg.conf create mode 100644 src/tools/mat/defterminalmatcommand.cpp create mode 100644 src/tools/mat/defterminalmatcommand.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 2272f42..4e6d65a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,6 +63,8 @@ set(QTXDGX_INTREE_INCLUDEDIR "${CMAKE_CURRENT_BINARY_DIR}/InTreeBuild/include") message(STATUS "Building ${PROJECT_NAME} with Qt ${Qt5Core_VERSION}") +file(GLOB QTXDG_CONFIG_FILES config/*.conf) + add_subdirectory(src) if(BUILD_TESTS) @@ -156,6 +158,11 @@ install(EXPORT COMPONENT Devel ) +install(FILES ${QTXDG_CONFIG_FILES} + DESTINATION "${CMAKE_INSTALL_FULL_DATADIR}/lxqt" + COMPONENT Runtime +) + # uninstall target configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/cmake/cmake_uninstall.cmake.in" diff --git a/config/lxqt-qtxdg.conf b/config/lxqt-qtxdg.conf new file mode 100644 index 0000000..170fcd0 --- /dev/null +++ b/config/lxqt-qtxdg.conf @@ -0,0 +1 @@ +TerminalEmulator=qterminal.desktop diff --git a/config/qtxdg.conf b/config/qtxdg.conf new file mode 100644 index 0000000..fc566bb --- /dev/null +++ b/config/qtxdg.conf @@ -0,0 +1 @@ +TerminalEmulator=xterm.desktop diff --git a/src/qtxdg/xdgdefaultapps.cpp b/src/qtxdg/xdgdefaultapps.cpp index 3160ca6..48322f0 100644 --- a/src/qtxdg/xdgdefaultapps.cpp +++ b/src/qtxdg/xdgdefaultapps.cpp @@ -21,15 +21,38 @@ #include "xdgdefaultapps.h" #include "xdgdesktopfile.h" +#include "xdgdirs.h" #include "xdgmimeapps.h" #include +#include #include #include #include #include + +static XdgDesktopFile *getTerminal(const QString &terminalName) +{ + XdgDesktopFile *t = new XdgDesktopFile{}; + if (t->load(terminalName) && t->isValid()) { + const QStringList cats = t->value(QL1S("Categories"), QString()).toString().split(QL1C(';'), Qt::SkipEmptyParts); + if (cats.contains(QL1S("TerminalEmulator"))) { + if (t->contains(QL1S("TryExec"))) { + if (t->tryExec()) { + return t; + } + } else { + return t; + } + } + } + + delete t; + return nullptr; +} + static QStringList getWebBrowserProtocolsGet() { // Protocols needed to quailify a application as the default browser @@ -52,6 +75,18 @@ static QStringList getWebBrowserProtocolsSet() return webBrowserProtocolsSet; } +static QString qtxdgConfigFilename() +{ + // first find the DE's qtxdg.conf file + QByteArray qtxdgConfig("qtxdg"); + QList desktopsList = qgetenv("XDG_CURRENT_DESKTOP").toLower().split(':'); + if (!desktopsList.isEmpty()) { + qtxdgConfig = desktopsList.at(0) + '-' + qtxdgConfig; + } + + return QString::fromLocal8Bit(qtxdgConfig); +} + // returns the list of apps that are from category and support protocols static QList categoryAndMimeTypeApps(const QString &category, const QStringList &protocols) { @@ -120,6 +155,17 @@ bool XdgDefaultApps::setFileManager(const XdgDesktopFile &app) return setDefaultApp(QL1S("inode/directory"), app); } +bool XdgDefaultApps::setTerminal(const XdgDesktopFile &app) +{ + if (!app.isValid()) + return false; + + const QString configFile = qtxdgConfigFilename(); + QSettings settings(QSettings::UserScope, configFile); + settings.setValue(QL1S("TerminalEmulator"), XdgDesktopFile::id(app.fileName())); + return true; +} + bool XdgDefaultApps::setWebBrowser(const XdgDesktopFile &app) { const QStringList protocols = @@ -132,6 +178,30 @@ bool XdgDefaultApps::setWebBrowser(const XdgDesktopFile &app) return true; } +XdgDesktopFile *XdgDefaultApps::terminal() +{ + const QString configFile = qtxdgConfigFilename(); + QSettings settings(QSettings::UserScope, configFile); + const QString terminalName = settings.value(QL1S("TerminalEmulator"), QString()).toString(); + return getTerminal(terminalName); +} + +QList XdgDefaultApps::terminals() +{ + XdgMimeApps db; + QList terminalList = db.categoryApps(QL1S("TerminalEmulator")); + QList::iterator it = terminalList.begin(); + while (it != terminalList.end()) { + if ((*it)->isShown()) { + ++it; + } else { + delete *it; + it = terminalList.erase(it); + } + } + return terminalList; +} + // To be qualified as the default browser all protocols must be set to the same // valid application XdgDesktopFile *XdgDefaultApps::webBrowser() diff --git a/src/qtxdg/xdgdefaultapps.h b/src/qtxdg/xdgdefaultapps.h index 9efd78a..3c567c5 100644 --- a/src/qtxdg/xdgdefaultapps.h +++ b/src/qtxdg/xdgdefaultapps.h @@ -69,6 +69,13 @@ class QTXDG_API XdgDefaultApps { */ static bool setFileManager(const XdgDesktopFile &app); + /*! + * \brief Sets the default terminal emulator + * \param The app to be set as the default terminal emulator + * \return True if successful, false otherwise + */ + static bool setTerminal(const XdgDesktopFile &app); + /*! * \brief Sets the default web browser * \param The app to be set as the default web browser @@ -76,6 +83,18 @@ class QTXDG_API XdgDefaultApps { */ static bool setWebBrowser(const XdgDesktopFile &app); + /*! + * \brief Gets the default terminal emulator + * \return The default terminal emulator. nullptr if it's not set or an error ocurred. + */ + static XdgDesktopFile *terminal(); + + /*! + * \brief Gets the installed terminal emulators + * \return A list of installed terminal emulators + */ + static QList terminals(); + /*! * \brief Gets the default web browser * \return The default web browser. nullptr if it's not set or a error ocurred. diff --git a/src/qtxdg/xdgdesktopfile.cpp b/src/qtxdg/xdgdesktopfile.cpp index 8311a2e..e1a0a68 100644 --- a/src/qtxdg/xdgdesktopfile.cpp +++ b/src/qtxdg/xdgdesktopfile.cpp @@ -34,6 +34,7 @@ #include "xdgicon.h" #include "application_interface.h" // generated interface for DBus org.freedesktop.Application #include "xdgmimeapps.h" +#include "xdgdefaultapps.h" #include #include @@ -87,7 +88,6 @@ static const QLatin1String urlKey("URL"); static const QLatin1String iconKey("Icon"); // Helper functions prototypes -bool checkTryExec(const QString& progName); QString &doEscape(QString& str, const QHash &repl); QString &doUnEscape(QString& str, const QHash &repl); QString &escape(QString& str); @@ -459,21 +459,37 @@ bool XdgDesktopFileData::startApplicationDetached(const XdgDesktopFile *q, const if (startByDBus(action, urls)) return true; } - QStringList args = action.isEmpty() + QStringList args; + QStringList appArgs = action.isEmpty() ? q->expandExecString(urls) : XdgDesktopAction{*q, action}.expandExecString(urls); - if (args.isEmpty()) + if (appArgs.isEmpty()) return false; if (q->value(QLatin1String("Terminal")).toBool()) { - QString term = QString::fromLocal8Bit(qgetenv("TERM")); - if (term.isEmpty()) - term = QLatin1String("xterm"); + XdgDesktopFile *terminal = XdgDefaultApps::terminal(); + QString terminalCommand; + if (terminal != nullptr && terminal->isValid()) + { + terminalCommand = terminal->value(execKey).toString(); + } + else + { + qWarning() << "XdgDesktopFileData::startApplicationDetached(): Using fallback terminal (xterm)."; + terminalCommand = QStringLiteral("xterm"); + } + + delete terminal; - args.prepend(QLatin1String("-e")); - args.prepend(term); + args.append(QProcess::splitCommand(terminalCommand)); + args.append(QLatin1String("-e")); + args.append(appArgs); + } + else + { + args = appArgs; } bool detach = StartDetachTruly::instance(); @@ -1203,23 +1219,6 @@ QStringList XdgDesktopFile::expandExecString(const QStringList& urls) const } -bool checkTryExec(const QString& progName) -{ - if (progName.startsWith(QDir::separator())) - return QFileInfo(progName).isExecutable(); - - const QStringList dirs = QFile::decodeName(qgetenv("PATH")).split(QLatin1Char(':')); - - for (const QString &dir : dirs) - { - if (QFileInfo(QDir(dir), progName).isExecutable()) - return true; - } - - return false; -} - - QString XdgDesktopFile::id(const QString &fileName, bool checkFileExists) { const QFileInfo f(fileName); @@ -1330,14 +1329,23 @@ bool XdgDesktopFile::isSuitable(bool excludeHidden, const QString &environment) } // actually installed. If not, entry may not show in menus, etc. - QString s = value(QLatin1String("TryExec")).toString(); - if (!s.isEmpty() && ! checkTryExec(s)) - return false; + if (contains(QLatin1String("TryExec"))) + return tryExec(); return true; } +bool XdgDesktopFile::tryExec() const +{ + const QString progName = value(QLatin1String("TryExec")).toString(); + if (progName.isEmpty()) + return false; + + return (QStandardPaths::findExecutable(progName).isEmpty()) ? false : true; +} + + QString expandDynamicUrl(QString url) { const QStringList env = QProcess::systemEnvironment(); diff --git a/src/qtxdg/xdgdesktopfile.h b/src/qtxdg/xdgdesktopfile.h index 7e04281..e368e3c 100644 --- a/src/qtxdg/xdgdesktopfile.h +++ b/src/qtxdg/xdgdesktopfile.h @@ -242,6 +242,15 @@ class QTXDG_API XdgDesktopFile */ bool isSuitable(bool excludeHidden = true, const QString &environment = QString()) const; + /*! Check if the executable file on disk used to determine if the program + is actually installed. If the path is not an absolute path, the file + is looked up in the $PATH environment variable. + Check TryExec entry existence with contains(). + @return false if the file is not present or if it is not executable. + If the TryExec entry isn't present returns false + */ + bool tryExec() const; + protected: virtual QString prefix() const { return QLatin1String("Desktop Entry"); } virtual bool check() const { return true; } diff --git a/src/tools/mat/CMakeLists.txt b/src/tools/mat/CMakeLists.txt index 78870ed..d3c1ce9 100644 --- a/src/tools/mat/CMakeLists.txt +++ b/src/tools/mat/CMakeLists.txt @@ -7,6 +7,7 @@ add_executable(qtxdg-mat defwebbrowsermatcommand.cpp defemailclientmatcommand.cpp deffilemanagermatcommand.cpp + defterminalmatcommand.cpp qtxdg-mat.cpp ) diff --git a/src/tools/mat/defterminalmatcommand.cpp b/src/tools/mat/defterminalmatcommand.cpp new file mode 100644 index 0000000..e4d5f71 --- /dev/null +++ b/src/tools/mat/defterminalmatcommand.cpp @@ -0,0 +1,187 @@ +/* + * libqtxdg - An Qt implementation of freedesktop.org xdg specs + * Copyright (C) 2021 Luís Pereira + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + */ + +#include "defterminalmatcommand.h" + +#include "matglobals.h" +#include "xdgmacros.h" +#include "xdgdefaultapps.h" +#include "xdgdesktopfile.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +enum DefTerminalCommandMode { + CommandModeGetDefTerminal, + CommandModeSetDefTerminal, + CommandModeListAvailableTerminals, +}; + +struct DefTerminalData { + DefTerminalData() : mode(CommandModeGetDefTerminal) {} + + DefTerminalCommandMode mode; + QString defTerminalName; +}; + +static CommandLineParseResult parseCommandLine(QCommandLineParser *parser, DefTerminalData *data, QString *errorMessage) +{ + parser->clearPositionalArguments(); + parser->setApplicationDescription(QL1S("Get/Set the default terminal")); + + parser->addPositionalArgument(QL1S("def-terminal"), QL1S()); + + const QCommandLineOption defTerminalNameOption(QStringList() << QSL("s") << QSL("set"), + QSL("Terminal to be set as default"), QSL("terminal")); + + const QCommandLineOption listAvailableOption(QStringList() << QSL("l") << QSL("list-available"), + QSL("List available terminals")); + + parser->addOption(defTerminalNameOption); + parser->addOption(listAvailableOption); + const QCommandLineOption helpOption = parser->addHelpOption(); + const QCommandLineOption versionOption = parser->addVersionOption(); + + if (!parser->parse(QCoreApplication::arguments())) { + *errorMessage = parser->errorText(); + return CommandLineError; + } + + if (parser->isSet(versionOption)) { + return CommandLineVersionRequested; + } + + if (parser->isSet(helpOption)) { + return CommandLineHelpRequested; + } + + const bool isListAvailableSet = parser->isSet(listAvailableOption); + const bool isDefTerminalNameSet = parser->isSet(defTerminalNameOption); + QString defTerminalName; + + if (isDefTerminalNameSet) + defTerminalName = parser->value(defTerminalNameOption); + + QStringList posArgs = parser->positionalArguments(); + posArgs.removeAt(0); + + if (isDefTerminalNameSet && !posArgs.empty()) { + *errorMessage = QSL("Extra arguments given: "); + errorMessage->append(posArgs.join(QLatin1Char(','))); + return CommandLineError; + } + + if (!isDefTerminalNameSet && !posArgs.empty()) { + *errorMessage = QSL("To set the default terminal use the -s/--set option"); + return CommandLineError; + } + + if (isListAvailableSet && (isDefTerminalNameSet || !posArgs.empty())) { + *errorMessage = QSL("list-available can't be used with other options and doesn't take arguments"); + return CommandLineError; + } + + if (isListAvailableSet) { + data->mode = CommandModeListAvailableTerminals; + } else { + data->mode = isDefTerminalNameSet ? CommandModeSetDefTerminal: CommandModeGetDefTerminal; + data->defTerminalName = defTerminalName; + } + + return CommandLineOk; +} + +DefTerminalMatCommand::DefTerminalMatCommand(QCommandLineParser *parser) + : MatCommandInterface(QL1S("def-terminal"), + QSL("Get/Set the default terminal"), + parser) +{ + Q_CHECK_PTR(parser); +} + +DefTerminalMatCommand::~DefTerminalMatCommand() = default; + +int DefTerminalMatCommand::run(const QStringList & /*arguments*/) +{ + bool success = true; + DefTerminalData data; + QString errorMessage; + if (!MatCommandInterface::parser()) { + qFatal("DefTerminalMatCommand::run: MatCommandInterface::parser() returned a null pointer"); + } + switch(parseCommandLine(parser(), &data, &errorMessage)) { + case CommandLineOk: + break; + case CommandLineError: + std::cerr << qPrintable(errorMessage); + std::cerr << "\n\n"; + std::cerr << qPrintable(parser()->helpText()); + return EXIT_FAILURE; + case CommandLineVersionRequested: + showVersion(); + Q_UNREACHABLE(); + case CommandLineHelpRequested: + showHelp(); + Q_UNREACHABLE(); + } + + if (data.mode == CommandModeListAvailableTerminals) { + const auto terminals = XdgDefaultApps::terminals(); + for (const auto *terminal : terminals) { + QFileInfo fi{terminal->fileName()}; + std::cout << qPrintable(fi.fileName()) << "\n"; + } + + qDeleteAll(terminals); + return EXIT_SUCCESS; + } + + if (data.mode == CommandModeGetDefTerminal) { // Get default terminal + XdgDesktopFile *defTerminal = XdgDefaultApps::terminal(); + if (defTerminal != nullptr && defTerminal->isValid()) { + QFileInfo f(defTerminal->fileName()); + std::cout << qPrintable(f.fileName()) << "\n"; + delete defTerminal; + } + } else { // Set default terminal + XdgDesktopFile toSetDefTerminal; + if (toSetDefTerminal.load(data.defTerminalName)) { + if (XdgDefaultApps::setTerminal(toSetDefTerminal)) { + std::cout << qPrintable(QSL("Set '%1' as the default terminal\n").arg(toSetDefTerminal.fileName())); + } else { + std::cerr << qPrintable(QSL("Could not set '%1' as the default terminal\n").arg(toSetDefTerminal.fileName())); + success = false; + } + } else { // could not load application file + std::cerr << qPrintable(QSL("Could not find find '%1'\n").arg(data.defTerminalName)); + success = false; + } + } + + return success ? EXIT_SUCCESS : EXIT_FAILURE; +} + diff --git a/src/tools/mat/defterminalmatcommand.h b/src/tools/mat/defterminalmatcommand.h new file mode 100644 index 0000000..3baebb2 --- /dev/null +++ b/src/tools/mat/defterminalmatcommand.h @@ -0,0 +1,34 @@ +/* + * libqtxdg - An Qt implementation of freedesktop.org xdg specs + * Copyright (C) 2021 Luís Pereira + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + */ + +#ifndef DEFTERMINALMATCOMMAND_H +#define DEFTERMINALMATCOMMAND_H + +#include "matcommandinterface.h" + +class DefTerminalMatCommand : public MatCommandInterface { +public: + explicit DefTerminalMatCommand(QCommandLineParser *parser); + ~DefTerminalMatCommand() override; + + int run(const QStringList &arguments) override; +}; + +#endif // DEFTERMINALMATCOMMAND_H diff --git a/src/tools/mat/qtxdg-mat.cpp b/src/tools/mat/qtxdg-mat.cpp index 61956ed..80cab1a 100644 --- a/src/tools/mat/qtxdg-mat.cpp +++ b/src/tools/mat/qtxdg-mat.cpp @@ -25,6 +25,7 @@ #include "defwebbrowsermatcommand.h" #include "defemailclientmatcommand.h" #include "deffilemanagermatcommand.h" +#include "defterminalmatcommand.h" #include "xdgmacros.h" @@ -88,6 +89,9 @@ int main(int argc, char *argv[]) MatCommandInterface *const defFileManagerCmd = new DefFileManagerMatCommand(&parser); manager->add(defFileManagerCmd); + MatCommandInterface *const defTerminalCmd = new DefTerminalMatCommand(&parser); + manager->add(defTerminalCmd); + // Find out the positional arguments. parser.parse(QCoreApplication::arguments()); const QStringList args = parser.positionalArguments();