Skip to content

Commit

Permalink
Added @forward-modifiers directive
Browse files Browse the repository at this point in the history
  • Loading branch information
houmain committed Sep 5, 2024
1 parent 5455fb6 commit 071ca7d
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 4 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/config/Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ struct Config {
};

struct Context {
bool system_filter_matched{ };
Filter window_class_filter;
Filter window_title_filter;
Filter window_path_filter;
Expand All @@ -26,6 +25,7 @@ struct Config {
std::vector<Input> inputs;
std::vector<KeySequence> outputs;
std::vector<CommandOutput> command_outputs;
bool system_filter_matched{ };
bool invert_modifier_filter{ };
bool fallthrough{ };
bool begin_stage{ };
Expand Down
94 changes: 92 additions & 2 deletions src/config/ParseConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand All @@ -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();

Expand All @@ -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))
Expand Down Expand Up @@ -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 + "'");
}
Expand Down Expand Up @@ -453,6 +469,30 @@ KeySequence ParseConfig::parse_modifier_list(std::string_view string) {
return sequence;
}

std::vector<Key> ParseConfig::parse_forward_modifiers_list(It* it, const It end) {
auto modifiers = std::vector<Key>();
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;
Expand Down Expand Up @@ -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
Expand All @@ -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<int>(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);
}
}
}
}
}
}
4 changes: 4 additions & 0 deletions src/config/ParseConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<Key> 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;
Expand All @@ -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);
Expand All @@ -80,4 +83,5 @@ class ParseConfig {
bool m_after_empty_context_block{ };
bool m_enforce_lowercase_commands{ };
bool m_allow_unmapped_commands{ };
std::vector<Key> m_forward_modifiers;
};
80 changes: 79 additions & 1 deletion src/test/test1_ParseConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 071ca7d

Please sign in to comment.