From 4e48c6eefb399abfcdefbe046a6a642bdcfcc1a6 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 28 Oct 2024 16:18:41 -0700 Subject: [PATCH] all: refactor windows code out of core There are still some links from core to window but its now separate enough to fix PanelWindow in qml tooling. --- CMakeLists.txt | 9 - src/CMakeLists.txt | 5 + src/build/CMakeLists.txt | 26 + src/{core => build}/build.hpp.in | 0 src/core/CMakeLists.txt | 34 +- src/core/main.cpp | 1031 --------------------- src/core/main.hpp | 7 - src/core/platformmenu.cpp | 4 +- src/core/platformmenu.hpp | 2 +- src/core/popupanchor.cpp | 4 +- src/core/popupanchor.hpp | 2 +- src/core/test/CMakeLists.txt | 3 +- src/core/test/popupwindow.hpp | 18 - src/io/ipc.hpp | 2 +- src/io/ipccomm.cpp | 4 +- src/io/ipccomm.hpp | 2 +- src/ipc/CMakeLists.txt | 7 + src/{core => ipc}/ipc.cpp | 4 +- src/{core => ipc}/ipc.hpp | 0 src/{core => ipc}/ipccommand.hpp | 0 src/main.cpp | 1031 ++++++++++++++++++++- src/wayland/hyprland/focus_grab/qml.cpp | 4 +- src/wayland/toplevel_management/qml.cpp | 6 +- src/wayland/toplevel_management/qml.hpp | 2 +- src/wayland/wlr_layershell.cpp | 4 +- src/wayland/wlr_layershell.hpp | 4 +- src/wayland/wlr_layershell/surface.cpp | 2 +- src/wayland/wlr_layershell/window.cpp | 2 +- src/wayland/wlr_layershell/window.hpp | 2 +- src/window/CMakeLists.txt | 23 + src/{core => window}/floatingwindow.cpp | 0 src/{core => window}/floatingwindow.hpp | 0 src/{core => window}/panelinterface.cpp | 0 src/{core => window}/panelinterface.hpp | 2 +- src/{core => window}/popupwindow.cpp | 5 +- src/{core => window}/popupwindow.hpp | 6 +- src/{core => window}/proxywindow.cpp | 10 +- src/{core => window}/proxywindow.hpp | 7 +- src/window/test/CMakeLists.txt | 7 + src/{core => window}/test/popupwindow.cpp | 0 src/window/test/popupwindow.hpp | 18 + src/{core => window}/windowinterface.cpp | 0 src/{core => window}/windowinterface.hpp | 6 +- src/x11/panel_window.cpp | 4 +- src/x11/panel_window.hpp | 4 +- 45 files changed, 1171 insertions(+), 1142 deletions(-) create mode 100644 src/build/CMakeLists.txt rename src/{core => build}/build.hpp.in (100%) delete mode 100644 src/core/main.cpp delete mode 100644 src/core/main.hpp create mode 100644 src/ipc/CMakeLists.txt rename src/{core => ipc}/ipc.cpp (98%) rename src/{core => ipc}/ipc.hpp (100%) rename src/{core => ipc}/ipccommand.hpp (100%) create mode 100644 src/window/CMakeLists.txt rename src/{core => window}/floatingwindow.cpp (100%) rename src/{core => window}/floatingwindow.hpp (100%) rename src/{core => window}/panelinterface.cpp (100%) rename src/{core => window}/panelinterface.hpp (99%) rename src/{core => window}/popupwindow.cpp (98%) rename src/{core => window}/popupwindow.hpp (97%) rename src/{core => window}/proxywindow.cpp (98%) rename src/{core => window}/proxywindow.hpp (97%) create mode 100644 src/window/test/CMakeLists.txt rename src/{core => window}/test/popupwindow.cpp (100%) create mode 100644 src/window/test/popupwindow.hpp rename src/{core => window}/windowinterface.cpp (100%) rename src/{core => window}/windowinterface.hpp (98%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ef18479..367b39e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,15 +64,6 @@ boption(SERVICE_GREETD "Greetd" ON) boption(SERVICE_UPOWER "UPower" ON) boption(SERVICE_NOTIFICATIONS "Notifications" ON) -if (NOT DEFINED GIT_REVISION) - execute_process( - COMMAND git rev-parse HEAD - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - OUTPUT_VARIABLE GIT_REVISION - OUTPUT_STRIP_TRAILING_WHITESPACE - ) -endif() - add_compile_options(-Wall -Wextra) if (FRAME_POINTERS) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 33554832..5b843543 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,8 +1,13 @@ qt_add_executable(quickshell main.cpp) +target_link_libraries(quickshell PRIVATE ${QT_DEPS} quickshell-build) + install(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +add_subdirectory(build) add_subdirectory(core) +add_subdirectory(ipc) +add_subdirectory(window) add_subdirectory(io) add_subdirectory(widgets) diff --git a/src/build/CMakeLists.txt b/src/build/CMakeLists.txt new file mode 100644 index 00000000..bb35da99 --- /dev/null +++ b/src/build/CMakeLists.txt @@ -0,0 +1,26 @@ +add_library(quickshell-build INTERFACE) + +if (NOT DEFINED GIT_REVISION) + execute_process( + COMMAND git rev-parse HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_REVISION + OUTPUT_STRIP_TRAILING_WHITESPACE + ) +endif() + +if (CRASH_REPORTER) + set(CRASH_REPORTER_DEF 1) +else() + set(CRASH_REPORTER_DEF 0) +endif() + +if (DISTRIBUTOR_DEBUGINFO_AVAILABLE) + set(DEBUGINFO_AVAILABLE 1) +else() + set(DEBUGINFO_AVAILABLE 0) +endif() + +configure_file(build.hpp.in build.hpp @ONLY ESCAPE_QUOTES) + +target_include_directories(quickshell-build INTERFACE ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/src/core/build.hpp.in b/src/build/build.hpp.in similarity index 100% rename from src/core/build.hpp.in rename to src/build/build.hpp.in diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 811965e7..b454f3a7 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,22 +1,16 @@ find_package(CLI11 CONFIG REQUIRED) qt_add_library(quickshell-core STATIC - main.cpp plugin.cpp shell.cpp variants.cpp rootwrapper.cpp - proxywindow.cpp reload.cpp rootwrapper.cpp qmlglobal.cpp qmlscreen.cpp region.cpp persistentprops.cpp - windowinterface.cpp - floatingwindow.cpp - panelinterface.cpp - popupwindow.cpp singleton.cpp generation.cpp scan.cpp @@ -43,32 +37,20 @@ qt_add_library(quickshell-core STATIC paths.cpp instanceinfo.cpp common.cpp - ipc.cpp ) -if (CRASH_REPORTER) - set(CRASH_REPORTER_DEF 1) -else() - set(CRASH_REPORTER_DEF 0) -endif() - -if (DISTRIBUTOR_DEBUGINFO_AVAILABLE) - set(DEBUGINFO_AVAILABLE 1) -else() - set(DEBUGINFO_AVAILABLE 0) -endif() - -add_library(quickshell-build INTERFACE) - -configure_file(build.hpp.in build.hpp @ONLY ESCAPE_QUOTES) - -target_include_directories(quickshell-build INTERFACE ${CMAKE_CURRENT_BINARY_DIR}) target_link_libraries(quickshell-core PRIVATE quickshell-build) -qt_add_qml_module(quickshell-core URI Quickshell VERSION 0.1) +qt_add_qml_module(quickshell-core + URI Quickshell + VERSION 0.1 + IMPORTS Quickshell._Window +) + +target_link_libraries(quickshell-core PRIVATE ${QT_DEPS} CLI11::CLI11) -target_link_libraries(quickshell-core PRIVATE ${QT_DEPS} Qt6::QuickPrivate CLI11::CLI11) qs_pch(quickshell-core) +qs_pch(quickshell-coreplugin) target_link_libraries(quickshell PRIVATE quickshell-coreplugin) diff --git a/src/core/main.cpp b/src/core/main.cpp deleted file mode 100644 index cc57cc61..00000000 --- a/src/core/main.cpp +++ /dev/null @@ -1,1031 +0,0 @@ -#include "main.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include // NOLINT: Need to include this for impls of some CLI11 classes -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../io/ipccomm.hpp" -#include "build.hpp" -#include "common.hpp" -#include "instanceinfo.hpp" -#include "ipc.hpp" -#include "logging.hpp" -#include "paths.hpp" -#include "plugin.hpp" -#include "rootwrapper.hpp" - -#if CRASH_REPORTER -#include "../crash/handler.hpp" -#include "../crash/main.hpp" -#endif - -namespace qs::launch { - -using qs::ipc::IpcClient; - -void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication); -int runCommand(int argc, char** argv, QCoreApplication* coreApplication); - -int DAEMON_PIPE = -1; // NOLINT -void exitDaemon(int code) { - if (DAEMON_PIPE == -1) return; - - if (write(DAEMON_PIPE, &code, sizeof(int)) == -1) { - qCritical().nospace() << "Failed to write daemon exit command with error code " << errno << ": " - << qt_error_string(); - } - - close(DAEMON_PIPE); - - close(STDIN_FILENO); - close(STDOUT_FILENO); - close(STDERR_FILENO); - - if (open("/dev/null", O_RDONLY) != STDIN_FILENO) { // NOLINT - qFatal() << "Failed to open /dev/null on stdin"; - } - - if (open("/dev/null", O_WRONLY) != STDOUT_FILENO) { // NOLINT - qFatal() << "Failed to open /dev/null on stdout"; - } - - if (open("/dev/null", O_WRONLY) != STDERR_FILENO) { // NOLINT - qFatal() << "Failed to open /dev/null on stderr"; - } -} - -int main(int argc, char** argv) { - QCoreApplication::setApplicationName("quickshell"); - -#if CRASH_REPORTER - qsCheckCrash(argc, argv); -#endif - - auto qArgC = 1; - auto* coreApplication = new QCoreApplication(qArgC, argv); - - checkCrashRelaunch(argv, coreApplication); - auto code = runCommand(argc, argv, coreApplication); - - exitDaemon(code); - return code; -} - -class QStringOption { -public: - QStringOption() = default; - QStringOption& operator=(const std::string& str) { - this->str = QString::fromStdString(str); - return *this; - } - - QString& operator*() { return this->str; } - QString* operator->() { return &this->str; } - -private: - QString str; -}; - -struct CommandState { - struct { - int argc = 0; - char** argv = nullptr; - } exec; - - struct { - bool timestamp = false; - bool noColor = !qEnvironmentVariableIsEmpty("NO_COLOR"); - bool sparse = false; - size_t verbosity = 0; - int tail = 0; - bool follow = false; - QStringOption rules; - QStringOption readoutRules; - QStringOption file; - } log; - - struct { - QStringOption path; - QStringOption manifest; - QStringOption name; - } config; - - struct { - int port = -1; - bool wait = false; - } debug; - - struct { - QStringOption id; - pid_t pid = -1; // NOLINT (include) - bool all = false; - } instance; - - struct { - bool json = false; - } output; - - struct { - bool info = false; - QStringOption target; - QStringOption function; - std::vector arguments; - } ipc; - - struct { - CLI::App* log = nullptr; - CLI::App* list = nullptr; - CLI::App* kill = nullptr; - CLI::App* msg = nullptr; - } subcommand; - - struct { - bool checkCompat = false; - bool printVersion = false; - bool killAll = false; - bool noDuplicate = false; - bool daemonize = false; - } misc; -}; - -int readLogFile(CommandState& cmd); -int listInstances(CommandState& cmd); -int killInstances(CommandState& cmd); -int msgInstance(CommandState& cmd); -int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication); - -struct LaunchArgs { - QString configPath; - int debugPort = -1; - bool waitForDebug = false; -}; - -int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication); - -int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { - auto state = CommandState(); - - state.exec = { - .argc = argc, - .argv = argv, - }; - - auto addConfigSelection = [&](CLI::App* cmd) { - auto* group = cmd->add_option_group("Config Selection") - ->description("If no options in this group are specified,\n" - "$XDG_CONFIG_HOME/quickshell/shell.qml will be used."); - - auto* path = group->add_option("-p,--path", state.config.path) - ->description("Path to a QML file.") - ->envname("QS_CONFIG_PATH"); - - group->add_option("-m,--manifest", state.config.manifest) - ->description("Path to a quickshell manifest.\n" - "Defaults to $XDG_CONFIG_HOME/quickshell/manifest.conf") - ->envname("QS_MANIFEST") - ->excludes(path); - - group->add_option("-c,--config", state.config.name) - ->description("Name of a quickshell configuration to run.\n" - "If -m is specified, this is a configuration in the manifest,\n" - "otherwise it is the name of a folder in $XDG_CONFIG_HOME/quickshell.") - ->envname("QS_CONFIG_NAME"); - - return group; - }; - - auto addDebugOptions = [&](CLI::App* cmd) { - auto* group = cmd->add_option_group("Debugging", "Options for QML debugging."); - - auto* debug = group->add_option("--debug", state.debug.port) - ->description("Open the given port for a QML debugger connection.") - ->check(CLI::Range(0, 65535)); - - group->add_flag("--waitfordebug", state.debug.wait) - ->description("Wait for a QML debugger to connect before executing the configuration.") - ->needs(debug); - - return group; - }; - - auto addLoggingOptions = [&](CLI::App* cmd, bool noGroup, bool noDisplay = false) { - auto* group = noGroup ? cmd : cmd->add_option_group(noDisplay ? "" : "Logging"); - - group->add_flag("--no-color", state.log.noColor) - ->description("Disables colored logging.\n" - "Colored logging can also be disabled by specifying a non empty value\n" - "for the NO_COLOR environment variable."); - - group->add_flag("--log-times", state.log.timestamp) - ->description("Log timestamps with each message."); - - group->add_option("--log-rules", state.log.rules) - ->description("Log rules to apply, in the format of QT_LOGGING_RULES."); - - group->add_flag("-v,--verbose", [&](size_t count) { state.log.verbosity = count; }) - ->description("Increases log verbosity.\n" - "-v will show INFO level internal logs.\n" - "-vv will show DEBUG level internal logs."); - - auto* hgroup = cmd->add_option_group(""); - hgroup->add_flag("--no-detailed-logs", state.log.sparse); - }; - - auto addInstanceSelection = [&](CLI::App* cmd) { - auto* group = cmd->add_option_group("Instance Selection"); - - group->add_option("-i,--id", state.instance.id) - ->description("The instance id to operate on.\n" - "You may also use a substring the id as long as it is unique,\n" - "for example \"abc\" will select \"abcdefg\"."); - - group->add_option("--pid", state.instance.pid) - ->description("The process id of the instance to operate on."); - - return group; - }; - - auto cli = CLI::App(); - - // Require 0-1 subcommands. Without this, positionals can be parsed as more subcommands. - cli.require_subcommand(0, 1); - - addConfigSelection(&cli); - addLoggingOptions(&cli, false); - addDebugOptions(&cli); - - { - cli.add_option_group("")->add_flag("--private-check-compat", state.misc.checkCompat); - - cli.add_flag("-V,--version", state.misc.printVersion) - ->description("Print quickshell's version and exit."); - - cli.add_flag("-n,--no-duplicate", state.misc.noDuplicate) - ->description("Exit immediately if another instance of the given config is running."); - - cli.add_flag("-d,--daemonize", state.misc.daemonize) - ->description("Detach from the controlling terminal."); - } - - { - auto* sub = cli.add_subcommand("log", "Print quickshell logs."); - - auto* file = sub->add_option("file", state.log.file, "Log file to read."); - - sub->add_option("-t,--tail", state.log.tail) - ->description("Maximum number of lines to print, starting from the bottom.") - ->check(CLI::Range(1, std::numeric_limits::max(), "INT > 0")); - - sub->add_flag("-f,--follow", state.log.follow) - ->description("Keep reading the log until the logging process terminates."); - - sub->add_option("-r,--rules", state.log.readoutRules, "Log file to read.") - ->description("Rules to apply to the log being read, in the format of QT_LOGGING_RULES."); - - auto* instance = addInstanceSelection(sub)->excludes(file); - addConfigSelection(sub)->excludes(instance)->excludes(file); - addLoggingOptions(sub, false); - - state.subcommand.log = sub; - } - - { - auto* sub = cli.add_subcommand("list", "List running quickshell instances."); - - auto* all = sub->add_flag("-a,--all", state.instance.all) - ->description("List all instances.\n" - "If unspecified, only instances of" - "the selected config will be listed."); - - sub->add_flag("-j,--json", state.output.json, "Output the list as a json."); - - addConfigSelection(sub)->excludes(all); - addLoggingOptions(sub, false, true); - - state.subcommand.list = sub; - } - - { - auto* sub = cli.add_subcommand("kill", "Kill quickshell instances."); - //sub->add_flag("-a,--all", "Kill all matching instances instead of just one."); - auto* instance = addInstanceSelection(sub); - addConfigSelection(sub)->excludes(instance); - addLoggingOptions(sub, false, true); - - state.subcommand.kill = sub; - } - - { - auto* sub = cli.add_subcommand("msg", "Send messages to IpcHandlers.")->require_option(); - - auto* target = sub->add_option("target", state.ipc.target, "The target to message."); - - auto* function = sub->add_option("function", state.ipc.function) - ->description("The function to call in the target.") - ->needs(target); - - auto* arguments = sub->add_option("arguments", state.ipc.arguments) - ->description("Arguments to the called function.") - ->needs(function) - ->allow_extra_args(); - - sub->add_flag("-s,--show", state.ipc.info) - ->description("Print information about a function or target if given, or all available " - "targets if not.") - ->excludes(arguments); - - auto* instance = addInstanceSelection(sub); - addConfigSelection(sub)->excludes(instance); - addLoggingOptions(sub, false, true); - - sub->require_option(); - - state.subcommand.msg = sub; - } - - CLI11_PARSE(cli, argc, argv); - - if (state.misc.checkCompat) { - if (strcmp(qVersion(), QT_VERSION_STR) != 0) { - QTextStream(stdout) << "\033[31mCOMPATIBILITY WARNING: Quickshell was built against Qt " - << QT_VERSION_STR << " but the system has updated to Qt " << qVersion() - << " without rebuilding the package. This is likely to cause crashes, so " - "you must rebuild the quickshell package.\n"; - return 1; - } - - return 0; - } - - // Has to happen before extra threads are spawned. - if (state.misc.daemonize) { - auto closepipes = std::array(); - if (pipe(closepipes.data()) == -1) { - qFatal().nospace() << "Failed to create messaging pipes for daemon with error " << errno - << ": " << qt_error_string(); - } - - DAEMON_PIPE = closepipes[1]; - - pid_t pid = fork(); // NOLINT (include) - - if (pid == -1) { - qFatal().nospace() << "Failed to fork daemon with error " << errno << ": " - << qt_error_string(); - } else if (pid == 0) { - close(closepipes[0]); - - if (setsid() == -1) { - qFatal().nospace() << "Failed to setsid with error " << errno << ": " << qt_error_string(); - } - } else { - close(closepipes[1]); - - int ret = 0; - if (read(closepipes[0], &ret, sizeof(int)) == -1) { - qFatal() << "Failed to wait for daemon launch (it may have crashed)"; - } - - return ret; - } - } - - { - auto level = state.log.verbosity == 0 ? QtWarningMsg - : state.log.verbosity == 1 ? QtInfoMsg - : QtDebugMsg; - - LogManager::init( - !state.log.noColor, - state.log.timestamp, - state.log.sparse, - level, - *state.log.rules, - *state.subcommand.log ? "READER" : "" - ); - } - - if (state.misc.printVersion) { - qCInfo(logBare).noquote().nospace() << "quickshell pre-release, revision " << GIT_REVISION - << ", distributed by: " << DISTRIBUTOR; - - if (state.log.verbosity > 1) { - qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR; - qCInfo(logBare).noquote() << "Runtime Qt Version:" << qVersion(); - qCInfo(logBare).noquote() << "Compiler:" << COMPILER; - qCInfo(logBare).noquote() << "Compile Flags:" << COMPILE_FLAGS; - } - - if (state.log.verbosity > 0) { - qCInfo(logBare).noquote() << "\nBuild Type:" << BUILD_TYPE; - qCInfo(logBare).noquote() << "Build configuration:"; - qCInfo(logBare).noquote().nospace() << BUILD_CONFIGURATION; - } - } else if (*state.subcommand.log) { - return readLogFile(state); - } else if (*state.subcommand.list) { - return listInstances(state); - } else if (*state.subcommand.kill) { - return killInstances(state); - } else if (*state.subcommand.msg) { - return msgInstance(state); - } else { - if (strcmp(qVersion(), QT_VERSION_STR) != 0) { - qWarning() << "\033[31mQuickshell was built against Qt" << QT_VERSION_STR - << "but the system has updated to Qt" << qVersion() - << "without rebuilding the package. This is likely to cause crashes, so " - "the quickshell package must be rebuilt.\n"; - } - - return launchFromCommand(state, coreApplication); - } - - return 0; -} - -int locateConfigFile(CommandState& cmd, QString& path) { - if (!cmd.config.path->isEmpty()) { - path = *cmd.config.path; - } else { - auto manifestPath = *cmd.config.manifest; - if (manifestPath.isEmpty()) { - auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)); - auto path = configDir.filePath("manifest.conf"); - if (QFileInfo(path).isFile()) manifestPath = path; - } - - if (!manifestPath.isEmpty()) { - auto file = QFile(manifestPath); - if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { - auto stream = QTextStream(&file); - while (!stream.atEnd()) { - auto line = stream.readLine(); - if (line.trimmed().startsWith("#")) continue; - if (line.trimmed().isEmpty()) continue; - - auto split = line.split('='); - if (split.length() != 2) { - qCritical() << "Manifest line not in expected format 'name = relativepath':" << line; - return -1; - } - - if (split[0].trimmed() == *cmd.config.name) { - path = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed()); - break; - } - } - - if (path.isEmpty()) { - qCCritical(logBare) << "Configuration" << *cmd.config.name - << "not found when searching manifest" << manifestPath; - return -1; - } - } else { - qCCritical(logBare) << "Could not open maifest at path" << *cmd.config.manifest; - return -1; - } - } else { - auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)); - - if (cmd.config.name->isEmpty()) { - path = configDir.path(); - } else { - path = configDir.filePath(*cmd.config.name); - } - } - } - - if (QFileInfo(path).isDir()) { - path = QDir(path).filePath("shell.qml"); - } - - if (!QFileInfo(path).isFile()) { - qCCritical(logBare) << "Could not open config file at" << path; - return -1; - } - - path = QFileInfo(path).canonicalFilePath(); - - return 0; -} - -void sortInstances(QVector& list) { - std::sort(list.begin(), list.end(), [](const InstanceLockInfo& a, const InstanceLockInfo& b) { - return a.instance.launchTime < b.instance.launchTime; - }); -}; - -int selectInstance(CommandState& cmd, InstanceLockInfo* instance) { - auto* basePath = QsPaths::instance()->baseRunDir(); - if (!basePath) return -1; - - QString path; - - if (cmd.instance.pid != -1) { - path = QDir(basePath->filePath("by-pid")).filePath(QString::number(cmd.instance.pid)); - if (!QsPaths::checkLock(path, instance)) { - qCInfo(logBare) << "No instance found for pid" << cmd.instance.pid; - return -1; - } - } else if (!cmd.instance.id->isEmpty()) { - path = basePath->filePath("by-pid"); - auto instances = QsPaths::collectInstances(path); - - auto itr = - std::remove_if(instances.begin(), instances.end(), [&](const InstanceLockInfo& info) { - return !info.instance.instanceId.startsWith(*cmd.instance.id); - }); - - instances.erase(itr, instances.end()); - - if (instances.isEmpty()) { - qCInfo(logBare) << "No running instances start with" << *cmd.instance.id; - return -1; - } else if (instances.length() != 1) { - qCInfo(logBare) << "More than one instance starts with" << *cmd.instance.id; - - for (auto& instance: instances) { - qCInfo(logBare).noquote() << " -" << instance.instance.instanceId; - } - - return -1; - } else { - *instance = instances.value(0); - } - } else { - QString configFilePath; - auto r = locateConfigFile(cmd, configFilePath); - if (r != 0) return r; - - auto pathId = - QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); - - path = QDir(basePath->filePath("by-path")).filePath(pathId); - - auto instances = QsPaths::collectInstances(path); - sortInstances(instances); - - if (instances.isEmpty()) { - qCInfo(logBare) << "No running instances for" << configFilePath; - return -1; - } - - *instance = instances.value(0); - } - - return 0; -} - -int readLogFile(CommandState& cmd) { - auto path = *cmd.log.file; - - if (path.isEmpty()) { - InstanceLockInfo instance; - auto r = selectInstance(cmd, &instance); - if (r != 0) return r; - - path = QDir(QsPaths::basePath(instance.instance.instanceId)).filePath("log.qslog"); - } - - auto file = QFile(path); - if (!file.open(QFile::ReadOnly)) { - qCCritical(logBare) << "Failed to open log file" << path; - return -1; - } - - return qs::log::readEncodedLogs( - &file, - path, - cmd.log.timestamp, - cmd.log.tail, - cmd.log.follow, - *cmd.log.readoutRules - ) - ? 0 - : -1; -} - -int listInstances(CommandState& cmd) { - auto* basePath = QsPaths::instance()->baseRunDir(); - if (!basePath) return -1; // NOLINT - - QString path; - QString configFilePath; - if (cmd.instance.all) { - path = basePath->filePath("by-pid"); - } else { - auto r = locateConfigFile(cmd, configFilePath); - - if (r != 0) { - qCInfo(logBare) << "Use --all to list all instances."; - return r; - } - - auto pathId = - QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); - - path = QDir(basePath->filePath("by-path")).filePath(pathId); - } - - auto instances = QsPaths::collectInstances(path); - - if (instances.isEmpty()) { - if (cmd.instance.all) { - qCInfo(logBare) << "No running instances."; - } else { - qCInfo(logBare) << "No running instances for" << configFilePath; - qCInfo(logBare) << "Use --all to list all instances."; - } - } else { - sortInstances(instances); - - if (cmd.output.json) { - auto array = QJsonArray(); - - for (auto& instance: instances) { - auto json = QJsonObject(); - - json["id"] = instance.instance.instanceId; - json["pid"] = instance.pid; - json["shell_id"] = instance.instance.shellId; - json["config_path"] = instance.instance.configPath; - json["launch_time"] = instance.instance.launchTime.toString(Qt::ISODate); - - array.push_back(json); - } - - auto document = QJsonDocument(array); - QTextStream(stdout) << document.toJson(QJsonDocument::Indented); - } else { - for (auto& instance: instances) { - auto launchTimeStr = instance.instance.launchTime.toString("yyyy-MM-dd hh:mm:ss"); - - auto runSeconds = instance.instance.launchTime.secsTo(QDateTime::currentDateTime()); - auto remSeconds = runSeconds % 60; - auto runMinutes = (runSeconds - remSeconds) / 60; - auto remMinutes = runMinutes % 60; - auto runHours = (runMinutes - remMinutes) / 60; - auto runtimeStr = QString("%1 hours, %2 minutes, %3 seconds") - .arg(runHours) - .arg(remMinutes) - .arg(remSeconds); - - qCInfo(logBare).noquote().nospace() - << "Instance " << instance.instance.instanceId << ":\n" - << " Process ID: " << instance.pid << '\n' - << " Shell ID: " << instance.instance.shellId << '\n' - << " Config path: " << instance.instance.configPath << '\n' - << " Launch time: " << launchTimeStr << " (running for " << runtimeStr << ")\n"; - } - } - } - - return 0; -} - -int killInstances(CommandState& cmd) { - InstanceLockInfo instance; - auto r = selectInstance(cmd, &instance); - if (r != 0) return r; - - return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) { - client.kill(); - qCInfo(logBare).noquote() << "Killed" << instance.instance.instanceId; - }); -} - -int msgInstance(CommandState& cmd) { - InstanceLockInfo instance; - auto r = selectInstance(cmd, &instance); - if (r != 0) return r; - - return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) { - if (cmd.ipc.info) { - return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.function); - } else { - QVector arguments; - for (auto& arg: cmd.ipc.arguments) { - arguments += *arg; - } - - return qs::io::ipc::comm::callFunction( - &client, - *cmd.ipc.target, - *cmd.ipc.function, - arguments - ); - } - - return -1; - }); -} - -template -QString base36Encode(T number) { - const QString digits = "0123456789abcdefghijklmnopqrstuvwxyz"; - QString result; - - do { - result.prepend(digits[number % 36]); - number /= 36; - } while (number > 0); - - for (auto i = 0; i < result.length() / 2; i++) { - auto opposite = result.length() - i - 1; - auto c = result.at(i); - result[i] = result.at(opposite); - result[opposite] = c; - } - - return result; -} - -void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) { -#if CRASH_REPORTER - auto lastInfoFdStr = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD"); - - if (!lastInfoFdStr.isEmpty()) { - auto lastInfoFd = lastInfoFdStr.toInt(); - - QFile file; - file.open(lastInfoFd, QFile::ReadOnly, QFile::AutoCloseHandle); - file.seek(0); - - auto ds = QDataStream(&file); - RelaunchInfo info; - ds >> info; - - LogManager::init( - !info.noColor, - info.timestamp, - info.sparseLogsOnly, - info.defaultLogLevel, - info.logRules - ); - - qCritical().nospace() << "Quickshell has crashed under pid " - << qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt() - << " (Coredumps will be available under that pid.)"; - - qCritical() << "Further crash information is stored under" - << QsPaths::crashDir(info.instance.instanceId).path(); - - if (info.instance.launchTime.msecsTo(QDateTime::currentDateTime()) < 10000) { - qCritical() << "Quickshell crashed within 10 seconds of launching. Not restarting to avoid " - "a crash loop."; - exit(-1); // NOLINT - } else { - qCritical() << "Quickshell has been restarted."; - - launch({.configPath = info.instance.configPath}, argv, coreApplication); - } - } -#endif -} - -int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication) { - QString configPath; - - auto r = locateConfigFile(cmd, configPath); - if (r != 0) return r; - - { - InstanceLockInfo info; - if (cmd.misc.noDuplicate && selectInstance(cmd, &info) == 0) { - qCInfo(logBare) << "An instance of this configuration is already running."; - return 0; - } - } - - return launch( - { - .configPath = configPath, - .debugPort = cmd.debug.port, - .waitForDebug = cmd.debug.wait, - }, - cmd.exec.argv, - coreApplication - ); -} - -int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication) { - auto pathId = QCryptographicHash::hash(args.configPath.toUtf8(), QCryptographicHash::Md5).toHex(); - auto shellId = QString(pathId); - - qInfo() << "Launching config:" << args.configPath; - - auto file = QFile(args.configPath); - if (!file.open(QFile::ReadOnly | QFile::Text)) { - qCritical() << "Could not open config file" << args.configPath; - return -1; - } - - struct { - bool useQApplication = false; - bool nativeTextRendering = false; - bool desktopSettingsAware = true; - QString iconTheme = qEnvironmentVariable("QS_ICON_THEME"); - QHash envOverrides; - } pragmas; - - 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") pragmas.useQApplication = true; - else if (pragma == "NativeTextRendering") pragmas.nativeTextRendering = true; - else if (pragma == "IgnoreSystemSettings") pragmas.desktopSettingsAware = false; - else if (pragma.startsWith("IconTheme ")) pragmas.iconTheme = pragma.sliced(10); - 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; - } - - auto var = envPragma.sliced(0, splitIdx).trimmed(); - auto val = envPragma.sliced(splitIdx + 1).trimmed(); - pragmas.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(); - - if (!pragmas.iconTheme.isEmpty()) { - QIcon::setThemeName(pragmas.iconTheme); - } - - qInfo() << "Shell ID:" << shellId << "Path ID" << pathId; - - auto launchTime = qs::Common::LAUNCH_TIME.toSecsSinceEpoch(); - InstanceInfo::CURRENT = InstanceInfo { - .instanceId = base36Encode(getpid()) + base36Encode(launchTime), - .configPath = args.configPath, - .shellId = shellId, - .launchTime = qs::Common::LAUNCH_TIME, - }; - -#if CRASH_REPORTER - auto crashHandler = crash::CrashHandler(); - crashHandler.init(); - - { - auto* log = LogManager::instance(); - crashHandler.setRelaunchInfo({ - .instance = InstanceInfo::CURRENT, - .noColor = !log->colorLogs, - .timestamp = log->timestampLogs, - .sparseLogsOnly = log->isSparse(), - .defaultLogLevel = log->defaultLevel(), - .logRules = log->rulesString(), - }); - } -#endif - - QsPaths::init(shellId, pathId); - QsPaths::instance()->linkRunDir(); - QsPaths::instance()->linkPathDir(); - LogManager::initFs(); - - for (auto [var, val]: pragmas.envOverrides.asKeyValueRange()) { - qputenv(var.toUtf8(), val.toUtf8()); - } - - // The qml engine currently refuses to cache non file (qsintercept) paths. - - // 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. - // This gets worse the more windows are open, as repaints trigger on all of them for - // some reason. See QTBUG-126099 for details. - - // if (!qEnvironmentVariableIsSet("QSG_USE_SIMPLE_ANIMATION_DRIVER")) { - // qputenv("QSG_USE_SIMPLE_ANIMATION_DRIVER", "1"); - // } - - // Some programs place icons in the pixmaps folder instead of the icons folder. - // This seems to be controlled by the QPA and qt6ct does not provide it. - { - QList dataPaths; - - if (qEnvironmentVariableIsSet("XDG_DATA_DIRS")) { - auto var = qEnvironmentVariable("XDG_DATA_DIRS"); - dataPaths = var.split(u':', Qt::SkipEmptyParts); - } else { - dataPaths.push_back("/usr/local/share"); - dataPaths.push_back("/usr/share"); - } - - auto fallbackPaths = QIcon::fallbackSearchPaths(); - - for (auto& path: dataPaths) { - auto newPath = QDir(path).filePath("pixmaps"); - - if (!fallbackPaths.contains(newPath)) { - fallbackPaths.push_back(newPath); - } - } - - QIcon::setFallbackSearchPaths(fallbackPaths); - } - - QGuiApplication::setDesktopSettingsAware(pragmas.desktopSettingsAware); - - delete coreApplication; - - QGuiApplication* app = nullptr; - auto qArgC = 0; - - if (pragmas.useQApplication) { - app = new QApplication(qArgC, argv); - } else { - app = new QGuiApplication(qArgC, argv); - } - - if (args.debugPort != -1) { - QQmlDebuggingEnabler::enableDebugging(true); - auto wait = args.waitForDebug ? QQmlDebuggingEnabler::WaitForClient - : QQmlDebuggingEnabler::DoNotWaitForClient; - QQmlDebuggingEnabler::startTcpDebugServer(args.debugPort, wait); - } - - QuickshellPlugin::initPlugins(); - - // Base window transparency appears to be additive. - // Use a fully transparent window with a colored rect. - QQuickWindow::setDefaultAlphaBuffer(true); - - if (pragmas.nativeTextRendering) { - QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering); - } - - qs::ipc::IpcServer::start(); - QsPaths::instance()->createLock(); - - auto root = RootWrapper(args.configPath, shellId); - QGuiApplication::setQuitOnLastWindowClosed(false); - - exitDaemon(0); - - auto code = QGuiApplication::exec(); - delete app; - return code; -} - -} // namespace qs::launch diff --git a/src/core/main.hpp b/src/core/main.hpp deleted file mode 100644 index 795bf7ad..00000000 --- a/src/core/main.hpp +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -namespace qs::launch { - -int main(int argc, char** argv); // NOLINT - -} diff --git a/src/core/platformmenu.cpp b/src/core/platformmenu.cpp index 37eb4654..09837ec9 100644 --- a/src/core/platformmenu.cpp +++ b/src/core/platformmenu.cpp @@ -17,11 +17,11 @@ #include #include +#include "../window/proxywindow.hpp" +#include "../window/windowinterface.hpp" #include "generation.hpp" #include "popupanchor.hpp" -#include "proxywindow.hpp" #include "qsmenu.hpp" -#include "windowinterface.hpp" namespace qs::menu::platform { diff --git a/src/core/platformmenu.hpp b/src/core/platformmenu.hpp index 85aaffac..5e8a0afe 100644 --- a/src/core/platformmenu.hpp +++ b/src/core/platformmenu.hpp @@ -13,7 +13,7 @@ #include #include -#include "popupanchor.hpp" +#include "../core/popupanchor.hpp" #include "qsmenu.hpp" namespace qs::menu::platform { diff --git a/src/core/popupanchor.cpp b/src/core/popupanchor.cpp index 1f4c5a76..0dc9c4a4 100644 --- a/src/core/popupanchor.cpp +++ b/src/core/popupanchor.cpp @@ -7,9 +7,9 @@ #include #include -#include "proxywindow.hpp" +#include "../window/proxywindow.hpp" +#include "../window/windowinterface.hpp" #include "types.hpp" -#include "windowinterface.hpp" bool PopupAnchorState::operator==(const PopupAnchorState& other) const { return this->rect == other.rect && this->edges == other.edges && this->gravity == other.gravity diff --git a/src/core/popupanchor.hpp b/src/core/popupanchor.hpp index 11b2ba20..0897928a 100644 --- a/src/core/popupanchor.hpp +++ b/src/core/popupanchor.hpp @@ -13,8 +13,8 @@ #include #include +#include "../window/proxywindow.hpp" #include "doc.hpp" -#include "proxywindow.hpp" #include "types.hpp" ///! Adjustment strategy for popups that do not fit on screen. diff --git a/src/core/test/CMakeLists.txt b/src/core/test/CMakeLists.txt index 3c057d3b..448881a6 100644 --- a/src/core/test/CMakeLists.txt +++ b/src/core/test/CMakeLists.txt @@ -1,9 +1,8 @@ function (qs_test name) add_executable(${name} ${ARGN}) - target_link_libraries(${name} PRIVATE ${QT_DEPS} Qt6::Test quickshell-core) + target_link_libraries(${name} PRIVATE ${QT_DEPS} Qt6::Test quickshell-core quickshell-window) add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $) endfunction() -qs_test(popupwindow popupwindow.cpp) qs_test(transformwatcher transformwatcher.cpp) qs_test(ringbuffer ringbuf.cpp) diff --git a/src/core/test/popupwindow.hpp b/src/core/test/popupwindow.hpp index bebc5154..e69de29b 100644 --- a/src/core/test/popupwindow.hpp +++ b/src/core/test/popupwindow.hpp @@ -1,18 +0,0 @@ -#pragma once - -#include -#include - -class TestPopupWindow: public QObject { - Q_OBJECT; - -private slots: - void initiallyVisible(); - void reloadReparent(); - void reloadUnparent(); - void invisibleWithoutParent(); - void moveWithParent(); - void attachParentLate(); - void reparentLate(); - void xMigrationFix(); -}; diff --git a/src/io/ipc.hpp b/src/io/ipc.hpp index 50a94759..924f045e 100644 --- a/src/io/ipc.hpp +++ b/src/io/ipc.hpp @@ -4,7 +4,7 @@ #include #include -#include "../core/ipc.hpp" +#include "../ipc/ipc.hpp" namespace qs::io::ipc { diff --git a/src/io/ipccomm.cpp b/src/io/ipccomm.cpp index 56381260..f2a9118a 100644 --- a/src/io/ipccomm.cpp +++ b/src/io/ipccomm.cpp @@ -9,9 +9,9 @@ #include #include "../core/generation.hpp" -#include "../core/ipc.hpp" -#include "../core/ipccommand.hpp" #include "../core/logging.hpp" +#include "../ipc/ipc.hpp" +#include "../ipc/ipccommand.hpp" #include "ipc.hpp" #include "ipchandler.hpp" diff --git a/src/io/ipccomm.hpp b/src/io/ipccomm.hpp index 7b1ec02a..69463983 100644 --- a/src/io/ipccomm.hpp +++ b/src/io/ipccomm.hpp @@ -3,7 +3,7 @@ #include #include -#include "../core/ipc.hpp" +#include "../ipc/ipc.hpp" namespace qs::io::ipc::comm { diff --git a/src/ipc/CMakeLists.txt b/src/ipc/CMakeLists.txt new file mode 100644 index 00000000..ff6093c6 --- /dev/null +++ b/src/ipc/CMakeLists.txt @@ -0,0 +1,7 @@ +qt_add_library(quickshell-ipc STATIC + ipc.cpp +) + +target_link_libraries(quickshell-ipc PRIVATE ${QT_DEPS}) + +target_link_libraries(quickshell PRIVATE quickshell-ipc) diff --git a/src/core/ipc.cpp b/src/ipc/ipc.cpp similarity index 98% rename from src/core/ipc.cpp rename to src/ipc/ipc.cpp index dd2cd1e8..3580e2be 100644 --- a/src/core/ipc.cpp +++ b/src/ipc/ipc.cpp @@ -9,9 +9,9 @@ #include #include -#include "generation.hpp" +#include "../core/generation.hpp" +#include "../core/paths.hpp" #include "ipccommand.hpp" -#include "paths.hpp" namespace qs::ipc { diff --git a/src/core/ipc.hpp b/src/ipc/ipc.hpp similarity index 100% rename from src/core/ipc.hpp rename to src/ipc/ipc.hpp diff --git a/src/core/ipccommand.hpp b/src/ipc/ipccommand.hpp similarity index 100% rename from src/core/ipccommand.hpp rename to src/ipc/ipccommand.hpp diff --git a/src/main.cpp b/src/main.cpp index 7e4811da..f18c2341 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,1032 @@ -#include "core/main.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include // NOLINT: Need to include this for impls of some CLI11 classes +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "build.hpp" +#include "core/common.hpp" +#include "core/instanceinfo.hpp" +#include "core/logging.hpp" +#include "core/paths.hpp" +#include "core/plugin.hpp" +#include "core/rootwrapper.hpp" +#include "io/ipccomm.hpp" +#include "ipc/ipc.hpp" + +#if CRASH_REPORTER +#include "crash/handler.hpp" +#include "crash/main.hpp" +#endif + +namespace qs::launch { + +using qs::ipc::IpcClient; + +void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication); +int runCommand(int argc, char** argv, QCoreApplication* coreApplication); + +int DAEMON_PIPE = -1; // NOLINT +void exitDaemon(int code) { + if (DAEMON_PIPE == -1) return; + + if (write(DAEMON_PIPE, &code, sizeof(int)) == -1) { + qCritical().nospace() << "Failed to write daemon exit command with error code " << errno << ": " + << qt_error_string(); + } + + close(DAEMON_PIPE); + + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + + if (open("/dev/null", O_RDONLY) != STDIN_FILENO) { // NOLINT + qFatal() << "Failed to open /dev/null on stdin"; + } + + if (open("/dev/null", O_WRONLY) != STDOUT_FILENO) { // NOLINT + qFatal() << "Failed to open /dev/null on stdout"; + } + + if (open("/dev/null", O_WRONLY) != STDERR_FILENO) { // NOLINT + qFatal() << "Failed to open /dev/null on stderr"; + } +} + +int main(int argc, char** argv) { + QCoreApplication::setApplicationName("quickshell"); + +#if CRASH_REPORTER + qsCheckCrash(argc, argv); +#endif + + auto qArgC = 1; + auto* coreApplication = new QCoreApplication(qArgC, argv); + + checkCrashRelaunch(argv, coreApplication); + auto code = runCommand(argc, argv, coreApplication); + + exitDaemon(code); + return code; +} + +class QStringOption { +public: + QStringOption() = default; + QStringOption& operator=(const std::string& str) { + this->str = QString::fromStdString(str); + return *this; + } + + QString& operator*() { return this->str; } + QString* operator->() { return &this->str; } + +private: + QString str; +}; + +struct CommandState { + struct { + int argc = 0; + char** argv = nullptr; + } exec; + + struct { + bool timestamp = false; + bool noColor = !qEnvironmentVariableIsEmpty("NO_COLOR"); + bool sparse = false; + size_t verbosity = 0; + int tail = 0; + bool follow = false; + QStringOption rules; + QStringOption readoutRules; + QStringOption file; + } log; + + struct { + QStringOption path; + QStringOption manifest; + QStringOption name; + } config; + + struct { + int port = -1; + bool wait = false; + } debug; + + struct { + QStringOption id; + pid_t pid = -1; // NOLINT (include) + bool all = false; + } instance; + + struct { + bool json = false; + } output; + + struct { + bool info = false; + QStringOption target; + QStringOption function; + std::vector arguments; + } ipc; + + struct { + CLI::App* log = nullptr; + CLI::App* list = nullptr; + CLI::App* kill = nullptr; + CLI::App* msg = nullptr; + } subcommand; + + struct { + bool checkCompat = false; + bool printVersion = false; + bool killAll = false; + bool noDuplicate = false; + bool daemonize = false; + } misc; +}; + +int readLogFile(CommandState& cmd); +int listInstances(CommandState& cmd); +int killInstances(CommandState& cmd); +int msgInstance(CommandState& cmd); +int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication); + +struct LaunchArgs { + QString configPath; + int debugPort = -1; + bool waitForDebug = false; +}; + +int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication); + +int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { + auto state = CommandState(); + + state.exec = { + .argc = argc, + .argv = argv, + }; + + auto addConfigSelection = [&](CLI::App* cmd) { + auto* group = cmd->add_option_group("Config Selection") + ->description("If no options in this group are specified,\n" + "$XDG_CONFIG_HOME/quickshell/shell.qml will be used."); + + auto* path = group->add_option("-p,--path", state.config.path) + ->description("Path to a QML file.") + ->envname("QS_CONFIG_PATH"); + + group->add_option("-m,--manifest", state.config.manifest) + ->description("Path to a quickshell manifest.\n" + "Defaults to $XDG_CONFIG_HOME/quickshell/manifest.conf") + ->envname("QS_MANIFEST") + ->excludes(path); + + group->add_option("-c,--config", state.config.name) + ->description("Name of a quickshell configuration to run.\n" + "If -m is specified, this is a configuration in the manifest,\n" + "otherwise it is the name of a folder in $XDG_CONFIG_HOME/quickshell.") + ->envname("QS_CONFIG_NAME"); + + return group; + }; + + auto addDebugOptions = [&](CLI::App* cmd) { + auto* group = cmd->add_option_group("Debugging", "Options for QML debugging."); + + auto* debug = group->add_option("--debug", state.debug.port) + ->description("Open the given port for a QML debugger connection.") + ->check(CLI::Range(0, 65535)); + + group->add_flag("--waitfordebug", state.debug.wait) + ->description("Wait for a QML debugger to connect before executing the configuration.") + ->needs(debug); + + return group; + }; + + auto addLoggingOptions = [&](CLI::App* cmd, bool noGroup, bool noDisplay = false) { + auto* group = noGroup ? cmd : cmd->add_option_group(noDisplay ? "" : "Logging"); + + group->add_flag("--no-color", state.log.noColor) + ->description("Disables colored logging.\n" + "Colored logging can also be disabled by specifying a non empty value\n" + "for the NO_COLOR environment variable."); + + group->add_flag("--log-times", state.log.timestamp) + ->description("Log timestamps with each message."); + + group->add_option("--log-rules", state.log.rules) + ->description("Log rules to apply, in the format of QT_LOGGING_RULES."); + + group->add_flag("-v,--verbose", [&](size_t count) { state.log.verbosity = count; }) + ->description("Increases log verbosity.\n" + "-v will show INFO level internal logs.\n" + "-vv will show DEBUG level internal logs."); + + auto* hgroup = cmd->add_option_group(""); + hgroup->add_flag("--no-detailed-logs", state.log.sparse); + }; + + auto addInstanceSelection = [&](CLI::App* cmd) { + auto* group = cmd->add_option_group("Instance Selection"); + + group->add_option("-i,--id", state.instance.id) + ->description("The instance id to operate on.\n" + "You may also use a substring the id as long as it is unique,\n" + "for example \"abc\" will select \"abcdefg\"."); + + group->add_option("--pid", state.instance.pid) + ->description("The process id of the instance to operate on."); + + return group; + }; + + auto cli = CLI::App(); + + // Require 0-1 subcommands. Without this, positionals can be parsed as more subcommands. + cli.require_subcommand(0, 1); + + addConfigSelection(&cli); + addLoggingOptions(&cli, false); + addDebugOptions(&cli); + + { + cli.add_option_group("")->add_flag("--private-check-compat", state.misc.checkCompat); + + cli.add_flag("-V,--version", state.misc.printVersion) + ->description("Print quickshell's version and exit."); + + cli.add_flag("-n,--no-duplicate", state.misc.noDuplicate) + ->description("Exit immediately if another instance of the given config is running."); + + cli.add_flag("-d,--daemonize", state.misc.daemonize) + ->description("Detach from the controlling terminal."); + } + + { + auto* sub = cli.add_subcommand("log", "Print quickshell logs."); + + auto* file = sub->add_option("file", state.log.file, "Log file to read."); + + sub->add_option("-t,--tail", state.log.tail) + ->description("Maximum number of lines to print, starting from the bottom.") + ->check(CLI::Range(1, std::numeric_limits::max(), "INT > 0")); + + sub->add_flag("-f,--follow", state.log.follow) + ->description("Keep reading the log until the logging process terminates."); + + sub->add_option("-r,--rules", state.log.readoutRules, "Log file to read.") + ->description("Rules to apply to the log being read, in the format of QT_LOGGING_RULES."); + + auto* instance = addInstanceSelection(sub)->excludes(file); + addConfigSelection(sub)->excludes(instance)->excludes(file); + addLoggingOptions(sub, false); + + state.subcommand.log = sub; + } + + { + auto* sub = cli.add_subcommand("list", "List running quickshell instances."); + + auto* all = sub->add_flag("-a,--all", state.instance.all) + ->description("List all instances.\n" + "If unspecified, only instances of" + "the selected config will be listed."); + + sub->add_flag("-j,--json", state.output.json, "Output the list as a json."); + + addConfigSelection(sub)->excludes(all); + addLoggingOptions(sub, false, true); + + state.subcommand.list = sub; + } + + { + auto* sub = cli.add_subcommand("kill", "Kill quickshell instances."); + //sub->add_flag("-a,--all", "Kill all matching instances instead of just one."); + auto* instance = addInstanceSelection(sub); + addConfigSelection(sub)->excludes(instance); + addLoggingOptions(sub, false, true); + + state.subcommand.kill = sub; + } + + { + auto* sub = cli.add_subcommand("msg", "Send messages to IpcHandlers.")->require_option(); + + auto* target = sub->add_option("target", state.ipc.target, "The target to message."); + + auto* function = sub->add_option("function", state.ipc.function) + ->description("The function to call in the target.") + ->needs(target); + + auto* arguments = sub->add_option("arguments", state.ipc.arguments) + ->description("Arguments to the called function.") + ->needs(function) + ->allow_extra_args(); + + sub->add_flag("-s,--show", state.ipc.info) + ->description("Print information about a function or target if given, or all available " + "targets if not.") + ->excludes(arguments); + + auto* instance = addInstanceSelection(sub); + addConfigSelection(sub)->excludes(instance); + addLoggingOptions(sub, false, true); + + sub->require_option(); + + state.subcommand.msg = sub; + } + + CLI11_PARSE(cli, argc, argv); + + if (state.misc.checkCompat) { + if (strcmp(qVersion(), QT_VERSION_STR) != 0) { + QTextStream(stdout) << "\033[31mCOMPATIBILITY WARNING: Quickshell was built against Qt " + << QT_VERSION_STR << " but the system has updated to Qt " << qVersion() + << " without rebuilding the package. This is likely to cause crashes, so " + "you must rebuild the quickshell package.\n"; + return 1; + } + + return 0; + } + + // Has to happen before extra threads are spawned. + if (state.misc.daemonize) { + auto closepipes = std::array(); + if (pipe(closepipes.data()) == -1) { + qFatal().nospace() << "Failed to create messaging pipes for daemon with error " << errno + << ": " << qt_error_string(); + } + + DAEMON_PIPE = closepipes[1]; + + pid_t pid = fork(); // NOLINT (include) + + if (pid == -1) { + qFatal().nospace() << "Failed to fork daemon with error " << errno << ": " + << qt_error_string(); + } else if (pid == 0) { + close(closepipes[0]); + + if (setsid() == -1) { + qFatal().nospace() << "Failed to setsid with error " << errno << ": " << qt_error_string(); + } + } else { + close(closepipes[1]); + + int ret = 0; + if (read(closepipes[0], &ret, sizeof(int)) == -1) { + qFatal() << "Failed to wait for daemon launch (it may have crashed)"; + } + + return ret; + } + } + + { + auto level = state.log.verbosity == 0 ? QtWarningMsg + : state.log.verbosity == 1 ? QtInfoMsg + : QtDebugMsg; + + LogManager::init( + !state.log.noColor, + state.log.timestamp, + state.log.sparse, + level, + *state.log.rules, + *state.subcommand.log ? "READER" : "" + ); + } + + if (state.misc.printVersion) { + qCInfo(logBare).noquote().nospace() << "quickshell pre-release, revision " << GIT_REVISION + << ", distributed by: " << DISTRIBUTOR; + + if (state.log.verbosity > 1) { + qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR; + qCInfo(logBare).noquote() << "Runtime Qt Version:" << qVersion(); + qCInfo(logBare).noquote() << "Compiler:" << COMPILER; + qCInfo(logBare).noquote() << "Compile Flags:" << COMPILE_FLAGS; + } + + if (state.log.verbosity > 0) { + qCInfo(logBare).noquote() << "\nBuild Type:" << BUILD_TYPE; + qCInfo(logBare).noquote() << "Build configuration:"; + qCInfo(logBare).noquote().nospace() << BUILD_CONFIGURATION; + } + } else if (*state.subcommand.log) { + return readLogFile(state); + } else if (*state.subcommand.list) { + return listInstances(state); + } else if (*state.subcommand.kill) { + return killInstances(state); + } else if (*state.subcommand.msg) { + return msgInstance(state); + } else { + if (strcmp(qVersion(), QT_VERSION_STR) != 0) { + qWarning() << "\033[31mQuickshell was built against Qt" << QT_VERSION_STR + << "but the system has updated to Qt" << qVersion() + << "without rebuilding the package. This is likely to cause crashes, so " + "the quickshell package must be rebuilt.\n"; + } + + return launchFromCommand(state, coreApplication); + } + + return 0; +} + +int locateConfigFile(CommandState& cmd, QString& path) { + if (!cmd.config.path->isEmpty()) { + path = *cmd.config.path; + } else { + auto manifestPath = *cmd.config.manifest; + if (manifestPath.isEmpty()) { + auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)); + auto path = configDir.filePath("manifest.conf"); + if (QFileInfo(path).isFile()) manifestPath = path; + } + + if (!manifestPath.isEmpty()) { + auto file = QFile(manifestPath); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { + auto stream = QTextStream(&file); + while (!stream.atEnd()) { + auto line = stream.readLine(); + if (line.trimmed().startsWith("#")) continue; + if (line.trimmed().isEmpty()) continue; + + auto split = line.split('='); + if (split.length() != 2) { + qCritical() << "Manifest line not in expected format 'name = relativepath':" << line; + return -1; + } + + if (split[0].trimmed() == *cmd.config.name) { + path = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed()); + break; + } + } + + if (path.isEmpty()) { + qCCritical(logBare) << "Configuration" << *cmd.config.name + << "not found when searching manifest" << manifestPath; + return -1; + } + } else { + qCCritical(logBare) << "Could not open maifest at path" << *cmd.config.manifest; + return -1; + } + } else { + auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)); + + if (cmd.config.name->isEmpty()) { + path = configDir.path(); + } else { + path = configDir.filePath(*cmd.config.name); + } + } + } + + if (QFileInfo(path).isDir()) { + path = QDir(path).filePath("shell.qml"); + } + + if (!QFileInfo(path).isFile()) { + qCCritical(logBare) << "Could not open config file at" << path; + return -1; + } + + path = QFileInfo(path).canonicalFilePath(); + + return 0; +} + +void sortInstances(QVector& list) { + std::sort(list.begin(), list.end(), [](const InstanceLockInfo& a, const InstanceLockInfo& b) { + return a.instance.launchTime < b.instance.launchTime; + }); +}; + +int selectInstance(CommandState& cmd, InstanceLockInfo* instance) { + auto* basePath = QsPaths::instance()->baseRunDir(); + if (!basePath) return -1; + + QString path; + + if (cmd.instance.pid != -1) { + path = QDir(basePath->filePath("by-pid")).filePath(QString::number(cmd.instance.pid)); + if (!QsPaths::checkLock(path, instance)) { + qCInfo(logBare) << "No instance found for pid" << cmd.instance.pid; + return -1; + } + } else if (!cmd.instance.id->isEmpty()) { + path = basePath->filePath("by-pid"); + auto instances = QsPaths::collectInstances(path); + + auto itr = + std::remove_if(instances.begin(), instances.end(), [&](const InstanceLockInfo& info) { + return !info.instance.instanceId.startsWith(*cmd.instance.id); + }); + + instances.erase(itr, instances.end()); + + if (instances.isEmpty()) { + qCInfo(logBare) << "No running instances start with" << *cmd.instance.id; + return -1; + } else if (instances.length() != 1) { + qCInfo(logBare) << "More than one instance starts with" << *cmd.instance.id; + + for (auto& instance: instances) { + qCInfo(logBare).noquote() << " -" << instance.instance.instanceId; + } + + return -1; + } else { + *instance = instances.value(0); + } + } else { + QString configFilePath; + auto r = locateConfigFile(cmd, configFilePath); + if (r != 0) return r; + + auto pathId = + QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); + + path = QDir(basePath->filePath("by-path")).filePath(pathId); + + auto instances = QsPaths::collectInstances(path); + sortInstances(instances); + + if (instances.isEmpty()) { + qCInfo(logBare) << "No running instances for" << configFilePath; + return -1; + } + + *instance = instances.value(0); + } + + return 0; +} + +int readLogFile(CommandState& cmd) { + auto path = *cmd.log.file; + + if (path.isEmpty()) { + InstanceLockInfo instance; + auto r = selectInstance(cmd, &instance); + if (r != 0) return r; + + path = QDir(QsPaths::basePath(instance.instance.instanceId)).filePath("log.qslog"); + } + + auto file = QFile(path); + if (!file.open(QFile::ReadOnly)) { + qCCritical(logBare) << "Failed to open log file" << path; + return -1; + } + + return qs::log::readEncodedLogs( + &file, + path, + cmd.log.timestamp, + cmd.log.tail, + cmd.log.follow, + *cmd.log.readoutRules + ) + ? 0 + : -1; +} + +int listInstances(CommandState& cmd) { + auto* basePath = QsPaths::instance()->baseRunDir(); + if (!basePath) return -1; // NOLINT + + QString path; + QString configFilePath; + if (cmd.instance.all) { + path = basePath->filePath("by-pid"); + } else { + auto r = locateConfigFile(cmd, configFilePath); + + if (r != 0) { + qCInfo(logBare) << "Use --all to list all instances."; + return r; + } + + auto pathId = + QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); + + path = QDir(basePath->filePath("by-path")).filePath(pathId); + } + + auto instances = QsPaths::collectInstances(path); + + if (instances.isEmpty()) { + if (cmd.instance.all) { + qCInfo(logBare) << "No running instances."; + } else { + qCInfo(logBare) << "No running instances for" << configFilePath; + qCInfo(logBare) << "Use --all to list all instances."; + } + } else { + sortInstances(instances); + + if (cmd.output.json) { + auto array = QJsonArray(); + + for (auto& instance: instances) { + auto json = QJsonObject(); + + json["id"] = instance.instance.instanceId; + json["pid"] = instance.pid; + json["shell_id"] = instance.instance.shellId; + json["config_path"] = instance.instance.configPath; + json["launch_time"] = instance.instance.launchTime.toString(Qt::ISODate); + + array.push_back(json); + } + + auto document = QJsonDocument(array); + QTextStream(stdout) << document.toJson(QJsonDocument::Indented); + } else { + for (auto& instance: instances) { + auto launchTimeStr = instance.instance.launchTime.toString("yyyy-MM-dd hh:mm:ss"); + + auto runSeconds = instance.instance.launchTime.secsTo(QDateTime::currentDateTime()); + auto remSeconds = runSeconds % 60; + auto runMinutes = (runSeconds - remSeconds) / 60; + auto remMinutes = runMinutes % 60; + auto runHours = (runMinutes - remMinutes) / 60; + auto runtimeStr = QString("%1 hours, %2 minutes, %3 seconds") + .arg(runHours) + .arg(remMinutes) + .arg(remSeconds); + + qCInfo(logBare).noquote().nospace() + << "Instance " << instance.instance.instanceId << ":\n" + << " Process ID: " << instance.pid << '\n' + << " Shell ID: " << instance.instance.shellId << '\n' + << " Config path: " << instance.instance.configPath << '\n' + << " Launch time: " << launchTimeStr << " (running for " << runtimeStr << ")\n"; + } + } + } + + return 0; +} + +int killInstances(CommandState& cmd) { + InstanceLockInfo instance; + auto r = selectInstance(cmd, &instance); + if (r != 0) return r; + + return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) { + client.kill(); + qCInfo(logBare).noquote() << "Killed" << instance.instance.instanceId; + }); +} + +int msgInstance(CommandState& cmd) { + InstanceLockInfo instance; + auto r = selectInstance(cmd, &instance); + if (r != 0) return r; + + return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) { + if (cmd.ipc.info) { + return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.function); + } else { + QVector arguments; + for (auto& arg: cmd.ipc.arguments) { + arguments += *arg; + } + + return qs::io::ipc::comm::callFunction( + &client, + *cmd.ipc.target, + *cmd.ipc.function, + arguments + ); + } + + return -1; + }); +} + +template +QString base36Encode(T number) { + const QString digits = "0123456789abcdefghijklmnopqrstuvwxyz"; + QString result; + + do { + result.prepend(digits[number % 36]); + number /= 36; + } while (number > 0); + + for (auto i = 0; i < result.length() / 2; i++) { + auto opposite = result.length() - i - 1; + auto c = result.at(i); + result[i] = result.at(opposite); + result[opposite] = c; + } + + return result; +} + +void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) { +#if CRASH_REPORTER + auto lastInfoFdStr = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD"); + + if (!lastInfoFdStr.isEmpty()) { + auto lastInfoFd = lastInfoFdStr.toInt(); + + QFile file; + file.open(lastInfoFd, QFile::ReadOnly, QFile::AutoCloseHandle); + file.seek(0); + + auto ds = QDataStream(&file); + RelaunchInfo info; + ds >> info; + + LogManager::init( + !info.noColor, + info.timestamp, + info.sparseLogsOnly, + info.defaultLogLevel, + info.logRules + ); + + qCritical().nospace() << "Quickshell has crashed under pid " + << qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt() + << " (Coredumps will be available under that pid.)"; + + qCritical() << "Further crash information is stored under" + << QsPaths::crashDir(info.instance.instanceId).path(); + + if (info.instance.launchTime.msecsTo(QDateTime::currentDateTime()) < 10000) { + qCritical() << "Quickshell crashed within 10 seconds of launching. Not restarting to avoid " + "a crash loop."; + exit(-1); // NOLINT + } else { + qCritical() << "Quickshell has been restarted."; + + launch({.configPath = info.instance.configPath}, argv, coreApplication); + } + } +#endif +} + +int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication) { + QString configPath; + + auto r = locateConfigFile(cmd, configPath); + if (r != 0) return r; + + { + InstanceLockInfo info; + if (cmd.misc.noDuplicate && selectInstance(cmd, &info) == 0) { + qCInfo(logBare) << "An instance of this configuration is already running."; + return 0; + } + } + + return launch( + { + .configPath = configPath, + .debugPort = cmd.debug.port, + .waitForDebug = cmd.debug.wait, + }, + cmd.exec.argv, + coreApplication + ); +} + +int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication) { + auto pathId = QCryptographicHash::hash(args.configPath.toUtf8(), QCryptographicHash::Md5).toHex(); + auto shellId = QString(pathId); + + qInfo() << "Launching config:" << args.configPath; + + auto file = QFile(args.configPath); + if (!file.open(QFile::ReadOnly | QFile::Text)) { + qCritical() << "Could not open config file" << args.configPath; + return -1; + } + + struct { + bool useQApplication = false; + bool nativeTextRendering = false; + bool desktopSettingsAware = true; + QString iconTheme = qEnvironmentVariable("QS_ICON_THEME"); + QHash envOverrides; + } pragmas; + + 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") pragmas.useQApplication = true; + else if (pragma == "NativeTextRendering") pragmas.nativeTextRendering = true; + else if (pragma == "IgnoreSystemSettings") pragmas.desktopSettingsAware = false; + else if (pragma.startsWith("IconTheme ")) pragmas.iconTheme = pragma.sliced(10); + 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; + } + + auto var = envPragma.sliced(0, splitIdx).trimmed(); + auto val = envPragma.sliced(splitIdx + 1).trimmed(); + pragmas.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(); + + if (!pragmas.iconTheme.isEmpty()) { + QIcon::setThemeName(pragmas.iconTheme); + } + + qInfo() << "Shell ID:" << shellId << "Path ID" << pathId; + + auto launchTime = qs::Common::LAUNCH_TIME.toSecsSinceEpoch(); + InstanceInfo::CURRENT = InstanceInfo { + .instanceId = base36Encode(getpid()) + base36Encode(launchTime), + .configPath = args.configPath, + .shellId = shellId, + .launchTime = qs::Common::LAUNCH_TIME, + }; + +#if CRASH_REPORTER + auto crashHandler = crash::CrashHandler(); + crashHandler.init(); + + { + auto* log = LogManager::instance(); + crashHandler.setRelaunchInfo({ + .instance = InstanceInfo::CURRENT, + .noColor = !log->colorLogs, + .timestamp = log->timestampLogs, + .sparseLogsOnly = log->isSparse(), + .defaultLogLevel = log->defaultLevel(), + .logRules = log->rulesString(), + }); + } +#endif + + QsPaths::init(shellId, pathId); + QsPaths::instance()->linkRunDir(); + QsPaths::instance()->linkPathDir(); + LogManager::initFs(); + + for (auto [var, val]: pragmas.envOverrides.asKeyValueRange()) { + qputenv(var.toUtf8(), val.toUtf8()); + } + + // The qml engine currently refuses to cache non file (qsintercept) paths. + + // 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. + // This gets worse the more windows are open, as repaints trigger on all of them for + // some reason. See QTBUG-126099 for details. + + // if (!qEnvironmentVariableIsSet("QSG_USE_SIMPLE_ANIMATION_DRIVER")) { + // qputenv("QSG_USE_SIMPLE_ANIMATION_DRIVER", "1"); + // } + + // Some programs place icons in the pixmaps folder instead of the icons folder. + // This seems to be controlled by the QPA and qt6ct does not provide it. + { + QList dataPaths; + + if (qEnvironmentVariableIsSet("XDG_DATA_DIRS")) { + auto var = qEnvironmentVariable("XDG_DATA_DIRS"); + dataPaths = var.split(u':', Qt::SkipEmptyParts); + } else { + dataPaths.push_back("/usr/local/share"); + dataPaths.push_back("/usr/share"); + } + + auto fallbackPaths = QIcon::fallbackSearchPaths(); + + for (auto& path: dataPaths) { + auto newPath = QDir(path).filePath("pixmaps"); + + if (!fallbackPaths.contains(newPath)) { + fallbackPaths.push_back(newPath); + } + } + + QIcon::setFallbackSearchPaths(fallbackPaths); + } + + QGuiApplication::setDesktopSettingsAware(pragmas.desktopSettingsAware); + + delete coreApplication; + + QGuiApplication* app = nullptr; + auto qArgC = 0; + + if (pragmas.useQApplication) { + app = new QApplication(qArgC, argv); + } else { + app = new QGuiApplication(qArgC, argv); + } + + if (args.debugPort != -1) { + QQmlDebuggingEnabler::enableDebugging(true); + auto wait = args.waitForDebug ? QQmlDebuggingEnabler::WaitForClient + : QQmlDebuggingEnabler::DoNotWaitForClient; + QQmlDebuggingEnabler::startTcpDebugServer(args.debugPort, wait); + } + + QuickshellPlugin::initPlugins(); + + // Base window transparency appears to be additive. + // Use a fully transparent window with a colored rect. + QQuickWindow::setDefaultAlphaBuffer(true); + + if (pragmas.nativeTextRendering) { + QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering); + } + + qs::ipc::IpcServer::start(); + QsPaths::instance()->createLock(); + + auto root = RootWrapper(args.configPath, shellId); + QGuiApplication::setQuitOnLastWindowClosed(false); + + exitDaemon(0); + + auto code = QGuiApplication::exec(); + delete app; + return code; +} + +} // namespace qs::launch int main(int argc, char** argv) { return qs::launch::main(argc, argv); } diff --git a/src/wayland/hyprland/focus_grab/qml.cpp b/src/wayland/hyprland/focus_grab/qml.cpp index 9ae309ff..9d09fc44 100644 --- a/src/wayland/hyprland/focus_grab/qml.cpp +++ b/src/wayland/hyprland/focus_grab/qml.cpp @@ -8,8 +8,8 @@ #include #include -#include "../../../core/proxywindow.hpp" -#include "../../../core/windowinterface.hpp" +#include "../../../window/proxywindow.hpp" +#include "../../../window/windowinterface.hpp" #include "grab.hpp" #include "manager.hpp" diff --git a/src/wayland/toplevel_management/qml.cpp b/src/wayland/toplevel_management/qml.cpp index 1f0d1fe8..153ed1fc 100644 --- a/src/wayland/toplevel_management/qml.cpp +++ b/src/wayland/toplevel_management/qml.cpp @@ -3,11 +3,11 @@ #include #include -#include "../../core/util.hpp" #include "../../core/model.hpp" -#include "../../core/proxywindow.hpp" #include "../../core/qmlscreen.hpp" -#include "../../core/windowinterface.hpp" +#include "../../core/util.hpp" +#include "../../window/proxywindow.hpp" +#include "../../window/windowinterface.hpp" #include "handle.hpp" #include "manager.hpp" diff --git a/src/wayland/toplevel_management/qml.hpp b/src/wayland/toplevel_management/qml.hpp index d50713c5..7fa7b65a 100644 --- a/src/wayland/toplevel_management/qml.hpp +++ b/src/wayland/toplevel_management/qml.hpp @@ -5,9 +5,9 @@ #include #include "../../core/model.hpp" -#include "../../core/proxywindow.hpp" #include "../../core/qmlscreen.hpp" #include "../../core/util.hpp" +#include "../../window/proxywindow.hpp" namespace qs::wayland::toplevel_management { diff --git a/src/wayland/wlr_layershell.cpp b/src/wayland/wlr_layershell.cpp index 6a381c01..1ce7b7fc 100644 --- a/src/wayland/wlr_layershell.cpp +++ b/src/wayland/wlr_layershell.cpp @@ -9,9 +9,9 @@ #include #include -#include "../core/panelinterface.hpp" -#include "../core/proxywindow.hpp" #include "../core/qmlscreen.hpp" +#include "../window/panelinterface.hpp" +#include "../window/proxywindow.hpp" #include "wlr_layershell/window.hpp" WlrLayershell::WlrLayershell(QObject* parent) diff --git a/src/wayland/wlr_layershell.hpp b/src/wayland/wlr_layershell.hpp index 7687c4f3..e7a1a077 100644 --- a/src/wayland/wlr_layershell.hpp +++ b/src/wayland/wlr_layershell.hpp @@ -8,8 +8,8 @@ #include #include "../core/doc.hpp" -#include "../core/panelinterface.hpp" -#include "../core/proxywindow.hpp" +#include "../window/panelinterface.hpp" +#include "../window/proxywindow.hpp" #include "wlr_layershell/window.hpp" ///! Wlroots layershell window diff --git a/src/wayland/wlr_layershell/surface.cpp b/src/wayland/wlr_layershell/surface.cpp index ca5e7d10..25b58ff8 100644 --- a/src/wayland/wlr_layershell/surface.cpp +++ b/src/wayland/wlr_layershell/surface.cpp @@ -14,7 +14,7 @@ #include #include -#include "../../core/panelinterface.hpp" +#include "../../window/panelinterface.hpp" #include "shell_integration.hpp" #include "window.hpp" diff --git a/src/wayland/wlr_layershell/window.cpp b/src/wayland/wlr_layershell/window.cpp index a671d59e..8139e841 100644 --- a/src/wayland/wlr_layershell/window.cpp +++ b/src/wayland/wlr_layershell/window.cpp @@ -9,7 +9,7 @@ #include #include -#include "../../core/panelinterface.hpp" +#include "../../window/panelinterface.hpp" #include "shell_integration.hpp" #include "surface.hpp" diff --git a/src/wayland/wlr_layershell/window.hpp b/src/wayland/wlr_layershell/window.hpp index ea38e6e9..59711b5f 100644 --- a/src/wayland/wlr_layershell/window.hpp +++ b/src/wayland/wlr_layershell/window.hpp @@ -7,7 +7,7 @@ #include #include -#include "../../core/panelinterface.hpp" +#include "../../window/panelinterface.hpp" ///! WlrLayershell layer. /// See @@WlrLayershell.layer. diff --git a/src/window/CMakeLists.txt b/src/window/CMakeLists.txt new file mode 100644 index 00000000..6b14c58a --- /dev/null +++ b/src/window/CMakeLists.txt @@ -0,0 +1,23 @@ +qt_add_library(quickshell-window STATIC + proxywindow.cpp + windowinterface.cpp + panelinterface.cpp + floatingwindow.cpp + popupwindow.cpp +) + +qt_add_qml_module(quickshell-window + URI Quickshell._Window + VERSION 0.1 +) + +target_link_libraries(quickshell-window PRIVATE ${QT_DEPS} Qt6::QuickPrivate) + +qs_pch(quickshell-window) +qs_pch(quickshell-windowplugin) + +target_link_libraries(quickshell PRIVATE quickshell-windowplugin) + +if (BUILD_TESTING) + add_subdirectory(test) +endif() diff --git a/src/core/floatingwindow.cpp b/src/window/floatingwindow.cpp similarity index 100% rename from src/core/floatingwindow.cpp rename to src/window/floatingwindow.cpp diff --git a/src/core/floatingwindow.hpp b/src/window/floatingwindow.hpp similarity index 100% rename from src/core/floatingwindow.hpp rename to src/window/floatingwindow.hpp diff --git a/src/core/panelinterface.cpp b/src/window/panelinterface.cpp similarity index 100% rename from src/core/panelinterface.cpp rename to src/window/panelinterface.cpp diff --git a/src/core/panelinterface.hpp b/src/window/panelinterface.hpp similarity index 99% rename from src/core/panelinterface.hpp rename to src/window/panelinterface.hpp index 78665df3..5ccb5186 100644 --- a/src/core/panelinterface.hpp +++ b/src/window/panelinterface.hpp @@ -3,7 +3,7 @@ #include #include -#include "doc.hpp" +#include "../core/doc.hpp" #include "windowinterface.hpp" class Anchors { diff --git a/src/core/popupwindow.cpp b/src/window/popupwindow.cpp similarity index 98% rename from src/core/popupwindow.cpp rename to src/window/popupwindow.cpp index 7a3d9316..b355238e 100644 --- a/src/core/popupwindow.cpp +++ b/src/window/popupwindow.cpp @@ -7,9 +7,8 @@ #include #include -#include "popupanchor.hpp" -#include "proxywindow.hpp" -#include "qmlscreen.hpp" +#include "../core/popupanchor.hpp" +#include "../core/qmlscreen.hpp" #include "windowinterface.hpp" ProxyPopupWindow::ProxyPopupWindow(QObject* parent): ProxyWindowBase(parent) { diff --git a/src/core/popupwindow.hpp b/src/window/popupwindow.hpp similarity index 97% rename from src/core/popupwindow.hpp rename to src/window/popupwindow.hpp index 47db4038..bb245eb8 100644 --- a/src/core/popupwindow.hpp +++ b/src/window/popupwindow.hpp @@ -6,10 +6,10 @@ #include #include -#include "doc.hpp" -#include "popupanchor.hpp" +#include "../core/doc.hpp" +#include "../core/popupanchor.hpp" +#include "../core/qmlscreen.hpp" #include "proxywindow.hpp" -#include "qmlscreen.hpp" #include "windowinterface.hpp" ///! Popup window. diff --git a/src/core/proxywindow.cpp b/src/window/proxywindow.cpp similarity index 98% rename from src/core/proxywindow.cpp rename to src/window/proxywindow.cpp index 5d4659dd..07f8a233 100644 --- a/src/core/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -14,11 +14,11 @@ #include #include -#include "generation.hpp" -#include "qmlglobal.hpp" -#include "qmlscreen.hpp" -#include "region.hpp" -#include "reload.hpp" +#include "../core/generation.hpp" +#include "../core/qmlglobal.hpp" +#include "../core/qmlscreen.hpp" +#include "../core/region.hpp" +#include "../core/reload.hpp" #include "windowinterface.hpp" ProxyWindowBase::ProxyWindowBase(QObject* parent) diff --git a/src/core/proxywindow.hpp b/src/window/proxywindow.hpp similarity index 97% rename from src/core/proxywindow.hpp rename to src/window/proxywindow.hpp index ce8228fe..dbbf1910 100644 --- a/src/core/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -12,10 +12,9 @@ #include #include -#include "qmlglobal.hpp" -#include "qmlscreen.hpp" -#include "region.hpp" -#include "reload.hpp" +#include "../core/qmlscreen.hpp" +#include "../core/region.hpp" +#include "../core/reload.hpp" #include "windowinterface.hpp" // Proxy to an actual window exposing a limited property set with the ability to diff --git a/src/window/test/CMakeLists.txt b/src/window/test/CMakeLists.txt new file mode 100644 index 00000000..ad9e5a0a --- /dev/null +++ b/src/window/test/CMakeLists.txt @@ -0,0 +1,7 @@ +function (qs_test name) + add_executable(${name} ${ARGN}) + target_link_libraries(${name} PRIVATE ${QT_DEPS} Qt6::Test quickshell-window quickshell-core) + add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $) +endfunction() + +qs_test(popupwindow popupwindow.cpp) diff --git a/src/core/test/popupwindow.cpp b/src/window/test/popupwindow.cpp similarity index 100% rename from src/core/test/popupwindow.cpp rename to src/window/test/popupwindow.cpp diff --git a/src/window/test/popupwindow.hpp b/src/window/test/popupwindow.hpp new file mode 100644 index 00000000..bebc5154 --- /dev/null +++ b/src/window/test/popupwindow.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +class TestPopupWindow: public QObject { + Q_OBJECT; + +private slots: + void initiallyVisible(); + void reloadReparent(); + void reloadUnparent(); + void invisibleWithoutParent(); + void moveWithParent(); + void attachParentLate(); + void reparentLate(); + void xMigrationFix(); +}; diff --git a/src/core/windowinterface.cpp b/src/window/windowinterface.cpp similarity index 100% rename from src/core/windowinterface.cpp rename to src/window/windowinterface.cpp diff --git a/src/core/windowinterface.hpp b/src/window/windowinterface.hpp similarity index 98% rename from src/core/windowinterface.hpp rename to src/window/windowinterface.hpp index f90df24c..c351cf34 100644 --- a/src/core/windowinterface.hpp +++ b/src/window/windowinterface.hpp @@ -8,9 +8,9 @@ #include #include -#include "qmlscreen.hpp" -#include "region.hpp" -#include "reload.hpp" +#include "../core/qmlscreen.hpp" +#include "../core/region.hpp" +#include "../core/reload.hpp" class ProxyWindowBase; class QsWindowAttached; diff --git a/src/x11/panel_window.cpp b/src/x11/panel_window.cpp index 3f0aa279..d0441adf 100644 --- a/src/x11/panel_window.cpp +++ b/src/x11/panel_window.cpp @@ -17,9 +17,9 @@ #include #include "../core/generation.hpp" -#include "../core/panelinterface.hpp" -#include "../core/proxywindow.hpp" #include "../core/qmlscreen.hpp" +#include "../window/panelinterface.hpp" +#include "../window/proxywindow.hpp" #include "util.hpp" class XPanelStack { diff --git a/src/x11/panel_window.hpp b/src/x11/panel_window.hpp index 9bcaf64d..b37c9c50 100644 --- a/src/x11/panel_window.hpp +++ b/src/x11/panel_window.hpp @@ -8,8 +8,8 @@ #include #include "../core/doc.hpp" -#include "../core/panelinterface.hpp" -#include "../core/proxywindow.hpp" +#include "../window/panelinterface.hpp" +#include "../window/proxywindow.hpp" class XPanelStack;