config+url: add support for user-defined regex patterns

Users can now define their own regex patterns, and use them via key
bindings:

    [regex:foo]
    regex=foo(bar)?
    launch=path-to-script-or-application {match}

    [key-bindings]
    regex-launch=[foo] Control+Shift+q
    regex-copy=[foo] Control+Mod1+Shift+q

That is, add a section called 'regex:', followed by an
identifier. Define a regex and a launcher command line.

Add a key-binding, regex-launch and/or regex-copy (similar to
show-urls-launch and show-urls-copy), and connect them to the regex
with the "[regex-name]" syntax (similar to how the pipe-* bindings
work).
This commit is contained in:
Daniel Eklöf 2025-02-03 08:55:47 +01:00
parent f718cb3fb0
commit 051cd6ecfc
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
8 changed files with 310 additions and 48 deletions

263
config.c
View file

@ -140,6 +140,8 @@ static const char *const binding_action_map[] = {
[BIND_ACTION_PROMPT_NEXT] = "prompt-next",
[BIND_ACTION_UNICODE_INPUT] = "unicode-input",
[BIND_ACTION_QUIT] = "quit",
[BIND_ACTION_REGEX_LAUNCH] = "regex-launch",
[BIND_ACTION_REGEX_COPY] = "regex-copy",
/* Mouse-specific actions */
[BIND_ACTION_SCROLLBACK_UP_MOUSE] = "scrollback-up-mouse",
@ -207,6 +209,7 @@ static_assert(ALEN(url_binding_action_map) == BIND_ACTION_URL_COUNT,
struct context {
struct config *conf;
const char *section;
const char *section_suffix;
const char *key;
const char *value;
@ -257,8 +260,9 @@ log_contextual(struct context *ctx, enum log_class log_class,
char *formatted_msg = xvasprintf(fmt, va);
va_end(va);
bool print_dot = ctx->key != NULL;
bool print_colon = ctx->value != NULL;
const bool print_dot = ctx->key != NULL;
const bool print_colon = ctx->value != NULL;
const bool print_section_suffix = ctx->section_suffix != NULL;
if (!print_dot)
ctx->key = "";
@ -266,10 +270,15 @@ log_contextual(struct context *ctx, enum log_class log_class,
if (!print_colon)
ctx->value = "";
if (!print_section_suffix)
ctx->section_suffix = "";
log_and_notify(
ctx->conf, log_class, file, lineno, "%s:%d: [%s]%s%s%s%s: %s",
ctx->path, ctx->lineno, ctx->section, print_dot ? "." : "",
ctx->key, print_colon ? ": " : "", ctx->value, formatted_msg);
ctx->conf, log_class, file, lineno, "%s:%d: [%s%s%s]%s%s%s%s: %s",
ctx->path, ctx->lineno, ctx->section,
print_section_suffix ? ":" : "", ctx->section_suffix,
print_dot ? "." : "", ctx->key, print_colon ? ": " : "",
ctx->value, formatted_msg);
free(formatted_msg);
}
@ -1261,6 +1270,72 @@ parse_section_url(struct context *ctx)
}
}
static bool
parse_section_regex(struct context *ctx)
{
struct config *conf = ctx->conf;
const char *key = ctx->key;
const char *regex_name =
ctx->section_suffix != NULL ? ctx->section_suffix : "";
struct custom_regex *regex = NULL;
tll_foreach(conf->custom_regexes, it) {
if (streq(it->item.name, regex_name)) {
regex = &it->item;
break;
}
}
if (streq(key, "regex")) {
const char *regex_string = ctx->value;
regex_t preg;
int r = regcomp(&preg, regex_string, REG_EXTENDED);
if (r != 0) {
char err_buf[128];
regerror(r, &preg, err_buf, sizeof(err_buf));
LOG_CONTEXTUAL_ERR("invalid regex: %s", err_buf);
return false;
}
if (regex == NULL) {
tll_push_back(conf->custom_regexes,
((struct custom_regex){.name = xstrdup(regex_name)}));
regex = &tll_back(conf->custom_regexes);
}
regfree(&regex->preg);
free(regex->regex);
regex->regex = xstrdup(regex_string);
regex->preg = preg;
return true;
}
else if (streq(key, "launch")) {
struct config_spawn_template launch;
if (!value_to_spawn_template(ctx, &launch))
return false;
if (regex == NULL) {
tll_push_back(conf->custom_regexes,
((struct custom_regex){.name = xstrdup(regex_name)}));
regex = &tll_back(conf->custom_regexes);
}
spawn_template_free(&regex->launch);
regex->launch = launch;
return true;
}
else {
LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
return false;
}
}
static bool
parse_section_colors(struct context *ctx)
{
@ -1602,6 +1677,7 @@ free_binding_aux(struct binding_aux *aux)
case BINDING_AUX_NONE: break;
case BINDING_AUX_PIPE: free_argv(&aux->pipe); break;
case BINDING_AUX_TEXT: free(aux->text.data); break;
case BINDING_AUX_REGEX: free(aux->regex_name); break;
}
}
@ -1691,7 +1767,10 @@ binding_aux_equal(const struct binding_aux *a,
case BINDING_AUX_TEXT:
return a->text.len == b->text.len &&
memcmp(a->text.data, b->text.data, a->text.len) == 0;
memcmp(a->text.data, b->text.data, a->text.len) == 0;
case BINDING_AUX_REGEX:
return streq(a->regex_name, b->regex_name);
}
BUG("invalid AUX type: %d", a->type);
@ -1965,19 +2044,23 @@ modifiers_disjoint(const config_modifier_list_t *mods1,
}
static char * NOINLINE
modifiers_to_str(const config_modifier_list_t *mods)
modifiers_to_str(const config_modifier_list_t *mods, bool strip_last_plus)
{
size_t len = tll_length(*mods); /* '+' , and NULL terminator */
size_t len = tll_length(*mods); /* '+' separator */
tll_foreach(*mods, it)
len += strlen(it->item);
char *ret = xmalloc(len);
char *ret = xmalloc(len + 1);
size_t idx = 0;
tll_foreach(*mods, it) {
idx += snprintf(&ret[idx], len - idx, "%s", it->item);
ret[idx++] = '+';
}
ret[--idx] = '\0';
if (strip_last_plus)
idx--;
ret[idx] = '\0';
return ret;
}
@ -2036,21 +2119,40 @@ pipe_argv_from_value(struct context *ctx, struct argv *argv)
return remove_len;
}
static ssize_t NOINLINE
regex_name_from_value(struct context *ctx, char **regex_name)
{
*regex_name = NULL;
if (ctx->value[0] != '[')
return 0;
const char *regex_end = strrchr(ctx->value, ']');
if (regex_end == NULL) {
LOG_CONTEXTUAL_ERR("unclosed '['");
return -1;
}
size_t regex_len = regex_end - ctx->value - 1;
*regex_name = xstrndup(&ctx->value[1], regex_len);
ssize_t remove_len = regex_end + 1 - ctx->value;
ctx->value = regex_end + 1;
while (isspace(*ctx->value)) {
ctx->value++;
remove_len++;
}
return remove_len;
}
static bool NOINLINE
parse_key_binding_section(struct context *ctx,
int action_count,
const char *const action_map[static action_count],
struct config_key_binding_list *bindings)
{
struct binding_aux aux;
ssize_t pipe_remove_len = pipe_argv_from_value(ctx, &aux.pipe);
if (pipe_remove_len < 0)
return false;
aux.type = pipe_remove_len == 0 ? BINDING_AUX_NONE : BINDING_AUX_PIPE;
aux.master_copy = true;
for (int action = 0; action < action_count; action++) {
if (action_map[action] == NULL)
continue;
@ -2058,6 +2160,33 @@ parse_key_binding_section(struct context *ctx,
if (!streq(ctx->key, action_map[action]))
continue;
struct binding_aux aux = {.type = BINDING_AUX_NONE, .master_copy = true};
/* TODO: this is ugly... */
if (action_map == binding_action_map &&
action >= BIND_ACTION_PIPE_SCROLLBACK &&
action <= BIND_ACTION_PIPE_COMMAND_OUTPUT)
{
ssize_t pipe_remove_len = pipe_argv_from_value(ctx, &aux.pipe);
if (pipe_remove_len <= 0)
return false;
aux.type = BINDING_AUX_PIPE;
aux.master_copy = true;
} else if (action_map == binding_action_map &&
action >= BIND_ACTION_REGEX_LAUNCH &&
action <= BIND_ACTION_REGEX_COPY)
{
char *regex_name = NULL;
ssize_t regex_remove_len = regex_name_from_value(ctx, &regex_name);
if (regex_remove_len <= 0)
return false;
aux.type = BINDING_AUX_REGEX;
aux.master_copy = true;
aux.regex_name = regex_name;
}
if (!value_to_key_combos(ctx, action, &aux, bindings, KEY_BINDING)) {
free_binding_aux(&aux);
return false;
@ -2067,7 +2196,6 @@ parse_key_binding_section(struct context *ctx,
}
LOG_CONTEXTUAL_ERR("not a valid action: %s", ctx->key);
free_binding_aux(&aux);
return false;
}
@ -2265,7 +2393,7 @@ resolve_key_binding_collisions(struct config *conf, const char *section_name,
}
if (collision_type != COLLISION_NONE) {
char *modifier_names = modifiers_to_str(mods1);
char *modifier_names = modifiers_to_str(mods1, false);
char sym_name[64];
switch (type){
@ -2307,7 +2435,7 @@ resolve_key_binding_collisions(struct config *conf, const char *section_name,
case COLLISION_OVERRIDE: {
char *override_names = modifiers_to_str(
&conf->mouse.selection_override_modifiers);
&conf->mouse.selection_override_modifiers, true);
if (override_names[0] != '\0')
override_names[strlen(override_names) - 1] = '\0';
@ -2646,7 +2774,7 @@ parse_section_touch(struct context *ctx) {
}
static bool
parse_key_value(char *kv, const char **section, const char **key, const char **value)
parse_key_value(char *kv, char **section, const char **key, const char **value)
{
bool section_is_needed = section != NULL;
@ -2715,6 +2843,7 @@ enum section {
SECTION_DESKTOP_NOTIFICATIONS,
SECTION_SCROLLBACK,
SECTION_URL,
SECTION_REGEX,
SECTION_COLORS,
SECTION_CURSOR,
SECTION_MOUSE,
@ -2736,6 +2865,7 @@ typedef bool (*parser_fun_t)(struct context *ctx);
static const struct {
parser_fun_t fun;
const char *name;
bool allow_colon_suffix;
} section_info[] = {
[SECTION_MAIN] = {&parse_section_main, "main"},
[SECTION_SECURITY] = {&parse_section_security, "security"},
@ -2743,6 +2873,7 @@ static const struct {
[SECTION_DESKTOP_NOTIFICATIONS] = {&parse_section_desktop_notifications, "desktop-notifications"},
[SECTION_SCROLLBACK] = {&parse_section_scrollback, "scrollback"},
[SECTION_URL] = {&parse_section_url, "url"},
[SECTION_REGEX] = {&parse_section_regex, "regex", true},
[SECTION_COLORS] = {&parse_section_colors, "colors"},
[SECTION_CURSOR] = {&parse_section_cursor, "cursor"},
[SECTION_MOUSE] = {&parse_section_mouse, "mouse"},
@ -2760,11 +2891,29 @@ static const struct {
static_assert(ALEN(section_info) == SECTION_COUNT, "section info array size mismatch");
static enum section
str_to_section(const char *str)
str_to_section(char *str, char **suffix)
{
*suffix = NULL;
for (enum section section = SECTION_MAIN; section < SECTION_COUNT; ++section) {
if (streq(str, section_info[section].name))
const char *name = section_info[section].name;
if (streq(str, name))
return section;
else if (section_info[section].allow_colon_suffix) {
const size_t str_len = strlen(str);
const size_t name_len = strlen(name);
/* At least "section:" chars? */
if (str_len > name_len + 1) {
if (strncmp(str, name, name_len) == 0 && str[name_len] == ':') {
str[name_len] = '\0';
*suffix = &str[name_len + 1];
return section;
}
}
}
}
return SECTION_COUNT;
}
@ -2788,10 +2937,12 @@ parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_ar
}
char *section_name = xstrdup("main");
char *section_suffix = NULL;
struct context context = {
.conf = conf,
.section = section_name,
.section_suffix = section_suffix,
.path = path,
.lineno = 0,
.errors_are_fatal = errors_are_fatal,
@ -2872,7 +3023,8 @@ parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_ar
error_or_continue();
}
section = str_to_section(key_value);
char *maybe_section_suffix;
section = str_to_section(key_value, &maybe_section_suffix);
if (section == SECTION_COUNT) {
context.section = key_value;
LOG_CONTEXTUAL_ERR("invalid section name: %s", key_value);
@ -2881,8 +3033,11 @@ parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_ar
}
free(section_name);
free(section_suffix);
section_name = xstrdup(key_value);
section_suffix = maybe_section_suffix != NULL ? xstrdup(maybe_section_suffix) : NULL;
context.section = section_name;
context.section_suffix = section_suffix;
/* Process next line */
continue;
@ -2922,6 +3077,7 @@ parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_ar
done:
free(section_name);
free(section_suffix);
free(_line);
return ret;
}
@ -3016,7 +3172,6 @@ add_default_search_bindings(struct config *conf)
{BIND_ACTION_SEARCH_DELETE_NEXT_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Delete}}},
{BIND_ACTION_SEARCH_DELETE_NEXT_WORD, m(XKB_MOD_NAME_ALT), {{XKB_KEY_d}}},
{BIND_ACTION_SEARCH_EXTEND_CHAR, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Right}}},
{BIND_ACTION_SEARCH_EXTEND_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_w}}},
{BIND_ACTION_SEARCH_EXTEND_WORD, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_Right}}},
{BIND_ACTION_SEARCH_EXTEND_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_w}}},
{BIND_ACTION_SEARCH_EXTEND_WORD_WS, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_w}}},
@ -3146,6 +3301,7 @@ config_load(struct config *conf, const char *conf_path,
.label_letters = xc32dup(U"sadfjklewcmpgh"),
.osc8_underline = OSC8_UNDERLINE_URL_MODE,
},
.custom_regexes = tll_init(),
.can_shape_grapheme = fcft_caps & FCFT_CAPABILITY_GRAPHEME_SHAPING,
.scrollback = {
.lines = 1000,
@ -3385,6 +3541,8 @@ bool
config_override_apply(struct config *conf, config_override_t *overrides,
bool errors_are_fatal)
{
char *section_name = NULL;
struct context context = {
.conf = conf,
.path = "override",
@ -3396,8 +3554,7 @@ config_override_apply(struct config *conf, config_override_t *overrides,
tll_foreach(*overrides, it) {
context.lineno++;
if (!parse_key_value(
it->item, &context.section, &context.key, &context.value))
if (!parse_key_value(it->item, &section_name, &context.key, &context.value))
{
LOG_CONTEXTUAL_ERR("syntax error: key/value pair has no %s",
context.key == NULL ? "key" : "value");
@ -3406,20 +3563,28 @@ config_override_apply(struct config *conf, config_override_t *overrides,
continue;
}
if (context.section[0] == '\0') {
if (section_name[0] == '\0') {
LOG_CONTEXTUAL_ERR("empty section name");
if (errors_are_fatal)
return false;
continue;
}
enum section section = str_to_section(context.section);
LOG_ERR("section-name=%s", section_name);
char *maybe_section_suffix = NULL;
enum section section = str_to_section(section_name, &maybe_section_suffix);
context.section = section_name;
context.section_suffix = maybe_section_suffix;
if (section == SECTION_COUNT) {
LOG_CONTEXTUAL_ERR("invalid section name: %s", context.section);
LOG_CONTEXTUAL_ERR("invalid section name: %s", section_name);
if (errors_are_fatal)
return false;
continue;
}
parser_fun_t section_parser = section_info[section].fun;
xassert(section_parser != NULL);
@ -3455,6 +3620,7 @@ key_binding_list_clone(struct config_key_binding_list *dst,
struct argv *last_master_argv = NULL;
uint8_t *last_master_text_data = NULL;
size_t last_master_text_len = 0;
char *last_master_regex_name = NULL;
dst->count = src->count;
dst->arr = xmalloc(src->count * sizeof(dst->arr[0]));
@ -3502,6 +3668,16 @@ key_binding_list_clone(struct config_key_binding_list *dst,
}
last_master_argv = NULL;
break;
case BINDING_AUX_REGEX:
if (old->aux.master_copy) {
new->aux.regex_name = xstrdup(old->aux.regex_name);
last_master_regex_name = new->aux.regex_name;
} else {
xassert(last_master_regex_name != NULL);
new->aux.regex_name = last_master_regex_name;
}
break;
}
}
}
@ -3536,6 +3712,20 @@ config_clone(const struct config *old)
conf->url.regex = xstrdup(old->url.regex);
regcomp(&conf->url.preg, conf->url.regex, REG_EXTENDED);
memset(&conf->custom_regexes, 0, sizeof(conf->custom_regexes));
tll_foreach(old->custom_regexes, it) {
const struct custom_regex *old_regex = &it->item;
tll_push_back(conf->custom_regexes,
((struct custom_regex){.name = xstrdup(old_regex->name),
.regex = xstrdup(old_regex->regex)}));
struct custom_regex *new_regex = &tll_back(conf->custom_regexes);
regcomp(&new_regex->preg, new_regex->regex, REG_EXTENDED);
spawn_template_clone(&new_regex->launch, &old_regex->launch);
}
key_binding_list_clone(&conf->bindings.key, &old->bindings.key);
key_binding_list_clone(&conf->bindings.search, &old->bindings.search);
key_binding_list_clone(&conf->bindings.url, &old->bindings.url);
@ -3618,6 +3808,15 @@ config_free(struct config *conf)
regfree(&conf->url.preg);
free(conf->url.regex);
tll_foreach(conf->custom_regexes, it) {
struct custom_regex *regex = &it->item;
free(regex->name);
free(regex->regex);
regfree(&regex->preg);
spawn_template_free(&regex->launch);
tll_remove(conf->custom_regexes, it);
}
free_key_binding_list(&conf->bindings.key);
free_key_binding_list(&conf->bindings.search);
free_key_binding_list(&conf->bindings.url);