Skip to content

Commit

Permalink
Regular expression support for context definition
Browse files Browse the repository at this point in the history
  • Loading branch information
houmain committed Jan 25, 2021
1 parent ea5fe75 commit 9f1c215
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 20 deletions.
29 changes: 23 additions & 6 deletions src/config/Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,30 @@

#include "runtime/KeyEvent.h"
#include <string>
#include <optional>
#include <regex>

struct Filter {
std::string string;
std::optional<std::regex> regex;

explicit operator bool() const { return !string.empty(); }

bool matches(const std::string& text, bool substring) const {
if (string.empty())
return true;
if (regex.has_value())
return std::regex_search(text, *regex);
return (substring ?
text.find(string) != std::string::npos :
text == string);
}
};

struct Context {
bool system_filter_matched;
std::string window_class_filter;
std::string window_title_filter;
Filter window_class_filter;
Filter window_title_filter;
};

struct ContextMapping {
Expand All @@ -33,12 +52,10 @@ inline int find_context(const Config& config,
for (auto i = 0u; i < config.contexts.size(); ++i) {
const auto& context = config.contexts[i];

if (!context.window_class_filter.empty() &&
window_class != context.window_class_filter)
if (!context.window_class_filter.matches(window_class, false))
continue;

if (!context.window_title_filter.empty() &&
window_title.find(context.window_title_filter) == std::string::npos)
if (!context.window_title_filter.matches(window_title, true))
continue;

return static_cast<int>(i);
Expand Down
50 changes: 41 additions & 9 deletions src/config/ParseConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ Config ParseConfig::operator()(std::istream& is, bool add_default_mappings) {
for (auto it = begin(m_config.contexts); it != end(m_config.contexts); ++context_index) {
auto& context = *it;
if (!context.system_filter_matched ||
(context.window_class_filter.empty() &&
context.window_title_filter.empty())) {
(!context.window_class_filter &&
!context.window_title_filter)) {
for (auto& command : m_config.commands) {
auto& mappings = command.context_mappings;
const auto mapping = std::find_if(cbegin(mappings), cend(mappings),
Expand Down Expand Up @@ -171,6 +171,39 @@ void ParseConfig::parse_line(It it, const It end) {
error("unexpected '" + std::string(it, end) + "'");
}

Filter ParseConfig::read_filter(It* it, const It end) {
const auto begin = *it;
if (skip(it, end, "/")) {
// a regular expression
for (;;) {
if (!skip_until(it, end, "/"))
error("unterminated regular expression");
// check for irregular number of preceding backslashes
auto prev = std::prev(*it, 2);
while (prev != begin && *prev == '\\')
prev = std::prev(prev);
if (std::distance(prev, *it) % 2 == 0)
break;
}
auto type = std::regex::ECMAScript;
const auto expr = std::string(begin, *it);
if (skip(it, end, "i"))
type |= std::regex::icase;
return Filter{ expr, std::regex(expr.substr(1, expr.size() - 2), type) };
}
else {
// a string
if (skip(it, end, "'") || skip(it, end, "\"")) {
const char mark[2] = { *(*it - 1), '\0' };
if (!skip_until(it, end, mark))
error("unterminated string");
return Filter{ std::string(begin + 1, *it - 1), { } };
}
skip_value(it, end);
return Filter{ std::string(begin, *it), { } };
}
}

void ParseConfig::parse_context(It it, const It end) {
skip_space(&it, end);

Expand All @@ -179,10 +212,9 @@ void ParseConfig::parse_context(It it, const It end) {
skip(&it, end, "Window");
skip_space(&it, end);

auto class_filter = std::string();
auto title_filter = std::string();
auto system_filter_matched = true;

auto class_filter = Filter();
auto title_filter = Filter();
do {
const auto attrib = read_ident(&it, end);
if (attrib.empty())
Expand All @@ -193,15 +225,15 @@ void ParseConfig::parse_context(It it, const It end) {
error("missing '='");

skip_space(&it, end);
auto value = read_value(&it, end);
if (attrib == "class") {
class_filter = std::move(value);
class_filter = read_filter(&it, end);
}
else if (attrib == "title") {
title_filter = std::move(value);
title_filter = read_filter(&it, end);
}
else if (attrib == "system") {
system_filter_matched = (to_lower(std::move(value)) == current_system);
system_filter_matched =
(to_lower(read_value(&it, end)) == current_system);
}
else {
error("unexpected '" + attrib + "'");
Expand Down
1 change: 1 addition & 0 deletions src/config/ParseConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class ParseConfig {
std::string preprocess_ident(std::string ident) const;
std::string preprocess(It begin, It end) const;
void replace_logical_modifiers(KeyCode both, KeyCode left, KeyCode right);
Filter read_filter(It* it, It end);

bool has_command(const std::string& name) const;
void add_command(std::string name, KeySequence input);
Expand Down
8 changes: 4 additions & 4 deletions src/linux/client/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,10 @@ int main(int argc, char* argv[]) {
if (override_set >= 0) {
verbose("sending 'active context #%i' to keymapperd:", override_set + 1);
const auto& context = config_file.config().contexts[override_set];
if (!context.window_class_filter.empty())
verbose(" class filter = '%s'", context.window_class_filter.c_str());
if (!context.window_title_filter.empty())
verbose(" title filter = '%s'", context.window_title_filter.c_str());
if (const auto& filter = context.window_class_filter)
verbose(" class filter = '%s'", filter.string.c_str());
if (const auto& filter = context.window_title_filter)
verbose(" title filter = '%s'", filter.string.c_str());
}
else {
verbose("sending 'no active context' to keymapperd");
Expand Down
67 changes: 66 additions & 1 deletion src/test/test1_ParseConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ TEST_CASE("Valid config", "[ParseConfig]") {
CommandA >> Y # comment
CommandB >> MyMacro # comment
[system='Linux'] # comment
[system='Linux' title=/firefox/i ] # comment
CommandA >> Shift{Y} # comment
CommandB >> Shift{MyMacro} # comment
)";
Expand Down Expand Up @@ -160,6 +160,71 @@ TEST_CASE("System contexts", "[ParseConfig]") {

//--------------------------------------------------------------------

TEST_CASE("Context filters", "[ParseConfig]") {
auto string = R"(
A >> command
[title = /Title1|Title2/ ]
command >> B
[title = /Title3/i]
command >> C
[title = "Title4"] # substring for titles
command >> D
[title = /^Title5$/]
command >> E
[class = /Class1|Class2/ ]
command >> F
[class = /Class3/i]
command >> G
[class = "Class4"] # exact string for classes
command >> H
[class = /^Class5$/]
command >> I
[class = /^Base\d+$/]
command >> J
)";
auto config = parse_config(string);
REQUIRE(find_context(config, "Some", "Title") == -1);
REQUIRE(find_context(config, "Some", "Title1") == 0);
REQUIRE(find_context(config, "Some", "Title2") == 0);
REQUIRE(find_context(config, "Some", "title1") == -1);
REQUIRE(find_context(config, "Some", "Title3") == 1);
REQUIRE(find_context(config, "Some", "title3") == 1);
REQUIRE(find_context(config, "Some", "Title4") == 2);
REQUIRE(find_context(config, "Some", "_Title4_") == 2);
REQUIRE(find_context(config, "Some", "title4") == -1);
REQUIRE(find_context(config, "Some", "Title5") == 3);
REQUIRE(find_context(config, "Some", "_Title5_") == -1);

REQUIRE(find_context(config, "Class", "Some") == -1);
REQUIRE(find_context(config, "Class1", "Some") == 4);
REQUIRE(find_context(config, "Class2", "Some") == 4);
REQUIRE(find_context(config, "class1", "Some") == -1);
REQUIRE(find_context(config, "Class3", "Some") == 5);
REQUIRE(find_context(config, "class3", "Some") == 5);
REQUIRE(find_context(config, "Class4", "Some") == 6);
REQUIRE(find_context(config, "_Class4_", "Some") == -1);
REQUIRE(find_context(config, "class4", "Some") == -1);
REQUIRE(find_context(config, "Class5", "Some") == 7);
REQUIRE(find_context(config, "_Class5_", "Some") == -1);
REQUIRE(find_context(config, "Base100", "Some") == 8);
REQUIRE(find_context(config, "Base100_", "Some") == -1);

REQUIRE(config.contexts[0].window_title_filter.string == "/Title1|Title2/");
REQUIRE(config.contexts[6].window_class_filter.string == "Class4");
REQUIRE(config.contexts[7].window_class_filter.string == "/^Class5$/");
}

//--------------------------------------------------------------------

TEST_CASE("Macros", "[ParseConfig]") {
// correct
auto string = R"(
Expand Down

0 comments on commit 9f1c215

Please sign in to comment.