From fc3846ffe418c5f65f984c6726b804917f0d668e Mon Sep 17 00:00:00 2001 From: taj_ny Date: Sun, 15 Mar 2026 12:31:25 +0100 Subject: [PATCH] add stuff for text expansion --- src/CMakeLists.txt | 4 +- src/libinputactions/InputActionsMain.cpp | 5 +- .../actions/ReplaceTextAction.cpp | 66 ++++++++++++++++ .../actions/ReplaceTextAction.h | 59 ++++++++++++++ .../conditions/CanReplaceTextCondition.cpp | 40 ++++++++++ .../conditions/CanReplaceTextCondition.h | 43 +++++++++++ src/libinputactions/config/parsers/core.cpp | 16 ++++ .../MultiTouchMotionTriggerHandler.cpp | 2 +- src/libinputactions/interfaces/TextInput.cpp | 77 +++++++++++++++++++ src/libinputactions/interfaces/TextInput.h | 14 ++++ .../variables/VariableManager.h | 3 +- tests/libinputactions/CMakeLists.txt | 2 + .../TestReplaceTextActionNodeParser.cpp | 49 ++++++++++++ .../TestCanReplaceTextConditionNodeParser.cpp | 32 ++++++++ 14 files changed, 408 insertions(+), 4 deletions(-) create mode 100644 src/libinputactions/actions/ReplaceTextAction.cpp create mode 100644 src/libinputactions/actions/ReplaceTextAction.h create mode 100644 src/libinputactions/conditions/CanReplaceTextCondition.cpp create mode 100644 src/libinputactions/conditions/CanReplaceTextCondition.h create mode 100644 src/libinputactions/interfaces/TextInput.cpp create mode 100644 tests/libinputactions/config/parsers/actions/TestReplaceTextActionNodeParser.cpp create mode 100644 tests/libinputactions/config/parsers/conditions/TestCanReplaceTextConditionNodeParser.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4f9bdbc..ef37821 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,6 +8,7 @@ set(libinputactions_SRCS libinputactions/actions/CustomAction.cpp libinputactions/actions/InputAction.cpp libinputactions/actions/PlasmaGlobalShortcutAction.cpp + libinputactions/actions/ReplaceTextAction.cpp libinputactions/actions/SleepAction.cpp libinputactions/actions/TriggerAction.cpp libinputactions/config/parsers/base.cpp @@ -27,6 +28,7 @@ set(libinputactions_SRCS libinputactions/config/GlobalConfig.cpp libinputactions/config/Node.cpp libinputactions/config/TextPosition.cpp + libinputactions/conditions/CanReplaceTextCondition.cpp libinputactions/conditions/Condition.cpp libinputactions/conditions/ConditionGroup.cpp libinputactions/conditions/CustomCondition.cpp @@ -75,7 +77,7 @@ set(libinputactions_SRCS libinputactions/interfaces/PointerPositionSetter.h libinputactions/interfaces/ProcessRunner.cpp libinputactions/interfaces/SessionLock.h - libinputactions/interfaces/TextInput.h + libinputactions/interfaces/TextInput.cpp libinputactions/interfaces/Window.h libinputactions/interfaces/WindowProvider.cpp libinputactions/triggers/core/DirectionalMotionTriggerCore.cpp diff --git a/src/libinputactions/InputActionsMain.cpp b/src/libinputactions/InputActionsMain.cpp index 343b8fe..eeb0b53 100644 --- a/src/libinputactions/InputActionsMain.cpp +++ b/src/libinputactions/InputActionsMain.cpp @@ -114,7 +114,7 @@ void InputActionsMain::registerGlobalVariables(VariableManager *variableManager, value = g_cursorShapeProvider->cursorShape(); }); variableManager->registerLocalVariable(BuiltinVariables::DeviceName); - for (auto i = 1; i <= s_fingerVariableCount; i++) { + for (auto i = 1; i <= FINGER_VARIABLE_COUNT; i++) { variableManager->registerLocalVariable(QString("finger_%1_initial_position_percentage").arg(i)); variableManager->registerLocalVariable(QString("finger_%1_position_percentage").arg(i)); variableManager->registerLocalVariable(QString("finger_%1_pressure").arg(i)); @@ -123,6 +123,9 @@ void InputActionsMain::registerGlobalVariables(VariableManager *variableManager, variableManager->registerRemoteVariable(BuiltinVariables::KeyboardModifiers, [](auto &value) { value = g_inputBackend->keyboardModifiers(); }); + for (auto i = 0; i < REGEX_MATCH_VARIABLE_COUNT; i++) { + variableManager->registerLocalVariable(QString("match_%1").arg(i)); + } variableManager->registerLocalVariable(BuiltinVariables::LastTriggerId); variableManager->registerLocalVariable(BuiltinVariables::LastTriggerTimestamp, true); variableManager->registerRemoteVariable("pointer_position_screen_percentage", [pointerPositionGetter](auto &value) { diff --git a/src/libinputactions/actions/ReplaceTextAction.cpp b/src/libinputactions/actions/ReplaceTextAction.cpp new file mode 100644 index 0000000..c006ba7 --- /dev/null +++ b/src/libinputactions/actions/ReplaceTextAction.cpp @@ -0,0 +1,66 @@ +/* + 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 "ReplaceTextAction.h" +#include +#include + +namespace InputActions +{ + +ReplaceTextAction::ReplaceTextAction(std::vector rules) + : m_rules(std::move(rules)) +{ +} + +void ReplaceTextAction::executeImpl(const ActionExecutionArguments &args) +{ + TextSubstitutionRule *rule{}; + QThreadHelpers::runOnThread( + QThreadHelpers::mainThread(), + [this, &rule] { + if (const auto it = std::ranges::find_if(m_rules, + [](const auto &rule) { + return g_textInput->canReplaceSurroundingText(rule.regex()); + }); + it != m_rules.end()) { + rule = &(*it); + } + }, + true); + if (!rule) { + return; + } + + g_textInput->replaceSurroundingText(rule->regex(), rule->newText()); +} + +bool ReplaceTextAction::async() const +{ + return std::ranges::any_of(m_rules, [](const auto &rule) { + return g_textInput->canReplaceSurroundingText(rule.regex()) && rule.newText().expensive(); + }); +} + +TextSubstitutionRule::TextSubstitutionRule(QRegularExpression regex, Value newText) + : m_regex(std::move(regex)) + , m_newText(std::move(newText)) +{ +} + +} \ No newline at end of file diff --git a/src/libinputactions/actions/ReplaceTextAction.h b/src/libinputactions/actions/ReplaceTextAction.h new file mode 100644 index 0000000..adeb265 --- /dev/null +++ b/src/libinputactions/actions/ReplaceTextAction.h @@ -0,0 +1,59 @@ +/* + 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 "Action.h" +#include +#include +#include + +namespace InputActions +{ + +class TextSubstitutionRule +{ +public: + TextSubstitutionRule() = default; + TextSubstitutionRule(QRegularExpression regex, Value newText); + + const QRegularExpression ®ex() const { return m_regex; } + const Value &newText() const { return m_newText; } + +private: + QRegularExpression m_regex; + Value m_newText; +}; + +class ReplaceTextAction : public Action +{ +public: + ReplaceTextAction(std::vector rules); + + const std::vector &rules() const { return m_rules; } + + bool async() const override; + +protected: + void executeImpl(const ActionExecutionArguments &args) override; + +private: + std::vector m_rules; +}; + +} \ No newline at end of file diff --git a/src/libinputactions/conditions/CanReplaceTextCondition.cpp b/src/libinputactions/conditions/CanReplaceTextCondition.cpp new file mode 100644 index 0000000..7fd8db8 --- /dev/null +++ b/src/libinputactions/conditions/CanReplaceTextCondition.cpp @@ -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 . +*/ + +#include "CanReplaceTextCondition.h" +#include +#include + +namespace InputActions +{ + +CanReplaceTextCondition::CanReplaceTextCondition(std::vector rules) + : m_rules(std::move(rules)) +{ +} + +CanReplaceTextCondition::~CanReplaceTextCondition() = default; + +bool CanReplaceTextCondition::evaluateImpl(const ConditionEvaluationArguments &arguments) +{ + return std::ranges::any_of(m_rules, [](const auto &rule) { + return g_textInput->canReplaceSurroundingText(rule.regex()); + }); +} + +} \ No newline at end of file diff --git a/src/libinputactions/conditions/CanReplaceTextCondition.h b/src/libinputactions/conditions/CanReplaceTextCondition.h new file mode 100644 index 0000000..064d8e5 --- /dev/null +++ b/src/libinputactions/conditions/CanReplaceTextCondition.h @@ -0,0 +1,43 @@ +/* + 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 "Condition.h" + +namespace InputActions +{ + +class TextSubstitutionRule; + +class CanReplaceTextCondition : public Condition +{ +public: + CanReplaceTextCondition(std::vector rules); + ~CanReplaceTextCondition() override; + + const std::vector &rules() const { return m_rules; } + +protected: + bool evaluateImpl(const ConditionEvaluationArguments &arguments) override; + +private: + std::vector m_rules; +}; + +} \ No newline at end of file diff --git a/src/libinputactions/config/parsers/core.cpp b/src/libinputactions/config/parsers/core.cpp index 45362dd..4bb4082 100644 --- a/src/libinputactions/config/parsers/core.cpp +++ b/src/libinputactions/config/parsers/core.cpp @@ -31,8 +31,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -154,6 +156,8 @@ void NodeParser>::parse(const Node *node, std::unique_pt } else if (const auto *plasmaShortcutNode = node->at("plasma_shortcut")) { const auto shortcut = parseSeparatedString2(plasmaShortcutNode, ','); 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 *sleepActionNode = node->at("sleep")) { result = std::make_unique(sleepActionNode->as()); } else if (const auto *oneNode = node->at("one")) { @@ -218,6 +222,10 @@ std::shared_ptr parseCondition(const Node *node, const VariableManage return group; } + if (const auto *canReplaceTextNode = node->at("can_replace_text")) { + return std::make_shared(canReplaceTextNode->as>(true)); + } + if (isLegacy(node)) { g_configIssueManager->addIssue(DeprecatedFeatureConfigIssue(node, DeprecatedFeature::LegacyConditions)); @@ -576,6 +584,14 @@ struct NodeParser> }; template struct NodeParser>; +template<> +void NodeParser::parse(const Node *node, TextSubstitutionRule &result) +{ + const auto regex = node->at("regex", true)->as(); + const auto newText = node->at("replace", true)->as>(); + result = {regex, newText}; +} + template<> void NodeParser>::parse(const Node *node, std::unique_ptr &result) { diff --git a/src/libinputactions/handlers/MultiTouchMotionTriggerHandler.cpp b/src/libinputactions/handlers/MultiTouchMotionTriggerHandler.cpp index 9eb5c56..949b5c4 100644 --- a/src/libinputactions/handlers/MultiTouchMotionTriggerHandler.cpp +++ b/src/libinputactions/handlers/MultiTouchMotionTriggerHandler.cpp @@ -105,7 +105,7 @@ void MultiTouchMotionTriggerHandler::updateVariables(const InputDevice *sender) bool hasThumb{}; const auto touchPoints = sender ? sender->physicalState().validTouchPoints() : std::vector(); - for (size_t i = 0; i < s_fingerVariableCount; i++) { + for (size_t i = 0; i < FINGER_VARIABLE_COUNT; i++) { const auto fingerVariableNumber = i + 1; auto initialPosition = g_variableManager->getVariable(QString("finger_%1_initial_position_percentage").arg(fingerVariableNumber)); auto position = g_variableManager->getVariable(QString("finger_%1_position_percentage").arg(fingerVariableNumber)); diff --git a/src/libinputactions/interfaces/TextInput.cpp b/src/libinputactions/interfaces/TextInput.cpp new file mode 100644 index 0000000..af90619 --- /dev/null +++ b/src/libinputactions/interfaces/TextInput.cpp @@ -0,0 +1,77 @@ +/* + 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 "TextInput.h" +#include +#include + +namespace InputActions +{ + +bool TextInput::canReplaceSurroundingText(const QRegularExpression ®ex) +{ + const auto text = surroundingText(); + if (!text) { + return false; + } + + const auto match = regex.match(text.value()); + if (!match.hasMatch()) { + return false; + } + + const auto cursorPos = surroundingTextCursorPosition(); + return !cursorPos || cursorPos == match.capturedEnd(); +} + +void TextInput::replaceSurroundingText(const QRegularExpression ®ex, const Value &newText) +{ + uint32_t capturedLength{}; + QThreadHelpers::runOnThread( + QThreadHelpers::mainThread(), + [this, ®ex, &capturedLength]() { + const auto match = regex.match(surroundingText().value()); + capturedLength = match.capturedLength(); + + for (auto i = 0; i < REGEX_MATCH_VARIABLE_COUNT; i++) { + auto *variable = g_variableManager->getVariable(QString("match_%1").arg(i)); + if (i > match.lastCapturedIndex()) { + variable->set({}); + continue; + } + + variable->set(match.capturedTexts()[i]); + } + }, + true); + + const auto text = newText.get(); + if (!text) { + return; + } + + QThreadHelpers::runOnThread( + QThreadHelpers::mainThread(), + [this, capturedLength, &text = text.value()]() { + deleteSurroundingText(capturedLength, 0); + writeText(text); + }, + true); +} + +} \ No newline at end of file diff --git a/src/libinputactions/interfaces/TextInput.h b/src/libinputactions/interfaces/TextInput.h index 8582270..d0ee848 100644 --- a/src/libinputactions/interfaces/TextInput.h +++ b/src/libinputactions/interfaces/TextInput.h @@ -19,6 +19,7 @@ #pragma once #include +#include namespace InputActions { @@ -29,6 +30,19 @@ class TextInput TextInput() = default; virtual ~TextInput() = default; + bool canReplaceSurroundingText(const QRegularExpression ®ex); + /** + * May be called from any thread. + */ + void replaceSurroundingText(const QRegularExpression ®ex, const Value &newText); + + /** + * @param beforeLength Number of characters before the cursor to delete. + * @param afterLength Number of characters after the cursor to delete. + */ + virtual void deleteSurroundingText(uint32_t beforeLength, uint32_t afterLength) {} + virtual std::optional surroundingText() { return {}; } + virtual std::optional surroundingTextCursorPosition() { return {}; } virtual void writeText(const QString &text) {} }; diff --git a/src/libinputactions/variables/VariableManager.h b/src/libinputactions/variables/VariableManager.h index d2eeb9f..a266f6b 100644 --- a/src/libinputactions/variables/VariableManager.h +++ b/src/libinputactions/variables/VariableManager.h @@ -32,7 +32,8 @@ Q_DECLARE_LOGGING_CATEGORY(INPUTACTIONS_VARIABLE_MANAGER) namespace InputActions { -const static uint8_t s_fingerVariableCount = 5; +static const uint8_t FINGER_VARIABLE_COUNT = 5; +static const uint8_t REGEX_MATCH_VARIABLE_COUNT = 5; class PointerPositionGetter; class Variable; diff --git a/tests/libinputactions/CMakeLists.txt b/tests/libinputactions/CMakeLists.txt index 91c4f73..e0759db 100644 --- a/tests/libinputactions/CMakeLists.txt +++ b/tests/libinputactions/CMakeLists.txt @@ -37,6 +37,7 @@ libinputactions_add_test(TestActionGroup SOURCES actions/TestActionGroup.cpp) libinputactions_add_test(TestActionGroupNodeParser SOURCES config/parsers/actions/TestActionGroupNodeParser.cpp) libinputactions_add_test(TestActionInterval SOURCES actions/TestActionInterval.cpp) libinputactions_add_test(TestBaseNodeParser SOURCES config/parsers/TestBaseNodeParser.cpp) +libinputactions_add_test(TestCanReplaceTextConditionNodeParser SOURCES config/parsers/conditions/TestCanReplaceTextConditionNodeParser.cpp) libinputactions_add_test(TestConditionGroup SOURCES conditions/TestConditionGroup.cpp) libinputactions_add_test(TestConditionGroupNodeParser SOURCES config/parsers/conditions/TestConditionGroupNodeParser.cpp) libinputactions_add_test(TestConditionNodeParser SOURCES config/parsers/conditions/TestConditionNodeParser.cpp) @@ -60,6 +61,7 @@ libinputactions_add_test(TestQStringNodeParser SOURCES config/parsers/qt/TestQSt libinputactions_add_test(TestQStringListNodeParser SOURCES config/parsers/qt/TestQStringListNodeParser.cpp) libinputactions_add_test(TestRange SOURCES TestRange.cpp) libinputactions_add_test(TestRangeNodeParser SOURCES config/parsers/TestRangeNodeParser.cpp) +libinputactions_add_test(TestReplaceTextActionNodeParser SOURCES config/parsers/actions/TestReplaceTextActionNodeParser.cpp) libinputactions_add_test(TestSeparatedStringNodeParser SOURCES config/parsers/TestSeparatedStringNodeParser.cpp) libinputactions_add_test(TestSetNodeParser SOURCES config/parsers/containers/TestSetNodeParser.cpp) libinputactions_add_test(TestStrokeTriggerCoreNodeParser SOURCES config/parsers/triggers/TestStrokeTriggerCoreNodeParser.cpp) diff --git a/tests/libinputactions/config/parsers/actions/TestReplaceTextActionNodeParser.cpp b/tests/libinputactions/config/parsers/actions/TestReplaceTextActionNodeParser.cpp new file mode 100644 index 0000000..d0466e8 --- /dev/null +++ b/tests/libinputactions/config/parsers/actions/TestReplaceTextActionNodeParser.cpp @@ -0,0 +1,49 @@ +#include "Test.h" +#include +#include +#include + +namespace InputActions +{ + +class TestReplaceTextActionNodeParser : public Test +{ + Q_OBJECT + +private slots: + void valid__parsesNodeCorrectly() + { + const auto node = Node::create(R"( + replace_text: + - regex: a + replace: b + )"); + const auto action = node->as>(); + + const auto *replaceTextAction = dynamic_cast(action.get()); + QVERIFY(replaceTextAction); + QCOMPARE(replaceTextAction->rules().size(), 1); + QCOMPARE(replaceTextAction->rules()[0].regex().pattern(), "a"); + QCOMPARE(replaceTextAction->rules()[0].newText().get(), "b"); + } + + void valid_command__parsesNodeCorrectly() + { + const auto node = Node::create(R"( + replace_text: + - regex: a + replace: + command: echo -n c + )"); + const auto action = node->as>(); + + const auto *replaceTextAction = dynamic_cast(action.get()); + QVERIFY(replaceTextAction); + QCOMPARE(replaceTextAction->rules()[0].newText().get(), "c"); + } +}; + +} + +QTEST_MAIN(InputActions::TestReplaceTextActionNodeParser) +#include "TestReplaceTextActionNodeParser.moc" \ No newline at end of file diff --git a/tests/libinputactions/config/parsers/conditions/TestCanReplaceTextConditionNodeParser.cpp b/tests/libinputactions/config/parsers/conditions/TestCanReplaceTextConditionNodeParser.cpp new file mode 100644 index 0000000..6208abc --- /dev/null +++ b/tests/libinputactions/config/parsers/conditions/TestCanReplaceTextConditionNodeParser.cpp @@ -0,0 +1,32 @@ +#include "Test.h" +#include +#include +#include + +namespace InputActions +{ + +class TestCanReplaceTextConditionNodeParser : public Test +{ + Q_OBJECT + +private slots: + void valid__parsesNodeCorrectly() + { + const auto node = Node::create(R"( + can_replace_text: + - regex: a + replace: _ + )"); + const auto condition = std::dynamic_pointer_cast(node->as>()); + + QVERIFY(condition); + QCOMPARE(condition->rules().size(), 1); + QCOMPARE(condition->rules()[0].regex().pattern(), "a"); + } +}; + +} + +QTEST_MAIN(InputActions::TestCanReplaceTextConditionNodeParser) +#include "TestCanReplaceTextConditionNodeParser.moc" \ No newline at end of file