From 291179ede2c6b3af490f1cc81326fafc398e8ac8 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 9 Aug 2024 19:22:18 -0700 Subject: [PATCH] core/command: rewrite command parser with CLI11 --- .clang-tidy | 1 + BUILD.md | 1 + default.nix | 2 + src/core/CMakeLists.txt | 4 +- src/core/logging.cpp | 11 +- src/core/logging.hpp | 4 +- src/core/main.cpp | 291 +++++++++++++++++++++++----------------- 7 files changed, 185 insertions(+), 129 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 14e9b9ae..5741bf61 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -15,6 +15,7 @@ Checks: > -cppcoreguidelines-non-private-member-variables-in-classes, -cppcoreguidelines-avoid-goto, -cppcoreguidelines-pro-bounds-array-to-pointer-decay, + -cppcoreguidelines-avoid-do-while, google-build-using-namespace. google-explicit-constructor, google-global-names-in-headers, diff --git a/BUILD.md b/BUILD.md index ea69cbd6..5fe6ebc6 100644 --- a/BUILD.md +++ b/BUILD.md @@ -9,6 +9,7 @@ Quickshell has a set of base dependencies you will always need, names vary by di - `qt6base` - `qt6declarative` - `pkg-config` +- `cli11` We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and svg icons will not work, including system ones. diff --git a/default.nix b/default.nix index 2393d8ab..34cc0f4b 100644 --- a/default.nix +++ b/default.nix @@ -8,6 +8,7 @@ cmake, ninja, qt6, + cli11, jemalloc, wayland, wayland-protocols, @@ -52,6 +53,7 @@ buildInputs = [ qt6.qtbase qt6.qtdeclarative + cli11 ] ++ (lib.optional withJemalloc jemalloc) ++ (lib.optional withQtSvg qt6.qtsvg) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 5ced5410..fb39287c 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,3 +1,5 @@ +find_package(CLI11 CONFIG REQUIRED) + qt_add_library(quickshell-core STATIC main.cpp plugin.cpp @@ -44,7 +46,7 @@ qt_add_library(quickshell-core STATIC set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") qt_add_qml_module(quickshell-core URI Quickshell VERSION 0.1) -target_link_libraries(quickshell-core PRIVATE ${QT_DEPS} Qt6::QuickPrivate) +target_link_libraries(quickshell-core PRIVATE ${QT_DEPS} Qt6::QuickPrivate CLI11::CLI11) qs_pch(quickshell-core) target_link_libraries(quickshell PRIVATE quickshell-coreplugin) diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 9271c07f..86b8bb37 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -81,9 +81,7 @@ void LogMessage::formatMessage( if (color && msg.type == QtFatalMsg) stream << "\033[0m"; } -LogManager::LogManager() - : colorLogs(qEnvironmentVariableIsEmpty("NO_COLOR")) - , stdoutStream(stdout) {} +LogManager::LogManager(): stdoutStream(stdout) {} void LogManager::messageHandler( QtMsgType type, @@ -105,8 +103,9 @@ LogManager* LogManager::instance() { return instance; } -void LogManager::init() { +void LogManager::init(bool color) { auto* instance = LogManager::instance(); + instance->colorLogs = color; qInstallMessageHandler(&LogManager::messageHandler); @@ -203,6 +202,8 @@ void ThreadLogging::initFs() { << path; delete file; file = nullptr; + } else { + qInfo() << "Saving logs to" << path; } // buffered by WriteBuffer @@ -212,6 +213,8 @@ void ThreadLogging::initFs() { << detailedPath; delete detailedFile; detailedFile = nullptr; + } else { + qCInfo(logLogging) << "Saving detailed logs to" << path; } qCDebug(logLogging) << "Copying memfd logs to log file..."; diff --git a/src/core/logging.hpp b/src/core/logging.hpp index 6f4c970f..9909b1a8 100644 --- a/src/core/logging.hpp +++ b/src/core/logging.hpp @@ -58,11 +58,11 @@ class LogManager: public QObject { Q_OBJECT; public: - static void init(); + static void init(bool color); static void initFs(); static LogManager* instance(); - bool colorLogs; + bool colorLogs = true; signals: void logMessage(LogMessage msg); diff --git a/src/core/main.cpp b/src/core/main.cpp index ad2b1a8d..b363692e 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -1,9 +1,11 @@ #include "main.hpp" #include +#include +#include // NOLINT: Need to include this for impls of some CLI11 classes +#include +#include #include -#include -#include #include #include #include @@ -28,87 +30,132 @@ #include "rootwrapper.hpp" int qs_main(int argc, char** argv) { - QString configFilePath; + + auto qArgC = 1; + auto* qArgV = argv; + + auto noColor = !qEnvironmentVariableIsEmpty("NO_COLOR"); + QString workingDirectory; + QString configFilePath; + QString shellId; + auto printInfo = false; + + auto debugPort = -1; + auto waitForDebug = false; auto useQApplication = false; auto nativeTextRendering = false; auto desktopSettingsAware = true; - auto shellId = QString(); QHash envOverrides; - int debugPort = -1; - bool waitForDebug = false; - bool printCurrent = false; - { - const auto app = QCoreApplication(argc, argv); - QCoreApplication::setApplicationName("quickshell"); - QCoreApplication::setApplicationVersion("0.1.0 (" GIT_REVISION ")"); + auto app = CLI::App(""); + + class QStringOption { + public: + QStringOption() = default; + QStringOption& operator=(const std::string& str) { + this->str = QString::fromStdString(str); + return *this; + } + + QString& operator*() { return this->str; } + + private: + QString str; + }; + + class QStringRefOption { + public: + QStringRefOption(QString* str): str(str) {} + QStringRefOption& operator=(const std::string& str) { + *this->str = QString::fromStdString(str); + return *this; + } + + private: + QString* str; + }; + + /// --- + QStringOption path; + QStringOption manifest; + QStringOption config; + QStringRefOption workdirRef(&workingDirectory); + + auto* selection = app.add_option_group( + "Config Selection", + "Select a configuration to run (defaults to $XDG_CONFIG_HOME/quickshell/shell.qml)" + ); + + auto* pathArg = + selection->add_option("-p,--path", path, "Path to a QML file to run. (Env:QS_CONFIG_PATH)"); + + auto* mfArg = selection->add_option( + "-m,--manifest", + manifest, + "Path to a manifest containing configurations. (Env:QS_MANIFEST)\n" + "(Defaults to $XDG_CONFIG_HOME/quickshell/manifest.conf)" + ); + + auto* cfgArg = selection->add_option( + "-c,--config", + config, + "Name of a configuration within a manifest. (Env:QS_CONFIG_NAME)" + ); + + selection->add_option("-d,--workdir", workdirRef, "Initial working directory."); + + pathArg->excludes(mfArg, cfgArg); + + /// --- + auto* debug = app.add_option_group("Debugging"); + + auto* debugPortArg = debug + ->add_option( + "--debugport", + debugPort, + "Open the given port for a QML debugger to connect to." + ) + ->check(CLI::Range(0, 65535)); + + debug + ->add_flag( + "--waitfordebug", + waitForDebug, + "Wait for a debugger to attach to the given port before launching." + ) + ->needs(debugPortArg); + + /// --- + app.add_flag("--info", printInfo, "Print information about the shell")->excludes(debugPortArg); + app.add_flag("--no-color", noColor, "Do not color the log output. (Env:NO_COLOR)"); + + /// --- + QStringOption logpath; + auto* readLog = app.add_subcommand("read-log", "Read a quickshell log file."); + readLog->add_option("path", logpath, "Path to the log file to read")->required(); + readLog->add_flag("--no-color", noColor, "Do not color the log output. (Env:NO_COLOR)"); + + CLI11_PARSE(app, argc, argv); + + const auto qApplication = QCoreApplication(qArgC, qArgV); // Start log manager - has to happen with an active event loop or offthread can't be started. - LogManager::init(); + LogManager::init(!noColor); - QCommandLineParser parser; - parser.addHelpOption(); - parser.addVersionOption(); - - // clang-format off - auto currentOption = QCommandLineOption("current", "Print information about the manifest and defaults."); - auto manifestOption = QCommandLineOption({"m", "manifest"}, "Path to a configuration manifest.", "path"); - auto configOption = QCommandLineOption({"c", "config"}, "Name of a configuration in the manifest.", "name"); - auto pathOption = QCommandLineOption({"p", "path"}, "Path to a configuration file.", "path"); - auto workdirOption = QCommandLineOption({"d", "workdir"}, "Initial working directory.", "path"); - auto debugPortOption = QCommandLineOption("debugport", "Enable the QML debugger.", "port"); - auto debugWaitOption = QCommandLineOption("waitfordebug", "Wait for debugger connection before launching."); - auto readLogOption = QCommandLineOption("read-log", "Read a quickshell log file to stdout.", "path"); - // clang-format on - - parser.addOption(currentOption); - parser.addOption(manifestOption); - parser.addOption(configOption); - parser.addOption(pathOption); - parser.addOption(workdirOption); - parser.addOption(debugPortOption); - parser.addOption(debugWaitOption); - parser.addOption(readLogOption); - parser.process(app); - - auto logOption = parser.value(readLogOption); - if (!logOption.isEmpty()) { - auto file = QFile(logOption); + if (*readLog) { + auto file = QFile(*logpath); if (!file.open(QFile::ReadOnly)) { - qCritical() << "Failed to open log for reading:" << logOption; + qCritical() << "Failed to open log for reading:" << *logpath; return -1; } else { - qInfo() << "Reading log" << logOption; + qInfo() << "Reading log" << *logpath; } return qs::log::readEncodedLogs(&file) ? 0 : -1; - } - - auto debugPortStr = parser.value(debugPortOption); - if (!debugPortStr.isEmpty()) { - auto ok = false; - debugPort = debugPortStr.toInt(&ok); - - if (!ok) { - qCritical() << "Debug port must be a valid port number."; - return -1; - } - } - - if (parser.isSet(debugWaitOption)) { - if (debugPort == -1) { - qCritical() << "Cannot wait for debugger without a debug port set."; - return -1; - } - - waitForDebug = true; - } - - { - printCurrent = parser.isSet(currentOption); + } else { // NOLINTBEGIN #define CHECK(rname, name, level, label, expr) \ @@ -116,7 +163,7 @@ int qs_main(int argc, char** argv) { if (rname.isEmpty() && !name.isEmpty()) { \ rname = name; \ rname##Level = level; \ - if (!printCurrent) goto label; \ + if (!printInfo) goto label; \ } #define OPTSTR(name) (name.isEmpty() ? "(unset)" : name.toStdString()) @@ -133,7 +180,7 @@ int qs_main(int argc, char** argv) { // clang-format on // NOLINTEND - if (printCurrent) { + if (printInfo) { // clang-format off std::cout << "Base path: " << OPTSTR(basePath) << "\n"; std::cout << " - Environment (QS_BASE_PATH): " << OPTSTR(envBasePath) << "\n"; @@ -147,11 +194,11 @@ int qs_main(int argc, char** argv) { int configPathLevel = 10; { // NOLINTBEGIN - CHECK(configPath, optionConfigPath, 0, foundpath, parser.value(pathOption)); + CHECK(configPath, optionConfigPath, 0, foundpath, *path); CHECK(configPath, envConfigPath, 1, foundpath, qEnvironmentVariable("QS_CONFIG_PATH")); // NOLINTEND - if (printCurrent) { + if (printInfo) { // clang-format off std::cout << "\nConfig path: " << OPTSTR(configPath) << "\n"; std::cout << " - Option: " << OPTSTR(optionConfigPath) << "\n"; @@ -166,13 +213,13 @@ int qs_main(int argc, char** argv) { { // NOLINTBEGIN // clang-format off - CHECK(manifestPath, optionManifestPath, 0, foundmf, parser.value(manifestOption)); + CHECK(manifestPath, optionManifestPath, 0, foundmf, *manifest); CHECK(manifestPath, envManifestPath, 1, foundmf, qEnvironmentVariable("QS_MANIFEST")); CHECK(manifestPath, defaultManifestPath, 2, foundmf, QDir(basePath).filePath("manifest.conf")); // clang-format on // NOLINTEND - if (printCurrent) { + if (printInfo) { // clang-format off std::cout << "\nManifest path: " << OPTSTR(manifestPath) << "\n"; std::cout << " - Option: " << OPTSTR(optionManifestPath) << "\n"; @@ -187,11 +234,11 @@ int qs_main(int argc, char** argv) { int configNameLevel = 10; { // NOLINTBEGIN - CHECK(configName, optionConfigName, 0, foundname, parser.value(configOption)); + CHECK(configName, optionConfigName, 0, foundname, *config); CHECK(configName, envConfigName, 1, foundname, qEnvironmentVariable("QS_CONFIG_NAME")); // NOLINTEND - if (printCurrent) { + if (printInfo) { // clang-format off std::cout << "\nConfig name: " << OPTSTR(configName) << "\n"; std::cout << " - Option: " << OPTSTR(optionConfigName) << "\n"; @@ -201,11 +248,6 @@ int qs_main(int argc, char** argv) { } foundname:; - if (configPathLevel == 0 && configNameLevel == 0) { - qCritical() << "Pass only one of --path or --config"; - return -1; - } - if (!configPath.isEmpty() && configPathLevel <= configNameLevel) { configFilePath = configPath; } else if (!configName.isEmpty()) { @@ -299,60 +341,56 @@ int qs_main(int argc, char** argv) { shellId = QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); - qInfo() << "config file path:" << configFilePath; - } + qInfo() << "Config file path:" << configFilePath; - if (!QFile(configFilePath).exists()) { - qCritical() << "config file does not exist"; - return -1; - } + if (!QFile(configFilePath).exists()) { + qCritical() << "config file does not exist"; + return -1; + } - if (parser.isSet(workdirOption)) { - workingDirectory = parser.value(workdirOption); - } + auto file = QFile(configFilePath); + if (!file.open(QFile::ReadOnly | QFile::Text)) { + qCritical() << "could not open config file"; + return -1; + } - auto file = QFile(configFilePath); - if (!file.open(QFile::ReadOnly | QFile::Text)) { - qCritical() << "could not open config file"; - return -1; - } + auto stream = QTextStream(&file); + while (!stream.atEnd()) { + auto line = stream.readLine().trimmed(); + if (line.startsWith("//@ pragma ")) { + auto pragma = line.sliced(11).trimmed(); - auto stream = QTextStream(&file); - while (!stream.atEnd()) { - auto line = stream.readLine().trimmed(); - if (line.startsWith("//@ pragma ")) { - auto pragma = line.sliced(11).trimmed(); + if (pragma == "UseQApplication") useQApplication = true; + else if (pragma == "NativeTextRendering") nativeTextRendering = true; + else if (pragma == "IgnoreSystemSettings") desktopSettingsAware = false; + else if (pragma.startsWith("Env ")) { + auto envPragma = pragma.sliced(4); + auto splitIdx = envPragma.indexOf('='); - if (pragma == "UseQApplication") useQApplication = true; - else if (pragma == "NativeTextRendering") nativeTextRendering = true; - else if (pragma == "IgnoreSystemSettings") desktopSettingsAware = false; - else if (pragma.startsWith("Env ")) { - auto envPragma = pragma.sliced(4); - auto splitIdx = envPragma.indexOf('='); + if (splitIdx == -1) { + qCritical() << "Env pragma" << pragma << "not in the form 'VAR = VALUE'"; + return -1; + } - if (splitIdx == -1) { - qCritical() << "Env pragma" << pragma << "not in the form 'VAR = VALUE'"; + auto var = envPragma.sliced(0, splitIdx).trimmed(); + auto val = envPragma.sliced(splitIdx + 1).trimmed(); + envOverrides.insert(var, val); + } else if (pragma.startsWith("ShellId ")) { + shellId = pragma.sliced(8).trimmed(); + } else { + qCritical() << "Unrecognized pragma" << pragma; return -1; } + } else if (line.startsWith("import")) break; + } - auto var = envPragma.sliced(0, splitIdx).trimmed(); - auto val = envPragma.sliced(splitIdx + 1).trimmed(); - envOverrides.insert(var, val); - } else if (pragma.startsWith("ShellId ")) { - shellId = pragma.sliced(8).trimmed(); - } else { - qCritical() << "Unrecognized pragma" << pragma; - return -1; - } - } else if (line.startsWith("import")) break; + file.close(); } - - file.close(); } - qInfo() << "shell id:" << shellId; + qInfo() << "Shell ID:" << shellId; - if (printCurrent) return 0; + if (printInfo) return 0; for (auto [var, val]: envOverrides.asKeyValueRange()) { qputenv(var.toUtf8(), val.toUtf8()); @@ -360,6 +398,15 @@ int qs_main(int argc, char** argv) { QsPaths::init(shellId); + if (auto* cacheDir = QsPaths::instance()->cacheDir()) { + auto qmlCacheDir = cacheDir->filePath("qml-engine-cache"); + qputenv("QML_DISK_CACHE_PATH", qmlCacheDir.toLocal8Bit()); + + if (!qEnvironmentVariableIsSet("QML_DISK_CACHE")) { + qputenv("QML_DISK_CACHE", "aot,qmlc"); + } + } + // While the simple animation driver can lead to better animations in some cases, // it also can cause excessive repainting at excessively high framerates which can // lead to noticeable amounts of gpu usage, including overheating on some systems. @@ -401,9 +448,9 @@ int qs_main(int argc, char** argv) { QGuiApplication* app = nullptr; if (useQApplication) { - app = new QApplication(argc, argv); + app = new QApplication(qArgC, qArgV); } else { - app = new QGuiApplication(argc, argv); + app = new QGuiApplication(qArgC, qArgV); } LogManager::initFs();