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);