Skip to content

Commit 071ca7d

Browse files
committed
Added @forward-modifiers directive
1 parent 5455fb6 commit 071ca7d

File tree

5 files changed

+192
-4
lines changed

5 files changed

+192
-4
lines changed

README.md

+16
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,22 @@ Meta{C} >> $(start powershell) ^
338338
339339
The following directives, which are lines starting with an `@`, can be inserted in the configuration file:
340340
341+
- `forward-modifiers` allows to set a list of keys which should never be [held back](#order-of-mappings). e.g.:
342+
```python
343+
@forward-modifiers Shift Control AltLeft
344+
```
345+
It effectively forwards these keys in each [stage](#multiple-stages) immediately, like:
346+
```bash
347+
Shift >> Shift
348+
Control >> Control
349+
AltLeft >> AltLeft
350+
```
351+
and automatically suppresses the forwarded keys in the output:
352+
```bash
353+
# implicitly turned into 'Control{A} >> !Control Shift{B}'
354+
Control{A} >> Shift{B}
355+
```
356+
341357
- `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.:
342358
```python
343359
@allow-unmapped-commands

src/config/Config.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ struct Config {
1616
};
1717

1818
struct Context {
19-
bool system_filter_matched{ };
2019
Filter window_class_filter;
2120
Filter window_title_filter;
2221
Filter window_path_filter;
@@ -26,6 +25,7 @@ struct Config {
2625
std::vector<Input> inputs;
2726
std::vector<KeySequence> outputs;
2827
std::vector<CommandOutput> command_outputs;
28+
bool system_filter_matched{ };
2929
bool invert_modifier_filter{ };
3030
bool fallthrough{ };
3131
bool begin_stage{ };

src/config/ParseConfig.cpp

+92-2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ namespace {
5757
}) != cend(sequence);
5858
}
5959

60+
bool contains(const KeySequence& sequence, KeyEvent event) {
61+
return std::find(cbegin(sequence),
62+
cend(sequence), event) != cend(sequence);
63+
}
64+
6065
void replace_key(KeySequence& sequence, Key both, Key key) {
6166
std::for_each(begin(sequence), end(sequence),
6267
[&](KeyEvent& event) {
@@ -178,9 +183,12 @@ Config ParseConfig::operator()(std::istream& is,
178183
m_after_empty_context_block = false;
179184
m_enforce_lowercase_commands = { };
180185
m_allow_unmapped_commands = { };
186+
m_forward_modifiers.clear();
181187

182188
// add default context
183-
m_config.contexts.push_back({ true, {}, {} });
189+
auto& default_context = m_config.contexts.emplace_back();
190+
default_context.begin_stage = true;
191+
default_context.system_filter_matched = true;
184192

185193
// register common logical keys
186194
add_logical_key("Shift", Key::ShiftLeft, Key::ShiftRight);
@@ -195,6 +203,9 @@ Config ParseConfig::operator()(std::istream& is,
195203
if (!command.mapped)
196204
throw ConfigError("Command '" + command.name + "' was not mapped");
197205

206+
// prepend forward-modifier mappings in each stage. e.g.: ShiftLeft >> ShiftLeft
207+
prepend_forward_modifier_mappings();
208+
198209
// remove contexts of other systems or which are empty
199210
optimize_contexts();
200211

@@ -204,6 +215,8 @@ Config ParseConfig::operator()(std::istream& is,
204215
replace_logical_key(both, left, right);
205216
}
206217

218+
suppress_forwarded_modifiers_in_outputs();
219+
207220
// collect virtual key aliases
208221
for (const auto& [name, value] : m_macros)
209222
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) {
391404
else if (ident == "allow-unmapped-commands") {
392405
m_allow_unmapped_commands = read_optional_bool();
393406
}
407+
else if (ident == "forward-modifiers") {
408+
m_forward_modifiers = parse_forward_modifiers_list(&it, end);
409+
}
394410
else {
395411
error("Unknown directive '" + ident + "'");
396412
}
@@ -453,6 +469,30 @@ KeySequence ParseConfig::parse_modifier_list(std::string_view string) {
453469
return sequence;
454470
}
455471

472+
std::vector<Key> ParseConfig::parse_forward_modifiers_list(It* it, const It end) {
473+
auto modifiers = std::vector<Key>();
474+
while (*it != end) {
475+
const auto name = read_ident(it, end);
476+
if (name.empty())
477+
error("Key name expected");
478+
479+
const auto it2 = std::find_if(m_logical_keys.begin(), m_logical_keys.end(),
480+
[&](const LogicalKey& key) { return key.name == name; });
481+
if (it2 != m_logical_keys.end()) {
482+
modifiers.push_back(it2->left);
483+
modifiers.push_back(it2->right);
484+
}
485+
else if (const auto key = ::get_key_by_name(name); is_device_key(key)) {
486+
modifiers.push_back(key);
487+
}
488+
else {
489+
error("Invalid key '" + name + "'");
490+
}
491+
skip_space(it, end);
492+
}
493+
return modifiers;
494+
}
495+
456496
void ParseConfig::parse_context(It it, const It end) {
457497
auto& context = m_config.contexts.emplace_back();
458498
context.system_filter_matched = true;
@@ -931,7 +971,7 @@ void ParseConfig::optimize_contexts() {
931971
const auto has_no_effect = (
932972
context.inputs.empty() &&
933973
context.command_outputs.empty() &&
934-
!context.begin_stage);
974+
(i == 0 || !context.begin_stage));
935975

936976
if (can_not_match || has_no_effect) {
937977
// convert fallthrough context when removing the non-fallthrough context
@@ -953,3 +993,53 @@ void ParseConfig::optimize_contexts() {
953993
}
954994
}
955995
}
996+
997+
void ParseConfig::prepend_forward_modifier_mappings() {
998+
for (auto& context : m_config.contexts)
999+
if (context.begin_stage)
1000+
for (auto it = m_forward_modifiers.rbegin();
1001+
it != m_forward_modifiers.rend(); ++it) {
1002+
const auto key = *it;
1003+
context.inputs.insert(context.inputs.begin(), {
1004+
KeySequence{
1005+
KeyEvent(key, KeyState::Down),
1006+
KeyEvent(key, KeyState::UpAsync)
1007+
},
1008+
static_cast<int>(context.outputs.size())
1009+
});
1010+
context.outputs.push_back({
1011+
KeyEvent(key, KeyState::Down)
1012+
});
1013+
}
1014+
}
1015+
1016+
void ParseConfig::suppress_forwarded_modifiers_in_outputs() {
1017+
if (m_forward_modifiers.empty())
1018+
return;
1019+
1020+
for (auto& context : m_config.contexts) {
1021+
for (auto key : m_forward_modifiers) {
1022+
const auto down_event = KeyEvent(key, KeyState::Down);
1023+
const auto not_event = KeyEvent(key, KeyState::Not);
1024+
const auto prepend_not_event = [&](KeySequence& output) {
1025+
if (!contains(output, down_event) &&
1026+
!contains(output, not_event))
1027+
output.insert(output.begin(), not_event);
1028+
};
1029+
1030+
for (auto& input : context.inputs)
1031+
if (contains(input.input, down_event)) {
1032+
if (input.output_index >= 0) {
1033+
prepend_not_event(context.outputs[input.output_index]);
1034+
}
1035+
else {
1036+
for (auto& following_context : m_config.contexts) {
1037+
for (auto& command : following_context.command_outputs)
1038+
if (command.index == input.output_index)
1039+
prepend_not_event(command.output);
1040+
}
1041+
}
1042+
}
1043+
}
1044+
}
1045+
}

src/config/ParseConfig.h

+4
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class ParseConfig {
4747
It out_begin, It out_end);
4848
KeySequence parse_input(It begin, It end);
4949
KeySequence parse_output(It begin, It end);
50+
std::vector<Key> parse_forward_modifiers_list(It* it, It end);
5051
std::string substitute_variables(std::string string) const;
5152
std::string preprocess(It begin, It end, bool apply_arguments = true) const;
5253
std::string preprocess(std::string expression) const;
@@ -59,6 +60,8 @@ class ParseConfig {
5960
Key get_key_by_name(std::string_view name) const;
6061
Key add_terminal_command_action(std::string_view command);
6162
void optimize_contexts();
63+
void prepend_forward_modifier_mappings();
64+
void suppress_forwarded_modifiers_in_outputs();
6265

6366
Config::Context& current_context();
6467
Command* find_command(const std::string& name);
@@ -80,4 +83,5 @@ class ParseConfig {
8083
bool m_after_empty_context_block{ };
8184
bool m_enforce_lowercase_commands{ };
8285
bool m_allow_unmapped_commands{ };
86+
std::vector<Key> m_forward_modifiers;
8387
};

src/test/test1_ParseConfig.cpp

+79-1
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,84 @@ TEST_CASE("Config directives", "[ParseConfig]") {
326326

327327
//--------------------------------------------------------------------
328328

329+
TEST_CASE("Forward modifiers directive", "[ParseConfig]") {
330+
CHECK_NOTHROW(parse_config(R"(
331+
@forward-modifiers
332+
@forward-modifiers A ; comment
333+
@forward-modifiers Shift Control # comment
334+
)"));
335+
CHECK_THROWS(parse_config(R"(@forward-modifiers Shft)"));
336+
CHECK_THROWS(parse_config(R"(@forward-modifiers Shift ^)"));
337+
CHECK_THROWS(parse_config(R"(@forward-modifiers 100ms)"));
338+
CHECK_THROWS(parse_config(R"(@forward-modifiers Any)"));
339+
CHECK_THROWS(parse_config(R"(@forward-modifiers ContextActive)"));
340+
341+
auto config = parse_config(R"(
342+
@forward-modifiers Shift Control AltLeft
343+
Control{A} >> Shift{X}
344+
Control{B} >> command
345+
ControlLeft{C} >> command
346+
AltLeft{D} >> command
347+
command >> Shift{Y}
348+
349+
[title="app"]
350+
command >> Control{Z}
351+
352+
[stage]
353+
ShiftRight{X} >> Z
354+
)");
355+
REQUIRE(config.contexts.size() == 3);
356+
const auto& c0 = config.contexts[0];
357+
const auto& c1 = config.contexts[1];
358+
const auto& c2 = config.contexts[2];
359+
REQUIRE(c0.inputs.size() == 11);
360+
REQUIRE(c0.outputs.size() == 6);
361+
REQUIRE(c0.command_outputs.size() == 1);
362+
// inputs are inserted at the front
363+
CHECK(format_sequence(c0.inputs[0].input) == "+ShiftLeft ~ShiftLeft");
364+
CHECK(format_sequence(c0.inputs[1].input) == "+ShiftRight ~ShiftRight");
365+
CHECK(format_sequence(c0.inputs[2].input) == "+ControlLeft ~ControlLeft");
366+
CHECK(format_sequence(c0.inputs[3].input) == "+ControlRight ~ControlRight");
367+
CHECK(format_sequence(c0.inputs[4].input) == "+AltLeft ~AltLeft");
368+
CHECK(format_sequence(c0.inputs[5].input) == "+ControlLeft +A ~A ~ControlLeft");
369+
CHECK(format_sequence(c0.inputs[6].input) == "+ControlRight +A ~A ~ControlRight");
370+
CHECK(format_sequence(c0.inputs[7].input) == "+ControlLeft +B ~B ~ControlLeft");
371+
CHECK(format_sequence(c0.inputs[8].input) == "+ControlRight +B ~B ~ControlRight");
372+
CHECK(format_sequence(c0.inputs[9].input) == "+ControlLeft +C ~C ~ControlLeft");
373+
CHECK(format_sequence(c0.inputs[10].input) == "+AltLeft +D ~D ~AltLeft");
374+
// forwarded modifiers in input are suppressed in output
375+
CHECK(format_sequence(c0.command_outputs[0].output) ==
376+
"!AltLeft !ControlRight !ControlLeft +ShiftLeft +Y -Y -ShiftLeft");
377+
CHECK(format_sequence(c0.outputs[0]) ==
378+
"!ControlRight !ControlLeft +ShiftLeft +X -X -ShiftLeft");
379+
// outputs are prepended (in reverse order)
380+
CHECK(format_sequence(c0.outputs[1]) == "+AltLeft");
381+
CHECK(format_sequence(c0.outputs[2]) == "+ControlRight");
382+
CHECK(format_sequence(c0.outputs[3]) == "+ControlLeft");
383+
CHECK(format_sequence(c0.outputs[4]) == "+ShiftRight");
384+
CHECK(format_sequence(c0.outputs[5]) == "+ShiftLeft");
385+
386+
REQUIRE(c1.inputs.size() == 0);
387+
REQUIRE(c1.outputs.size() == 0);
388+
REQUIRE(c1.command_outputs.size() == 1);
389+
CHECK(format_sequence(c1.command_outputs[0].output) ==
390+
"!AltLeft !ControlRight +ControlLeft +Z -Z -ControlLeft");
391+
392+
// modifiers are forwarded in each stage
393+
REQUIRE(c2.inputs.size() == 6);
394+
REQUIRE(c2.outputs.size() == 6);
395+
REQUIRE(c2.command_outputs.empty());
396+
CHECK(format_sequence(c2.inputs[0].input) == "+ShiftLeft ~ShiftLeft");
397+
CHECK(format_sequence(c2.inputs[1].input) == "+ShiftRight ~ShiftRight");
398+
CHECK(format_sequence(c2.inputs[2].input) == "+ControlLeft ~ControlLeft");
399+
CHECK(format_sequence(c2.inputs[3].input) == "+ControlRight ~ControlRight");
400+
CHECK(format_sequence(c2.inputs[4].input) == "+AltLeft ~AltLeft");
401+
CHECK(format_sequence(c2.inputs[5].input) == "+ShiftRight +X ~X ~ShiftRight");
402+
CHECK(format_sequence(c2.outputs[0]) == "!ShiftRight +Z");
403+
}
404+
405+
//--------------------------------------------------------------------
406+
329407
TEST_CASE("System contexts", "[ParseConfig]") {
330408
auto string = R"(
331409
[default]
@@ -609,7 +687,7 @@ TEST_CASE("Stages", "[ParseConfig]") {
609687

610688
auto config = parse_config(string);
611689
REQUIRE(config.contexts.size() == 9);
612-
CHECK(!config.contexts[0].begin_stage);
690+
CHECK(config.contexts[0].begin_stage);
613691
CHECK(config.contexts[1].begin_stage);
614692
CHECK(!config.contexts[2].begin_stage);
615693
CHECK(config.contexts[3].begin_stage);

0 commit comments

Comments
 (0)