From e2d83ff7f5d2c653c7ce298aadd9790b57f86ff2 Mon Sep 17 00:00:00 2001 From: tokyo4j Date: Sun, 7 Dec 2025 17:44:36 +0900 Subject: [PATCH 01/43] rcxml: sync rcxml.window_switcher with XML format --- include/config/rcxml.h | 14 +++++++----- include/cycle.h | 2 +- src/config/rcxml.c | 46 +++++++++++++++++++-------------------- src/cycle/cycle.c | 8 +++---- src/cycle/osd-classic.c | 4 ++-- src/cycle/osd-thumbnail.c | 2 +- 6 files changed, 39 insertions(+), 37 deletions(-) diff --git a/include/config/rcxml.h b/include/config/rcxml.h index 94eb6ebe..edfb9e55 100644 --- a/include/config/rcxml.h +++ b/include/config/rcxml.h @@ -177,16 +177,18 @@ struct rcxml { /* Window Switcher */ struct { - bool show; bool preview; bool outlines; bool unshade; - enum lab_view_criteria criteria; - struct wl_list fields; /* struct window_switcher_field.link */ - enum cycle_osd_style style; - enum cycle_osd_output_criteria output_criteria; - char *thumbnail_label_format; enum window_switcher_order order; + enum lab_view_criteria criteria; + struct { + bool show; + enum cycle_osd_style style; + enum cycle_osd_output_criteria output_criteria; + char *thumbnail_label_format; + struct wl_list fields; /* struct cycle_osd_field.link */ + } osd; } window_switcher; struct wl_list window_rules; /* struct window_rule.link */ diff --git a/include/cycle.h b/include/cycle.h index aaecff50..ba58b88d 100644 --- a/include/cycle.h +++ b/include/cycle.h @@ -39,7 +39,7 @@ struct cycle_osd_field { enum cycle_osd_field_content content; int width; char *format; - struct wl_list link; /* struct rcxml.window_switcher.fields */ + struct wl_list link; /* struct rcxml.window_switcher.osd.fields */ }; struct buf; diff --git a/src/config/rcxml.c b/src/config/rcxml.c index cc744228..bb868fb2 100644 --- a/src/config/rcxml.c +++ b/src/config/rcxml.c @@ -324,7 +324,7 @@ static void clear_window_switcher_fields(void) { struct cycle_osd_field *field, *field_tmp; - wl_list_for_each_safe(field, field_tmp, &rc.window_switcher.fields, link) { + wl_list_for_each_safe(field, field_tmp, &rc.window_switcher.osd.fields, link) { wl_list_remove(&field->link); cycle_osd_field_free(field); } @@ -334,7 +334,7 @@ static void fill_window_switcher_field(xmlNode *node) { struct cycle_osd_field *field = znew(*field); - wl_list_append(&rc.window_switcher.fields, &field->link); + wl_list_append(&rc.window_switcher.osd.fields, &field->link); xmlNode *child; char *key, *content; @@ -1071,7 +1071,7 @@ entry(xmlNode *node, char *nodename, char *content) } else if (!strcasecmp(nodename, "prefix.desktops")) { xstrdup_replace(rc.workspace_config.prefix, content); } else if (!strcasecmp(nodename, "thumbnailLabelFormat.osd.windowSwitcher")) { - xstrdup_replace(rc.window_switcher.thumbnail_label_format, content); + xstrdup_replace(rc.window_switcher.osd.thumbnail_label_format, content); } else if (!lab_xml_node_is_leaf(node)) { /* parse children of nested nodes other than above */ @@ -1219,23 +1219,23 @@ entry(xmlNode *node, char *nodename, char *content) * thumnailLabelFormat is handled above to allow for an empty value */ } else if (!strcasecmp(nodename, "show.osd.windowSwitcher")) { - set_bool(content, &rc.window_switcher.show); + set_bool(content, &rc.window_switcher.osd.show); } else if (!strcasecmp(nodename, "style.osd.windowSwitcher")) { if (!strcasecmp(content, "classic")) { - rc.window_switcher.style = CYCLE_OSD_STYLE_CLASSIC; + rc.window_switcher.osd.style = CYCLE_OSD_STYLE_CLASSIC; } else if (!strcasecmp(content, "thumbnail")) { - rc.window_switcher.style = CYCLE_OSD_STYLE_THUMBNAIL; + rc.window_switcher.osd.style = CYCLE_OSD_STYLE_THUMBNAIL; } else { wlr_log(WLR_ERROR, "Invalid windowSwitcher style '%s': " "should be one of classic|thumbnail", content); } } else if (!strcasecmp(nodename, "output.osd.windowSwitcher")) { if (!strcasecmp(content, "all")) { - rc.window_switcher.output_criteria = CYCLE_OSD_OUTPUT_ALL; + rc.window_switcher.osd.output_criteria = CYCLE_OSD_OUTPUT_ALL; } else if (!strcasecmp(content, "cursor")) { - rc.window_switcher.output_criteria = CYCLE_OSD_OUTPUT_CURSOR; + rc.window_switcher.osd.output_criteria = CYCLE_OSD_OUTPUT_CURSOR; } else if (!strcasecmp(content, "focused")) { - rc.window_switcher.output_criteria = CYCLE_OSD_OUTPUT_FOCUSED; + rc.window_switcher.osd.output_criteria = CYCLE_OSD_OUTPUT_FOCUSED; } else { wlr_log(WLR_ERROR, "Invalid windowSwitcher output '%s': " "should be one of all|focused|cursor", content); @@ -1252,14 +1252,14 @@ entry(xmlNode *node, char *nodename, char *content) /* The following two are for backward compatibility only. */ } else if (!strcasecmp(nodename, "show.windowSwitcher")) { - set_bool(content, &rc.window_switcher.show); + set_bool(content, &rc.window_switcher.osd.show); wlr_log(WLR_ERROR, " is deprecated." " Use "); } else if (!strcasecmp(nodename, "style.windowSwitcher")) { if (!strcasecmp(content, "classic")) { - rc.window_switcher.style = CYCLE_OSD_STYLE_CLASSIC; + rc.window_switcher.osd.style = CYCLE_OSD_STYLE_CLASSIC; } else if (!strcasecmp(content, "thumbnail")) { - rc.window_switcher.style = CYCLE_OSD_STYLE_THUMBNAIL; + rc.window_switcher.osd.style = CYCLE_OSD_STYLE_THUMBNAIL; } wlr_log(WLR_ERROR, " is deprecated." " Use "); @@ -1282,7 +1282,7 @@ entry(xmlNode *node, char *nodename, char *content) /* The following three are for backward compatibility only */ } else if (!strcasecmp(nodename, "show.windowSwitcher.core")) { - set_bool(content, &rc.window_switcher.show); + set_bool(content, &rc.window_switcher.osd.show); } else if (!strcasecmp(nodename, "preview.windowSwitcher.core")) { set_bool(content, &rc.window_switcher.preview); } else if (!strcasecmp(nodename, "outlines.windowSwitcher.core")) { @@ -1290,7 +1290,7 @@ entry(xmlNode *node, char *nodename, char *content) /* The following three are for backward compatibility only */ } else if (!strcasecmp(nodename, "cycleViewOSD.core")) { - set_bool(content, &rc.window_switcher.show); + set_bool(content, &rc.window_switcher.osd.show); wlr_log(WLR_ERROR, " is deprecated." " Use "); } else if (!strcasecmp(nodename, "cycleViewPreview.core")) { @@ -1419,7 +1419,7 @@ rcxml_init(void) wl_list_init(&rc.libinput_categories); wl_list_init(&rc.workspace_config.workspaces); wl_list_init(&rc.regions); - wl_list_init(&rc.window_switcher.fields); + wl_list_init(&rc.window_switcher.osd.fields); wl_list_init(&rc.window_rules); wl_list_init(&rc.touch_configs); } @@ -1484,10 +1484,10 @@ rcxml_init(void) rc.snap_top_maximize = true; rc.snap_tiling_events_mode = LAB_TILING_EVENTS_ALWAYS; - rc.window_switcher.show = true; - rc.window_switcher.style = CYCLE_OSD_STYLE_CLASSIC; - rc.window_switcher.output_criteria = CYCLE_OSD_OUTPUT_ALL; - rc.window_switcher.thumbnail_label_format = xstrdup("%T"); + rc.window_switcher.osd.show = true; + rc.window_switcher.osd.style = CYCLE_OSD_STYLE_CLASSIC; + rc.window_switcher.osd.output_criteria = CYCLE_OSD_OUTPUT_ALL; + rc.window_switcher.osd.thumbnail_label_format = xstrdup("%T"); rc.window_switcher.preview = true; rc.window_switcher.outlines = true; rc.window_switcher.unshade = true; @@ -1673,7 +1673,7 @@ load_default_window_switcher_fields(void) struct cycle_osd_field *field = znew(*field); field->content = fields[i].content; field->width = fields[i].width; - wl_list_append(&rc.window_switcher.fields, &field->link); + wl_list_append(&rc.window_switcher.osd.fields, &field->link); } } @@ -1794,7 +1794,7 @@ post_processing(void) if (rc.workspace_config.popuptime == INT_MIN) { rc.workspace_config.popuptime = 1000; } - if (!wl_list_length(&rc.window_switcher.fields)) { + if (!wl_list_length(&rc.window_switcher.osd.fields)) { wlr_log(WLR_INFO, "load default window switcher fields"); load_default_window_switcher_fields(); } @@ -1888,7 +1888,7 @@ validate(void) /* OSD fields */ int field_width_sum = 0; struct cycle_osd_field *field, *field_tmp; - wl_list_for_each_safe(field, field_tmp, &rc.window_switcher.fields, link) { + wl_list_for_each_safe(field, field_tmp, &rc.window_switcher.osd.fields, link) { field_width_sum += field->width; if (!cycle_osd_field_is_valid(field) || field_width_sum > 100) { wlr_log(WLR_ERROR, "Deleting invalid window switcher field %p", field); @@ -1964,7 +1964,7 @@ rcxml_finish(void) zfree(rc.fallback_app_icon_name); zfree(rc.workspace_config.prefix); zfree(rc.tablet.output_name); - zfree(rc.window_switcher.thumbnail_label_format); + zfree(rc.window_switcher.osd.thumbnail_label_format); clear_title_layout(); diff --git a/src/cycle/cycle.c b/src/cycle/cycle.c index adf9b05e..a775f78c 100644 --- a/src/cycle/cycle.c +++ b/src/cycle/cycle.c @@ -257,7 +257,7 @@ preview_selected_view(struct view *view) static struct cycle_osd_impl * get_osd_impl(void) { - switch (rc.window_switcher.style) { + switch (rc.window_switcher.osd.style) { case CYCLE_OSD_STYLE_CLASSIC: return &cycle_osd_classic_impl; case CYCLE_OSD_STYLE_THUMBNAIL: @@ -307,9 +307,9 @@ init_cycle(struct server *server) return false; } - if (rc.window_switcher.show) { + if (rc.window_switcher.osd.show) { /* Create OSD */ - switch (rc.window_switcher.output_criteria) { + switch (rc.window_switcher.osd.output_criteria) { case CYCLE_OSD_OUTPUT_ALL: { struct output *output; wl_list_for_each(output, &server->outputs, link) { @@ -342,7 +342,7 @@ update_cycle(struct server *server) { struct cycle_state *cycle = &server->cycle; - if (rc.window_switcher.show) { + if (rc.window_switcher.osd.show) { struct output *output; wl_list_for_each(output, &server->outputs, link) { if (output->cycle_osd.tree) { diff --git a/src/cycle/osd-classic.c b/src/cycle/osd-classic.c index 944b9063..8e01dc24 100644 --- a/src/cycle/osd-classic.c +++ b/src/cycle/osd-classic.c @@ -36,7 +36,7 @@ create_fields_scene(struct server *server, struct view *view, &theme->osd_window_switcher_classic; struct cycle_osd_field *field; - wl_list_for_each(field, &rc.window_switcher.fields, link) { + wl_list_for_each(field, &rc.window_switcher.osd.fields, link) { int field_width = field_widths_sum * field->width / 100.0; struct wlr_scene_node *node = NULL; int height = -1; @@ -144,7 +144,7 @@ cycle_osd_classic_create(struct output *output) y += switcher_theme->item_height; } - int nr_fields = wl_list_length(&rc.window_switcher.fields); + int nr_fields = wl_list_length(&rc.window_switcher.osd.fields); /* This is the width of the area available for text fields */ int field_widths_sum = w - 2 * padding diff --git a/src/cycle/osd-thumbnail.c b/src/cycle/osd-thumbnail.c index 2245d1ad..d17b1aa0 100644 --- a/src/cycle/osd-thumbnail.c +++ b/src/cycle/osd-thumbnail.c @@ -103,7 +103,7 @@ create_label(struct wlr_scene_tree *parent, struct view *view, { struct buf buf = BUF_INIT; cycle_osd_field_set_custom(&buf, view, - rc.window_switcher.thumbnail_label_format); + rc.window_switcher.osd.thumbnail_label_format); struct scaled_font_buffer *buffer = scaled_font_buffer_create(parent); scaled_font_buffer_update(buffer, buf.data, From 64af206114a35772a999b20e2dc192922ef7dbd6 Mon Sep 17 00:00:00 2001 From: tokyo4j Date: Fri, 12 Dec 2025 03:38:57 +0900 Subject: [PATCH 02/43] Rename cycle_osd_output_criteria to cycle_output_filter --- include/config/rcxml.h | 2 +- include/config/types.h | 8 ++++---- src/config/rcxml.c | 8 ++++---- src/cycle/cycle.c | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/include/config/rcxml.h b/include/config/rcxml.h index edfb9e55..65b07172 100644 --- a/include/config/rcxml.h +++ b/include/config/rcxml.h @@ -185,7 +185,7 @@ struct rcxml { struct { bool show; enum cycle_osd_style style; - enum cycle_osd_output_criteria output_criteria; + enum cycle_output_filter output_filter; char *thumbnail_label_format; struct wl_list fields; /* struct cycle_osd_field.link */ } osd; diff --git a/include/config/types.h b/include/config/types.h index 99c5929e..540f609f 100644 --- a/include/config/types.h +++ b/include/config/types.h @@ -117,10 +117,10 @@ enum cycle_osd_style { CYCLE_OSD_STYLE_THUMBNAIL, }; -enum cycle_osd_output_criteria { - CYCLE_OSD_OUTPUT_ALL, - CYCLE_OSD_OUTPUT_CURSOR, - CYCLE_OSD_OUTPUT_FOCUSED, +enum cycle_output_filter { + CYCLE_OUTPUT_ALL, + CYCLE_OUTPUT_CURSOR, + CYCLE_OUTPUT_FOCUSED, }; #endif /* LABWC_CONFIG_TYPES_H */ diff --git a/src/config/rcxml.c b/src/config/rcxml.c index bb868fb2..68c1135a 100644 --- a/src/config/rcxml.c +++ b/src/config/rcxml.c @@ -1231,11 +1231,11 @@ entry(xmlNode *node, char *nodename, char *content) } } else if (!strcasecmp(nodename, "output.osd.windowSwitcher")) { if (!strcasecmp(content, "all")) { - rc.window_switcher.osd.output_criteria = CYCLE_OSD_OUTPUT_ALL; + rc.window_switcher.osd.output_filter = CYCLE_OUTPUT_ALL; } else if (!strcasecmp(content, "cursor")) { - rc.window_switcher.osd.output_criteria = CYCLE_OSD_OUTPUT_CURSOR; + rc.window_switcher.osd.output_filter = CYCLE_OUTPUT_CURSOR; } else if (!strcasecmp(content, "focused")) { - rc.window_switcher.osd.output_criteria = CYCLE_OSD_OUTPUT_FOCUSED; + rc.window_switcher.osd.output_filter = CYCLE_OUTPUT_FOCUSED; } else { wlr_log(WLR_ERROR, "Invalid windowSwitcher output '%s': " "should be one of all|focused|cursor", content); @@ -1486,7 +1486,7 @@ rcxml_init(void) rc.window_switcher.osd.show = true; rc.window_switcher.osd.style = CYCLE_OSD_STYLE_CLASSIC; - rc.window_switcher.osd.output_criteria = CYCLE_OSD_OUTPUT_ALL; + rc.window_switcher.osd.output_filter = CYCLE_OUTPUT_ALL; rc.window_switcher.osd.thumbnail_label_format = xstrdup("%T"); rc.window_switcher.preview = true; rc.window_switcher.outlines = true; diff --git a/src/cycle/cycle.c b/src/cycle/cycle.c index a775f78c..f14bff9d 100644 --- a/src/cycle/cycle.c +++ b/src/cycle/cycle.c @@ -309,18 +309,18 @@ init_cycle(struct server *server) if (rc.window_switcher.osd.show) { /* Create OSD */ - switch (rc.window_switcher.osd.output_criteria) { - case CYCLE_OSD_OUTPUT_ALL: { + switch (rc.window_switcher.osd.output_filter) { + case CYCLE_OUTPUT_ALL: { struct output *output; wl_list_for_each(output, &server->outputs, link) { create_osd_on_output(output); } break; } - case CYCLE_OSD_OUTPUT_CURSOR: + case CYCLE_OUTPUT_CURSOR: create_osd_on_output(output_nearest_to_cursor(server)); break; - case CYCLE_OSD_OUTPUT_FOCUSED: { + case CYCLE_OUTPUT_FOCUSED: { struct output *output; if (server->active_view) { output = server->active_view->output; From c9b088e3438a0a24aa66913b9fb02cdcd62621bb Mon Sep 17 00:00:00 2001 From: tokyo4j Date: Fri, 12 Dec 2025 04:27:32 +0900 Subject: [PATCH 03/43] cycle: add and use get_outputs_by_filter() This function can be reused for filtering windows to cycle through. --- src/cycle/cycle.c | 64 ++++++++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/src/cycle/cycle.c b/src/cycle/cycle.c index f14bff9d..067c9929 100644 --- a/src/cycle/cycle.c +++ b/src/cycle/cycle.c @@ -266,14 +266,36 @@ get_osd_impl(void) return NULL; } -static void -create_osd_on_output(struct output *output) +static uint64_t +get_outputs_by_filter(struct server *server, + enum cycle_output_filter output_filter) { - if (!output_is_usable(output)) { - return; + struct output *output = NULL; + + switch (output_filter) { + case CYCLE_OUTPUT_ALL: + break; + case CYCLE_OUTPUT_CURSOR: + output = output_nearest_to_cursor(server); + break; + case CYCLE_OUTPUT_FOCUSED: { + struct view *view = server->active_view; + if (view && output_is_usable(view->output)) { + output = view->output; + } else { + /* Fallback to pointer */ + output = output_nearest_to_cursor(server); + } + break; + } + } + + if (output) { + return output->id_bit; + } else { + /* bitmask for all outputs */ + return UINT64_MAX; } - get_osd_impl()->create(output); - assert(output->cycle_osd.tree); } static void @@ -309,28 +331,18 @@ init_cycle(struct server *server) if (rc.window_switcher.osd.show) { /* Create OSD */ - switch (rc.window_switcher.osd.output_filter) { - case CYCLE_OUTPUT_ALL: { - struct output *output; - wl_list_for_each(output, &server->outputs, link) { - create_osd_on_output(output); + uint64_t osd_outputs = get_outputs_by_filter(server, + rc.window_switcher.osd.output_filter); + struct output *output; + wl_list_for_each(output, &server->outputs, link) { + if (!(osd_outputs & output->id_bit)) { + continue; } - break; - } - case CYCLE_OUTPUT_CURSOR: - create_osd_on_output(output_nearest_to_cursor(server)); - break; - case CYCLE_OUTPUT_FOCUSED: { - struct output *output; - if (server->active_view) { - output = server->active_view->output; - } else { - /* Fallback to pointer, if there is no active_view */ - output = output_nearest_to_cursor(server); + if (!output_is_usable(output)) { + continue; } - create_osd_on_output(output); - break; - } + get_osd_impl()->create(output); + assert(output->cycle_osd.tree); } } From 64aec6ff5d7c6efc6a128ab84e2468e64e6e4c28 Mon Sep 17 00:00:00 2001 From: Cameron Scott McCreery Date: Mon, 22 Dec 2025 16:17:43 -0500 Subject: [PATCH 04/43] workspaces: add config option for initial workspace selection --- docs/labwc-config.5.scd | 5 +++ docs/rc.xml.all | 1 + include/config/rcxml.h | 1 + src/config/rcxml.c | 3 ++ src/workspaces.c | 83 +++++++++++++++++++++++++++-------------- 5 files changed, 64 insertions(+), 29 deletions(-) diff --git a/docs/labwc-config.5.scd b/docs/labwc-config.5.scd index 1c2a4af6..85363354 100644 --- a/docs/labwc-config.5.scd +++ b/docs/labwc-config.5.scd @@ -575,6 +575,11 @@ extending outward from the snapped edge. is 1. The number attribute is optional. If the number attribute is specified, names.name is not required. +** + Define the initial starting workspace. This must match one of the names + defined in or must be an index equal to or lower than . + If not set, the first workspace is used. + ** Define the timeout after which to hide the workspace OSD. A setting of 0 disables the OSD. Default is 1000 ms. diff --git a/docs/rc.xml.all b/docs/rc.xml.all index ea4e30ac..a99aa997 100644 --- a/docs/rc.xml.all +++ b/docs/rc.xml.all @@ -175,6 +175,7 @@ Workspaces can be configured like this: 1000 + Workspace 1 Workspace 1 Workspace 2 diff --git a/include/config/rcxml.h b/include/config/rcxml.h index 65b07172..8fd11aa0 100644 --- a/include/config/rcxml.h +++ b/include/config/rcxml.h @@ -168,6 +168,7 @@ struct rcxml { struct { int popuptime; int min_nr_workspaces; + char *initial_workspace_name; char *prefix; struct wl_list workspaces; /* struct workspace.link */ } workspace_config; diff --git a/src/config/rcxml.c b/src/config/rcxml.c index 68c1135a..60e709f2 100644 --- a/src/config/rcxml.c +++ b/src/config/rcxml.c @@ -1308,6 +1308,8 @@ entry(xmlNode *node, char *nodename, char *content) wl_list_append(&rc.workspace_config.workspaces, &workspace->link); } else if (!strcasecmp(nodename, "popupTime.desktops")) { rc.workspace_config.popuptime = atoi(content); + } else if (!strcasecmp(nodename, "initial.desktops")) { + xstrdup_replace(rc.workspace_config.initial_workspace_name, content); } else if (!strcasecmp(nodename, "number.desktops")) { rc.workspace_config.min_nr_workspaces = MAX(1, atoi(content)); } else if (!strcasecmp(nodename, "popupShow.resize")) { @@ -1963,6 +1965,7 @@ rcxml_finish(void) zfree(rc.icon_theme_name); zfree(rc.fallback_app_icon_name); zfree(rc.workspace_config.prefix); + zfree(rc.workspace_config.initial_workspace_name); zfree(rc.tablet.output_name); zfree(rc.window_switcher.osd.thumbnail_label_format); diff --git a/src/workspaces.c b/src/workspaces.c index f56d170a..7b0b8e96 100644 --- a/src/workspaces.c +++ b/src/workspaces.c @@ -182,6 +182,33 @@ _osd_update(struct server *server) } } +static struct workspace * +workspace_find_by_name(struct server *server, const char *name) +{ + struct workspace *workspace; + + /* by index */ + size_t parsed_index = parse_workspace_index(name); + if (parsed_index) { + size_t index = 0; + wl_list_for_each(workspace, &server->workspaces.all, link) { + if (parsed_index == ++index) { + return workspace; + } + } + } + + /* by name */ + wl_list_for_each(workspace, &server->workspaces.all, link) { + if (!strcmp(workspace->name, name)) { + return workspace; + } + } + + wlr_log(WLR_ERROR, "Workspace '%s' not found", name); + return NULL; +} + /* cosmic workspace handlers */ static void handle_cosmic_workspace_activate(struct wl_listener *listener, void *data) @@ -209,18 +236,11 @@ add_workspace(struct server *server, const char *name) workspace->name = xstrdup(name); workspace->tree = wlr_scene_tree_create(server->view_tree); wl_list_append(&server->workspaces.all, &workspace->link); - if (!server->workspaces.current) { - server->workspaces.current = workspace; - } else { - wlr_scene_node_set_enabled(&workspace->tree->node, false); - } - - bool active = server->workspaces.current == workspace; + wlr_scene_node_set_enabled(&workspace->tree->node, false); /* cosmic */ workspace->cosmic_workspace = lab_cosmic_workspace_create(server->workspaces.cosmic_group); lab_cosmic_workspace_set_name(workspace->cosmic_workspace, name); - lab_cosmic_workspace_set_active(workspace->cosmic_workspace, active); workspace->on_cosmic.activate.notify = handle_cosmic_workspace_activate; wl_signal_add(&workspace->cosmic_workspace->events.activate, @@ -231,7 +251,6 @@ add_workspace(struct server *server, const char *name) server->workspaces.ext_manager, /*id*/ NULL); lab_ext_workspace_assign_to_group(workspace->ext_workspace, server->workspaces.ext_group); lab_ext_workspace_set_name(workspace->ext_workspace, name); - lab_ext_workspace_set_active(workspace->ext_workspace, active); workspace->on_ext.activate.notify = handle_ext_workspace_activate; wl_signal_add(&workspace->ext_workspace->events.activate, @@ -398,6 +417,27 @@ workspaces_init(struct server *server) wl_list_for_each(conf, &rc.workspace_config.workspaces, link) { add_workspace(server, conf->name); } + + /* + * After adding workspaces, check if there is an initial workspace + * selected and set that as the initial workspace. + */ + char *initial_name = rc.workspace_config.initial_workspace_name; + struct workspace *initial = NULL; + struct workspace *first = wl_container_of( + server->workspaces.all.next, first, link); + + if (initial_name) { + initial = workspace_find_by_name(server, initial_name); + } + if (!initial) { + initial = first; + } + + server->workspaces.current = initial; + wlr_scene_node_set_enabled(&initial->tree->node, true); + lab_cosmic_workspace_set_active(initial->cosmic_workspace, true); + lab_ext_workspace_set_active(initial->ext_workspace, true); } /* @@ -507,21 +547,13 @@ workspaces_find(struct workspace *anchor, const char *name, bool wrap) if (!name) { return NULL; } - size_t index = 0; - struct workspace *target; - size_t wants_index = parse_workspace_index(name); - struct wl_list *workspaces = &anchor->server->workspaces.all; + struct server *server = anchor->server; + struct wl_list *workspaces = &server->workspaces.all; - if (wants_index) { - wl_list_for_each(target, workspaces, link) { - if (wants_index == ++index) { - return target; - } - } - } else if (!strcasecmp(name, "current")) { + if (!strcasecmp(name, "current")) { return anchor; } else if (!strcasecmp(name, "last")) { - return anchor->server->workspaces.last; + return server->workspaces.last; } else if (!strcasecmp(name, "left")) { return get_prev(anchor, workspaces, wrap); } else if (!strcasecmp(name, "right")) { @@ -530,15 +562,8 @@ workspaces_find(struct workspace *anchor, const char *name, bool wrap) return get_prev_occupied(anchor, workspaces, wrap); } else if (!strcasecmp(name, "right-occupied")) { return get_next_occupied(anchor, workspaces, wrap); - } else { - wl_list_for_each(target, workspaces, link) { - if (!strcasecmp(target->name, name)) { - return target; - } - } } - wlr_log(WLR_ERROR, "Workspace '%s' not found", name); - return NULL; + return workspace_find_by_name(server, name); } static void From caa9b90e80baf0773e697c116879ff808a4d239f Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:04:50 +0100 Subject: [PATCH 05/43] docs: add example for GTK4 composing --- docs/environment | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/environment b/docs/environment index f9c59b17..abc9c8b4 100644 --- a/docs/environment +++ b/docs/environment @@ -22,6 +22,13 @@ # XKB_DEFAULT_OPTIONS=grp:alt_shift_toggle # XKB_DEFAULT_OPTIONS=grp:shift_caps_toggle +## GTK4 started to require input methods like fcitx5 or ibus to handle +## simple compose sequences. If you do not use input methods, uncomment +## the following line to force GTK4 internal composing. For further +## information see https://labwc.github.io/integration.html#gtk +## +# GTK_IM_MODULE=simple + ## ## Set cursor theme and size. Find system icons themes with: ## `find /usr/share/icons/ -type d -name "cursors"` From a5c6ff499c7db6d405263c5fc6bed2c01b74366b Mon Sep 17 00:00:00 2001 From: tokyo4j Date: Fri, 12 Dec 2025 04:10:54 +0900 Subject: [PATCH 06/43] cycle: support This commit deprecates and adds per-action argument . --- docs/labwc-actions.5.scd | 12 ++++++++---- docs/labwc-config.5.scd | 8 ++------ docs/rc.xml.all | 4 ++-- include/config/rcxml.h | 2 +- include/config/types.h | 5 +++++ include/cycle.h | 8 +++++++- include/labwc.h | 2 ++ src/action.c | 33 ++++++++++++++++++++++++--------- src/config/rcxml.c | 16 ++++++++++------ src/cycle/cycle.c | 23 +++++++++++++++++------ 10 files changed, 78 insertions(+), 35 deletions(-) diff --git a/docs/labwc-actions.5.scd b/docs/labwc-actions.5.scd index 7788d551..56c840a6 100644 --- a/docs/labwc-actions.5.scd +++ b/docs/labwc-actions.5.scd @@ -125,13 +125,17 @@ Actions are used in menus and keyboard/mouse bindings. Resize and move the active window back to its untiled or unmaximized position if it had been maximized or tiled to a direction or region. -**++ -** +**++ +** Cycle focus to next/previous window, respectively. - Default keybind for NextWindow is Alt-Tab. + Default keybinds for NextWindow and PreviousWindow are Alt-Tab and + Shift-Alt-Tab. While cycling through windows, the arrow keys move the + selected window forwards/backwards and the escape key halts the cycling. - The arrow keys are used to move forwards/backwards while cycling. + *workspace* [all|current] + This determines whether to cycle through windows on all workspaces or the + current workspace. Default is "current". ** Re-load configuration and theme files. diff --git a/docs/labwc-config.5.scd b/docs/labwc-config.5.scd index 85363354..a1cedb20 100644 --- a/docs/labwc-config.5.scd +++ b/docs/labwc-config.5.scd @@ -339,7 +339,7 @@ this is for compatibility with Openbox. ## WINDOW SWITCHER ``` - + @@ -349,17 +349,13 @@ this is for compatibility with Openbox. ``` -** +** *preview* [yes|no] Preview the contents of the selected window when switching between windows. Default is yes. *outlines* [yes|no] Draw an outline around the selected window when switching between windows. Default is yes. - *allWorkspaces* [yes|no] Show windows regardless of what workspace - they are on. Default no (that is only windows on the current workspace - are shown). - *unshade* [yes|no] Temporarily unshade windows when switching between them and permanently unshade on the final selection. Default is yes. diff --git a/docs/rc.xml.all b/docs/rc.xml.all index a99aa997..dd348300 100644 --- a/docs/rc.xml.all +++ b/docs/rc.xml.all @@ -77,7 +77,7 @@ - + @@ -119,7 +119,7 @@ then workspace name, then identifier/app-id, then the window title. It uses 100% of OSD window width. - + diff --git a/include/config/rcxml.h b/include/config/rcxml.h index 8fd11aa0..6036be9c 100644 --- a/include/config/rcxml.h +++ b/include/config/rcxml.h @@ -182,7 +182,7 @@ struct rcxml { bool outlines; bool unshade; enum window_switcher_order order; - enum lab_view_criteria criteria; + enum cycle_workspace_filter workspace_filter; /* deprecated */ struct { bool show; enum cycle_osd_style style; diff --git a/include/config/types.h b/include/config/types.h index 540f609f..81d8fd36 100644 --- a/include/config/types.h +++ b/include/config/types.h @@ -117,6 +117,11 @@ enum cycle_osd_style { CYCLE_OSD_STYLE_THUMBNAIL, }; +enum cycle_workspace_filter { + CYCLE_WORKSPACE_ALL, + CYCLE_WORKSPACE_CURRENT, +}; + enum cycle_output_filter { CYCLE_OUTPUT_ALL, CYCLE_OUTPUT_CURSOR, diff --git a/include/cycle.h b/include/cycle.h index ba58b88d..2f2c5a2a 100644 --- a/include/cycle.h +++ b/include/cycle.h @@ -4,6 +4,7 @@ #include #include +#include "config/types.h" struct output; @@ -42,13 +43,18 @@ struct cycle_osd_field { struct wl_list link; /* struct rcxml.window_switcher.osd.fields */ }; +struct cycle_filter { + enum cycle_workspace_filter workspace; +}; + struct buf; struct view; struct server; struct wlr_scene_node; /* Begin window switcher */ -void cycle_begin(struct server *server, enum lab_cycle_dir direction); +void cycle_begin(struct server *server, enum lab_cycle_dir direction, + struct cycle_filter filter); /* Cycle the selected view in the window switcher */ void cycle_step(struct server *server, enum lab_cycle_dir direction); diff --git a/include/labwc.h b/include/labwc.h index 3d3ca2a3..ee10af12 100644 --- a/include/labwc.h +++ b/include/labwc.h @@ -5,6 +5,7 @@ #include #include #include "common/set.h" +#include "cycle.h" #include "input/cursor.h" #include "overlay.h" @@ -310,6 +311,7 @@ struct server { struct wlr_scene_node *preview_node; struct wlr_scene_node *preview_dummy; struct lab_scene_rect *preview_outline; + struct cycle_filter filter; } cycle; struct theme *theme; diff --git a/src/action.c b/src/action.c index 7ddba0be..9fb7ca22 100644 --- a/src/action.c +++ b/src/action.c @@ -366,6 +366,20 @@ action_arg_from_xml_node(struct action *action, const char *nodename, const char goto cleanup; } break; + case ACTION_TYPE_NEXT_WINDOW: + case ACTION_TYPE_PREVIOUS_WINDOW: + if (!strcasecmp(argument, "workspace")) { + if (!strcasecmp(content, "all")) { + action_arg_add_int(action, argument, CYCLE_WORKSPACE_ALL); + } else if (!strcasecmp(content, "current")) { + action_arg_add_int(action, argument, CYCLE_WORKSPACE_CURRENT); + } else { + wlr_log(WLR_ERROR, "Invalid argument for action %s: '%s' (%s)", + action_names[action->type], argument, content); + } + goto cleanup; + } + break; case ACTION_TYPE_SHOW_MENU: if (!strcmp(argument, "menu")) { action_arg_add_str(action, argument, content); @@ -1126,19 +1140,20 @@ run_action(struct view *view, struct server *server, struct action *action, } break; case ACTION_TYPE_NEXT_WINDOW: + case ACTION_TYPE_PREVIOUS_WINDOW: { + enum lab_cycle_dir dir = (action->type == ACTION_TYPE_NEXT_WINDOW) ? + LAB_CYCLE_DIR_FORWARD : LAB_CYCLE_DIR_BACKWARD; + struct cycle_filter filter = { + .workspace = action_get_int(action, "workspace", + rc.window_switcher.workspace_filter), + }; if (server->input_mode == LAB_INPUT_STATE_CYCLE) { - cycle_step(server, LAB_CYCLE_DIR_FORWARD); + cycle_step(server, dir); } else { - cycle_begin(server, LAB_CYCLE_DIR_FORWARD); - } - break; - case ACTION_TYPE_PREVIOUS_WINDOW: - if (server->input_mode == LAB_INPUT_STATE_CYCLE) { - cycle_step(server, LAB_CYCLE_DIR_BACKWARD); - } else { - cycle_begin(server, LAB_CYCLE_DIR_BACKWARD); + cycle_begin(server, dir, filter); } break; + } case ACTION_TYPE_RECONFIGURE: kill(getpid(), SIGHUP); break; diff --git a/src/config/rcxml.c b/src/config/rcxml.c index 60e709f2..00217596 100644 --- a/src/config/rcxml.c +++ b/src/config/rcxml.c @@ -1269,10 +1269,16 @@ entry(xmlNode *node, char *nodename, char *content) } else if (!strcasecmp(nodename, "outlines.windowSwitcher")) { set_bool(content, &rc.window_switcher.outlines); } else if (!strcasecmp(nodename, "allWorkspaces.windowSwitcher")) { - if (parse_bool(content, -1) == true) { - rc.window_switcher.criteria &= - ~LAB_VIEW_CRITERIA_CURRENT_WORKSPACE; + int ret = parse_bool(content, -1); + if (ret < 0) { + wlr_log(WLR_ERROR, "Invalid value for : '%s'", content); + } else { + rc.window_switcher.workspace_filter = ret ? + CYCLE_WORKSPACE_ALL : CYCLE_WORKSPACE_CURRENT; } + wlr_log(WLR_ERROR, " is deprecated." + " Use instead."); } else if (!strcasecmp(nodename, "unshade.windowSwitcher")) { set_bool(content, &rc.window_switcher.unshade); @@ -1493,9 +1499,7 @@ rcxml_init(void) rc.window_switcher.preview = true; rc.window_switcher.outlines = true; rc.window_switcher.unshade = true; - rc.window_switcher.criteria = LAB_VIEW_CRITERIA_CURRENT_WORKSPACE - | LAB_VIEW_CRITERIA_ROOT_TOPLEVEL - | LAB_VIEW_CRITERIA_NO_SKIP_WINDOW_SWITCHER; + rc.window_switcher.workspace_filter = CYCLE_WORKSPACE_CURRENT; rc.window_switcher.order = WINDOW_SWITCHER_ORDER_FOCUS; rc.resize_indicator = LAB_RESIZE_INDICATOR_NEVER; diff --git a/src/cycle/cycle.c b/src/cycle/cycle.c index 067c9929..9ae5b9b0 100644 --- a/src/cycle/cycle.c +++ b/src/cycle/cycle.c @@ -17,7 +17,7 @@ #include "theme.h" #include "view.h" -static bool init_cycle(struct server *server); +static bool init_cycle(struct server *server, struct cycle_filter filter); static void update_cycle(struct server *server); static void destroy_cycle(struct server *server); @@ -93,9 +93,10 @@ cycle_reinitialize(struct server *server) struct view *selected_view = cycle->selected_view; struct view *selected_view_prev = get_next_selected_view(server, LAB_CYCLE_DIR_BACKWARD); + struct cycle_filter filter = cycle->filter; destroy_cycle(server); - if (init_cycle(server)) { + if (init_cycle(server, filter)) { /* * Preserve the selected view (or its previous view) if it's * still in the cycle list @@ -152,13 +153,14 @@ restore_preview_node(struct server *server) } void -cycle_begin(struct server *server, enum lab_cycle_dir direction) +cycle_begin(struct server *server, enum lab_cycle_dir direction, + struct cycle_filter filter) { if (server->input_mode != LAB_INPUT_STATE_PASSTHROUGH) { return; } - if (!init_cycle(server)) { + if (!init_cycle(server, filter)) { return; } @@ -314,10 +316,17 @@ insert_view_ordered_by_age(struct wl_list *views, struct view *new_view) /* Return false on failure */ static bool -init_cycle(struct server *server) +init_cycle(struct server *server, struct cycle_filter filter) { + enum lab_view_criteria criteria = + LAB_VIEW_CRITERIA_NO_SKIP_WINDOW_SWITCHER + | LAB_VIEW_CRITERIA_ROOT_TOPLEVEL; + if (filter.workspace == CYCLE_WORKSPACE_CURRENT) { + criteria |= LAB_VIEW_CRITERIA_CURRENT_WORKSPACE; + } + struct view *view; - for_each_view(view, &server->views, rc.window_switcher.criteria) { + for_each_view(view, &server->views, criteria) { if (rc.window_switcher.order == WINDOW_SWITCHER_ORDER_AGE) { insert_view_ordered_by_age(&server->cycle.views, view); } else { @@ -328,6 +337,7 @@ init_cycle(struct server *server) wlr_log(WLR_DEBUG, "no views to switch between"); return false; } + server->cycle.filter = filter; if (rc.window_switcher.osd.show) { /* Create OSD */ @@ -406,4 +416,5 @@ destroy_cycle(struct server *server) } server->cycle.selected_view = NULL; + server->cycle.filter = (struct cycle_filter){0}; } From 610d8695612953910256e84b8f58a166292dc9d4 Mon Sep 17 00:00:00 2001 From: tokyo4j Date: Fri, 12 Dec 2025 04:34:38 +0900 Subject: [PATCH 07/43] cycle: add output="all|focused|cursor" filters windows by the output they are on. identifier="all|current" filters windows by their app-id. --- docs/labwc-actions.5.scd | 12 ++++++++++-- docs/rc.xml.all | 2 +- include/config/types.h | 5 +++++ include/cycle.h | 2 ++ src/action.c | 28 ++++++++++++++++++++++++++++ src/cycle/cycle.c | 15 +++++++++++++++ 6 files changed, 61 insertions(+), 3 deletions(-) diff --git a/docs/labwc-actions.5.scd b/docs/labwc-actions.5.scd index 56c840a6..fe468cd1 100644 --- a/docs/labwc-actions.5.scd +++ b/docs/labwc-actions.5.scd @@ -125,8 +125,8 @@ Actions are used in menus and keyboard/mouse bindings. Resize and move the active window back to its untiled or unmaximized position if it had been maximized or tiled to a direction or region. -**++ -** +**++ +** Cycle focus to next/previous window, respectively. Default keybinds for NextWindow and PreviousWindow are Alt-Tab and @@ -137,6 +137,14 @@ Actions are used in menus and keyboard/mouse bindings. This determines whether to cycle through windows on all workspaces or the current workspace. Default is "current". + *output* [all|focused|cursor] + This determines whether to cycle through windows on all outputs, the focused + output, or the output under the cursor. Default is "all". + + *identifier* [all|current] + This determines whether to cycle through all windows or only windows of the + same application as the currently focused window. Default is "all". + ** Re-load configuration and theme files. diff --git a/docs/rc.xml.all b/docs/rc.xml.all index dd348300..bc9566fe 100644 --- a/docs/rc.xml.all +++ b/docs/rc.xml.all @@ -98,7 +98,7 @@ Some contents are fixed-length and others are variable-length. See "man 5 labwc-config" for details. - + diff --git a/include/config/types.h b/include/config/types.h index 81d8fd36..fc293cd8 100644 --- a/include/config/types.h +++ b/include/config/types.h @@ -128,4 +128,9 @@ enum cycle_output_filter { CYCLE_OUTPUT_FOCUSED, }; +enum cycle_app_id_filter { + CYCLE_APP_ID_ALL, + CYCLE_APP_ID_CURRENT, +}; + #endif /* LABWC_CONFIG_TYPES_H */ diff --git a/include/cycle.h b/include/cycle.h index 2f2c5a2a..d5430cb4 100644 --- a/include/cycle.h +++ b/include/cycle.h @@ -45,6 +45,8 @@ struct cycle_osd_field { struct cycle_filter { enum cycle_workspace_filter workspace; + enum cycle_output_filter output; + enum cycle_app_id_filter app_id; }; struct buf; diff --git a/src/action.c b/src/action.c index 9fb7ca22..5a01d157 100644 --- a/src/action.c +++ b/src/action.c @@ -379,6 +379,30 @@ action_arg_from_xml_node(struct action *action, const char *nodename, const char } goto cleanup; } + if (!strcasecmp(argument, "output")) { + if (!strcasecmp(content, "all")) { + action_arg_add_int(action, argument, CYCLE_OUTPUT_ALL); + } else if (!strcasecmp(content, "cursor")) { + action_arg_add_int(action, argument, CYCLE_OUTPUT_CURSOR); + } else if (!strcasecmp(content, "focused")) { + action_arg_add_int(action, argument, CYCLE_OUTPUT_FOCUSED); + } else { + wlr_log(WLR_ERROR, "Invalid argument for action %s: '%s' (%s)", + action_names[action->type], argument, content); + } + goto cleanup; + } + if (!strcasecmp(argument, "identifier")) { + if (!strcasecmp(content, "all")) { + action_arg_add_int(action, argument, CYCLE_APP_ID_ALL); + } else if (!strcasecmp(content, "current")) { + action_arg_add_int(action, argument, CYCLE_APP_ID_CURRENT); + } else { + wlr_log(WLR_ERROR, "Invalid argument for action %s: '%s' (%s)", + action_names[action->type], argument, content); + } + goto cleanup; + } break; case ACTION_TYPE_SHOW_MENU: if (!strcmp(argument, "menu")) { @@ -1146,6 +1170,10 @@ run_action(struct view *view, struct server *server, struct action *action, struct cycle_filter filter = { .workspace = action_get_int(action, "workspace", rc.window_switcher.workspace_filter), + .output = action_get_int(action, "output", + CYCLE_OUTPUT_ALL), + .app_id = action_get_int(action, "identifier", + CYCLE_APP_ID_ALL), }; if (server->input_mode == LAB_INPUT_STATE_CYCLE) { cycle_step(server, dir); diff --git a/src/cycle/cycle.c b/src/cycle/cycle.c index 9ae5b9b0..e0391f1d 100644 --- a/src/cycle/cycle.c +++ b/src/cycle/cycle.c @@ -325,8 +325,23 @@ init_cycle(struct server *server, struct cycle_filter filter) criteria |= LAB_VIEW_CRITERIA_CURRENT_WORKSPACE; } + uint64_t cycle_outputs = + get_outputs_by_filter(server, filter.output); + + const char *cycle_app_id = NULL; + if (filter.app_id == CYCLE_APP_ID_CURRENT && server->active_view) { + cycle_app_id = server->active_view->app_id; + } + struct view *view; for_each_view(view, &server->views, criteria) { + if (!(cycle_outputs & view->output->id_bit)) { + continue; + } + if (cycle_app_id && strcmp(view->app_id, cycle_app_id) != 0) { + continue; + } + if (rc.window_switcher.order == WINDOW_SWITCHER_ORDER_AGE) { insert_view_ordered_by_age(&server->cycle.views, view); } else { From ec9579fdc19749ac204ca4150920bb9606f08242 Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Fri, 26 Dec 2025 16:07:27 +0100 Subject: [PATCH 08/43] CI: disable IRC notifications for now Libera started requiring an account for the IP ranges of GH CI. This currently results in the IRC notification job running for more than one hour because the IRC client it uses doesn't terminate properly after giving up and thus eats our CI minutes. The patch disables the notifications for now until we figure out a better way, either by registering a libera account for the bot and supplying the credentials via secret or by changing to GH webhooks to a publicly available service like pipe.pico.sh which then can be listened to by our existing IRC bot. --- .github/workflows/{irc.yml => irc.yml.disabled} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{irc.yml => irc.yml.disabled} (100%) diff --git a/.github/workflows/irc.yml b/.github/workflows/irc.yml.disabled similarity index 100% rename from .github/workflows/irc.yml rename to .github/workflows/irc.yml.disabled From 276d4e61f9ebbd6cbb74632c6ff50cc5a8556274 Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Fri, 26 Dec 2025 18:06:21 +0100 Subject: [PATCH 09/43] config/rcxml.c: prevent wrong parse_bool() err message by only parsing the boolean when it is not `disabledOnExternalMouse`. Fixes: #3293 --- src/config/rcxml.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/config/rcxml.c b/src/config/rcxml.c index 00217596..c1b73c35 100644 --- a/src/config/rcxml.c +++ b/src/config/rcxml.c @@ -684,6 +684,10 @@ get_send_events_mode(const char *s) goto err; } + if (!strcasecmp(s, "disabledOnExternalMouse")) { + return LIBINPUT_CONFIG_SEND_EVENTS_DISABLED_ON_EXTERNAL_MOUSE; + } + int ret = parse_bool(s, -1); if (ret >= 0) { return ret @@ -691,10 +695,6 @@ get_send_events_mode(const char *s) : LIBINPUT_CONFIG_SEND_EVENTS_DISABLED; } - if (!strcasecmp(s, "disabledOnExternalMouse")) { - return LIBINPUT_CONFIG_SEND_EVENTS_DISABLED_ON_EXTERNAL_MOUSE; - } - err: wlr_log(WLR_INFO, "Not a recognised send events mode"); return -1; From dfe428ae14e65944f8d7d4ed4d7bb83e8d895ae2 Mon Sep 17 00:00:00 2001 From: tokyo4j Date: Mon, 29 Dec 2025 02:50:45 +0900 Subject: [PATCH 10/43] cycle: refactor to aggregate type definitions into cycle.h We declared `cycle_state` struct in `labwc.h` and `cycle_osd_scene` struct in `output.h`, which was unclean in terms of separation of concerns. So this commit firstly moves `cycle_state` to `cycle.h`, then replaces `cycle_osd_scene` in `output.h` with `cycle_osd_output` in `cycle.h` which is dynamically allocated in a similar manner to `session_lock_output`. This ensures that all states about alt-tabbing are stored in `server->cycle`. Also, this commit fixes a rare memory leak in `output->cycle_osd.items` when an output is destroyed while alt-tabbing, by freeing it when the osd tree is destroyed. --- include/cycle.h | 33 +++++++++++++++++---- include/labwc.h | 11 +------ include/output.h | 5 ---- src/cycle/cycle.c | 60 +++++++++++++++++++++++++-------------- src/cycle/osd-classic.c | 26 ++++++++--------- src/cycle/osd-thumbnail.c | 30 ++++++++++---------- src/output.c | 1 - src/server.c | 1 + 8 files changed, 95 insertions(+), 72 deletions(-) diff --git a/include/cycle.h b/include/cycle.h index d5430cb4..2e5b57ec 100644 --- a/include/cycle.h +++ b/include/cycle.h @@ -4,6 +4,7 @@ #include #include +#include #include "config/types.h" struct output; @@ -49,6 +50,27 @@ struct cycle_filter { enum cycle_app_id_filter app_id; }; +struct cycle_state { + struct view *selected_view; + struct wl_list views; + struct wl_list osd_outputs; /* struct cycle_osd_output.link */ + bool preview_was_shaded; + bool preview_was_enabled; + struct wlr_scene_node *preview_node; + struct wlr_scene_node *preview_dummy; + struct lab_scene_rect *preview_outline; + struct cycle_filter filter; +}; + +struct cycle_osd_output { + struct wl_list link; /* struct cycle_state.osd_outputs */ + struct output *output; + struct wl_listener tree_destroy; + + struct wl_list items; /* struct cycle_osd_item.link */ + struct wlr_scene_tree *tree; +}; + struct buf; struct view; struct server; @@ -92,15 +114,14 @@ struct cycle_osd_item { struct cycle_osd_impl { /* - * Create a scene-tree of OSD for an output. - * This sets output->cycle_osd.{items,tree}. + * Create a scene-tree of OSD for an output and fill + * osd_output->items. */ - void (*create)(struct output *output); + void (*init)(struct cycle_osd_output *osd_output); /* - * Update output->cycle_osd.tree to highlight - * server->cycle_state.selected_view. + * Update the OSD to highlight server->cycle.selected_view. */ - void (*update)(struct output *output); + void (*update)(struct cycle_osd_output *osd_output); }; extern struct cycle_osd_impl cycle_osd_classic_impl; diff --git a/include/labwc.h b/include/labwc.h index ee10af12..47af8875 100644 --- a/include/labwc.h +++ b/include/labwc.h @@ -303,16 +303,7 @@ struct server { struct wlr_security_context_manager_v1 *security_context_manager_v1; /* Set when in cycle (alt-tab) mode */ - struct cycle_state { - struct view *selected_view; - struct wl_list views; - bool preview_was_shaded; - bool preview_was_enabled; - struct wlr_scene_node *preview_node; - struct wlr_scene_node *preview_dummy; - struct lab_scene_rect *preview_outline; - struct cycle_filter filter; - } cycle; + struct cycle_state cycle; struct theme *theme; diff --git a/include/output.h b/include/output.h index 25001247..2b1e4bcf 100644 --- a/include/output.h +++ b/include/output.h @@ -19,11 +19,6 @@ struct output { struct wlr_scene_tree *session_lock_tree; struct wlr_scene_buffer *workspace_osd; - struct cycle_osd_scene { - struct wl_list items; /* struct cycle_osd_item */ - struct wlr_scene_tree *tree; - } cycle_osd; - /* In output-relative scene coordinates */ struct wlr_box usable_area; diff --git a/src/cycle/cycle.c b/src/cycle/cycle.c index e0391f1d..88e713f1 100644 --- a/src/cycle/cycle.c +++ b/src/cycle/cycle.c @@ -6,6 +6,7 @@ #include #include "common/lab-scene-rect.h" #include "common/list.h" +#include "common/mem.h" #include "common/scene-helpers.h" #include "config/rcxml.h" #include "labwc.h" @@ -314,6 +315,21 @@ insert_view_ordered_by_age(struct wl_list *views, struct view *new_view) wl_list_insert(link, &new_view->cycle_link); } +static void +handle_osd_tree_destroy(struct wl_listener *listener, void *data) +{ + struct cycle_osd_output *osd_output = + wl_container_of(listener, osd_output, tree_destroy); + struct cycle_osd_item *item, *tmp; + wl_list_for_each_safe(item, tmp, &osd_output->items, link) { + wl_list_remove(&item->link); + free(item); + } + wl_list_remove(&osd_output->tree_destroy.link); + wl_list_remove(&osd_output->link); + free(osd_output); +} + /* Return false on failure */ static bool init_cycle(struct server *server, struct cycle_filter filter) @@ -366,8 +382,17 @@ init_cycle(struct server *server, struct cycle_filter filter) if (!output_is_usable(output)) { continue; } - get_osd_impl()->create(output); - assert(output->cycle_osd.tree); + + struct cycle_osd_output *osd_output = znew(*osd_output); + wl_list_append(&server->cycle.osd_outputs, &osd_output->link); + osd_output->output = output; + wl_list_init(&osd_output->items); + + get_osd_impl()->init(osd_output); + + osd_output->tree_destroy.notify = handle_osd_tree_destroy; + wl_signal_add(&osd_output->tree->node.events.destroy, + &osd_output->tree_destroy); } } @@ -380,11 +405,9 @@ update_cycle(struct server *server) struct cycle_state *cycle = &server->cycle; if (rc.window_switcher.osd.show) { - struct output *output; - wl_list_for_each(output, &server->outputs, link) { - if (output->cycle_osd.tree) { - get_osd_impl()->update(output); - } + struct cycle_osd_output *osd_output; + wl_list_for_each(osd_output, &cycle->osd_outputs, link) { + get_osd_impl()->update(osd_output); } } @@ -394,8 +417,8 @@ update_cycle(struct server *server) /* Outline current window */ if (rc.window_switcher.outlines) { - if (view_is_focusable(server->cycle.selected_view)) { - update_preview_outlines(server->cycle.selected_view); + if (view_is_focusable(cycle->selected_view)) { + update_preview_outlines(cycle->selected_view); } } } @@ -404,17 +427,10 @@ update_cycle(struct server *server) static void destroy_cycle(struct server *server) { - struct output *output; - wl_list_for_each(output, &server->outputs, link) { - struct cycle_osd_item *item, *tmp; - wl_list_for_each_safe(item, tmp, &output->cycle_osd.items, link) { - wl_list_remove(&item->link); - free(item); - } - if (output->cycle_osd.tree) { - wlr_scene_node_destroy(&output->cycle_osd.tree->node); - output->cycle_osd.tree = NULL; - } + struct cycle_osd_output *osd_output, *tmp; + wl_list_for_each_safe(osd_output, tmp, &server->cycle.osd_outputs, link) { + /* calls handle_osd_tree_destroy() */ + wlr_scene_node_destroy(&osd_output->tree->node); } restore_preview_node(server); @@ -424,8 +440,8 @@ destroy_cycle(struct server *server) server->cycle.preview_outline = NULL; } - struct view *view, *tmp; - wl_list_for_each_safe(view, tmp, &server->cycle.views, cycle_link) { + struct view *view, *tmp2; + wl_list_for_each_safe(view, tmp2, &server->cycle.views, cycle_link) { wl_list_remove(&view->cycle_link); view->cycle_link = (struct wl_list){0}; } diff --git a/src/cycle/osd-classic.c b/src/cycle/osd-classic.c index 8e01dc24..c0509249 100644 --- a/src/cycle/osd-classic.c +++ b/src/cycle/osd-classic.c @@ -77,10 +77,9 @@ create_fields_scene(struct server *server, struct view *view, } static void -cycle_osd_classic_create(struct output *output) +cycle_osd_classic_init(struct cycle_osd_output *osd_output) { - assert(!output->cycle_osd.tree && wl_list_empty(&output->cycle_osd.items)); - + struct output *output = osd_output->output; struct server *server = output->server; struct theme *theme = server->theme; struct window_switcher_classic_theme *switcher_theme = @@ -104,7 +103,7 @@ cycle_osd_classic_create(struct output *output) h += switcher_theme->item_height; } - output->cycle_osd.tree = wlr_scene_tree_create(output->cycle_osd_tree); + osd_output->tree = wlr_scene_tree_create(output->cycle_osd_tree); float *text_color = theme->osd_label_text_color; float *bg_color = theme->osd_bg_color; @@ -118,7 +117,7 @@ cycle_osd_classic_create(struct output *output) .width = w, .height = h, }; - lab_scene_rect_create(output->cycle_osd.tree, &bg_opts); + lab_scene_rect_create(osd_output->tree, &bg_opts); int y = padding; @@ -136,7 +135,7 @@ cycle_osd_classic_create(struct output *output) } struct scaled_font_buffer *font_buffer = - scaled_font_buffer_create(output->cycle_osd.tree); + scaled_font_buffer_create(osd_output->tree); wlr_scene_node_set_position(&font_buffer->scene_buffer->node, x, y + (switcher_theme->item_height - font_height(&font)) / 2); scaled_font_buffer_update(font_buffer, workspace_name, 0, @@ -159,9 +158,9 @@ cycle_osd_classic_create(struct output *output) struct view *view; wl_list_for_each(view, &server->cycle.views, cycle_link) { struct cycle_osd_classic_item *item = znew(*item); - wl_list_append(&output->cycle_osd.items, &item->base.link); + wl_list_append(&osd_output->items, &item->base.link); item->base.view = view; - item->base.tree = wlr_scene_tree_create(output->cycle_osd.tree); + item->base.tree = wlr_scene_tree_create(osd_output->tree); node_descriptor_create(&item->base.tree->node, LAB_NODE_CYCLE_OSD_ITEM, NULL, item); /* @@ -218,23 +217,24 @@ cycle_osd_classic_create(struct output *output) error:; /* Center OSD */ - wlr_scene_node_set_position(&output->cycle_osd.tree->node, + wlr_scene_node_set_position(&osd_output->tree->node, output_box.x + (output_box.width - w) / 2, output_box.y + (output_box.height - h) / 2); } static void -cycle_osd_classic_update(struct output *output) +cycle_osd_classic_update(struct cycle_osd_output *osd_output) { + struct server *server = osd_output->output->server; struct cycle_osd_classic_item *item; - wl_list_for_each(item, &output->cycle_osd.items, base.link) { - bool active = item->base.view == output->server->cycle.selected_view; + wl_list_for_each(item, &osd_output->items, base.link) { + bool active = item->base.view == server->cycle.selected_view; wlr_scene_node_set_enabled(&item->normal_tree->node, !active); wlr_scene_node_set_enabled(&item->active_tree->node, active); } } struct cycle_osd_impl cycle_osd_classic_impl = { - .create = cycle_osd_classic_create, + .init = cycle_osd_classic_init, .update = cycle_osd_classic_update, }; diff --git a/src/cycle/osd-thumbnail.c b/src/cycle/osd-thumbnail.c index d17b1aa0..dde48786 100644 --- a/src/cycle/osd-thumbnail.c +++ b/src/cycle/osd-thumbnail.c @@ -117,9 +117,9 @@ create_label(struct wlr_scene_tree *parent, struct view *view, static struct cycle_osd_thumbnail_item * create_item_scene(struct wlr_scene_tree *parent, struct view *view, - struct output *output) + struct cycle_osd_output *osd_output) { - struct server *server = output->server; + struct server *server = osd_output->output->server; struct theme *theme = server->theme; struct window_switcher_thumbnail_theme *switcher_theme = &theme->osd_window_switcher_thumbnail; @@ -137,7 +137,7 @@ create_item_scene(struct wlr_scene_tree *parent, struct view *view, } struct cycle_osd_thumbnail_item *item = znew(*item); - wl_list_append(&output->cycle_osd.items, &item->base.link); + wl_list_append(&osd_output->items, &item->base.link); struct wlr_scene_tree *tree = wlr_scene_tree_create(parent); node_descriptor_create(&tree->node, LAB_NODE_CYCLE_OSD_ITEM, NULL, item); item->base.tree = tree; @@ -159,7 +159,7 @@ create_item_scene(struct wlr_scene_tree *parent, struct view *view, switcher_theme->item_height, (float[4]) {0}); /* thumbnail */ - struct wlr_buffer *thumb_buffer = render_thumb(output, view); + struct wlr_buffer *thumb_buffer = render_thumb(osd_output->output, view); if (thumb_buffer) { struct wlr_scene_buffer *thumb_scene_buffer = wlr_scene_buffer_create(tree, thumb_buffer); @@ -226,17 +226,16 @@ get_items_geometry(struct output *output, struct theme *theme, } static void -cycle_osd_thumbnail_create(struct output *output) +cycle_osd_thumbnail_init(struct cycle_osd_output *osd_output) { - assert(!output->cycle_osd.tree && wl_list_empty(&output->cycle_osd.items)); - + struct output *output = osd_output->output; struct server *server = output->server; struct theme *theme = server->theme; struct window_switcher_thumbnail_theme *switcher_theme = &theme->osd_window_switcher_thumbnail; int padding = theme->osd_border_width + switcher_theme->padding; - output->cycle_osd.tree = wlr_scene_tree_create(output->cycle_osd_tree); + osd_output->tree = wlr_scene_tree_create(output->cycle_osd_tree); int nr_views = wl_list_length(&server->cycle.views); assert(nr_views > 0); @@ -248,7 +247,7 @@ cycle_osd_thumbnail_create(struct output *output) int index = 0; wl_list_for_each(view, &server->cycle.views, cycle_link) { struct cycle_osd_thumbnail_item *item = create_item_scene( - output->cycle_osd.tree, view, output); + osd_output->tree, view, osd_output); if (!item) { break; } @@ -268,7 +267,7 @@ cycle_osd_thumbnail_create(struct output *output) .height = nr_rows * switcher_theme->item_height + 2 * padding, }; struct lab_scene_rect *bg = - lab_scene_rect_create(output->cycle_osd.tree, &bg_opts); + lab_scene_rect_create(osd_output->tree, &bg_opts); wlr_scene_node_lower_to_bottom(&bg->tree->node); /* center */ @@ -277,15 +276,16 @@ cycle_osd_thumbnail_create(struct output *output) &output_box); int lx = output_box.x + (output_box.width - bg_opts.width) / 2; int ly = output_box.y + (output_box.height - bg_opts.height) / 2; - wlr_scene_node_set_position(&output->cycle_osd.tree->node, lx, ly); + wlr_scene_node_set_position(&osd_output->tree->node, lx, ly); } static void -cycle_osd_thumbnail_update(struct output *output) +cycle_osd_thumbnail_update(struct cycle_osd_output *osd_output) { + struct server *server = osd_output->output->server; struct cycle_osd_thumbnail_item *item; - wl_list_for_each(item, &output->cycle_osd.items, base.link) { - bool active = (item->base.view == output->server->cycle.selected_view); + wl_list_for_each(item, &osd_output->items, base.link) { + bool active = (item->base.view == server->cycle.selected_view); wlr_scene_node_set_enabled(&item->active_bg->tree->node, active); wlr_scene_node_set_enabled( &item->active_label->scene_buffer->node, active); @@ -295,6 +295,6 @@ cycle_osd_thumbnail_update(struct output *output) } struct cycle_osd_impl cycle_osd_thumbnail_impl = { - .create = cycle_osd_thumbnail_create, + .init = cycle_osd_thumbnail_init, .update = cycle_osd_thumbnail_update, }; diff --git a/src/output.c b/src/output.c index f8c19c56..d25d2736 100644 --- a/src/output.c +++ b/src/output.c @@ -542,7 +542,6 @@ handle_new_output(struct wl_listener *listener, void *data) wl_signal_add(&wlr_output->events.request_state, &output->request_state); wl_list_init(&output->regions); - wl_list_init(&output->cycle_osd.items); /* * Create layer-trees (background, bottom, top and overlay) and diff --git a/src/server.c b/src/server.c index 9f271b10..23b83d3b 100644 --- a/src/server.c +++ b/src/server.c @@ -550,6 +550,7 @@ server_init(struct server *server) wl_list_init(&server->views); wl_list_init(&server->unmanaged_surfaces); wl_list_init(&server->cycle.views); + wl_list_init(&server->cycle.osd_outputs); server->scene = wlr_scene_create(); if (!server->scene) { From 8ad96c0410ea7db34d6f93e735901ad695a8eb19 Mon Sep 17 00:00:00 2001 From: tokyo4j Date: Mon, 8 Dec 2025 17:14:31 +0900 Subject: [PATCH 11/43] cycle: add server->cycle_preview_tree This doesn't change any behaviors. --- include/labwc.h | 1 + src/cycle/cycle.c | 10 +++------- src/server.c | 32 +++++++++++++++++--------------- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/include/labwc.h b/include/labwc.h index 47af8875..c4848a11 100644 --- a/include/labwc.h +++ b/include/labwc.h @@ -238,6 +238,7 @@ struct server { /* Tree for unmanaged xsurfaces without initialized view (usually popups) */ struct wlr_scene_tree *unmanaged_tree; #endif + struct wlr_scene_tree *cycle_preview_tree; /* Tree for built in menu */ struct wlr_scene_tree *menu_tree; diff --git a/src/cycle/cycle.c b/src/cycle/cycle.c index 88e713f1..390cf06c 100644 --- a/src/cycle/cycle.c +++ b/src/cycle/cycle.c @@ -40,7 +40,8 @@ update_preview_outlines(struct view *view) .border_width = theme->osd_window_switcher_preview_border_width, }; rect = lab_scene_rect_create(&server->scene->tree, &opts); - wlr_scene_node_place_above(&rect->tree->node, &server->menu_tree->node); + wlr_scene_node_place_above(&rect->tree->node, + &server->cycle_preview_tree->node); server->cycle.preview_outline = rect; } @@ -245,13 +246,8 @@ preview_selected_view(struct view *view) cycle->preview_was_shaded = true; } - /* - * FIXME: This abuses an implementation detail of the always-on-top tree. - * Create a permanent server->osd_preview_tree instead that can - * also be used as parent for the preview outlines. - */ wlr_scene_node_reparent(cycle->preview_node, - view->server->view_tree_always_on_top); + view->server->cycle_preview_tree); /* Finally raise selected node to the top */ wlr_scene_node_raise_to_top(cycle->preview_node); diff --git a/src/server.c b/src/server.c index 23b83d3b..2e52fb45 100644 --- a/src/server.c +++ b/src/server.c @@ -564,21 +564,22 @@ server_init(struct server *server) * z-order for nodes which cover the whole work-area. For per-output * scene-trees, see handle_new_output() in src/output.c * - * | Type | Scene Tree | Per Output | Example - * | ------------------- | ---------------- | ---------- | ------- - * | ext-session | lock-screen | Yes | swaylock - * | window switcher OSD | cycle_osd_tree | Yes | - * | compositor-menu | menu_tree | No | root-menu - * | layer-shell | layer-popups | Yes | - * | layer-shell | overlay-layer | Yes | - * | layer-shell | top-layer | Yes | waybar - * | xwayland-OR | unmanaged | No | dmenu - * | xdg-popups | xdg-popups | No | - * | toplevels windows | always-on-top | No | - * | toplevels windows | normal | No | firefox - * | toplevels windows | always-on-bottom | No | pcmanfm-qt --desktop - * | layer-shell | bottom-layer | Yes | waybar - * | layer-shell | background-layer | Yes | swaybg + * | Scene Tree | Description + * | ---------------------------------- | ------------------------------------- + * | output->session_lock_tree | session lock surfaces (e.g. swaylock) + * | output->cycle_osd_tree | window switcher's on-screen display + * | server->cycle_preview_tree | window switcher's previewed window + * | server->menu_tree | labwc's server-side menus + * | output->layer_popup_tree | xdg popups on layer surfaces + * | output->layer_tree[3] | overlay layer surfaces (e.g. rofi) + * | output->layer_tree[2] | top layer surfaces (e.g. waybar) + * | server->unmanaged_tree | unmanaged X11 surfaces (e.g. dmenu) + * | server->xdg_popup_tree | xdg popups on xdg windows + * | server->view_tree_always_on_top | always-on-top xdg/X11 windows + * | server->view_tree | normal xdg/X11 windows (e.g. firefox) + * | server->view_tree_always_on_bottom | always-on-bottom xdg/X11 windows + * | output->layer_tree[1] | bottom layer surfaces + * | output->layer_tree[0] | background layer surfaces (e.g. swaybg) */ server->view_tree_always_on_bottom = wlr_scene_tree_create(&server->scene->tree); @@ -589,6 +590,7 @@ server_init(struct server *server) server->unmanaged_tree = wlr_scene_tree_create(&server->scene->tree); #endif server->menu_tree = wlr_scene_tree_create(&server->scene->tree); + server->cycle_preview_tree = wlr_scene_tree_create(&server->scene->tree); workspaces_init(server); From 9f50971e0bd0b7c4eaa5d6fc44edadb9b677d976 Mon Sep 17 00:00:00 2001 From: tokyo4j Date: Wed, 31 Dec 2025 19:15:11 +0900 Subject: [PATCH 12/43] Halt window switcher on Reconfigure There was an invalid memory access (since introduction of thumbnail style in 2e9292b) with following steps: 1. Press Alt-Tab 2. Update `