From d609c9e3f936421c14df5246e05155e40f805538 Mon Sep 17 00:00:00 2001 From: Johan Malm Date: Fri, 28 Apr 2023 21:41:41 +0100 Subject: [PATCH] Support window-rules Two types of window rules are supported, actions and properties. They are defined as shown below. Rules are applied if windows match the criteria defined by the 'identifier' attribute which relates to app_id for native Wayland windows and WM_CLASS for XWayland clients. Matching against patterns with '*' (wildcard) and '?' (joker) is supported. Add 'serverDecoration' property. --- docs/labwc-config.5.scd | 42 +++++++++++++++++++++ docs/rc.xml.all | 9 +++++ include/config/rcxml.h | 2 + include/window-rules.h | 37 +++++++++++++++++++ src/common/match.c | 1 - src/config/rcxml.c | 82 +++++++++++++++++++++++++++++++++++++++++ src/meson.build | 1 + src/view-impl-common.c | 4 ++ src/view.c | 2 + src/window-rules.c | 76 ++++++++++++++++++++++++++++++++++++++ src/xdg.c | 13 ++++++- src/xwayland.c | 15 +++++++- 12 files changed, 281 insertions(+), 3 deletions(-) create mode 100644 include/window-rules.h create mode 100644 src/window-rules.c diff --git a/docs/labwc-config.5.scd b/docs/labwc-config.5.scd index 14f68038..be70b596 100644 --- a/docs/labwc-config.5.scd +++ b/docs/labwc-config.5.scd @@ -330,6 +330,48 @@ The rest of this man page describes configuration options. any motion events while a keyboard is typing, and for a short while after as well. +## WINDOW RULES + +Two types of window rules are supported, actions and properties. They are +defined as shown below. + +``` + + + + + + + + + + + +``` + +*Actions* + +** + Define a window rule for any window which matches the criteria defined + by the attribute *identifier*. Matching against patterns with '\*' + (wildcard) and '?' (joker) is supported. Pattern matching is + case-insensitive. + + *identifier* relates to app_id for native Wayland windows and WM_CLASS + for XWayland clients. + +*Properties* + +Property values can be *yes*, *no* or *default*. + +If a window matches criteria for multiple rules which set the same property, +later config entries have higher priority. *default* can be useful in this +situation. + +** [yes|no|default] + *serverDecoration* over-rules any other setting for server-side window + decoration on first map. + ## ENVIRONMENT VARIABLES *XCURSOR_THEME* and *XCURSOR_SIZE* are supported to set cursor theme diff --git a/docs/rc.xml.all b/docs/rc.xml.all index 648bb5f3..3de44aae 100644 --- a/docs/rc.xml.all +++ b/docs/rc.xml.all @@ -374,4 +374,13 @@ + + diff --git a/include/config/rcxml.h b/include/config/rcxml.h index 46792397..ad50ec82 100644 --- a/include/config/rcxml.h +++ b/include/config/rcxml.h @@ -80,6 +80,8 @@ struct rcxml { bool outlines; struct wl_list fields; /* struct window_switcher_field.link */ } window_switcher; + + struct wl_list window_rules; /* struct window_rule.link */ }; extern struct rcxml rc; diff --git a/include/window-rules.h b/include/window-rules.h new file mode 100644 index 00000000..5d7f0c51 --- /dev/null +++ b/include/window-rules.h @@ -0,0 +1,37 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef __WINDOW_RULES_H +#define __WINDOW_RULES_H + +enum window_rule_event { + LAB_WINDOW_RULE_EVENT_ON_FIRST_MAP = 0, +}; + +enum property { + LAB_PROP_UNSPECIFIED = 0, + LAB_PROP_UNSET, + LAB_PROP_FALSE, + LAB_PROP_TRUE, +}; + +/* + * 'identifier' represents: + * - 'app_id' for native Wayland windows + * - 'WM_CLASS' for XWayland clients + */ +struct window_rule { + char *identifier; + + enum window_rule_event event; + struct wl_list actions; + + enum property server_decoration; + + struct wl_list link; /* struct rcxml.window_rules */ +}; + +struct view; + +void window_rules_apply(struct view *view, enum window_rule_event event); +enum property window_rules_get_property(struct view *view, const char *property); + +#endif /* __WINDOW_RULES_H */ diff --git a/src/common/match.c b/src/common/match.c index 8b6ca19c..eb975f63 100644 --- a/src/common/match.c +++ b/src/common/match.c @@ -1,6 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-only #define _POSIX_C_SOURCE 200809L -#include #include #include "common/match.h" diff --git a/src/config/rcxml.c b/src/config/rcxml.c index 283affc0..37b5067c 100644 --- a/src/config/rcxml.c +++ b/src/config/rcxml.c @@ -25,6 +25,7 @@ #include "config/mousebind.h" #include "config/rcxml.h" #include "regions.h" +#include "window-rules.h" #include "workspaces.h" static bool in_regions; @@ -32,6 +33,7 @@ static bool in_keybind; static bool in_mousebind; static bool in_libinput_category; static bool in_window_switcher_field; +static bool in_window_rules; static struct keybind *current_keybind; static struct mousebind *current_mousebind; @@ -41,6 +43,8 @@ static struct action *current_keybind_action; static struct action *current_mousebind_action; 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; enum font_place { FONT_PLACE_NONE = 0, @@ -54,6 +58,63 @@ enum font_place { static void load_default_key_bindings(void); static void load_default_mouse_bindings(void); +/* Does a boolean-parse but also allows 'default' */ +static void +set_property(const char *str, enum property *variable) +{ + if (!str || !strcasecmp(str, "default")) { + *variable = LAB_PROP_UNSET; + return; + } + int ret = parse_bool(str, -1); + if (ret < 0) { + return; + } + *variable = ret ? LAB_PROP_TRUE : LAB_PROP_FALSE; +} + +static void +fill_window_rule(char *nodename, char *content) +{ + if (!strcasecmp(nodename, "windowRule.windowRules")) { + current_window_rule = znew(*current_window_rule); + wl_list_append(&rc.window_rules, ¤t_window_rule->link); + wl_list_init(¤t_window_rule->actions); + return; + } + + string_truncate_at_pattern(nodename, ".windowrule.windowrules"); + if (!content) { + /* nop */ + } else if (!current_window_rule) { + wlr_log(WLR_ERROR, "no window-rule"); + } else if (!strcmp(nodename, "identifier")) { + free(current_window_rule->identifier); + current_window_rule->identifier = xstrdup(content); + } else if (!strcmp(nodename, "event")) { + /* + * This is just in readiness for adding any other types of + * events in the future. We default to onFirstMap anyway. + */ + if (!strcasecmp(content, "onFirstMap")) { + current_window_rule->event = LAB_WINDOW_RULE_EVENT_ON_FIRST_MAP; + } + } else if (!strcasecmp(nodename, "serverDecoration")) { + set_property(content, ¤t_window_rule->server_decoration); + } else if (!strcmp(nodename, "name.action")) { + current_window_rule_action = action_create(content); + if (current_window_rule_action) { + wl_list_append(¤t_window_rule->actions, + ¤t_window_rule_action->link); + } + } else if (!current_window_rule_action) { + wlr_log(WLR_ERROR, "expect element first. " + "nodename: '%s' content: '%s'", nodename, content); + } else { + action_arg_from_xml_node(current_window_rule_action, nodename, content); + } +} + static void fill_window_switcher_field(char *nodename, char *content) { @@ -417,6 +478,10 @@ entry(xmlNode *node, char *nodename, char *content) fill_window_switcher_field(nodename, content); return; } + if (in_window_rules) { + fill_window_rule(nodename, content); + return; + } /* handle nodes without content, e.g. */ if (!strcmp(nodename, "default.keyboard")) { @@ -600,6 +665,12 @@ xml_tree_walk(xmlNode *node) in_window_switcher_field = false; continue; } + if (!strcasecmp((char *)n->name, "windowRules")) { + in_window_rules = true; + traverse(n); + in_window_rules = false; + continue; + } traverse(n); } } @@ -638,6 +709,7 @@ rcxml_init(void) wl_list_init(&rc.workspace_config.workspaces); wl_list_init(&rc.regions); wl_list_init(&rc.window_switcher.fields); + wl_list_init(&rc.window_rules); } has_run = true; @@ -1059,6 +1131,14 @@ rcxml_finish(void) zfree(field); } + struct window_rule *rule, *rule_tmp; + wl_list_for_each_safe(rule, rule_tmp, &rc.window_rules, link) { + wl_list_remove(&rule->link); + zfree(rule->identifier); + action_list_free(&rule->actions); + zfree(rule); + } + /* Reset state vars for starting fresh when Reload is triggered */ current_keybind = NULL; current_mousebind = NULL; @@ -1068,4 +1148,6 @@ rcxml_finish(void) current_mousebind_action = NULL; current_region = NULL; current_field = NULL; + current_window_rule = NULL; + current_window_rule_action = NULL; } diff --git a/src/meson.build b/src/meson.build index 5087d331..5aab9a62 100644 --- a/src/meson.build +++ b/src/meson.build @@ -23,6 +23,7 @@ labwc_sources = files( 'theme.c', 'view.c', 'view-impl-common.c', + 'window-rules.c', 'workspaces.c', 'xdg.c', 'xdg-popup.c', diff --git a/src/view-impl-common.c b/src/view-impl-common.c index 98d92529..dacfc9b0 100644 --- a/src/view-impl-common.c +++ b/src/view-impl-common.c @@ -6,6 +6,7 @@ #include "labwc.h" #include "view.h" #include "view-impl-common.h" +#include "window-rules.h" void view_impl_move_to_front(struct view *view) @@ -26,6 +27,9 @@ view_impl_move_to_back(struct view *view) void view_impl_map(struct view *view) { + if (!view->been_mapped) { + window_rules_apply(view, LAB_WINDOW_RULE_EVENT_ON_FIRST_MAP); + } desktop_focus_and_activate_view(&view->server->seat, view); view_move_to_front(view); view_update_title(view); diff --git a/src/view.c b/src/view.c index 0f728872..ea7ce086 100644 --- a/src/view.c +++ b/src/view.c @@ -9,6 +9,7 @@ #include "regions.h" #include "ssd.h" #include "view.h" +#include "window-rules.h" #include "workspaces.h" #include "xwayland.h" @@ -658,6 +659,7 @@ void view_set_decorations(struct view *view, bool decorations) { assert(view); + if (view->ssd_enabled != decorations && !view->fullscreen) { /* * Set view->ssd_enabled first since it is referenced diff --git a/src/window-rules.c b/src/window-rules.c new file mode 100644 index 00000000..5fb76317 --- /dev/null +++ b/src/window-rules.c @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0-only +#define _POSIX_C_SOURCE 200809L +#include "config.h" +#include +#include +#include +#include +#include +#include "action.h" +#include "common/match.h" +#include "config/rcxml.h" +#include "labwc.h" +#include "view.h" +#include "window-rules.h" + +void +window_rules_apply(struct view *view, enum window_rule_event event) +{ + const char *identifier = view_get_string_prop(view, "app_id"); + if (!identifier) { + return; + } + + struct window_rule *rule; + wl_list_for_each(rule, &rc.window_rules, link) { + if (!rule->identifier || rule->event != event) { + continue; + } + if (match_glob(rule->identifier, identifier)) { + actions_run(view, view->server, &rule->actions, 0); + } + } +} + +enum property +window_rules_get_property(struct view *view, const char *property) +{ + assert(property); + + const char *identifier = view_get_string_prop(view, "app_id"); + if (!identifier) { + goto out; + } + + /* + * We iterate in reverse here because later items in list have higher + * priority. For example, in the config below we want the return value + * for foot's "serverDecoration" property to be "default". + * + * + * + * + * + */ + struct window_rule *rule; + wl_list_for_each_reverse(rule, &rc.window_rules, link) { + if (!rule->identifier) { + continue; + } + /* + * Only return if property != LAB_PROP_UNSPECIFIED otherwise a + * which does not set a particular property + * attribute would still return here if that property was asked + * for. + */ + if (match_glob(rule->identifier, identifier)) { + if (!strcasecmp(property, "serverDecoration")) { + if (rule->server_decoration) { + return rule->server_decoration; + } + } + } + } +out: + return LAB_PROP_UNSPECIFIED; +} diff --git a/src/xdg.c b/src/xdg.c index 1afc1f22..6d5a9928 100644 --- a/src/xdg.c +++ b/src/xdg.c @@ -6,6 +6,7 @@ #include "node.h" #include "view.h" #include "view-impl-common.h" +#include "window-rules.h" #include "workspaces.h" #define CONFIGURE_TIMEOUT_MS 100 @@ -49,6 +50,16 @@ handle_new_xdg_popup(struct wl_listener *listener, void *data) static bool has_ssd(struct view *view) { + /* Window-rules take priority if they exist for this view */ + switch (window_rules_get_property(view, "serverDecoration")) { + case LAB_PROP_TRUE: + return true; + case LAB_PROP_FALSE: + return false; + default: + break; + } + /* * view->ssd_preference may be set by the decoration implementation * e.g. src/decorations/xdg-deco.c or src/decorations/kde-deco.c. @@ -425,13 +436,13 @@ xdg_toplevel_view_map(struct view *view) view->current.y = view->pending.y; view_moved(view); - view->been_mapped = true; } view->commit.notify = handle_commit; wl_signal_add(&xdg_surface->surface->events.commit, &view->commit); view_impl_map(view); + view->been_mapped = true; } static void diff --git a/src/xwayland.c b/src/xwayland.c index 7a950310..2684263e 100644 --- a/src/xwayland.c +++ b/src/xwayland.c @@ -9,6 +9,7 @@ #include "ssd.h" #include "view.h" #include "view-impl-common.h" +#include "window-rules.h" #include "workspaces.h" #include "xwayland.h" @@ -103,6 +104,18 @@ ensure_initial_geometry_and_output(struct view *view) static bool want_deco(struct wlr_xwayland_surface *xwayland_surface) { + struct view *view = (struct view *)xwayland_surface->data; + + /* Window-rules take priority if they exist for this view */ + switch (window_rules_get_property(view, "serverDecoration")) { + case LAB_PROP_TRUE: + return true; + case LAB_PROP_FALSE: + return false; + default: + break; + } + return xwayland_surface->decorations == WLR_XWAYLAND_SURFACE_DECORATIONS_ALL; } @@ -464,7 +477,6 @@ xwayland_view_map(struct view *view) */ view->current = view->pending; view_moved(view); - view->been_mapped = true; } if (view->ssd_enabled && view_is_floating(view)) { @@ -476,6 +488,7 @@ xwayland_view_map(struct view *view) view->commit.notify = handle_commit; view_impl_map(view); + view->been_mapped = true; } static void