From 6ce25978e7328ee7263ad7642e942f68c792914f Mon Sep 17 00:00:00 2001
From: elviosak <33790211+elviosak@users.noreply.github.com>
Date: Tue, 28 Apr 2026 22:42:39 -0300
Subject: [PATCH] Add Next/PreviousWindowImmediate to switch windows without
OSD
Co-authored-by: @johanmalm
---
docs/labwc-actions.5.scd | 7 ++++-
include/cycle.h | 4 +++
include/view.h | 2 ++
src/action.c | 19 ++++++++++++++
src/cycle/cycle.c | 55 ++++++++++++++++++++++++++++++++++++++++
src/view.c | 10 ++++----
src/window-rules.c | 6 ++---
7 files changed, 94 insertions(+), 9 deletions(-)
diff --git a/docs/labwc-actions.5.scd b/docs/labwc-actions.5.scd
index fb616340..47ee1801 100644
--- a/docs/labwc-actions.5.scd
+++ b/docs/labwc-actions.5.scd
@@ -126,13 +126,18 @@ Actions are used in menus and keyboard/mouse bindings.
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
Shift-Alt-Tab. While cycling through windows, the arrow keys move the
selected window forwards/backwards and the escape key halts the cycling.
+ NextWindowImmediate and PreviousWindowImmediate skip the Window Switcher
+ and OSD, useful for binding to keys without modifiers.
+
*workspace* [all|current]
This determines whether to cycle through windows on all workspaces or the
current workspace. Default is "current".
diff --git a/include/cycle.h b/include/cycle.h
index 9bb4cb69..440b938f 100644
--- a/include/cycle.h
+++ b/include/cycle.h
@@ -103,6 +103,10 @@ void cycle_finish(bool switch_focus);
/* Re-initialize the window switcher */
void cycle_reinitialize(void);
+/* Immediately cycle to next/previous window */
+void cycle_immediate(enum lab_cycle_dir direction,
+ struct cycle_filter filter);
+
/* Focus the clicked window and close OSD */
void cycle_on_cursor_release(struct wlr_scene_node *node);
diff --git a/include/view.h b/include/view.h
index da7aef02..a179338e 100644
--- a/include/view.h
+++ b/include/view.h
@@ -630,4 +630,6 @@ enum lab_placement_policy view_placement_parse(const char *policy);
/* xdg.c */
struct wlr_xdg_surface *xdg_surface_from_view(struct view *view);
+bool view_matches_criteria(struct view *view, enum lab_view_criteria criteria);
+
#endif /* LABWC_VIEW_H */
diff --git a/src/action.c b/src/action.c
index 34435e02..daf8fb30 100644
--- a/src/action.c
+++ b/src/action.c
@@ -84,6 +84,8 @@ struct action_arg_list {
X(SHRINK_TO_EDGE, "ShrinkToEdge") \
X(NEXT_WINDOW, "NextWindow") \
X(PREVIOUS_WINDOW, "PreviousWindow") \
+ X(NEXT_WINDOW_IMMEDIATE, "NextWindowImmediate") \
+ X(PREVIOUS_WINDOW_IMMEDIATE, "PreviousWindowImmediate") \
X(RECONFIGURE, "Reconfigure") \
X(SHOW_MENU, "ShowMenu") \
X(TOGGLE_MAXIMIZE, "ToggleMaximize") \
@@ -337,6 +339,8 @@ action_arg_from_xml_node(struct action *action, const char *nodename, const char
break;
case ACTION_TYPE_NEXT_WINDOW:
case ACTION_TYPE_PREVIOUS_WINDOW:
+ case ACTION_TYPE_NEXT_WINDOW_IMMEDIATE:
+ case ACTION_TYPE_PREVIOUS_WINDOW_IMMEDIATE:
if (!strcasecmp(argument, "workspace")) {
if (!strcasecmp(content, "all")) {
action_arg_add_int(action, argument, CYCLE_WORKSPACE_ALL);
@@ -1146,6 +1150,21 @@ run_action(struct view *view, struct action *action,
}
break;
}
+ case ACTION_TYPE_NEXT_WINDOW_IMMEDIATE:
+ case ACTION_TYPE_PREVIOUS_WINDOW_IMMEDIATE: {
+ enum lab_cycle_dir dir = (action->type == ACTION_TYPE_NEXT_WINDOW_IMMEDIATE) ?
+ LAB_CYCLE_DIR_FORWARD : LAB_CYCLE_DIR_BACKWARD;
+ 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),
+ };
+ cycle_immediate(dir, filter);
+ break;
+ }
case ACTION_TYPE_RECONFIGURE:
kill(getpid(), SIGHUP);
break;
diff --git a/src/cycle/cycle.c b/src/cycle/cycle.c
index 2649f1e5..14682c9f 100644
--- a/src/cycle/cycle.c
+++ b/src/cycle/cycle.c
@@ -322,6 +322,61 @@ handle_osd_tree_destroy(struct wl_listener *listener, void *data)
free(osd_output);
}
+static struct wl_list *prev(struct wl_list *elm) { return elm->prev; }
+static struct wl_list *next(struct wl_list *elm) { return elm->next; }
+
+void
+cycle_immediate(enum lab_cycle_dir direction, struct cycle_filter filter)
+{
+ if (wl_list_empty(&server.views)) {
+ return;
+ }
+ enum lab_view_criteria criteria =
+ LAB_VIEW_CRITERIA_NO_SKIP_WINDOW_SWITCHER
+ | LAB_VIEW_CRITERIA_NO_DIALOG;
+ if (filter.workspace == CYCLE_WORKSPACE_CURRENT) {
+ criteria |= LAB_VIEW_CRITERIA_CURRENT_WORKSPACE;
+ }
+ uint64_t cycle_outputs = get_outputs_by_filter(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 wl_list *head = &server.views;
+ struct wl_list *(*iter)(struct wl_list *list);
+ iter = direction == LAB_CYCLE_DIR_FORWARD ? next : prev;
+
+ struct wl_list *from = (direction == LAB_CYCLE_DIR_FORWARD) && server.active_view
+ ? &server.active_view->link : head;
+
+ for (struct wl_list *elm = iter(from); elm != head; elm = iter(elm)) {
+ struct view *view = wl_container_of(elm, view, link);
+ if (!view_matches_criteria(view, criteria)) {
+ continue;
+ }
+ if (filter.output != CYCLE_OUTPUT_ALL) {
+ if (!view->output || !(cycle_outputs & view->output->id_bit)) {
+ continue;
+ }
+ }
+ if (cycle_app_id && strcmp(view->app_id, cycle_app_id) != 0) {
+ continue;
+ }
+ if (server.active_view && direction == LAB_CYCLE_DIR_FORWARD) {
+ /*
+ * When cycling forward, the current active view needs to be
+ * sent to back to keep the same sequence and avoid getting
+ * stuck in the 2 topmost views.
+ */
+ view_move_to_back(server.active_view);
+ }
+ desktop_focus_view(view, true);
+ break;
+ }
+ cursor_update_focus();
+}
+
/* Return false on failure */
static bool
init_cycle(struct cycle_filter filter)
diff --git a/src/view.c b/src/view.c
index 9b6604ad..bcbd366e 100644
--- a/src/view.c
+++ b/src/view.c
@@ -80,7 +80,7 @@ struct view_query *
view_query_create(void)
{
struct view_query *query = znew(*query);
- /* Must be synced with view_matches_criteria() in window-rules.c */
+ /* Must be synced with view_matches_rule() in window-rules.c */
query->window_type = LAB_WINDOW_TYPE_INVALID;
query->maximized = VIEW_AXIS_INVALID;
query->decoration = LAB_SSD_MODE_INVALID;
@@ -263,8 +263,8 @@ view_get_root(struct view *view)
return view;
}
-static bool
-matches_criteria(struct view *view, enum lab_view_criteria criteria)
+bool
+view_matches_criteria(struct view *view, enum lab_view_criteria criteria)
{
if (!view_is_focusable(view)) {
return false;
@@ -316,7 +316,7 @@ view_next(struct wl_list *head, struct view *view, enum lab_view_criteria criter
for (elm = elm->next; elm != head; elm = elm->next) {
view = wl_container_of(elm, view, link);
- if (matches_criteria(view, criteria)) {
+ if (view_matches_criteria(view, criteria)) {
return view;
}
}
@@ -332,7 +332,7 @@ view_prev(struct wl_list *head, struct view *view, enum lab_view_criteria criter
for (elm = elm->prev; elm != head; elm = elm->prev) {
view = wl_container_of(elm, view, link);
- if (matches_criteria(view, criteria)) {
+ if (view_matches_criteria(view, criteria)) {
return view;
}
}
diff --git a/src/window-rules.c b/src/window-rules.c
index 3911be7d..f43b92f4 100644
--- a/src/window-rules.c
+++ b/src/window-rules.c
@@ -24,7 +24,7 @@ other_instances_exist(struct view *self, struct view_query *query)
}
static bool
-view_matches_criteria(struct window_rule *rule, struct view *view)
+view_matches_rule(struct window_rule *rule, struct view *view)
{
struct view_query query = {
.identifier = rule->identifier,
@@ -52,7 +52,7 @@ window_rules_apply(struct view *view, enum window_rule_event event)
if (rule->event != event) {
continue;
}
- if (view_matches_criteria(rule, view)) {
+ if (view_matches_rule(rule, view)) {
actions_run(view, &rule->actions, NULL);
}
}
@@ -81,7 +81,7 @@ window_rules_get_property(struct view *view, const char *property)
* attribute would still return here if that property was asked
* for.
*/
- if (view_matches_criteria(rule, view)) {
+ if (view_matches_rule(rule, view)) {
if (rule->server_decoration
&& !strcasecmp(property, "serverDecoration")) {
return rule->server_decoration;