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