From 22e3be40e5247f376160ba64dc4535a090c6cbd1 Mon Sep 17 00:00:00 2001 From: Consus Date: Mon, 28 Aug 2023 19:14:04 +0300 Subject: [PATCH] action: add If and ForEach actions Add If and ForEach actions as described in OpenBox specification. Limitations: - If and ForEach cannot contain nested If and ForEach. --- docs/labwc-actions.5.scd | 52 +++++++++++++++- include/action.h | 5 ++ src/action.c | 128 +++++++++++++++++++++++++++++++++++++++ src/config/rcxml.c | 125 +++++++++++++++++++++++++++++++++++++- 4 files changed, 307 insertions(+), 3 deletions(-) diff --git a/docs/labwc-actions.5.scd b/docs/labwc-actions.5.scd index 287eb28e..d21a79b8 100644 --- a/docs/labwc-actions.5.scd +++ b/docs/labwc-actions.5.scd @@ -148,6 +148,56 @@ Actions are used in menus and keyboard/mouse bindings. ** If used as the only action for a binding: clear an earlier defined binding. +# CONDITIONAL ACTIONS + +Actions that execute other actions. Used in keyboard/mouse bindings. + +** + This action will execute one set of actions if the focused window + matches the criteria, or another if it does not. + + The arguments are as follows: + + ``` + + + + + + ``` + + *query* + Define a query with zero or more conditions. All conditions must + be evaluated as true in order for the window to match this + query. Multiple queries can be defined. + + Pattern matching is done according to glob(7) and is + case-insensitive. + + Conditions are as follows: + + *identifier* + XDG shell app_id for Wayland clients, WM_CLASS for + XWayland clients. + + *title* + XDG shell title for Wayland clients, WM_NAME for + XWayland clients. + + This argument is optional. + + *then* + A list of actions to be executed if the window matches any + query. This argument is optional. + + *else* + A list of actions to be executed if the window does not match + any query. This argument is optional. + +** + Identical to "If" action, but applies to all windows, not just the + focused one. + # SEE ALSO -labwc(1), labwc-config(5), labwc-theme(5) +labwc(1), labwc-config(5), labwc-theme(5), glob(7) diff --git a/include/action.h b/include/action.h index a76aecf5..76dd54f8 100644 --- a/include/action.h +++ b/include/action.h @@ -23,6 +23,11 @@ struct action *action_create(const char *action_name); bool action_is_valid(struct action *action); void action_arg_add_str(struct action *action, const char *key, const char *value); +void action_arg_add_actionlist(struct action *action, const char *key); +void action_arg_add_querylist(struct action *action, const char *key); + +struct wl_list *action_get_actionlist(struct action *action, const char *key); +struct wl_list *action_get_querylist(struct action *action, const char *key); void action_arg_from_xml_node(struct action *action, const char *nodename, const char *content); diff --git a/src/action.c b/src/action.c index 7d91c49a..414c8492 100644 --- a/src/action.c +++ b/src/action.c @@ -7,6 +7,7 @@ #include #include #include "action.h" +#include "common/array-size.h" #include "common/list.h" #include "common/mem.h" #include "common/parse-bool.h" @@ -24,6 +25,8 @@ enum action_arg_type { LAB_ACTION_ARG_STR = 0, LAB_ACTION_ARG_BOOL, LAB_ACTION_ARG_INT, + LAB_ACTION_ARG_QUERY_LIST, + LAB_ACTION_ARG_ACTION_LIST, }; struct action_arg { @@ -48,6 +51,11 @@ struct action_arg_int { int value; }; +struct action_arg_list { + struct action_arg base; + struct wl_list value; +}; + enum action_type { ACTION_TYPE_INVALID = 0, ACTION_TYPE_NONE, @@ -82,6 +90,8 @@ enum action_type { ACTION_TYPE_SNAP_TO_REGION, ACTION_TYPE_TOGGLE_KEYBINDS, ACTION_TYPE_FOCUS_OUTPUT, + ACTION_TYPE_IF, + ACTION_TYPE_FOR_EACH, }; const char *action_names[] = { @@ -118,6 +128,8 @@ const char *action_names[] = { "SnapToRegion", "ToggleKeybinds", "FocusOutput", + "If", + "ForEach", NULL }; @@ -158,6 +170,30 @@ action_arg_add_int(struct action *action, const char *key, int value) wl_list_append(&action->args, &arg->base.link); } +static void +action_arg_add_list(struct action *action, const char *key, enum action_arg_type type) +{ + assert(action); + assert(key); + struct action_arg_list *arg = znew(*arg); + arg->base.type = type; + arg->base.key = xstrdup(key); + wl_list_init(&arg->value); + wl_list_append(&action->args, &arg->base.link); +} + +void +action_arg_add_querylist(struct action *action, const char *key) +{ + action_arg_add_list(action, key, LAB_ACTION_ARG_QUERY_LIST); +} + +void +action_arg_add_actionlist(struct action *action, const char *key) +{ + action_arg_add_list(action, key, LAB_ACTION_ARG_ACTION_LIST); +} + static void * action_get_arg(struct action *action, const char *key, enum action_arg_type type) { @@ -193,6 +229,20 @@ action_get_int(struct action *action, const char *key, int default_value) return arg ? arg->value : default_value; } +struct wl_list * +action_get_querylist(struct action *action, const char *key) +{ + struct action_arg_list *arg = action_get_arg(action, key, LAB_ACTION_ARG_QUERY_LIST); + return arg ? &arg->value : NULL; +} + +struct wl_list * +action_get_actionlist(struct action *action, const char *key) +{ + struct action_arg_list *arg = action_get_arg(action, key, LAB_ACTION_ARG_ACTION_LIST); + return arg ? &arg->value : NULL; +} + void action_arg_from_xml_node(struct action *action, const char *nodename, const char *content) { @@ -327,6 +377,20 @@ actions_contain_toggle_keybinds(struct wl_list *action_list) return false; } +static bool +action_list_is_valid(struct wl_list *actions) +{ + assert(actions); + + struct action *action; + wl_list_for_each(action, actions, link) { + if (!action_is_valid(action)) { + return false; + } + } + return true; +} + /* Checks for *required* arguments */ bool action_is_valid(struct action *action) @@ -356,6 +420,19 @@ action_is_valid(struct action *action) case ACTION_TYPE_FOCUS_OUTPUT: arg_name = "output"; break; + case ACTION_TYPE_IF: + case ACTION_TYPE_FOR_EACH: + ; /* works around "a label can only be part of a statement" */ + static const char * const branches[] = { "then", "else" }; + for (size_t i = 0; i < ARRAY_SIZE(branches); i++) { + struct wl_list *children = action_get_actionlist(action, branches[i]); + if (children && !action_list_is_valid(children)) { + wlr_log(WLR_ERROR, "Invalid action in %s '%s' branch", + action_names[action->type], branches[i]); + return false; + } + } + return true; default: /* No arguments required */ return true; @@ -381,6 +458,15 @@ action_free(struct action *action) if (arg->type == LAB_ACTION_ARG_STR) { struct action_arg_str *str_arg = (struct action_arg_str *)arg; zfree(str_arg->value); + } else if (arg->type == LAB_ACTION_ARG_ACTION_LIST) { + struct action_arg_list *list_arg = (struct action_arg_list *)arg; + action_list_free(&list_arg->value); + } else if (arg->type == LAB_ACTION_ARG_QUERY_LIST) { + struct action_arg_list *list_arg = (struct action_arg_list *)arg; + struct view_query *elm, *next; + wl_list_for_each_safe(elm, next, &list_arg->value, link) { + view_query_free(elm); + } } zfree(arg); } @@ -466,6 +552,31 @@ view_for_action(struct view *activator, struct server *server, } } +static void +run_if_action(struct view *view, struct server *server, struct action *action) +{ + struct view_query *query; + struct wl_list *queries, *actions; + const char *branch = "then"; + + queries = action_get_querylist(action, "query"); + if (queries) { + branch = "else"; + /* All queries are OR'ed */ + wl_list_for_each(query, queries, link) { + if (view_matches_query(view, query)) { + branch = "then"; + break; + } + } + } + + actions = action_get_actionlist(action, branch); + if (actions) { + actions_run(view, server, actions, 0); + } +} + void actions_run(struct view *activator, struct server *server, struct wl_list *actions, uint32_t resize_edges) @@ -693,6 +804,23 @@ actions_run(struct view *activator, struct server *server, desktop_focus_output(output_from_name(server, output_name)); } break; + case ACTION_TYPE_IF: + if (view) { + run_if_action(view, server, action); + } + break; + case ACTION_TYPE_FOR_EACH: + { + struct wl_array views; + struct view **item; + wl_array_init(&views); + view_array_append(server, &views, LAB_VIEW_CRITERIA_NONE); + wl_array_for_each(item, &views) { + run_if_action(*item, server, action); + } + wl_array_release(&views); + } + break; case ACTION_TYPE_INVALID: wlr_log(WLR_ERROR, "Not executing unknown action"); break; diff --git a/src/config/rcxml.c b/src/config/rcxml.c index c8da660b..ed0ae759 100644 --- a/src/config/rcxml.c +++ b/src/config/rcxml.c @@ -27,6 +27,7 @@ #include "config/rcxml.h" #include "labwc.h" #include "regions.h" +#include "view.h" #include "window-rules.h" #include "workspaces.h" @@ -37,6 +38,9 @@ static bool in_mousebind; static bool in_libinput_category; static bool in_window_switcher_field; static bool in_window_rules; +static bool in_action_query; +static bool in_action_then_branch; +static bool in_action_else_branch; static struct usable_area_override *current_usable_area_override; static struct keybind *current_keybind; @@ -49,6 +53,8 @@ static struct region *current_region; static struct window_switcher_field *current_field; static struct window_rule *current_window_rule; static struct action *current_window_rule_action; +static struct view_query *current_view_query; +static struct action *current_child_action; enum font_place { FONT_PLACE_NONE = 0, @@ -246,6 +252,79 @@ fill_region(char *nodename, char *content) } } +static void +fill_action_query(char *nodename, char *content, struct action *action) +{ + string_truncate_at_pattern(nodename, ".keybind.keyboard"); + string_truncate_at_pattern(nodename, ".mousebind.context.mouse"); + + if (!strcasecmp(nodename, "query.action")) { + current_view_query = NULL; + } + + string_truncate_at_pattern(nodename, ".query.action"); + + if (!content) { + return; + } + + if (!current_view_query) { + struct wl_list *queries = action_get_querylist(action, "query"); + if (!queries) { + action_arg_add_querylist(action, "query"); + queries = action_get_querylist(action, "query"); + } + current_view_query = znew(*current_view_query); + wl_list_append(queries, ¤t_view_query->link); + } + + if (!strcasecmp(nodename, "identifier")) { + current_view_query->identifier = xstrdup(content); + } else if (!strcasecmp(nodename, "title")) { + current_view_query->title = xstrdup(content); + } +} + +static void +fill_child_action(char *nodename, char *content, struct action *parent, + const char *branch_name) +{ + string_truncate_at_pattern(nodename, ".keybind.keyboard"); + string_truncate_at_pattern(nodename, ".mousebind.context.mouse"); + string_truncate_at_pattern(nodename, ".then.action"); + string_truncate_at_pattern(nodename, ".else.action"); + + if (!strcasecmp(nodename, "action")) { + current_child_action = NULL; + } + + if (!content) { + return; + } + + struct wl_list *siblings = action_get_actionlist(parent, branch_name); + if (!siblings) { + action_arg_add_actionlist(parent, branch_name); + siblings = action_get_actionlist(parent, branch_name); + } + + if (!strcasecmp(nodename, "name.action")) { + if (!strcasecmp(content, "If") || !strcasecmp(content, "ForEach")) { + wlr_log(WLR_ERROR, "action '%s' cannot be a child action", content); + return; + } + current_child_action = action_create(content); + if (current_child_action) { + wl_list_append(siblings, ¤t_child_action->link); + } + } else if (!current_child_action) { + wlr_log(WLR_ERROR, "expect element first. " + "nodename: '%s' content: '%s'", nodename, content); + } else { + action_arg_from_xml_node(current_child_action, nodename, content); + } +} + static void fill_keybind(char *nodename, char *content) { @@ -539,10 +618,32 @@ entry(xmlNode *node, char *nodename, char *content) fill_usable_area_override(nodename, content); } if (in_keybind) { - fill_keybind(nodename, content); + if (in_action_query) { + fill_action_query(nodename, content, + current_keybind_action); + } else if (in_action_then_branch) { + fill_child_action(nodename, content, + current_keybind_action, "then"); + } else if (in_action_else_branch) { + fill_child_action(nodename, content, + current_keybind_action, "else"); + } else { + fill_keybind(nodename, content); + } } if (in_mousebind) { - fill_mousebind(nodename, content); + if (in_action_query) { + fill_action_query(nodename, content, + current_mousebind_action); + } else if (in_action_then_branch) { + fill_child_action(nodename, content, + current_mousebind_action, "then"); + } else if (in_action_else_branch) { + fill_child_action(nodename, content, + current_mousebind_action, "else"); + } else { + fill_mousebind(nodename, content); + } } if (in_libinput_category) { fill_libinput_category(nodename, content); @@ -771,6 +872,24 @@ xml_tree_walk(xmlNode *node) in_window_rules = false; continue; } + if (!strcasecmp((char *)n->name, "query")) { + in_action_query = true; + traverse(n); + in_action_query = false; + continue; + } + if (!strcasecmp((char *)n->name, "then")) { + in_action_then_branch = true; + traverse(n); + in_action_then_branch = false; + continue; + } + if (!strcasecmp((char *)n->name, "else")) { + in_action_else_branch = true; + traverse(n); + in_action_else_branch = false; + continue; + } traverse(n); } } @@ -1366,6 +1485,8 @@ rcxml_finish(void) current_mouse_context = NULL; current_keybind_action = NULL; current_mousebind_action = NULL; + current_child_action = NULL; + current_view_query = NULL; current_region = NULL; current_field = NULL; current_window_rule = NULL;