diff --git a/README.md b/README.md index 1134eee..4ad26b3 100644 --- a/README.md +++ b/README.md @@ -338,6 +338,22 @@ Meta{C} >> $(start powershell) ^ The following directives, which are lines starting with an `@`, can be inserted in the configuration file: +- `forward-modifiers` allows to set a list of keys which should never be [held back](#order-of-mappings). e.g.: + ```python + @forward-modifiers Shift Control AltLeft + ``` + It effectively forwards these keys in each [stage](#multiple-stages) immediately, like: + ```bash + Shift >> Shift + Control >> Control + AltLeft >> AltLeft + ``` + and automatically suppresses the forwarded keys in the output: + ```bash + # implicitly turned into 'Control{A} >> !Control Shift{B}' + Control{A} >> Shift{B} + ``` + - `allow-unmapped-commands` and `enforce-lowercase-commands` change the way [commands](#abstract-commands) are validated. When used, then best together, so typing errors in key names are still detected. e.g.: ```python @allow-unmapped-commands diff --git a/src/config/Config.h b/src/config/Config.h index 080362d..4b9e5d4 100644 --- a/src/config/Config.h +++ b/src/config/Config.h @@ -16,7 +16,6 @@ struct Config { }; struct Context { - bool system_filter_matched{ }; Filter window_class_filter; Filter window_title_filter; Filter window_path_filter; @@ -26,6 +25,7 @@ struct Config { std::vector inputs; std::vector outputs; std::vector command_outputs; + bool system_filter_matched{ }; bool invert_modifier_filter{ }; bool fallthrough{ }; bool begin_stage{ }; diff --git a/src/config/ParseConfig.cpp b/src/config/ParseConfig.cpp index 19dde34..eefa08b 100644 --- a/src/config/ParseConfig.cpp +++ b/src/config/ParseConfig.cpp @@ -57,6 +57,11 @@ namespace { }) != cend(sequence); } + bool contains(const KeySequence& sequence, KeyEvent event) { + return std::find(cbegin(sequence), + cend(sequence), event) != cend(sequence); + } + void replace_key(KeySequence& sequence, Key both, Key key) { std::for_each(begin(sequence), end(sequence), [&](KeyEvent& event) { @@ -178,9 +183,12 @@ Config ParseConfig::operator()(std::istream& is, m_after_empty_context_block = false; m_enforce_lowercase_commands = { }; m_allow_unmapped_commands = { }; + m_forward_modifiers.clear(); // add default context - m_config.contexts.push_back({ true, {}, {} }); + auto& default_context = m_config.contexts.emplace_back(); + default_context.begin_stage = true; + default_context.system_filter_matched = true; // register common logical keys add_logical_key("Shift", Key::ShiftLeft, Key::ShiftRight); @@ -195,6 +203,9 @@ Config ParseConfig::operator()(std::istream& is, if (!command.mapped) throw ConfigError("Command '" + command.name + "' was not mapped"); + // prepend forward-modifier mappings in each stage. e.g.: ShiftLeft >> ShiftLeft + prepend_forward_modifier_mappings(); + // remove contexts of other systems or which are empty optimize_contexts(); @@ -204,6 +215,8 @@ Config ParseConfig::operator()(std::istream& is, replace_logical_key(both, left, right); } + suppress_forwarded_modifiers_in_outputs(); + // collect virtual key aliases for (const auto& [name, value] : m_macros) if (auto key = get_key_by_name(value); is_virtual_key(key)) @@ -391,6 +404,9 @@ void ParseConfig::parse_directive(It it, const It end) { else if (ident == "allow-unmapped-commands") { m_allow_unmapped_commands = read_optional_bool(); } + else if (ident == "forward-modifiers") { + m_forward_modifiers = parse_forward_modifiers_list(&it, end); + } else { error("Unknown directive '" + ident + "'"); } @@ -453,6 +469,30 @@ KeySequence ParseConfig::parse_modifier_list(std::string_view string) { return sequence; } +std::vector ParseConfig::parse_forward_modifiers_list(It* it, const It end) { + auto modifiers = std::vector(); + while (*it != end) { + const auto name = read_ident(it, end); + if (name.empty()) + error("Key name expected"); + + const auto it2 = std::find_if(m_logical_keys.begin(), m_logical_keys.end(), + [&](const LogicalKey& key) { return key.name == name; }); + if (it2 != m_logical_keys.end()) { + modifiers.push_back(it2->left); + modifiers.push_back(it2->right); + } + else if (const auto key = ::get_key_by_name(name); is_device_key(key)) { + modifiers.push_back(key); + } + else { + error("Invalid key '" + name + "'"); + } + skip_space(it, end); + } + return modifiers; +} + void ParseConfig::parse_context(It it, const It end) { auto& context = m_config.contexts.emplace_back(); context.system_filter_matched = true; @@ -931,7 +971,7 @@ void ParseConfig::optimize_contexts() { const auto has_no_effect = ( context.inputs.empty() && context.command_outputs.empty() && - !context.begin_stage); + (i == 0 || !context.begin_stage)); if (can_not_match || has_no_effect) { // convert fallthrough context when removing the non-fallthrough context @@ -953,3 +993,53 @@ void ParseConfig::optimize_contexts() { } } } + +void ParseConfig::prepend_forward_modifier_mappings() { + for (auto& context : m_config.contexts) + if (context.begin_stage) + for (auto it = m_forward_modifiers.rbegin(); + it != m_forward_modifiers.rend(); ++it) { + const auto key = *it; + context.inputs.insert(context.inputs.begin(), { + KeySequence{ + KeyEvent(key, KeyState::Down), + KeyEvent(key, KeyState::UpAsync) + }, + static_cast(context.outputs.size()) + }); + context.outputs.push_back({ + KeyEvent(key, KeyState::Down) + }); + } +} + +void ParseConfig::suppress_forwarded_modifiers_in_outputs() { + if (m_forward_modifiers.empty()) + return; + + for (auto& context : m_config.contexts) { + for (auto key : m_forward_modifiers) { + const auto down_event = KeyEvent(key, KeyState::Down); + const auto not_event = KeyEvent(key, KeyState::Not); + const auto prepend_not_event = [&](KeySequence& output) { + if (!contains(output, down_event) && + !contains(output, not_event)) + output.insert(output.begin(), not_event); + }; + + for (auto& input : context.inputs) + if (contains(input.input, down_event)) { + if (input.output_index >= 0) { + prepend_not_event(context.outputs[input.output_index]); + } + else { + for (auto& following_context : m_config.contexts) { + for (auto& command : following_context.command_outputs) + if (command.index == input.output_index) + prepend_not_event(command.output); + } + } + } + } + } +} diff --git a/src/config/ParseConfig.h b/src/config/ParseConfig.h index 6a695a8..7d333b0 100644 --- a/src/config/ParseConfig.h +++ b/src/config/ParseConfig.h @@ -47,6 +47,7 @@ class ParseConfig { It out_begin, It out_end); KeySequence parse_input(It begin, It end); KeySequence parse_output(It begin, It end); + std::vector parse_forward_modifiers_list(It* it, It end); std::string substitute_variables(std::string string) const; std::string preprocess(It begin, It end, bool apply_arguments = true) const; std::string preprocess(std::string expression) const; @@ -59,6 +60,8 @@ class ParseConfig { Key get_key_by_name(std::string_view name) const; Key add_terminal_command_action(std::string_view command); void optimize_contexts(); + void prepend_forward_modifier_mappings(); + void suppress_forwarded_modifiers_in_outputs(); Config::Context& current_context(); Command* find_command(const std::string& name); @@ -80,4 +83,5 @@ class ParseConfig { bool m_after_empty_context_block{ }; bool m_enforce_lowercase_commands{ }; bool m_allow_unmapped_commands{ }; + std::vector m_forward_modifiers; }; diff --git a/src/test/test1_ParseConfig.cpp b/src/test/test1_ParseConfig.cpp index 617abd6..d5cbcd2 100644 --- a/src/test/test1_ParseConfig.cpp +++ b/src/test/test1_ParseConfig.cpp @@ -326,6 +326,84 @@ TEST_CASE("Config directives", "[ParseConfig]") { //-------------------------------------------------------------------- +TEST_CASE("Forward modifiers directive", "[ParseConfig]") { + CHECK_NOTHROW(parse_config(R"( + @forward-modifiers + @forward-modifiers A ; comment + @forward-modifiers Shift Control # comment + )")); + CHECK_THROWS(parse_config(R"(@forward-modifiers Shft)")); + CHECK_THROWS(parse_config(R"(@forward-modifiers Shift ^)")); + CHECK_THROWS(parse_config(R"(@forward-modifiers 100ms)")); + CHECK_THROWS(parse_config(R"(@forward-modifiers Any)")); + CHECK_THROWS(parse_config(R"(@forward-modifiers ContextActive)")); + + auto config = parse_config(R"( + @forward-modifiers Shift Control AltLeft + Control{A} >> Shift{X} + Control{B} >> command + ControlLeft{C} >> command + AltLeft{D} >> command + command >> Shift{Y} + + [title="app"] + command >> Control{Z} + + [stage] + ShiftRight{X} >> Z + )"); + REQUIRE(config.contexts.size() == 3); + const auto& c0 = config.contexts[0]; + const auto& c1 = config.contexts[1]; + const auto& c2 = config.contexts[2]; + REQUIRE(c0.inputs.size() == 11); + REQUIRE(c0.outputs.size() == 6); + REQUIRE(c0.command_outputs.size() == 1); + // inputs are inserted at the front + CHECK(format_sequence(c0.inputs[0].input) == "+ShiftLeft ~ShiftLeft"); + CHECK(format_sequence(c0.inputs[1].input) == "+ShiftRight ~ShiftRight"); + CHECK(format_sequence(c0.inputs[2].input) == "+ControlLeft ~ControlLeft"); + CHECK(format_sequence(c0.inputs[3].input) == "+ControlRight ~ControlRight"); + CHECK(format_sequence(c0.inputs[4].input) == "+AltLeft ~AltLeft"); + CHECK(format_sequence(c0.inputs[5].input) == "+ControlLeft +A ~A ~ControlLeft"); + CHECK(format_sequence(c0.inputs[6].input) == "+ControlRight +A ~A ~ControlRight"); + CHECK(format_sequence(c0.inputs[7].input) == "+ControlLeft +B ~B ~ControlLeft"); + CHECK(format_sequence(c0.inputs[8].input) == "+ControlRight +B ~B ~ControlRight"); + CHECK(format_sequence(c0.inputs[9].input) == "+ControlLeft +C ~C ~ControlLeft"); + CHECK(format_sequence(c0.inputs[10].input) == "+AltLeft +D ~D ~AltLeft"); + // forwarded modifiers in input are suppressed in output + CHECK(format_sequence(c0.command_outputs[0].output) == + "!AltLeft !ControlRight !ControlLeft +ShiftLeft +Y -Y -ShiftLeft"); + CHECK(format_sequence(c0.outputs[0]) == + "!ControlRight !ControlLeft +ShiftLeft +X -X -ShiftLeft"); + // outputs are prepended (in reverse order) + CHECK(format_sequence(c0.outputs[1]) == "+AltLeft"); + CHECK(format_sequence(c0.outputs[2]) == "+ControlRight"); + CHECK(format_sequence(c0.outputs[3]) == "+ControlLeft"); + CHECK(format_sequence(c0.outputs[4]) == "+ShiftRight"); + CHECK(format_sequence(c0.outputs[5]) == "+ShiftLeft"); + + REQUIRE(c1.inputs.size() == 0); + REQUIRE(c1.outputs.size() == 0); + REQUIRE(c1.command_outputs.size() == 1); + CHECK(format_sequence(c1.command_outputs[0].output) == + "!AltLeft !ControlRight +ControlLeft +Z -Z -ControlLeft"); + + // modifiers are forwarded in each stage + REQUIRE(c2.inputs.size() == 6); + REQUIRE(c2.outputs.size() == 6); + REQUIRE(c2.command_outputs.empty()); + CHECK(format_sequence(c2.inputs[0].input) == "+ShiftLeft ~ShiftLeft"); + CHECK(format_sequence(c2.inputs[1].input) == "+ShiftRight ~ShiftRight"); + CHECK(format_sequence(c2.inputs[2].input) == "+ControlLeft ~ControlLeft"); + CHECK(format_sequence(c2.inputs[3].input) == "+ControlRight ~ControlRight"); + CHECK(format_sequence(c2.inputs[4].input) == "+AltLeft ~AltLeft"); + CHECK(format_sequence(c2.inputs[5].input) == "+ShiftRight +X ~X ~ShiftRight"); + CHECK(format_sequence(c2.outputs[0]) == "!ShiftRight +Z"); +} + +//-------------------------------------------------------------------- + TEST_CASE("System contexts", "[ParseConfig]") { auto string = R"( [default] @@ -609,7 +687,7 @@ TEST_CASE("Stages", "[ParseConfig]") { auto config = parse_config(string); REQUIRE(config.contexts.size() == 9); - CHECK(!config.contexts[0].begin_stage); + CHECK(config.contexts[0].begin_stage); CHECK(config.contexts[1].begin_stage); CHECK(!config.contexts[2].begin_stage); CHECK(config.contexts[3].begin_stage);