From 0499518143b232b949a177c5fea929f2ceed58ec Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 15 Jun 2025 00:07:01 -0700 Subject: [PATCH] core/qmlglobal: add execDetached functions for spawning processes --- src/core/qmlglobal.cpp | 33 ++++++++++++++++++ src/core/qmlglobal.hpp | 63 ++++++++++++++++++++++++++++++++++ src/core/test/CMakeLists.txt | 2 +- src/io/CMakeLists.txt | 1 + src/io/process.cpp | 28 ++++----------- src/io/process.hpp | 13 ++++--- src/io/processcore.cpp | 37 ++++++++++++++++++++ src/io/processcore.hpp | 16 +++++++++ src/io/test/CMakeLists.txt | 2 +- src/window/test/CMakeLists.txt | 2 +- 10 files changed, 167 insertions(+), 30 deletions(-) create mode 100644 src/io/processcore.cpp create mode 100644 src/io/processcore.hpp diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index cbddee40..c447c554 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -8,8 +8,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -21,6 +23,7 @@ #include #include +#include "../io/processcore.hpp" #include "generation.hpp" #include "iconimageprovider.hpp" #include "paths.hpp" @@ -246,6 +249,36 @@ QVariant QuickshellGlobal::env(const QString& variable) { // NOLINT return qEnvironmentVariable(vstr.data()); } +void QuickshellGlobal::execDetached(QList command) { + QuickshellGlobal::execDetached(ProcessContext(std::move(command))); +} + +void QuickshellGlobal::execDetached(const ProcessContext& context) { + if (context.command.isEmpty()) { + qWarning() << "Cannot start process as command is empty."; + return; + } + + const auto& cmd = context.command.first(); + auto args = context.command.sliced(1); + + QProcess process; + + qs::core::process::setupProcessEnvironment( + &process, + context.clearEnvironment, + context.environment + ); + + if (!context.workingDirectory.isEmpty()) { + process.setWorkingDirectory(context.workingDirectory); + } + + process.setProgram(cmd); + process.setArguments(args); + process.startDetached(); +} + QString QuickshellGlobal::iconPath(const QString& icon) { return IconImageProvider::requestString(icon); } diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index afb2f7e0..d5b98447 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -1,8 +1,12 @@ #pragma once +#include + #include #include +#include #include +#include #include #include #include @@ -15,6 +19,25 @@ #include "qmlscreen.hpp" +class ProcessContext { + Q_PROPERTY(QList command MEMBER command); + Q_PROPERTY(QHash environment MEMBER environment); + Q_PROPERTY(bool clearEnvironment MEMBER clearEnvironment); + Q_PROPERTY(QString workingDirectory MEMBER workingDirectory); + Q_GADGET; + QML_STRUCTURED_VALUE; + QML_VALUE_TYPE(processContext); + +public: + ProcessContext() = default; + explicit ProcessContext(QList command): command(std::move(command)) {} + + QList command; + QHash environment; + bool clearEnvironment = false; + QString workingDirectory; +}; + ///! Accessor for some options under the Quickshell type. class QuickshellSettings: public QObject { Q_OBJECT; @@ -152,6 +175,46 @@ public: /// Returns the string value of an environment variable or null if it is not set. Q_INVOKABLE QVariant env(const QString& variable); + /// Launch a process detached from Quickshell. + /// + /// Each command argument is its own string, meaning arguments do + /// not have to be escaped. + /// + /// > [!WARNING] This does not run command in a shell. All arguments to the command + /// > must be in separate values in the list, e.g. `["echo", "hello"]` + /// > and not `["echo hello"]`. + /// > + /// > Additionally, shell scripts must be run by your shell, + /// > e.g. `["sh", "script.sh"]` instead of `["script.sh"]` unless the script + /// > has a shebang. + /// + /// > [!INFO] You can use `["sh", "-c", ]` to execute your command with + /// > the system shell. + /// + /// This function is equivalent to @@Quickshell.Io.Process.startDetached(). + Q_INVOKABLE static void execDetached(QList command); + /// Launch a process detached from Quickshell. + /// + /// The context parameter is a JS object with the following fields: + /// - `command`: A list containing the command and all its arguments. See @@Quickshell.Io.Process.command. + /// - `environment`: Changes to make to the process environment. See @@Quickshell.Io.Process.environment. + /// - `clearEnvironment`: Removes all variables from the environment if true. + /// - `workingDirectory`: The working directory the command should run in. + /// + /// > [!WARNING] This does not run command in a shell. All arguments to the command + /// > must be in separate values in the list, e.g. `["echo", "hello"]` + /// > and not `["echo hello"]`. + /// > + /// > Additionally, shell scripts must be run by your shell, + /// > e.g. `["sh", "script.sh"]` instead of `["script.sh"]` unless the script + /// > has a shebang. + /// + /// > [!INFO] You can use `["sh", "-c", ]` to execute your command with + /// > the system shell. + /// + /// This function is equivalent to @@Quickshell.Io.Process.startDetached(). + Q_INVOKABLE static void execDetached(const ProcessContext& context); + /// Returns a string usable for a @@QtQuick.Image.source for a given system icon. /// /// > [!INFO] By default, icons are loaded from the theme selected by the qt platform theme, diff --git a/src/core/test/CMakeLists.txt b/src/core/test/CMakeLists.txt index c4005c8b..4e66c627 100644 --- a/src/core/test/CMakeLists.txt +++ b/src/core/test/CMakeLists.txt @@ -1,6 +1,6 @@ function (qs_test name) add_executable(${name} ${ARGN}) - target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-core quickshell-window quickshell-ui) + target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-core quickshell-window quickshell-ui quickshell-io) add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $) endfunction() diff --git a/src/io/CMakeLists.txt b/src/io/CMakeLists.txt index 6bb8e704..8b5c20a1 100644 --- a/src/io/CMakeLists.txt +++ b/src/io/CMakeLists.txt @@ -1,5 +1,6 @@ qt_add_library(quickshell-io STATIC datastream.cpp + processcore.cpp process.cpp fileview.cpp jsonadapter.cpp diff --git a/src/io/process.cpp b/src/io/process.cpp index 143fdec8..43637d42 100644 --- a/src/io/process.cpp +++ b/src/io/process.cpp @@ -3,9 +3,9 @@ #include #include +#include #include #include -#include #include #include #include @@ -13,10 +13,10 @@ #include #include -#include "../core/common.hpp" #include "../core/generation.hpp" #include "../core/qmlglobal.hpp" #include "datastream.hpp" +#include "processcore.hpp" Process::Process(QObject* parent): QObject(parent) { QObject::connect( @@ -79,9 +79,10 @@ void Process::onGlobalWorkingDirectoryChanged() { } } -QMap Process::environment() const { return this->mEnvironment; } +QHash Process::environment() const { return this->mEnvironment; } -void Process::setEnvironment(QMap environment) { +void Process::setEnvironment(QHash environment) { + qDebug() << "setEnv" << environment; if (environment == this->mEnvironment) return; this->mEnvironment = std::move(environment); emit this->environmentChanged(); @@ -224,24 +225,7 @@ void Process::setupEnvironment(QProcess* process) { process->setWorkingDirectory(this->mWorkingDirectory); } - const auto& sysenv = qs::Common::INITIAL_ENVIRONMENT; - auto env = this->mClearEnvironment ? QProcessEnvironment() : sysenv; - - for (auto& name: this->mEnvironment.keys()) { - auto value = this->mEnvironment.value(name); - if (!value.isValid()) continue; - - if (this->mClearEnvironment) { - if (value.isNull()) { - if (sysenv.contains(name)) env.insert(name, sysenv.value(name)); - } else env.insert(name, value.toString()); - } else { - if (value.isNull()) env.remove(name); - else env.insert(name, value.toString()); - } - } - - process->setProcessEnvironment(env); + qs::core::process::setupProcessEnvironment(process, this->mClearEnvironment, this->mEnvironment); } void Process::onStarted() { diff --git a/src/io/process.hpp b/src/io/process.hpp index e93004f1..2d7e1fd4 100644 --- a/src/io/process.hpp +++ b/src/io/process.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -98,7 +99,7 @@ class Process: public QObject { /// If the process is already running changing this property will affect the next /// started process. If the property has been changed after starting a process it will /// return the new value, not the one for the currently running process. - Q_PROPERTY(QMap environment READ environment WRITE setEnvironment NOTIFY environmentChanged); + Q_PROPERTY(QHash environment READ environment WRITE setEnvironment NOTIFY environmentChanged); /// If the process's environment should be cleared prior to applying @@environment. /// Defaults to false. /// @@ -140,10 +141,12 @@ public: /// Writes to the process's stdin. Does nothing if @@running is false. Q_INVOKABLE void write(const QString& data); - /// Launches an instance of the process detached from quickshell. + /// Launches an instance of the process detached from Quickshell. /// /// The subprocess will not be tracked, @@running will be false, /// and the subprocess will not be killed by Quickshell. + /// + /// This function is equivalent to @@Quickshell.Quickshell.execDetached(). Q_INVOKABLE void startDetached(); [[nodiscard]] bool isRunning() const; @@ -157,8 +160,8 @@ public: [[nodiscard]] QString workingDirectory() const; void setWorkingDirectory(const QString& workingDirectory); - [[nodiscard]] QMap environment() const; - void setEnvironment(QMap environment); + [[nodiscard]] QHash environment() const; + void setEnvironment(QHash environment); [[nodiscard]] bool environmentCleared() const; void setEnvironmentCleared(bool cleared); @@ -203,7 +206,7 @@ private: QProcess* process = nullptr; QList mCommand; QString mWorkingDirectory; - QMap mEnvironment; + QHash mEnvironment; DataStreamParser* mStdoutParser = nullptr; DataStreamParser* mStderrParser = nullptr; QByteArray stdoutBuffer; diff --git a/src/io/processcore.cpp b/src/io/processcore.cpp new file mode 100644 index 00000000..572045e4 --- /dev/null +++ b/src/io/processcore.cpp @@ -0,0 +1,37 @@ +#include "processcore.hpp" + +#include +#include +#include +#include + +#include "../core/common.hpp" + +namespace qs::core::process { + +void setupProcessEnvironment( + QProcess* process, + bool clear, + const QHash& envChanges +) { + const auto& sysenv = qs::Common::INITIAL_ENVIRONMENT; + auto env = clear ? QProcessEnvironment() : sysenv; + + for (auto& name: envChanges.keys()) { + auto value = envChanges.value(name); + if (!value.isValid()) continue; + + if (clear) { + if (value.isNull()) { + if (sysenv.contains(name)) env.insert(name, sysenv.value(name)); + } else env.insert(name, value.toString()); + } else { + if (value.isNull()) env.remove(name); + else env.insert(name, value.toString()); + } + } + + process->setProcessEnvironment(env); +} + +} // namespace qs::core::process diff --git a/src/io/processcore.hpp b/src/io/processcore.hpp new file mode 100644 index 00000000..fe8bda7b --- /dev/null +++ b/src/io/processcore.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#include +#include + +namespace qs::core::process { + +void setupProcessEnvironment( + QProcess* process, + bool clear, + const QHash& envChanges +); + +} diff --git a/src/io/test/CMakeLists.txt b/src/io/test/CMakeLists.txt index 4ab51739..88755660 100644 --- a/src/io/test/CMakeLists.txt +++ b/src/io/test/CMakeLists.txt @@ -1,6 +1,6 @@ function (qs_test name) add_executable(${name} ${ARGN}) - target_link_libraries(${name} PRIVATE Qt::Quick Qt::Network Qt::Test) + target_link_libraries(${name} PRIVATE Qt::Quick Qt::Network Qt::Test quickshell-io) add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $) endfunction() diff --git a/src/window/test/CMakeLists.txt b/src/window/test/CMakeLists.txt index 70615119..8a0d65cf 100644 --- a/src/window/test/CMakeLists.txt +++ b/src/window/test/CMakeLists.txt @@ -1,6 +1,6 @@ function (qs_test name) add_executable(${name} ${ARGN}) - target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-window quickshell-core quickshell-ui) + target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-window quickshell-core quickshell-ui quickshell-io) add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $) endfunction()