mirror of
https://codeberg.org/dnkl/foot.git
synced 2026-02-05 04:06:08 -05:00
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:
parent
f718cb3fb0
commit
051cd6ecfc
8 changed files with 310 additions and 48 deletions
263
config.c
263
config.c
|
|
@ -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(®ex->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(®ex->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, ®ex_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, §ion_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(®ex->preg);
|
||||
spawn_template_free(®ex->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);
|
||||
|
|
|
|||
12
config.h
12
config.h
|
|
@ -61,6 +61,7 @@ enum binding_aux_type {
|
|||
BINDING_AUX_NONE,
|
||||
BINDING_AUX_PIPE,
|
||||
BINDING_AUX_TEXT,
|
||||
BINDING_AUX_REGEX,
|
||||
};
|
||||
|
||||
struct binding_aux {
|
||||
|
|
@ -74,6 +75,8 @@ struct binding_aux {
|
|||
uint8_t *data;
|
||||
size_t len;
|
||||
} text;
|
||||
|
||||
char *regex_name;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -121,6 +124,13 @@ struct env_var {
|
|||
};
|
||||
typedef tll(struct env_var) env_var_list_t;
|
||||
|
||||
struct custom_regex {
|
||||
char *name;
|
||||
char *regex;
|
||||
regex_t preg;
|
||||
struct config_spawn_template launch;
|
||||
};
|
||||
|
||||
struct config {
|
||||
char *term;
|
||||
char *shell;
|
||||
|
|
@ -225,6 +235,8 @@ struct config {
|
|||
regex_t preg;
|
||||
} url;
|
||||
|
||||
tll(struct custom_regex) custom_regexes;
|
||||
|
||||
struct {
|
||||
uint32_t fg;
|
||||
uint32_t bg;
|
||||
|
|
|
|||
4
foot.ini
4
foot.ini
|
|
@ -71,7 +71,9 @@
|
|||
# osc8-underline=url-mode
|
||||
# regex=([a-z][[:alnum:]-]+:(/{1,3}|[a-z0-9%])|www[:digit:]{0,3}[.])([^[:space:](){}<>]+|\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))*\])+(\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))*\]|[^]\[[:space:]`!(){};:'".,<>?«»“”‘’])
|
||||
|
||||
|
||||
# [regex:your-fancy-name]
|
||||
# regex=<a POSIX-Extended Regular Expression>
|
||||
# launch=<path to script or application> {match}
|
||||
|
||||
[cursor]
|
||||
# style=block
|
||||
|
|
|
|||
40
input.c
40
input.c
|
|
@ -349,9 +349,9 @@ execute_binding(struct seat *seat, struct terminal *term,
|
|||
action == BIND_ACTION_SHOW_URLS_LAUNCH ? URL_ACTION_LAUNCH :
|
||||
URL_ACTION_PERSISTENT;
|
||||
|
||||
urls_collect(term, url_action, &term->urls);
|
||||
urls_collect(term, url_action, &term->conf->url.preg, true, &term->urls);
|
||||
urls_assign_key_combos(term->conf, &term->urls);
|
||||
urls_render(term);
|
||||
urls_render(term, &term->conf->url.launch);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -448,6 +448,42 @@ execute_binding(struct seat *seat, struct terminal *term,
|
|||
term_shutdown(term);
|
||||
return true;
|
||||
|
||||
case BIND_ACTION_REGEX_LAUNCH:
|
||||
case BIND_ACTION_REGEX_COPY:
|
||||
if (binding->aux->type != BINDING_AUX_REGEX)
|
||||
return true;
|
||||
|
||||
tll_foreach(term->conf->custom_regexes, it) {
|
||||
const struct custom_regex *regex = &it->item;
|
||||
|
||||
if (streq(regex->name, binding->aux->regex_name)) {
|
||||
xassert(!urls_mode_is_active(term));
|
||||
|
||||
enum url_action url_action = action == BIND_ACTION_REGEX_LAUNCH
|
||||
? URL_ACTION_LAUNCH : URL_ACTION_COPY;
|
||||
|
||||
if (regex->regex == NULL) {
|
||||
LOG_ERR("regex:%s has no regex defined", regex->name);
|
||||
return true;
|
||||
}
|
||||
if (url_action == URL_ACTION_LAUNCH && regex->launch.argv.args == NULL) {
|
||||
LOG_ERR("regex:%s has no launch command defined", regex->name);
|
||||
return true;
|
||||
}
|
||||
|
||||
urls_collect(term, url_action, ®ex->preg, false, &term->urls);
|
||||
urls_assign_key_combos(term->conf, &term->urls);
|
||||
urls_render(term, ®ex->launch);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_ERR(
|
||||
"no regex section named '%s' defined in the configuration",
|
||||
binding->aux->regex_name);
|
||||
|
||||
return true;
|
||||
|
||||
case BIND_ACTION_SELECT_BEGIN:
|
||||
selection_start(
|
||||
term, seat->mouse.col, seat->mouse.row, SELECTION_CHAR_WISE, false);
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ enum bind_action_normal {
|
|||
BIND_ACTION_PROMPT_NEXT,
|
||||
BIND_ACTION_UNICODE_INPUT,
|
||||
BIND_ACTION_QUIT,
|
||||
BIND_ACTION_REGEX_LAUNCH,
|
||||
BIND_ACTION_REGEX_COPY,
|
||||
|
||||
/* Mouse specific actions - i.e. they require a mouse coordinate */
|
||||
BIND_ACTION_SCROLLBACK_UP_MOUSE,
|
||||
|
|
@ -54,7 +56,7 @@ enum bind_action_normal {
|
|||
BIND_ACTION_SELECT_QUOTE,
|
||||
BIND_ACTION_SELECT_ROW,
|
||||
|
||||
BIND_ACTION_KEY_COUNT = BIND_ACTION_QUIT + 1,
|
||||
BIND_ACTION_KEY_COUNT = BIND_ACTION_REGEX_COPY + 1,
|
||||
BIND_ACTION_COUNT = BIND_ACTION_SELECT_ROW + 1,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -789,6 +789,7 @@ struct terminal {
|
|||
bool urls_show_uri_on_jump_label;
|
||||
struct grid *url_grid_snapshot;
|
||||
bool ime_reenable_after_url_mode;
|
||||
const struct config_spawn_template *url_launch;
|
||||
|
||||
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
|
||||
bool ime_enabled;
|
||||
|
|
|
|||
29
url-mode.c
29
url-mode.c
|
|
@ -67,12 +67,13 @@ spawn_url_launcher_with_token(struct terminal *term,
|
|||
return false;
|
||||
}
|
||||
|
||||
xassert(term->url_launch != NULL);
|
||||
bool ret = false;
|
||||
|
||||
if (spawn_expand_template(
|
||||
&term->conf->url.launch, 1,
|
||||
(const char *[]){"url"},
|
||||
(const char *[]){url},
|
||||
term->url_launch, 2,
|
||||
(const char *[]){"url", "match"},
|
||||
(const char *[]){url, url},
|
||||
&argc, &argv))
|
||||
{
|
||||
ret = spawn(
|
||||
|
|
@ -84,6 +85,8 @@ spawn_url_launcher_with_token(struct terminal *term,
|
|||
free(argv);
|
||||
}
|
||||
|
||||
term->url_launch = NULL;
|
||||
|
||||
close(dev_null);
|
||||
return ret;
|
||||
}
|
||||
|
|
@ -107,6 +110,8 @@ static bool
|
|||
spawn_url_launcher(struct seat *seat, struct terminal *term, const char *url,
|
||||
uint32_t serial)
|
||||
{
|
||||
xassert(term->url_launch != NULL);
|
||||
|
||||
struct spawn_activation_context *ctx = xmalloc(sizeof(*ctx));
|
||||
*ctx = (struct spawn_activation_context){
|
||||
.term = term,
|
||||
|
|
@ -300,7 +305,8 @@ struct vline {
|
|||
};
|
||||
|
||||
static void
|
||||
regex_detected(const struct terminal *term, enum url_action action, url_list_t *urls)
|
||||
regex_detected(const struct terminal *term, enum url_action action,
|
||||
const regex_t *preg, url_list_t *urls)
|
||||
{
|
||||
/*
|
||||
* Use regcomp()+regexec() to find patterns.
|
||||
|
|
@ -381,8 +387,6 @@ regex_detected(const struct terminal *term, enum url_action action, url_list_t *
|
|||
}
|
||||
}
|
||||
|
||||
const regex_t *preg = &term->conf->url.preg;
|
||||
|
||||
for (size_t i = 0; i < ALEN(vlines); i++) {
|
||||
const struct vline *v = &vlines[i];
|
||||
if (v->utf8 == NULL)
|
||||
|
|
@ -523,11 +527,13 @@ remove_overlapping(url_list_t *urls, int cols)
|
|||
}
|
||||
|
||||
void
|
||||
urls_collect(const struct terminal *term, enum url_action action, url_list_t *urls)
|
||||
urls_collect(const struct terminal *term, enum url_action action,
|
||||
const regex_t *preg, bool osc8, url_list_t *urls)
|
||||
{
|
||||
xassert(tll_length(term->urls) == 0);
|
||||
osc8_uris(term, action, urls);
|
||||
regex_detected(term, action, urls);
|
||||
if (osc8)
|
||||
osc8_uris(term, action, urls);
|
||||
regex_detected(term, action, preg, urls);
|
||||
remove_overlapping(urls, term->grid->num_cols);
|
||||
}
|
||||
|
||||
|
|
@ -710,7 +716,7 @@ tag_cells_for_url(struct terminal *term, const struct url *url, bool value)
|
|||
}
|
||||
|
||||
void
|
||||
urls_render(struct terminal *term)
|
||||
urls_render(struct terminal *term, const struct config_spawn_template *launch)
|
||||
{
|
||||
struct wl_window *win = term->window;
|
||||
|
||||
|
|
@ -745,6 +751,9 @@ urls_render(struct terminal *term)
|
|||
/* Snapshot the current grid */
|
||||
term->url_grid_snapshot = grid_snapshot(term->grid);
|
||||
|
||||
/* Remember which launcher to use */
|
||||
term->url_launch = launch;
|
||||
|
||||
xassert(tll_length(win->urls) == 0);
|
||||
tll_foreach(win->term->urls, it) {
|
||||
struct wl_url url = {.url = &it->item};
|
||||
|
|
|
|||
|
|
@ -14,10 +14,11 @@ static inline bool urls_mode_is_active(const struct terminal *term)
|
|||
}
|
||||
|
||||
void urls_collect(
|
||||
const struct terminal *term, enum url_action action, url_list_t *urls);
|
||||
const struct terminal *term, enum url_action action, const regex_t *preg,
|
||||
bool osc8, url_list_t *urls);
|
||||
void urls_assign_key_combos(const struct config *conf, url_list_t *urls);
|
||||
|
||||
void urls_render(struct terminal *term);
|
||||
void urls_render(struct terminal *term, const struct config_spawn_template *launch);
|
||||
void urls_reset(struct terminal *term);
|
||||
|
||||
void urls_input(struct seat *seat, struct terminal *term,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue