From 52c29a3a577d793c84f1a740c3363bf7e2c83556 Mon Sep 17 00:00:00 2001 From: taj-ny <79316397+taj-ny@users.noreply.github.com> Date: Fri, 22 May 2026 16:14:14 +0200 Subject: [PATCH 01/12] scripting: init --- CMakeLists.txt | 1 + src/CMakeLists.txt | 5 +++ src/libinputactions/config/ConfigLoader.cpp | 7 ++++ src/libinputactions/config/ConfigLoader.h | 6 +++ src/libinputactions/config/parsers/core.cpp | 8 ++++ .../config/parsers/scripting.cpp | 38 +++++++++++++++++ .../config/parsers/scripting.h | 30 +++++++++++++ .../scripting/ScriptAction.cpp | 34 +++++++++++++++ src/libinputactions/scripting/ScriptAction.h | 42 +++++++++++++++++++ .../scripting/ScriptCondition.cpp | 34 +++++++++++++++ .../scripting/ScriptCondition.h | 42 +++++++++++++++++++ .../scripting/ScriptingManager.cpp | 34 +++++++++++++++ .../scripting/ScriptingManager.h | 40 ++++++++++++++++++ 13 files changed, 321 insertions(+) create mode 100644 src/libinputactions/config/parsers/scripting.cpp create mode 100644 src/libinputactions/config/parsers/scripting.h create mode 100644 src/libinputactions/scripting/ScriptAction.cpp create mode 100644 src/libinputactions/scripting/ScriptAction.h create mode 100644 src/libinputactions/scripting/ScriptCondition.cpp create mode 100644 src/libinputactions/scripting/ScriptCondition.h create mode 100644 src/libinputactions/scripting/ScriptingManager.cpp create mode 100644 src/libinputactions/scripting/ScriptingManager.h diff --git a/CMakeLists.txt b/CMakeLists.txt index dbf7ac9..598780d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,7 @@ include(KDECMakeSettings) find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Core DBus + Qml ) find_package(PkgConfig REQUIRED) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6228500..ac39c2d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,6 +18,7 @@ set(libinputactions_SRCS libinputactions/config/parsers/flags.cpp libinputactions/config/parsers/NodeParser.h libinputactions/config/parsers/qt.cpp + libinputactions/config/parsers/scripting.cpp libinputactions/config/parsers/separated-string.h libinputactions/config/parsers/std.cpp libinputactions/config/parsers/triggers.cpp @@ -80,6 +81,9 @@ set(libinputactions_SRCS libinputactions/interfaces/TextInput.cpp libinputactions/interfaces/Window.h libinputactions/interfaces/WindowProvider.cpp + libinputactions/scripting/ScriptAction.cpp + libinputactions/scripting/ScriptCondition.cpp + libinputactions/scripting/ScriptingManager.cpp libinputactions/triggers/core/DirectionalMotionTriggerCore.cpp libinputactions/triggers/core/MotionTriggerCore.cpp libinputactions/triggers/core/StrokeTriggerCore.cpp @@ -132,6 +136,7 @@ target_link_libraries(libinputactions PUBLIC libevdev-cpp Qt6::Core Qt6::DBus + Qt6::Qml ${LIBEVDEV_LIBRARIES} ) target_compile_definitions(libinputactions PUBLIC TEST_VIRTUAL=$,virtual,>) diff --git a/src/libinputactions/config/ConfigLoader.cpp b/src/libinputactions/config/ConfigLoader.cpp index c984130..d254e34 100644 --- a/src/libinputactions/config/ConfigLoader.cpp +++ b/src/libinputactions/config/ConfigLoader.cpp @@ -33,6 +33,7 @@ #include #include #include +#include namespace InputActions { @@ -52,6 +53,8 @@ struct Config std::vector deviceRules; std::set emergencyCombination = {KEY_BACKSPACE, KEY_SPACE, KEY_ENTER}; + + std::unique_ptr scriptingManager = std::make_unique(); }; void ConfigLoader::loadEmpty() @@ -97,6 +100,8 @@ Config ConfigLoader::createConfig(const QString &raw) } Config config; + m_scriptingManager = config.scriptingManager.get(); + loadMember(config.autoReload, root->at("autoreload")); loadMember(config.allowExternalVariableAccess, root->at("external_variable_access")); if (const auto *notificationsNode = root->mapAt("notifications")) { @@ -133,6 +138,7 @@ void ConfigLoader::activateConfig(Config config, bool initialize) g_inputBackend->reset(); // Okay because required keys are not cleared g_actionExecutor->clearQueue(); g_actionExecutor->waitForDone(); + g_scriptingManager.reset(); g_globalConfig->setAllowExternalVariableAccess(config.allowExternalVariableAccess); g_globalConfig->setAutoReload(config.autoReload); @@ -150,6 +156,7 @@ void ConfigLoader::activateConfig(Config config, bool initialize) g_inputBackend->setDeviceRules(config.deviceRules); g_inputBackend->setEmergencyCombination(config.emergencyCombination); + g_scriptingManager = std::move(config.scriptingManager); if (initialize) { g_inputBackend->initialize(); } diff --git a/src/libinputactions/config/ConfigLoader.h b/src/libinputactions/config/ConfigLoader.h index b802531..c1212e3 100644 --- a/src/libinputactions/config/ConfigLoader.h +++ b/src/libinputactions/config/ConfigLoader.h @@ -18,6 +18,7 @@ #pragma once +#include #include #include #include @@ -26,6 +27,7 @@ namespace InputActions { struct Config; +class ScriptingManager; struct ConfigLoadSettings { @@ -42,6 +44,8 @@ struct ConfigLoadSettings class ConfigLoader { public: + ScriptingManager &scriptingManager() const { return *m_scriptingManager; } + /** * @return Whether the operation was successful. Errors may be obtained from ConfigIssueManager. */ @@ -55,6 +59,8 @@ class ConfigLoader private: Config createConfig(const QString &raw); void activateConfig(Config config, bool initialize); + + ScriptingManager *m_scriptingManager; }; inline std::shared_ptr g_configLoader; diff --git a/src/libinputactions/config/parsers/core.cpp b/src/libinputactions/config/parsers/core.cpp index 04d7ddf..d802032 100644 --- a/src/libinputactions/config/parsers/core.cpp +++ b/src/libinputactions/config/parsers/core.cpp @@ -20,6 +20,7 @@ #include "containers.h" #include "flags.h" #include "globals.h" +#include "scripting.h" #include "separated-string.h" #include "triggers.h" #include "utils.h" @@ -51,6 +52,8 @@ #include #include #include +#include +#include #include #include #include @@ -158,6 +161,8 @@ void NodeParser>::parse(const Node *node, std::unique_pt result = std::make_unique(shortcut.first, shortcut.second); } else if (const auto *replaceTextNode = node->at("replace_text")) { result = std::make_unique(replaceTextNode->as>(true)); + } else if (const auto *scriptActionNode = node->at("script")) { + result = std::make_unique(parseScriptFunction(scriptActionNode)); } else if (const auto *sleepActionNode = node->at("sleep")) { result = std::make_unique(sleepActionNode->as()); } else if (const auto *oneNode = node->at("one")) { @@ -225,6 +230,9 @@ std::shared_ptr parseCondition(const Node *node, const VariableRegist if (const auto *canReplaceTextNode = node->at("can_replace_text")) { return std::make_shared(canReplaceTextNode->as>(true)); } + if (const auto *scriptNode = node->at("script")) { + return std::make_shared(parseScriptFunction(scriptNode)); + } if (isLegacy(node)) { g_configIssueManager->addIssue(DeprecatedFeatureConfigIssue(node, DeprecatedFeature::LegacyConditions)); diff --git a/src/libinputactions/config/parsers/scripting.cpp b/src/libinputactions/config/parsers/scripting.cpp new file mode 100644 index 0000000..86aba0f --- /dev/null +++ b/src/libinputactions/config/parsers/scripting.cpp @@ -0,0 +1,38 @@ +/* + Input Actions - Input handler that executes user-defined actions + Copyright (C) 2024-2026 Marcin Woźniak + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "scripting.h" +#include "config/ConfigIssueManager.h" +#include +#include +#include + +namespace InputActions +{ + +QJSValue parseScriptFunction(const Node *node) +{ + auto value = g_configLoader->scriptingManager().evaluate(node->as()); + if (!value.isCallable()) { + throw InvalidValueConfigException(node, "Expression is not a function."); + } + + return value; +} + +} diff --git a/src/libinputactions/config/parsers/scripting.h b/src/libinputactions/config/parsers/scripting.h new file mode 100644 index 0000000..948c9f3 --- /dev/null +++ b/src/libinputactions/config/parsers/scripting.h @@ -0,0 +1,30 @@ +/* + Input Actions - Input handler that executes user-defined actions + Copyright (C) 2024-2026 Marcin Woźniak + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#pragma once + +#include + +namespace InputActions +{ + +class Node; + +QJSValue parseScriptFunction(const Node *node); + +} diff --git a/src/libinputactions/scripting/ScriptAction.cpp b/src/libinputactions/scripting/ScriptAction.cpp new file mode 100644 index 0000000..c66dc00 --- /dev/null +++ b/src/libinputactions/scripting/ScriptAction.cpp @@ -0,0 +1,34 @@ +/* + Input Actions - Input handler that executes user-defined actions + Copyright (C) 2024-2026 Marcin Woźniak + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "ScriptAction.h" + +namespace InputActions +{ + +ScriptAction::ScriptAction(QJSValue function) + : m_function(std::move(function)) +{ +} + +void ScriptAction::doExecute(const ActionExecutionArguments &args) +{ + m_function.call(); +} + +} \ No newline at end of file diff --git a/src/libinputactions/scripting/ScriptAction.h b/src/libinputactions/scripting/ScriptAction.h new file mode 100644 index 0000000..c577f55 --- /dev/null +++ b/src/libinputactions/scripting/ScriptAction.h @@ -0,0 +1,42 @@ +/* + Input Actions - Input handler that executes user-defined actions + Copyright (C) 2024-2026 Marcin Woźniak + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#pragma once + +#include +#include + +namespace InputActions +{ + +class ScriptAction : public Action +{ +public: + /** + * @param function Must be a function, called with no arguments when the action is executed. The return value is ignored. + */ + ScriptAction(QJSValue function); + +protected: + void doExecute(const ActionExecutionArguments &args) override; + +private: + QJSValue m_function; +}; + +} \ No newline at end of file diff --git a/src/libinputactions/scripting/ScriptCondition.cpp b/src/libinputactions/scripting/ScriptCondition.cpp new file mode 100644 index 0000000..cf4d8d3 --- /dev/null +++ b/src/libinputactions/scripting/ScriptCondition.cpp @@ -0,0 +1,34 @@ +/* + Input Actions - Input handler that executes user-defined actions + Copyright (C) 2024-2026 Marcin Woźniak + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "ScriptCondition.h" + +namespace InputActions +{ + +ScriptCondition::ScriptCondition(QJSValue function) + : m_function(std::move(function)) +{ +} + +bool ScriptCondition::doEvaluate(const ConditionEvaluationArguments &arguments) +{ + return m_function.call().toBool(); +} + +} \ No newline at end of file diff --git a/src/libinputactions/scripting/ScriptCondition.h b/src/libinputactions/scripting/ScriptCondition.h new file mode 100644 index 0000000..0e98765 --- /dev/null +++ b/src/libinputactions/scripting/ScriptCondition.h @@ -0,0 +1,42 @@ +/* + Input Actions - Input handler that executes user-defined actions + Copyright (C) 2024-2026 Marcin Woźniak + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#pragma once + +#include +#include + +namespace InputActions +{ + +class ScriptCondition : public Condition +{ +public: + /** + * @param function Must be a function, called with no arguments when the action is executed. The return value is ignored. + */ + ScriptCondition(QJSValue function); + +protected: + bool doEvaluate(const ConditionEvaluationArguments &arguments) override; + +private: + QJSValue m_function; +}; + +} \ No newline at end of file diff --git a/src/libinputactions/scripting/ScriptingManager.cpp b/src/libinputactions/scripting/ScriptingManager.cpp new file mode 100644 index 0000000..d8acb56 --- /dev/null +++ b/src/libinputactions/scripting/ScriptingManager.cpp @@ -0,0 +1,34 @@ +/* + Input Actions - Input handler that executes user-defined actions + Copyright (C) 2024-2026 Marcin Woźniak + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "ScriptingManager.h" + +namespace InputActions +{ + +ScriptingManager::ScriptingManager() +{ + m_engine.installExtensions(QJSEngine::ConsoleExtension); +} + +QJSValue ScriptingManager::evaluate(const QString &script) +{ + return m_engine.evaluate(script); +} + +} \ No newline at end of file diff --git a/src/libinputactions/scripting/ScriptingManager.h b/src/libinputactions/scripting/ScriptingManager.h new file mode 100644 index 0000000..812227a --- /dev/null +++ b/src/libinputactions/scripting/ScriptingManager.h @@ -0,0 +1,40 @@ +/* + Input Actions - Input handler that executes user-defined actions + Copyright (C) 2024-2026 Marcin Woźniak + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#pragma once + +#include +#include + +namespace InputActions +{ + +class ScriptingManager +{ +public: + ScriptingManager(); + + QJSValue evaluate(const QString &script); + +private: + QJSEngine m_engine; +}; + +inline std::unique_ptr g_scriptingManager; + +} \ No newline at end of file From c225a1af19844d7622c340e1d569ccae927bceda Mon Sep 17 00:00:00 2001 From: taj-ny <79316397+taj-ny@users.noreply.github.com> Date: Fri, 22 May 2026 16:18:21 +0200 Subject: [PATCH 02/12] ci: add missing dependency --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 903bfaa..dfbaee5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: run: pacman -Syu --noconfirm - name: Install dependencies - run: pacman -S --needed --noconfirm base-devel git extra-cmake-modules qt6-tools yaml-cpp gtest + run: pacman -S --needed --noconfirm base-devel git extra-cmake-modules qt6-declarative qt6-tools yaml-cpp gtest - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 From e1f743fe22d948d921bf4d072f5b3d98f3484432 Mon Sep 17 00:00:00 2001 From: taj-ny <79316397+taj-ny@users.noreply.github.com> Date: Sat, 23 May 2026 16:57:58 +0200 Subject: [PATCH 03/12] fix tests --- src/libinputactions/InputActionsMain.cpp | 3 +++ tests/libinputactions/Test.cpp | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/libinputactions/InputActionsMain.cpp b/src/libinputactions/InputActionsMain.cpp index e598ad7..a09a47e 100644 --- a/src/libinputactions/InputActionsMain.cpp +++ b/src/libinputactions/InputActionsMain.cpp @@ -22,6 +22,7 @@ #include "interfaces/implementations/DBusPlasmaGlobalShortcutInvoker.h" #include "interfaces/implementations/FileConfigProvider.h" #include "interfaces/implementations/ProcessRunnerImpl.h" +#include "scripting/ScriptingManager.h" #include "variables/VariableRegistry.h" #include #include @@ -53,6 +54,7 @@ InputActionsMain::~InputActionsMain() g_globalConfig.reset(); g_configProvider.reset(); g_inputBackend.reset(); + g_scriptingManager.reset(); g_strokeRecorder.reset(); g_variableRegistry.reset(); } @@ -98,6 +100,7 @@ void InputActionsMain::setMissingImplementations() setMissingImplementation(g_configLoader); setMissingImplementation(g_globalConfig); setMissingImplementation(g_inputBackend); + setMissingImplementation(g_scriptingManager); setMissingImplementation(g_strokeRecorder); setMissingImplementation(g_variableRegistry); } diff --git a/tests/libinputactions/Test.cpp b/tests/libinputactions/Test.cpp index 8757498..1f85cf9 100644 --- a/tests/libinputactions/Test.cpp +++ b/tests/libinputactions/Test.cpp @@ -8,6 +8,9 @@ namespace InputActions void Test::initMain() { + int argc = 0; + QCoreApplication app(argc, nullptr); + auto *inputActions = new InputActionsMain; g_configProvider = std::make_shared(); // don't watch config inputActions->setMissingImplementations(); From 62b0c3c660ee9115820c5da816013f987d925780 Mon Sep 17 00:00:00 2001 From: taj-ny <79316397+taj-ny@users.noreply.github.com> Date: Fri, 22 May 2026 20:37:06 +0200 Subject: [PATCH 04/12] scripting: add watchdog --- .../scripting/ScriptingManager.cpp | 40 +++++++++++++++++++ .../scripting/ScriptingManager.h | 15 ++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/libinputactions/scripting/ScriptingManager.cpp b/src/libinputactions/scripting/ScriptingManager.cpp index d8acb56..c309785 100644 --- a/src/libinputactions/scripting/ScriptingManager.cpp +++ b/src/libinputactions/scripting/ScriptingManager.cpp @@ -17,13 +17,48 @@ */ #include "ScriptingManager.h" +#include +#include +#include namespace InputActions { +static const std::chrono::milliseconds WATCHDOG_TIMER_TIMEOUT{2000}; +static const std::chrono::milliseconds WATCHDOG_TIMER_RESET_INTERVAL{1000}; + ScriptingManager::ScriptingManager() + : m_watchdogTimerThread(new QThread) + , m_watchdogTimer(new QTimer) { m_engine.installExtensions(QJSEngine::ConsoleExtension); + + m_watchdogTimer->setInterval(WATCHDOG_TIMER_TIMEOUT); + m_watchdogTimer->moveToThread(m_watchdogTimerThread); + connect(m_watchdogTimer, &QTimer::timeout, [this]() { + m_engine.setInterrupted(true); + QThreadHelpers::runOnThread(QThreadHelpers::mainThread(), []() { + g_notificationManager + ->sendNotification("Infinite loop detected", + "A script has likely entered an infinite loop and frozen the main thread. InputActions has been suspended."); + g_inputActions->suspend(); + }); + }); + m_watchdogTimerThread->start(); + + connect(&m_watchdogRestartTimer, &QTimer::timeout, this, &ScriptingManager::onWatchdogValueRestartTimerTick); + m_watchdogRestartTimer.setInterval(WATCHDOG_TIMER_RESET_INTERVAL); + m_watchdogRestartTimer.start(); +} + +ScriptingManager::~ScriptingManager() +{ + QMetaObject::invokeMethod(m_watchdogTimer, "stop", Qt::BlockingQueuedConnection); + m_watchdogTimerThread->quit(); + m_watchdogTimerThread->wait(); + + m_watchdogTimer->deleteLater(); + m_watchdogTimerThread->deleteLater(); } QJSValue ScriptingManager::evaluate(const QString &script) @@ -31,4 +66,9 @@ QJSValue ScriptingManager::evaluate(const QString &script) return m_engine.evaluate(script); } +void ScriptingManager::onWatchdogValueRestartTimerTick() +{ + QMetaObject::invokeMethod(m_watchdogTimer, "start", Qt::QueuedConnection); +} + } \ No newline at end of file diff --git a/src/libinputactions/scripting/ScriptingManager.h b/src/libinputactions/scripting/ScriptingManager.h index 812227a..b9aa2c2 100644 --- a/src/libinputactions/scripting/ScriptingManager.h +++ b/src/libinputactions/scripting/ScriptingManager.h @@ -19,20 +19,33 @@ #pragma once #include +#include #include namespace InputActions { -class ScriptingManager +class ScriptingManager : public QObject { + Q_OBJECT + public: ScriptingManager(); + ~ScriptingManager() override; QJSValue evaluate(const QString &script); + void initialize(); + +private slots: + void onWatchdogValueRestartTimerTick(); + private: QJSEngine m_engine; + + QThread *m_watchdogTimerThread; + QTimer *m_watchdogTimer; + QTimer m_watchdogRestartTimer; }; inline std::unique_ptr g_scriptingManager; From 406e205259b3fc1cfdc80416ef45663650d91f3e Mon Sep 17 00:00:00 2001 From: taj-ny <79316397+taj-ny@users.noreply.github.com> Date: Fri, 22 May 2026 20:56:15 +0200 Subject: [PATCH 05/12] scripting: add JSFunction wrapper --- src/CMakeLists.txt | 2 +- src/libinputactions/config/parsers/core.cpp | 19 +++++++++++--- .../JSFunction.cpp} | 25 +++++++++++-------- .../scripting.h => scripting/JSFunction.h} | 20 ++++++++++++--- .../scripting/ScriptAction.cpp | 2 +- src/libinputactions/scripting/ScriptAction.h | 8 +++--- .../scripting/ScriptCondition.cpp | 2 +- .../scripting/ScriptCondition.h | 8 +++--- 8 files changed, 59 insertions(+), 27 deletions(-) rename src/libinputactions/{config/parsers/scripting.cpp => scripting/JSFunction.cpp} (65%) rename src/libinputactions/{config/parsers/scripting.h => scripting/JSFunction.h} (71%) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ac39c2d..3c61fb6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,7 +18,6 @@ set(libinputactions_SRCS libinputactions/config/parsers/flags.cpp libinputactions/config/parsers/NodeParser.h libinputactions/config/parsers/qt.cpp - libinputactions/config/parsers/scripting.cpp libinputactions/config/parsers/separated-string.h libinputactions/config/parsers/std.cpp libinputactions/config/parsers/triggers.cpp @@ -81,6 +80,7 @@ set(libinputactions_SRCS libinputactions/interfaces/TextInput.cpp libinputactions/interfaces/Window.h libinputactions/interfaces/WindowProvider.cpp + libinputactions/scripting/JSFunction.cpp libinputactions/scripting/ScriptAction.cpp libinputactions/scripting/ScriptCondition.cpp libinputactions/scripting/ScriptingManager.cpp diff --git a/src/libinputactions/config/parsers/core.cpp b/src/libinputactions/config/parsers/core.cpp index d802032..8184e27 100644 --- a/src/libinputactions/config/parsers/core.cpp +++ b/src/libinputactions/config/parsers/core.cpp @@ -20,7 +20,6 @@ #include "containers.h" #include "flags.h" #include "globals.h" -#include "scripting.h" #include "separated-string.h" #include "triggers.h" #include "utils.h" @@ -40,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -54,6 +54,7 @@ #include #include #include +#include #include #include #include @@ -162,7 +163,7 @@ void NodeParser>::parse(const Node *node, std::unique_pt } else if (const auto *replaceTextNode = node->at("replace_text")) { result = std::make_unique(replaceTextNode->as>(true)); } else if (const auto *scriptActionNode = node->at("script")) { - result = std::make_unique(parseScriptFunction(scriptActionNode)); + result = std::make_unique(scriptActionNode->as()); } else if (const auto *sleepActionNode = node->at("sleep")) { result = std::make_unique(sleepActionNode->as()); } else if (const auto *oneNode = node->at("one")) { @@ -231,7 +232,7 @@ std::shared_ptr parseCondition(const Node *node, const VariableRegist return std::make_shared(canReplaceTextNode->as>(true)); } if (const auto *scriptNode = node->at("script")) { - return std::make_shared(parseScriptFunction(scriptNode)); + return std::make_shared(scriptNode->as()); } if (isLegacy(node)) { @@ -545,6 +546,18 @@ void NodeParser>::parse(const Node *node, std::vect } } +template<> +void NodeParser::parse(const Node *node, JSFunction &result) +{ + auto function = JSFunction::create(g_configLoader->scriptingManager().evaluate(node->as())); + if (function) { + result = function.value(); + return; + } + + throw InvalidValueConfigException(node, "Expression is not a function."); +} + template<> void NodeParser::parse(const Node *node, KeyboardKey &result) { diff --git a/src/libinputactions/config/parsers/scripting.cpp b/src/libinputactions/scripting/JSFunction.cpp similarity index 65% rename from src/libinputactions/config/parsers/scripting.cpp rename to src/libinputactions/scripting/JSFunction.cpp index 86aba0f..703d037 100644 --- a/src/libinputactions/config/parsers/scripting.cpp +++ b/src/libinputactions/scripting/JSFunction.cpp @@ -16,23 +16,28 @@ along with this program. If not, see . */ -#include "scripting.h" -#include "config/ConfigIssueManager.h" -#include -#include -#include +#include "JSFunction.h" +#include namespace InputActions { -QJSValue parseScriptFunction(const Node *node) +JSFunction::JSFunction(QJSValue function) + : m_function(std::move(function)) +{ +} + +std::optional JSFunction::create(QJSValue value) { - auto value = g_configLoader->scriptingManager().evaluate(node->as()); if (!value.isCallable()) { - throw InvalidValueConfigException(node, "Expression is not a function."); + return {}; } - - return value; + return JSFunction(value); } +QJSValue JSFunction::call(const QJSValueList &args) const +{ + return m_function.call(args); } + +} \ No newline at end of file diff --git a/src/libinputactions/config/parsers/scripting.h b/src/libinputactions/scripting/JSFunction.h similarity index 71% rename from src/libinputactions/config/parsers/scripting.h rename to src/libinputactions/scripting/JSFunction.h index 948c9f3..dc9cc3f 100644 --- a/src/libinputactions/config/parsers/scripting.h +++ b/src/libinputactions/scripting/JSFunction.h @@ -23,8 +23,22 @@ namespace InputActions { -class Node; +class JSFunction +{ +public: + JSFunction() = default; + + /** + * @return Empty if the specified value is not a function. + */ + static std::optional create(QJSValue function); + + QJSValue call(const QJSValueList &args = {}) const; + +private: + JSFunction(QJSValue value); -QJSValue parseScriptFunction(const Node *node); + QJSValue m_function; +}; -} +} \ No newline at end of file diff --git a/src/libinputactions/scripting/ScriptAction.cpp b/src/libinputactions/scripting/ScriptAction.cpp index c66dc00..3e72423 100644 --- a/src/libinputactions/scripting/ScriptAction.cpp +++ b/src/libinputactions/scripting/ScriptAction.cpp @@ -21,7 +21,7 @@ namespace InputActions { -ScriptAction::ScriptAction(QJSValue function) +ScriptAction::ScriptAction(JSFunction function) : m_function(std::move(function)) { } diff --git a/src/libinputactions/scripting/ScriptAction.h b/src/libinputactions/scripting/ScriptAction.h index c577f55..8d4d2b8 100644 --- a/src/libinputactions/scripting/ScriptAction.h +++ b/src/libinputactions/scripting/ScriptAction.h @@ -18,7 +18,7 @@ #pragma once -#include +#include "JSFunction.h" #include namespace InputActions @@ -28,15 +28,15 @@ class ScriptAction : public Action { public: /** - * @param function Must be a function, called with no arguments when the action is executed. The return value is ignored. + * @param function Called with no arguments when the action is executed. The return value is ignored. */ - ScriptAction(QJSValue function); + ScriptAction(JSFunction function); protected: void doExecute(const ActionExecutionArguments &args) override; private: - QJSValue m_function; + JSFunction m_function; }; } \ No newline at end of file diff --git a/src/libinputactions/scripting/ScriptCondition.cpp b/src/libinputactions/scripting/ScriptCondition.cpp index cf4d8d3..3642304 100644 --- a/src/libinputactions/scripting/ScriptCondition.cpp +++ b/src/libinputactions/scripting/ScriptCondition.cpp @@ -21,7 +21,7 @@ namespace InputActions { -ScriptCondition::ScriptCondition(QJSValue function) +ScriptCondition::ScriptCondition(JSFunction function) : m_function(std::move(function)) { } diff --git a/src/libinputactions/scripting/ScriptCondition.h b/src/libinputactions/scripting/ScriptCondition.h index 0e98765..398124f 100644 --- a/src/libinputactions/scripting/ScriptCondition.h +++ b/src/libinputactions/scripting/ScriptCondition.h @@ -18,7 +18,7 @@ #pragma once -#include +#include "JSFunction.h" #include namespace InputActions @@ -28,15 +28,15 @@ class ScriptCondition : public Condition { public: /** - * @param function Must be a function, called with no arguments when the action is executed. The return value is ignored. + * @param function Called with no arguments when the condition is evaluated. The condition is satisfied when the return value is true. */ - ScriptCondition(QJSValue function); + ScriptCondition(JSFunction function); protected: bool doEvaluate(const ConditionEvaluationArguments &arguments) override; private: - QJSValue m_function; + JSFunction m_function; }; } \ No newline at end of file From b152fb7b30a9fa741c40ae084c647c0742148304 Mon Sep 17 00:00:00 2001 From: taj-ny <79316397+taj-ny@users.noreply.github.com> Date: Fri, 22 May 2026 21:10:08 +0200 Subject: [PATCH 06/12] scripting: improve documentation --- src/libinputactions/config/ConfigLoader.h | 3 +++ src/libinputactions/scripting/ScriptingManager.h | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/src/libinputactions/config/ConfigLoader.h b/src/libinputactions/config/ConfigLoader.h index c1212e3..50f7833 100644 --- a/src/libinputactions/config/ConfigLoader.h +++ b/src/libinputactions/config/ConfigLoader.h @@ -44,6 +44,9 @@ struct ConfigLoadSettings class ConfigLoader { public: + /** + * Scripting manager for the configuration that is currently being created. + */ ScriptingManager &scriptingManager() const { return *m_scriptingManager; } /** diff --git a/src/libinputactions/scripting/ScriptingManager.h b/src/libinputactions/scripting/ScriptingManager.h index b9aa2c2..d0e46b5 100644 --- a/src/libinputactions/scripting/ScriptingManager.h +++ b/src/libinputactions/scripting/ScriptingManager.h @@ -48,6 +48,11 @@ private slots: QTimer m_watchdogRestartTimer; }; +/** + * Do not use this instance during the creation of a configuration, use ConfigLoader::scriptingManager instead. + * + * A new instance is created on each config activation. + */ inline std::unique_ptr g_scriptingManager; } \ No newline at end of file From be860b50423252051a79b6517c89e6d23e69915d Mon Sep 17 00:00:00 2001 From: taj-ny <79316397+taj-ny@users.noreply.github.com> Date: Sat, 23 May 2026 17:22:49 +0200 Subject: [PATCH 07/12] scripting: expose global VariableRegistry --- src/CMakeLists.txt | 1 + .../scripting/GlobalObject.cpp | 30 +++++++++++++++ src/libinputactions/scripting/GlobalObject.h | 37 +++++++++++++++++++ .../scripting/ScriptingManager.cpp | 8 ++++ .../scripting/ScriptingManager.h | 4 ++ src/libinputactions/variables/Variable.cpp | 2 + src/libinputactions/variables/Variable.h | 7 +++- .../variables/VariableRegistry.h | 16 +++++--- 8 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 src/libinputactions/scripting/GlobalObject.cpp create mode 100644 src/libinputactions/scripting/GlobalObject.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3c61fb6..424ebd4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -80,6 +80,7 @@ set(libinputactions_SRCS libinputactions/interfaces/TextInput.cpp libinputactions/interfaces/Window.h libinputactions/interfaces/WindowProvider.cpp + libinputactions/scripting/GlobalObject.cpp libinputactions/scripting/JSFunction.cpp libinputactions/scripting/ScriptAction.cpp libinputactions/scripting/ScriptCondition.cpp diff --git a/src/libinputactions/scripting/GlobalObject.cpp b/src/libinputactions/scripting/GlobalObject.cpp new file mode 100644 index 0000000..30ac34e --- /dev/null +++ b/src/libinputactions/scripting/GlobalObject.cpp @@ -0,0 +1,30 @@ +/* + Input Actions - Input handler that executes user-defined actions + Copyright (C) 2024-2026 Marcin Woźniak + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "GlobalObject.h" +#include + +namespace InputActions +{ + +VariableRegistry *GlobalObject::variableRegistry() const +{ + return g_variableRegistry.get(); +} + +} \ No newline at end of file diff --git a/src/libinputactions/scripting/GlobalObject.h b/src/libinputactions/scripting/GlobalObject.h new file mode 100644 index 0000000..015e4d5 --- /dev/null +++ b/src/libinputactions/scripting/GlobalObject.h @@ -0,0 +1,37 @@ +/* + Input Actions - Input handler that executes user-defined actions + Copyright (C) 2024-2026 Marcin Woźniak + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#pragma once + +#include + +namespace InputActions +{ + +class VariableRegistry; + +class GlobalObject : public QObject +{ + Q_OBJECT + Q_PROPERTY(VariableRegistry *variableRegistry READ variableRegistry) + +public: + VariableRegistry *variableRegistry() const; +}; + +} \ No newline at end of file diff --git a/src/libinputactions/scripting/ScriptingManager.cpp b/src/libinputactions/scripting/ScriptingManager.cpp index c309785..54e965d 100644 --- a/src/libinputactions/scripting/ScriptingManager.cpp +++ b/src/libinputactions/scripting/ScriptingManager.cpp @@ -49,6 +49,8 @@ ScriptingManager::ScriptingManager() connect(&m_watchdogRestartTimer, &QTimer::timeout, this, &ScriptingManager::onWatchdogValueRestartTimerTick); m_watchdogRestartTimer.setInterval(WATCHDOG_TIMER_RESET_INTERVAL); m_watchdogRestartTimer.start(); + + registerApi(); } ScriptingManager::~ScriptingManager() @@ -66,6 +68,12 @@ QJSValue ScriptingManager::evaluate(const QString &script) return m_engine.evaluate(script); } +void ScriptingManager::registerApi() +{ + m_globalObject.emplace(); + m_engine.globalObject().setProperty("ia", m_engine.newQObject(&m_globalObject.value())); +} + void ScriptingManager::onWatchdogValueRestartTimerTick() { QMetaObject::invokeMethod(m_watchdogTimer, "start", Qt::QueuedConnection); diff --git a/src/libinputactions/scripting/ScriptingManager.h b/src/libinputactions/scripting/ScriptingManager.h index d0e46b5..cebbff3 100644 --- a/src/libinputactions/scripting/ScriptingManager.h +++ b/src/libinputactions/scripting/ScriptingManager.h @@ -18,6 +18,7 @@ #pragma once +#include "GlobalObject.h" #include #include #include @@ -41,7 +42,10 @@ private slots: void onWatchdogValueRestartTimerTick(); private: + void registerApi(); + QJSEngine m_engine; + std::optional m_globalObject; QThread *m_watchdogTimerThread; QTimer *m_watchdogTimer; diff --git a/src/libinputactions/variables/Variable.cpp b/src/libinputactions/variables/Variable.cpp index 3524b3d..07d260e 100644 --- a/src/libinputactions/variables/Variable.cpp +++ b/src/libinputactions/variables/Variable.cpp @@ -17,6 +17,7 @@ */ #include "Variable.h" +#include namespace InputActions { @@ -25,6 +26,7 @@ Variable::Variable(QMetaType type) : m_type(std::move(type)) , m_operations(VariableOperationsBase::create(this)) { + QJSEngine::setObjectOwnership(this, QJSEngine::CppOwnership); } const VariableOperationsBase *Variable::operations() const diff --git a/src/libinputactions/variables/Variable.h b/src/libinputactions/variables/Variable.h index e72a6c1..21e308f 100644 --- a/src/libinputactions/variables/Variable.h +++ b/src/libinputactions/variables/Variable.h @@ -19,14 +19,17 @@ #pragma once #include "VariableOperations.h" +#include #include -#include namespace InputActions { -class Variable +class Variable : public QObject { + Q_OBJECT + Q_PROPERTY(QVariant value READ value) + public: Variable(QMetaType type); virtual ~Variable() = default; diff --git a/src/libinputactions/variables/VariableRegistry.h b/src/libinputactions/variables/VariableRegistry.h index 90f4641..9aa6cf9 100644 --- a/src/libinputactions/variables/VariableRegistry.h +++ b/src/libinputactions/variables/VariableRegistry.h @@ -21,6 +21,7 @@ #include "ComputedVariable.h" #include "StoredVariable.h" #include "VariableWrapper.h" +#include #include #include #include @@ -56,11 +57,18 @@ struct BuiltinVariables inline static const VariableInfo LastTriggerTimestamp{QStringLiteral("last_trigger_timestamp")}; }; -class VariableRegistry +class VariableRegistry : public QObject { + Q_OBJECT + public: VariableRegistry(); - ~VariableRegistry(); + ~VariableRegistry() override; + + /** + * @return The variable with the specified name or nullptr if not found. + */ + Q_INVOKABLE Variable *variable(const QString &name) const; template std::optional> variable(const VariableInfo &info) const @@ -87,10 +95,6 @@ class VariableRegistry } bool contains(const QString &name) const; - /** - * @return The variable with the specified name or nullptr if not found. - */ - Variable *variable(const QString &name) const; Variable *registerVariable(const QString &name, std::unique_ptr variable, bool hidden = false); template From 61dc374923c9d6bc9d8046d0885dc6a65d637642 Mon Sep 17 00:00:00 2001 From: taj-ny <79316397+taj-ny@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:15:46 +0200 Subject: [PATCH 08/12] scripting: rename ScriptingManager to ScriptingEngine --- src/CMakeLists.txt | 2 +- src/libinputactions/InputActionsMain.cpp | 6 +++--- src/libinputactions/config/ConfigLoader.cpp | 10 +++++----- src/libinputactions/config/ConfigLoader.h | 8 ++++---- src/libinputactions/config/parsers/core.cpp | 4 ++-- .../{ScriptingManager.cpp => ScriptingEngine.cpp} | 14 +++++++------- .../{ScriptingManager.h => ScriptingEngine.h} | 10 +++++----- 7 files changed, 27 insertions(+), 27 deletions(-) rename src/libinputactions/scripting/{ScriptingManager.cpp => ScriptingEngine.cpp} (89%) rename src/libinputactions/scripting/{ScriptingManager.h => ScriptingEngine.h} (87%) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 424ebd4..0d34660 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -84,7 +84,7 @@ set(libinputactions_SRCS libinputactions/scripting/JSFunction.cpp libinputactions/scripting/ScriptAction.cpp libinputactions/scripting/ScriptCondition.cpp - libinputactions/scripting/ScriptingManager.cpp + libinputactions/scripting/ScriptingEngine.cpp libinputactions/triggers/core/DirectionalMotionTriggerCore.cpp libinputactions/triggers/core/MotionTriggerCore.cpp libinputactions/triggers/core/StrokeTriggerCore.cpp diff --git a/src/libinputactions/InputActionsMain.cpp b/src/libinputactions/InputActionsMain.cpp index a09a47e..2fd7b90 100644 --- a/src/libinputactions/InputActionsMain.cpp +++ b/src/libinputactions/InputActionsMain.cpp @@ -22,7 +22,7 @@ #include "interfaces/implementations/DBusPlasmaGlobalShortcutInvoker.h" #include "interfaces/implementations/FileConfigProvider.h" #include "interfaces/implementations/ProcessRunnerImpl.h" -#include "scripting/ScriptingManager.h" +#include "scripting/ScriptingEngine.h" #include "variables/VariableRegistry.h" #include #include @@ -54,7 +54,7 @@ InputActionsMain::~InputActionsMain() g_globalConfig.reset(); g_configProvider.reset(); g_inputBackend.reset(); - g_scriptingManager.reset(); + g_scriptingEngine.reset(); g_strokeRecorder.reset(); g_variableRegistry.reset(); } @@ -100,7 +100,7 @@ void InputActionsMain::setMissingImplementations() setMissingImplementation(g_configLoader); setMissingImplementation(g_globalConfig); setMissingImplementation(g_inputBackend); - setMissingImplementation(g_scriptingManager); + setMissingImplementation(g_scriptingEngine); setMissingImplementation(g_strokeRecorder); setMissingImplementation(g_variableRegistry); } diff --git a/src/libinputactions/config/ConfigLoader.cpp b/src/libinputactions/config/ConfigLoader.cpp index d254e34..a10c303 100644 --- a/src/libinputactions/config/ConfigLoader.cpp +++ b/src/libinputactions/config/ConfigLoader.cpp @@ -33,7 +33,7 @@ #include #include #include -#include +#include namespace InputActions { @@ -54,7 +54,7 @@ struct Config std::vector deviceRules; std::set emergencyCombination = {KEY_BACKSPACE, KEY_SPACE, KEY_ENTER}; - std::unique_ptr scriptingManager = std::make_unique(); + std::unique_ptr scriptingEngine = std::make_unique(); }; void ConfigLoader::loadEmpty() @@ -100,7 +100,7 @@ Config ConfigLoader::createConfig(const QString &raw) } Config config; - m_scriptingManager = config.scriptingManager.get(); + m_scriptingEngine = config.scriptingEngine.get(); loadMember(config.autoReload, root->at("autoreload")); loadMember(config.allowExternalVariableAccess, root->at("external_variable_access")); @@ -138,7 +138,7 @@ void ConfigLoader::activateConfig(Config config, bool initialize) g_inputBackend->reset(); // Okay because required keys are not cleared g_actionExecutor->clearQueue(); g_actionExecutor->waitForDone(); - g_scriptingManager.reset(); + g_scriptingEngine.reset(); g_globalConfig->setAllowExternalVariableAccess(config.allowExternalVariableAccess); g_globalConfig->setAutoReload(config.autoReload); @@ -156,7 +156,7 @@ void ConfigLoader::activateConfig(Config config, bool initialize) g_inputBackend->setDeviceRules(config.deviceRules); g_inputBackend->setEmergencyCombination(config.emergencyCombination); - g_scriptingManager = std::move(config.scriptingManager); + g_scriptingEngine = std::move(config.scriptingEngine); if (initialize) { g_inputBackend->initialize(); } diff --git a/src/libinputactions/config/ConfigLoader.h b/src/libinputactions/config/ConfigLoader.h index 50f7833..fd123ae 100644 --- a/src/libinputactions/config/ConfigLoader.h +++ b/src/libinputactions/config/ConfigLoader.h @@ -27,7 +27,7 @@ namespace InputActions { struct Config; -class ScriptingManager; +class ScriptingEngine; struct ConfigLoadSettings { @@ -45,9 +45,9 @@ class ConfigLoader { public: /** - * Scripting manager for the configuration that is currently being created. + * Scripting engine for the configuration that is currently being loaded. */ - ScriptingManager &scriptingManager() const { return *m_scriptingManager; } + ScriptingEngine &futureScriptingEngine() const { return *m_scriptingEngine; } /** * @return Whether the operation was successful. Errors may be obtained from ConfigIssueManager. @@ -63,7 +63,7 @@ class ConfigLoader Config createConfig(const QString &raw); void activateConfig(Config config, bool initialize); - ScriptingManager *m_scriptingManager; + ScriptingEngine *m_scriptingEngine; }; inline std::shared_ptr g_configLoader; diff --git a/src/libinputactions/config/parsers/core.cpp b/src/libinputactions/config/parsers/core.cpp index 8184e27..5121a6c 100644 --- a/src/libinputactions/config/parsers/core.cpp +++ b/src/libinputactions/config/parsers/core.cpp @@ -54,7 +54,7 @@ #include #include #include -#include +#include #include #include #include @@ -549,7 +549,7 @@ void NodeParser>::parse(const Node *node, std::vect template<> void NodeParser::parse(const Node *node, JSFunction &result) { - auto function = JSFunction::create(g_configLoader->scriptingManager().evaluate(node->as())); + auto function = JSFunction::create(g_configLoader->futureScriptingEngine().evaluate(node->as())); if (function) { result = function.value(); return; diff --git a/src/libinputactions/scripting/ScriptingManager.cpp b/src/libinputactions/scripting/ScriptingEngine.cpp similarity index 89% rename from src/libinputactions/scripting/ScriptingManager.cpp rename to src/libinputactions/scripting/ScriptingEngine.cpp index 54e965d..fd88111 100644 --- a/src/libinputactions/scripting/ScriptingManager.cpp +++ b/src/libinputactions/scripting/ScriptingEngine.cpp @@ -16,7 +16,7 @@ along with this program. If not, see . */ -#include "ScriptingManager.h" +#include "ScriptingEngine.h" #include #include #include @@ -27,7 +27,7 @@ namespace InputActions static const std::chrono::milliseconds WATCHDOG_TIMER_TIMEOUT{2000}; static const std::chrono::milliseconds WATCHDOG_TIMER_RESET_INTERVAL{1000}; -ScriptingManager::ScriptingManager() +ScriptingEngine::ScriptingEngine() : m_watchdogTimerThread(new QThread) , m_watchdogTimer(new QTimer) { @@ -46,14 +46,14 @@ ScriptingManager::ScriptingManager() }); m_watchdogTimerThread->start(); - connect(&m_watchdogRestartTimer, &QTimer::timeout, this, &ScriptingManager::onWatchdogValueRestartTimerTick); + connect(&m_watchdogRestartTimer, &QTimer::timeout, this, &ScriptingEngine::onWatchdogValueRestartTimerTick); m_watchdogRestartTimer.setInterval(WATCHDOG_TIMER_RESET_INTERVAL); m_watchdogRestartTimer.start(); registerApi(); } -ScriptingManager::~ScriptingManager() +ScriptingEngine::~ScriptingEngine() { QMetaObject::invokeMethod(m_watchdogTimer, "stop", Qt::BlockingQueuedConnection); m_watchdogTimerThread->quit(); @@ -63,18 +63,18 @@ ScriptingManager::~ScriptingManager() m_watchdogTimerThread->deleteLater(); } -QJSValue ScriptingManager::evaluate(const QString &script) +QJSValue ScriptingEngine::evaluate(const QString &script) { return m_engine.evaluate(script); } -void ScriptingManager::registerApi() +void ScriptingEngine::registerApi() { m_globalObject.emplace(); m_engine.globalObject().setProperty("ia", m_engine.newQObject(&m_globalObject.value())); } -void ScriptingManager::onWatchdogValueRestartTimerTick() +void ScriptingEngine::onWatchdogValueRestartTimerTick() { QMetaObject::invokeMethod(m_watchdogTimer, "start", Qt::QueuedConnection); } diff --git a/src/libinputactions/scripting/ScriptingManager.h b/src/libinputactions/scripting/ScriptingEngine.h similarity index 87% rename from src/libinputactions/scripting/ScriptingManager.h rename to src/libinputactions/scripting/ScriptingEngine.h index cebbff3..e145389 100644 --- a/src/libinputactions/scripting/ScriptingManager.h +++ b/src/libinputactions/scripting/ScriptingEngine.h @@ -26,13 +26,13 @@ namespace InputActions { -class ScriptingManager : public QObject +class ScriptingEngine : public QObject { Q_OBJECT public: - ScriptingManager(); - ~ScriptingManager() override; + ScriptingEngine(); + ~ScriptingEngine() override; QJSValue evaluate(const QString &script); @@ -53,10 +53,10 @@ private slots: }; /** - * Do not use this instance during the creation of a configuration, use ConfigLoader::scriptingManager instead. + * Do not use this instance during the creation of a configuration, use ConfigLoader::futureScriptingEngine instead. * * A new instance is created on each config activation. */ -inline std::unique_ptr g_scriptingManager; +inline std::unique_ptr g_scriptingEngine; } \ No newline at end of file From 58e5fd7f44df93c5cbc5d36a228e838105a9ba5a Mon Sep 17 00:00:00 2001 From: taj-ny <79316397+taj-ny@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:28:05 +0200 Subject: [PATCH 09/12] scripting: lazily initialize the engine --- .../scripting/ScriptingEngine.cpp | 56 ++++++++++++------- .../scripting/ScriptingEngine.h | 20 ++++--- 2 files changed, 48 insertions(+), 28 deletions(-) diff --git a/src/libinputactions/scripting/ScriptingEngine.cpp b/src/libinputactions/scripting/ScriptingEngine.cpp index fd88111..4140c71 100644 --- a/src/libinputactions/scripting/ScriptingEngine.cpp +++ b/src/libinputactions/scripting/ScriptingEngine.cpp @@ -27,16 +27,40 @@ namespace InputActions static const std::chrono::milliseconds WATCHDOG_TIMER_TIMEOUT{2000}; static const std::chrono::milliseconds WATCHDOG_TIMER_RESET_INTERVAL{1000}; -ScriptingEngine::ScriptingEngine() - : m_watchdogTimerThread(new QThread) - , m_watchdogTimer(new QTimer) +ScriptingEngine::~ScriptingEngine() { - m_engine.installExtensions(QJSEngine::ConsoleExtension); + if (!m_engine) { + return; + } + + QMetaObject::invokeMethod(m_watchdogTimer, "stop", Qt::BlockingQueuedConnection); + m_watchdogTimerThread->quit(); + m_watchdogTimerThread->wait(); + + m_watchdogTimer->deleteLater(); + m_watchdogTimerThread->deleteLater(); +} + +void ScriptingEngine::initialize() +{ + m_engine.emplace(); + m_engine->installExtensions(QJSEngine::ConsoleExtension); + + initializeWatchdog(); + + m_globalObject.emplace(); + m_engine->globalObject().setProperty("ia", m_engine->newQObject(&m_globalObject.value())); +} + +void ScriptingEngine::initializeWatchdog() +{ + m_watchdogTimerThread = new QThread; + m_watchdogTimer = new QTimer; m_watchdogTimer->setInterval(WATCHDOG_TIMER_TIMEOUT); m_watchdogTimer->moveToThread(m_watchdogTimerThread); connect(m_watchdogTimer, &QTimer::timeout, [this]() { - m_engine.setInterrupted(true); + m_engine->setInterrupted(true); QThreadHelpers::runOnThread(QThreadHelpers::mainThread(), []() { g_notificationManager ->sendNotification("Infinite loop detected", @@ -49,29 +73,19 @@ ScriptingEngine::ScriptingEngine() connect(&m_watchdogRestartTimer, &QTimer::timeout, this, &ScriptingEngine::onWatchdogValueRestartTimerTick); m_watchdogRestartTimer.setInterval(WATCHDOG_TIMER_RESET_INTERVAL); m_watchdogRestartTimer.start(); - - registerApi(); -} - -ScriptingEngine::~ScriptingEngine() -{ - QMetaObject::invokeMethod(m_watchdogTimer, "stop", Qt::BlockingQueuedConnection); - m_watchdogTimerThread->quit(); - m_watchdogTimerThread->wait(); - - m_watchdogTimer->deleteLater(); - m_watchdogTimerThread->deleteLater(); } QJSValue ScriptingEngine::evaluate(const QString &script) { - return m_engine.evaluate(script); + return engine().evaluate(script); } -void ScriptingEngine::registerApi() +QJSEngine &ScriptingEngine::engine() { - m_globalObject.emplace(); - m_engine.globalObject().setProperty("ia", m_engine.newQObject(&m_globalObject.value())); + if (!m_engine) { + initialize(); + } + return m_engine.value(); } void ScriptingEngine::onWatchdogValueRestartTimerTick() diff --git a/src/libinputactions/scripting/ScriptingEngine.h b/src/libinputactions/scripting/ScriptingEngine.h index e145389..6009b61 100644 --- a/src/libinputactions/scripting/ScriptingEngine.h +++ b/src/libinputactions/scripting/ScriptingEngine.h @@ -26,29 +26,35 @@ namespace InputActions { +/** + * Lazily initializated. + */ class ScriptingEngine : public QObject { Q_OBJECT public: - ScriptingEngine(); + ScriptingEngine() = default; ~ScriptingEngine() override; QJSValue evaluate(const QString &script); - void initialize(); - private slots: void onWatchdogValueRestartTimerTick(); private: - void registerApi(); + /** + * Returns an instance of the engine. Initializes the engine if it has not been initialized yet. + */ + QJSEngine &engine(); + void initialize(); + void initializeWatchdog(); - QJSEngine m_engine; + std::optional m_engine; std::optional m_globalObject; - QThread *m_watchdogTimerThread; - QTimer *m_watchdogTimer; + QThread *m_watchdogTimerThread{}; + QTimer *m_watchdogTimer{}; QTimer m_watchdogRestartTimer; }; From e3708628c49d1f8caec3dd1b23982b1d9d386054 Mon Sep 17 00:00:00 2001 From: taj-ny <79316397+taj-ny@users.noreply.github.com> Date: Mon, 1 Jun 2026 18:17:22 +0200 Subject: [PATCH 10/12] scripting: use modules --- src/CMakeLists.txt | 3 +- .../scripting/FunctionWrapper.cpp | 35 +++++++++ .../scripting/FunctionWrapper.h | 76 +++++++++++++++++++ .../scripting/ScriptingEngine.cpp | 23 +++++- .../scripting/ScriptingEngine.h | 16 +++- .../CoreModule.cpp} | 4 +- .../{GlobalObject.h => modules/CoreModule.h} | 2 +- 7 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 src/libinputactions/scripting/FunctionWrapper.cpp create mode 100644 src/libinputactions/scripting/FunctionWrapper.h rename src/libinputactions/scripting/{GlobalObject.cpp => modules/CoreModule.cpp} (91%) rename src/libinputactions/scripting/{GlobalObject.h => modules/CoreModule.h} (96%) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0d34660..9149c6e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -80,7 +80,8 @@ set(libinputactions_SRCS libinputactions/interfaces/TextInput.cpp libinputactions/interfaces/Window.h libinputactions/interfaces/WindowProvider.cpp - libinputactions/scripting/GlobalObject.cpp + libinputactions/scripting/modules/CoreModule.cpp + libinputactions/scripting/FunctionWrapper.cpp libinputactions/scripting/JSFunction.cpp libinputactions/scripting/ScriptAction.cpp libinputactions/scripting/ScriptCondition.cpp diff --git a/src/libinputactions/scripting/FunctionWrapper.cpp b/src/libinputactions/scripting/FunctionWrapper.cpp new file mode 100644 index 0000000..3d2a407 --- /dev/null +++ b/src/libinputactions/scripting/FunctionWrapper.cpp @@ -0,0 +1,35 @@ +/* + Input Actions - Input handler that executes user-defined actions + Copyright (C) 2024-2026 Marcin Woźniak + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "FunctionWrapper.h" + +namespace InputActions +{ + +FunctionWrapper::FunctionWrapper(QJSEngine *engine, std::function function) + : m_engine(engine) + , m_function(std::move(function)) +{ +} + +QJSValue FunctionWrapper::call(QJSValueList args) +{ + return m_function(args); +} + +} \ No newline at end of file diff --git a/src/libinputactions/scripting/FunctionWrapper.h b/src/libinputactions/scripting/FunctionWrapper.h new file mode 100644 index 0000000..2c9f10b --- /dev/null +++ b/src/libinputactions/scripting/FunctionWrapper.h @@ -0,0 +1,76 @@ +/* + Input Actions - Input handler that executes user-defined actions + Copyright (C) 2024-2026 Marcin Woźniak + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#pragma once + +#include +#include + +namespace InputActions +{ + +/** + * Wraps a function in a QObject to make it invokable from JavaScript. + */ +class FunctionWrapper : public QObject +{ + Q_OBJECT + +public: + template + static FunctionWrapper *create(QJSEngine *engine, TFunction &&func) + { + std::function function = std::forward(func); + const auto wrapper = [engine, function = std::move(function)](QJSValueList args) -> QJSValue { + if (args.size() != sizeof...(TArgs)) { + engine->throwError(QString("Invalid argument count.")); + return {}; + } + + return call(engine, function, args, std::index_sequence_for()); + }; + + return new FunctionWrapper(engine, wrapper); + } + + Q_INVOKABLE QJSValue call(QJSValueList args); + +private: + FunctionWrapper(QJSEngine *engine, std::function function); + + template + static QJSValue call(QJSEngine *engine, const std::function &function, const QJSValueList &args, std::index_sequence) + { + if constexpr (std::is_void_v) { + function(engine->fromScriptValue(args[I])...); + return {}; + } + + const auto result = function(engine->fromScriptValue(args[I])...); + if constexpr (std::is_same_v) { + return result; + } + + return engine->toScriptValue(std::move(result)); + } + + QJSEngine *m_engine; + std::function m_function; +}; + +} \ No newline at end of file diff --git a/src/libinputactions/scripting/ScriptingEngine.cpp b/src/libinputactions/scripting/ScriptingEngine.cpp index 4140c71..49eb59a 100644 --- a/src/libinputactions/scripting/ScriptingEngine.cpp +++ b/src/libinputactions/scripting/ScriptingEngine.cpp @@ -17,6 +17,7 @@ */ #include "ScriptingEngine.h" +#include "modules/CoreModule.h" #include #include #include @@ -48,8 +49,16 @@ void ScriptingEngine::initialize() initializeWatchdog(); - m_globalObject.emplace(); - m_engine->globalObject().setProperty("ia", m_engine->newQObject(&m_globalObject.value())); + registerBuiltinModule("inputactions/core", m_engine->newQObject(new CoreModule)); + + auto globalObject = m_engine->globalObject(); + globalObject.setProperty("require", newFunction([this](QString module) { + if (m_builtinModules.contains(module)) { + return m_builtinModules[module]; + } + + return m_engine->importModule(module); + })); } void ScriptingEngine::initializeWatchdog() @@ -75,12 +84,18 @@ void ScriptingEngine::initializeWatchdog() m_watchdogRestartTimer.start(); } +void ScriptingEngine::registerBuiltinModule(const QString &name, QJSValue value) +{ + ensureEngine().registerModule(name, value); + m_builtinModules[name] = std::move(value); +} + QJSValue ScriptingEngine::evaluate(const QString &script) { - return engine().evaluate(script); + return ensureEngine().evaluate(script); } -QJSEngine &ScriptingEngine::engine() +QJSEngine &ScriptingEngine::ensureEngine() { if (!m_engine) { initialize(); diff --git a/src/libinputactions/scripting/ScriptingEngine.h b/src/libinputactions/scripting/ScriptingEngine.h index 6009b61..07fa5f3 100644 --- a/src/libinputactions/scripting/ScriptingEngine.h +++ b/src/libinputactions/scripting/ScriptingEngine.h @@ -18,7 +18,7 @@ #pragma once -#include "GlobalObject.h" +#include "FunctionWrapper.h" #include #include #include @@ -39,6 +39,14 @@ class ScriptingEngine : public QObject QJSValue evaluate(const QString &script); + template + QJSValue newFunction(TFunction &&function) + { + auto &engine = ensureEngine(); + auto *wrapper = FunctionWrapper::create(&engine, std::forward(function)); + return evaluate(QString("obj => (...args) => obj.call(args);")).call({engine.newQObject(wrapper)}); + } + private slots: void onWatchdogValueRestartTimerTick(); @@ -46,12 +54,14 @@ private slots: /** * Returns an instance of the engine. Initializes the engine if it has not been initialized yet. */ - QJSEngine &engine(); + QJSEngine &ensureEngine(); void initialize(); void initializeWatchdog(); + void registerBuiltinModule(const QString &name, QJSValue value); + std::optional m_engine; - std::optional m_globalObject; + std::map m_builtinModules; QThread *m_watchdogTimerThread{}; QTimer *m_watchdogTimer{}; diff --git a/src/libinputactions/scripting/GlobalObject.cpp b/src/libinputactions/scripting/modules/CoreModule.cpp similarity index 91% rename from src/libinputactions/scripting/GlobalObject.cpp rename to src/libinputactions/scripting/modules/CoreModule.cpp index 30ac34e..35ac5aa 100644 --- a/src/libinputactions/scripting/GlobalObject.cpp +++ b/src/libinputactions/scripting/modules/CoreModule.cpp @@ -16,13 +16,13 @@ along with this program. If not, see . */ -#include "GlobalObject.h" +#include "CoreModule.h" #include namespace InputActions { -VariableRegistry *GlobalObject::variableRegistry() const +VariableRegistry *CoreModule::variableRegistry() const { return g_variableRegistry.get(); } diff --git a/src/libinputactions/scripting/GlobalObject.h b/src/libinputactions/scripting/modules/CoreModule.h similarity index 96% rename from src/libinputactions/scripting/GlobalObject.h rename to src/libinputactions/scripting/modules/CoreModule.h index 015e4d5..14e3b17 100644 --- a/src/libinputactions/scripting/GlobalObject.h +++ b/src/libinputactions/scripting/modules/CoreModule.h @@ -25,7 +25,7 @@ namespace InputActions class VariableRegistry; -class GlobalObject : public QObject +class CoreModule : public QObject { Q_OBJECT Q_PROPERTY(VariableRegistry *variableRegistry READ variableRegistry) From 3fe195d663ec04a95da9d74e92cd374081d9088b Mon Sep 17 00:00:00 2001 From: taj-ny <79316397+taj-ny@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:31:32 +0200 Subject: [PATCH 11/12] scripting: add error handling --- src/CMakeLists.txt | 2 +- src/libinputactions/config/ConfigIssue.cpp | 25 ++++++++++++ src/libinputactions/config/ConfigIssue.h | 27 +++++++++++++ src/libinputactions/config/ConfigLoader.cpp | 14 +++++++ src/libinputactions/config/parsers/core.cpp | 17 ++------ .../parsers/scripting.cpp} | 27 ++++++------- .../parsers/scripting.h} | 18 +-------- .../scripting/ScriptAction.cpp | 11 +++++- src/libinputactions/scripting/ScriptAction.h | 10 +++-- .../scripting/ScriptCondition.cpp | 14 ++++++- .../scripting/ScriptCondition.h | 10 +++-- .../scripting/ScriptingEngine.cpp | 39 ++++++++++++++++++- .../scripting/ScriptingEngine.h | 11 ++++++ 13 files changed, 168 insertions(+), 57 deletions(-) rename src/libinputactions/{scripting/JSFunction.cpp => config/parsers/scripting.cpp} (60%) rename src/libinputactions/{scripting/JSFunction.h => config/parsers/scripting.h} (71%) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9149c6e..3067c2a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,6 +18,7 @@ set(libinputactions_SRCS libinputactions/config/parsers/flags.cpp libinputactions/config/parsers/NodeParser.h libinputactions/config/parsers/qt.cpp + libinputactions/config/parsers/scripting.cpp libinputactions/config/parsers/separated-string.h libinputactions/config/parsers/std.cpp libinputactions/config/parsers/triggers.cpp @@ -82,7 +83,6 @@ set(libinputactions_SRCS libinputactions/interfaces/WindowProvider.cpp libinputactions/scripting/modules/CoreModule.cpp libinputactions/scripting/FunctionWrapper.cpp - libinputactions/scripting/JSFunction.cpp libinputactions/scripting/ScriptAction.cpp libinputactions/scripting/ScriptCondition.cpp libinputactions/scripting/ScriptingEngine.cpp diff --git a/src/libinputactions/config/ConfigIssue.cpp b/src/libinputactions/config/ConfigIssue.cpp index 6c51c67..6fdb901 100644 --- a/src/libinputactions/config/ConfigIssue.cpp +++ b/src/libinputactions/config/ConfigIssue.cpp @@ -18,9 +18,12 @@ #include "ConfigIssue.h" #include "ConfigIssueManager.h" +#include "ConfigLoader.h" #include "Node.h" #include #include +#include +#include namespace InputActions { @@ -183,6 +186,17 @@ QString UnusedPropertyConfigIssue::message() const return QString("Property '%1' does not exist or has no effect in this context.").arg(m_property); } +UncaughtScriptErrorConfigIssue::UncaughtScriptErrorConfigIssue(const Node *node, QJSValue error) + : ConfigIssue(node) + , m_message(g_configLoader->futureScriptingEngine().errorToString(error)) +{ +} + +QString UncaughtScriptErrorConfigIssue::message() const +{ + return QString("Uncaught script error\n\n%1").arg(m_message); +} + MissingRequiredPropertyConfigException::MissingRequiredPropertyConfigException(const Node *node, QString property) : ConfigException(node) , m_property(std::move(property)) @@ -194,6 +208,17 @@ QString MissingRequiredPropertyConfigException::message() const return QString("Required property '%1' was not specified.").arg(m_property); } +UncaughtScriptErrorConfigException::UncaughtScriptErrorConfigException(const Node *node, QJSValue error) + : ConfigException(node) + , m_message(g_configLoader->futureScriptingEngine().errorToString(error)) +{ +} + +QString UncaughtScriptErrorConfigException::message() const +{ + return QString("Uncaught script error\n\n%1").arg(m_message); +} + YamlCppConfigException::YamlCppConfigException(TextPosition position, QString message) : ConfigException(position) , m_message(std::move(message)) diff --git a/src/libinputactions/config/ConfigIssue.h b/src/libinputactions/config/ConfigIssue.h index a4e591f..95482a5 100644 --- a/src/libinputactions/config/ConfigIssue.h +++ b/src/libinputactions/config/ConfigIssue.h @@ -19,6 +19,7 @@ #pragma once #include "TextPosition.h" +#include #include #include @@ -121,6 +122,19 @@ class UnusedPropertyConfigIssue QString m_property; }; +class UncaughtScriptErrorConfigIssue + : public ConfigIssue + , public virtual Copyable +{ +public: + UncaughtScriptErrorConfigIssue(const Node *node, QJSValue error); + + QString message() const override; + +private: + QString m_message; +}; + class DuplicateSetItemConfigException : public ConfigException , public virtual Copyable @@ -212,6 +226,19 @@ class MissingRequiredPropertyConfigException QString m_property; }; +class UncaughtScriptErrorConfigException + : public ConfigException + , public virtual Copyable +{ +public: + UncaughtScriptErrorConfigException(const Node *node, QJSValue error); + + QString message() const override; + +private: + QString m_message; +}; + class YamlCppConfigException : public ConfigException , public virtual Copyable diff --git a/src/libinputactions/config/ConfigLoader.cpp b/src/libinputactions/config/ConfigLoader.cpp index a10c303..19f0995 100644 --- a/src/libinputactions/config/ConfigLoader.cpp +++ b/src/libinputactions/config/ConfigLoader.cpp @@ -20,6 +20,7 @@ #include "ConfigIssueManager.h" #include "GlobalConfig.h" #include "Node.h" +#include "config/ConfigIssue.h" #include "interfaces/ConfigProvider.h" #include "parsers/containers.h" #include "parsers/core.h" @@ -102,6 +103,19 @@ Config ConfigLoader::createConfig(const QString &raw) Config config; m_scriptingEngine = config.scriptingEngine.get(); + if (const auto *scriptingNode = root->mapAt("scripting")) { + if (const auto *scriptsNode = scriptingNode->at("scripts")) { + for (const auto *scriptNode : scriptsNode->sequenceItems()) { + const auto *sourceNode = scriptNode->at("source", true); + const auto source = sourceNode->as(); + const auto result = m_scriptingEngine->evaluate(sourceNode->as()); + if (result.isError()) { + throw UncaughtScriptErrorConfigException(sourceNode, result); + } + } + } + } + loadMember(config.autoReload, root->at("autoreload")); loadMember(config.allowExternalVariableAccess, root->at("external_variable_access")); if (const auto *notificationsNode = root->mapAt("notifications")) { diff --git a/src/libinputactions/config/parsers/core.cpp b/src/libinputactions/config/parsers/core.cpp index 5121a6c..598d00f 100644 --- a/src/libinputactions/config/parsers/core.cpp +++ b/src/libinputactions/config/parsers/core.cpp @@ -20,6 +20,7 @@ #include "containers.h" #include "flags.h" #include "globals.h" +#include "scripting.h" #include "separated-string.h" #include "triggers.h" #include "utils.h" @@ -163,7 +164,7 @@ void NodeParser>::parse(const Node *node, std::unique_pt } else if (const auto *replaceTextNode = node->at("replace_text")) { result = std::make_unique(replaceTextNode->as>(true)); } else if (const auto *scriptActionNode = node->at("script")) { - result = std::make_unique(scriptActionNode->as()); + result = std::make_unique(parseFunction(scriptActionNode), scriptActionNode->shared_from_this()); } else if (const auto *sleepActionNode = node->at("sleep")) { result = std::make_unique(sleepActionNode->as()); } else if (const auto *oneNode = node->at("one")) { @@ -232,7 +233,7 @@ std::shared_ptr parseCondition(const Node *node, const VariableRegist return std::make_shared(canReplaceTextNode->as>(true)); } if (const auto *scriptNode = node->at("script")) { - return std::make_shared(scriptNode->as()); + return std::make_shared(parseFunction(scriptNode), scriptNode->shared_from_this()); } if (isLegacy(node)) { @@ -546,18 +547,6 @@ void NodeParser>::parse(const Node *node, std::vect } } -template<> -void NodeParser::parse(const Node *node, JSFunction &result) -{ - auto function = JSFunction::create(g_configLoader->futureScriptingEngine().evaluate(node->as())); - if (function) { - result = function.value(); - return; - } - - throw InvalidValueConfigException(node, "Expression is not a function."); -} - template<> void NodeParser::parse(const Node *node, KeyboardKey &result) { diff --git a/src/libinputactions/scripting/JSFunction.cpp b/src/libinputactions/config/parsers/scripting.cpp similarity index 60% rename from src/libinputactions/scripting/JSFunction.cpp rename to src/libinputactions/config/parsers/scripting.cpp index 703d037..97e1994 100644 --- a/src/libinputactions/scripting/JSFunction.cpp +++ b/src/libinputactions/config/parsers/scripting.cpp @@ -16,28 +16,25 @@ along with this program. If not, see . */ -#include "JSFunction.h" -#include +#include "scripting.h" +#include +#include +#include namespace InputActions { -JSFunction::JSFunction(QJSValue function) - : m_function(std::move(function)) +QJSValue parseFunction(const Node *node) { -} - -std::optional JSFunction::create(QJSValue value) -{ - if (!value.isCallable()) { - return {}; + auto result = g_configLoader->futureScriptingEngine().evaluate(node->as()); + if (result.isError()) { + throw UncaughtScriptErrorConfigException(node, result); + } + if (!result.isCallable()) { + throw InvalidValueConfigException(node, "Expression is not a function."); } - return JSFunction(value); -} -QJSValue JSFunction::call(const QJSValueList &args) const -{ - return m_function.call(args); + return result; } } \ No newline at end of file diff --git a/src/libinputactions/scripting/JSFunction.h b/src/libinputactions/config/parsers/scripting.h similarity index 71% rename from src/libinputactions/scripting/JSFunction.h rename to src/libinputactions/config/parsers/scripting.h index dc9cc3f..87f31f0 100644 --- a/src/libinputactions/scripting/JSFunction.h +++ b/src/libinputactions/config/parsers/scripting.h @@ -23,22 +23,8 @@ namespace InputActions { -class JSFunction -{ -public: - JSFunction() = default; - - /** - * @return Empty if the specified value is not a function. - */ - static std::optional create(QJSValue function); - - QJSValue call(const QJSValueList &args = {}) const; - -private: - JSFunction(QJSValue value); +class Node; - QJSValue m_function; -}; +QJSValue parseFunction(const Node *node); } \ No newline at end of file diff --git a/src/libinputactions/scripting/ScriptAction.cpp b/src/libinputactions/scripting/ScriptAction.cpp index 3e72423..91f5e4b 100644 --- a/src/libinputactions/scripting/ScriptAction.cpp +++ b/src/libinputactions/scripting/ScriptAction.cpp @@ -17,18 +17,25 @@ */ #include "ScriptAction.h" +#include +#include +#include namespace InputActions { -ScriptAction::ScriptAction(JSFunction function) +ScriptAction::ScriptAction(QJSValue function, std::shared_ptr sourceNode) : m_function(std::move(function)) + , m_sourceNode(std::move(sourceNode)) { } void ScriptAction::doExecute(const ActionExecutionArguments &args) { - m_function.call(); + const auto result = g_scriptingEngine->call(m_function); + if (result.isError() && m_sourceNode) { + g_configIssueManager->addIssue(UncaughtScriptErrorConfigIssue(m_sourceNode.get(), result)); + } } } \ No newline at end of file diff --git a/src/libinputactions/scripting/ScriptAction.h b/src/libinputactions/scripting/ScriptAction.h index 8d4d2b8..7f30f93 100644 --- a/src/libinputactions/scripting/ScriptAction.h +++ b/src/libinputactions/scripting/ScriptAction.h @@ -18,25 +18,29 @@ #pragma once -#include "JSFunction.h" +#include #include namespace InputActions { +class Node; + class ScriptAction : public Action { public: /** * @param function Called with no arguments when the action is executed. The return value is ignored. + * @param sourceNode The configuration node this action was defined in. May be nullptr. */ - ScriptAction(JSFunction function); + ScriptAction(QJSValue function, std::shared_ptr sourceNode); protected: void doExecute(const ActionExecutionArguments &args) override; private: - JSFunction m_function; + QJSValue m_function; + std::shared_ptr m_sourceNode; }; } \ No newline at end of file diff --git a/src/libinputactions/scripting/ScriptCondition.cpp b/src/libinputactions/scripting/ScriptCondition.cpp index 3642304..187aeff 100644 --- a/src/libinputactions/scripting/ScriptCondition.cpp +++ b/src/libinputactions/scripting/ScriptCondition.cpp @@ -17,18 +17,28 @@ */ #include "ScriptCondition.h" +#include +#include +#include namespace InputActions { -ScriptCondition::ScriptCondition(JSFunction function) +ScriptCondition::ScriptCondition(QJSValue function, std::shared_ptr sourceNode) : m_function(std::move(function)) + , m_sourceNode(std::move(sourceNode)) { } bool ScriptCondition::doEvaluate(const ConditionEvaluationArguments &arguments) { - return m_function.call().toBool(); + const auto result = g_scriptingEngine->call(m_function); + if (result.isError() && m_sourceNode) { + g_configIssueManager->addIssue(UncaughtScriptErrorConfigIssue(m_sourceNode.get(), result)); + return false; + } + + return result.toBool(); } } \ No newline at end of file diff --git a/src/libinputactions/scripting/ScriptCondition.h b/src/libinputactions/scripting/ScriptCondition.h index 398124f..15f6dc2 100644 --- a/src/libinputactions/scripting/ScriptCondition.h +++ b/src/libinputactions/scripting/ScriptCondition.h @@ -18,25 +18,29 @@ #pragma once -#include "JSFunction.h" +#include #include namespace InputActions { +class Node; + class ScriptCondition : public Condition { public: /** * @param function Called with no arguments when the condition is evaluated. The condition is satisfied when the return value is true. + * @param sourceNode The configuration node this action was defined in. May be nullptr. */ - ScriptCondition(JSFunction function); + ScriptCondition(QJSValue function, std::shared_ptr sourceNode); protected: bool doEvaluate(const ConditionEvaluationArguments &arguments) override; private: - JSFunction m_function; + QJSValue m_function; + std::shared_ptr m_sourceNode; }; } \ No newline at end of file diff --git a/src/libinputactions/scripting/ScriptingEngine.cpp b/src/libinputactions/scripting/ScriptingEngine.cpp index 49eb59a..0898ebf 100644 --- a/src/libinputactions/scripting/ScriptingEngine.cpp +++ b/src/libinputactions/scripting/ScriptingEngine.cpp @@ -19,9 +19,12 @@ #include "ScriptingEngine.h" #include "modules/CoreModule.h" #include +#include #include #include +Q_LOGGING_CATEGORY(INPUTACTIONS_SCRIPTING, "inputactions.scripting", QtWarningMsg) + namespace InputActions { @@ -92,7 +95,41 @@ void ScriptingEngine::registerBuiltinModule(const QString &name, QJSValue value) QJSValue ScriptingEngine::evaluate(const QString &script) { - return ensureEngine().evaluate(script); + const auto result = ensureEngine().evaluate(script); + if (result.isError()) { + logError(result); + } + + return result; +} + +QJSValue ScriptingEngine::call(const QJSValue &function, const QJSValueList &args) const +{ + const auto result = function.call(args); + if (result.isError()) { + logError(result); + } + + return result; +} + +QString ScriptingEngine::errorToString(const QJSValue &error) const +{ + const auto name = error.property("name").toString(); + const auto message = error.property("message").toString(); + auto file = error.property("fileName").toString(); + if (file.isEmpty()) { + file = "None (defined in a YAML file)"; + } + const auto lineNumber = error.property("lineNumber").toUInt(); + const auto stack = error.property("stack").toString(); + + return QString("%1: %2\nFile: %3\nLine: %4\nStack:\n%5\n").arg(name, message, file, QString::number(lineNumber), QStringHelpers::indented(stack, 4)); +} + +void ScriptingEngine::logError(const QJSValue &error) const +{ + qCCritical(INPUTACTIONS_SCRIPTING).nospace().noquote() << "Uncaught script error\n" << errorToString(error); } QJSEngine &ScriptingEngine::ensureEngine() diff --git a/src/libinputactions/scripting/ScriptingEngine.h b/src/libinputactions/scripting/ScriptingEngine.h index 07fa5f3..31dbf95 100644 --- a/src/libinputactions/scripting/ScriptingEngine.h +++ b/src/libinputactions/scripting/ScriptingEngine.h @@ -37,8 +37,19 @@ class ScriptingEngine : public QObject ScriptingEngine() = default; ~ScriptingEngine() override; + /** + * Same as QJSEngine::evaluate but with error logging. + */ QJSValue evaluate(const QString &script); + /** + * Same as QJSValue::call but with error logging. + */ + QJSValue call(const QJSValue &function, const QJSValueList &args = {}) const; + + QString errorToString(const QJSValue &error) const; + void logError(const QJSValue &error) const; + template QJSValue newFunction(TFunction &&function) { From 74e95ecc10acee552f15af96683f77c74dbace73 Mon Sep 17 00:00:00 2001 From: taj-ny <79316397+taj-ny@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:33:19 +0200 Subject: [PATCH 12/12] scripting: move CoreModule to modules/core --- src/CMakeLists.txt | 2 +- src/libinputactions/scripting/ScriptingEngine.cpp | 2 +- src/libinputactions/scripting/modules/{ => core}/CoreModule.cpp | 0 src/libinputactions/scripting/modules/{ => core}/CoreModule.h | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename src/libinputactions/scripting/modules/{ => core}/CoreModule.cpp (100%) rename src/libinputactions/scripting/modules/{ => core}/CoreModule.h (100%) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3067c2a..17355b3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -81,7 +81,7 @@ set(libinputactions_SRCS libinputactions/interfaces/TextInput.cpp libinputactions/interfaces/Window.h libinputactions/interfaces/WindowProvider.cpp - libinputactions/scripting/modules/CoreModule.cpp + libinputactions/scripting/modules/core/CoreModule.cpp libinputactions/scripting/FunctionWrapper.cpp libinputactions/scripting/ScriptAction.cpp libinputactions/scripting/ScriptCondition.cpp diff --git a/src/libinputactions/scripting/ScriptingEngine.cpp b/src/libinputactions/scripting/ScriptingEngine.cpp index 0898ebf..b0a2de0 100644 --- a/src/libinputactions/scripting/ScriptingEngine.cpp +++ b/src/libinputactions/scripting/ScriptingEngine.cpp @@ -17,7 +17,7 @@ */ #include "ScriptingEngine.h" -#include "modules/CoreModule.h" +#include "modules/core/CoreModule.h" #include #include #include diff --git a/src/libinputactions/scripting/modules/CoreModule.cpp b/src/libinputactions/scripting/modules/core/CoreModule.cpp similarity index 100% rename from src/libinputactions/scripting/modules/CoreModule.cpp rename to src/libinputactions/scripting/modules/core/CoreModule.cpp diff --git a/src/libinputactions/scripting/modules/CoreModule.h b/src/libinputactions/scripting/modules/core/CoreModule.h similarity index 100% rename from src/libinputactions/scripting/modules/CoreModule.h rename to src/libinputactions/scripting/modules/core/CoreModule.h