diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a9ccbb06..b7329c7b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -18,6 +18,7 @@ on:
- 'include/**'
- 'protocols/**'
- 'clients/**'
+ - 't/**'
- 'scripts/**'
- '.github/workflows/**'
diff --git a/NEWS.md b/NEWS.md
index 1ae97c51..e7d8b383 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -115,6 +115,51 @@ differently [#3099]. There is a pending fix [wlroots-5159].
[unreleased-commits]
+### Added
+
+- With the window-switcher custom field state specifiers 's' and 'S', show 's'
+ for shaded window @domo141 [#2895]
+- Support `xdg-dialog` protocol to enable better handling of modal dialogs @xi
+ [#3134]
+- labnag: add --keyboard-focus option @tokyo4j [#3120]
+- Allow window switcher to temporarily unshade windows using config option
+ `` @Amodio @Consolatis [#3124]
+- For the 'classic' style window-switcher, add the following theme options:
+ - `osd.window-switcher.style-classic.item.active.border.color`
+ - `osd.window-switcher.style-classic.item.active.bg.color`
+ @tokyo4j [#3118]
+
+### Fixed
+
+- Don't remove newlines when parsing config, menu and XBM because doing so can
+ cause parser error in some unusual situations like the one shown below.
+ @tokyo4j [#3148]
+
+```
+
+```
+
+### Changed
+
+- If XML documents (like rc.xml and menu.xml) have an XML declaration (typically
+ ``), this XML declaration must be the first thing in the
+ document. In previous versions, line breaks (`\n`) were allowed before due to
+ the way the files were parsed, but this is approach caused other issues like
+ [#3145] and is contrary to XML syntax. [#3148] [#3153]
+- With the window-switcher custom field state specifiers 's' and 'S', change the
+ display order from M|m|F to m|s|M|F; and increase the size from three
+ characters wide to four. @domo141 [#2895]
+- Call labnag with on-demand keyboard interactivity by default @tokyo4j [#3120]
+- Temporarily unshade windows when switching windows. Restore old behaviour with
+ `` @Amodio @Consolatis [#3124]
+- In the classic style window-switcher, the default color of the selected window
+ item has been changed to inherit the border color but with 15% opacity
+ @tokyo4j [#3118]
+
## 0.9.2 - 2025-10-10
[0.9.2-commits]
@@ -2804,6 +2849,7 @@ Compile with wlroots 0.12.0 and wayland-server >=1.16
[#2886]: https://github.com/labwc/labwc/pull/2886
[#2887]: https://github.com/labwc/labwc/pull/2887
[#2891]: https://github.com/labwc/labwc/pull/2891
+[#2895]: https://github.com/labwc/labwc/pull/2895
[#2909]: https://github.com/labwc/labwc/pull/2909
[#2910]: https://github.com/labwc/labwc/pull/2910
[#2914]: https://github.com/labwc/labwc/pull/2914
@@ -2840,4 +2886,11 @@ Compile with wlroots 0.12.0 and wayland-server >=1.16
[#3081]: https://github.com/labwc/labwc/pull/3081
[#3097]: https://github.com/labwc/labwc/pull/3097
[#3099]: https://github.com/labwc/labwc/pull/3099
+[#3118]: https://github.com/labwc/labwc/pull/3118
+[#3120]: https://github.com/labwc/labwc/pull/3120
+[#3124]: https://github.com/labwc/labwc/pull/3124
[#3126]: https://github.com/labwc/labwc/pull/3126
+[#3134]: https://github.com/labwc/labwc/pull/3134
+[#3145]: https://github.com/labwc/labwc/pull/3145
+[#3148]: https://github.com/labwc/labwc/pull/3148
+[#3153]: https://github.com/labwc/labwc/pull/3153
diff --git a/clients/labnag.c b/clients/labnag.c
index 23e0be1f..31ac4e9d 100644
--- a/clients/labnag.c
+++ b/clients/labnag.c
@@ -19,6 +19,7 @@
#ifdef __FreeBSD__
#include /* For signalfd() */
#endif
+#include
#include
#include
#include
@@ -26,6 +27,7 @@
#include
#include
#include
+#include
#include "action-prompt-codes.h"
#include "pool-buffer.h"
#include "cursor-shape-v1-client-protocol.h"
@@ -38,6 +40,7 @@ struct conf {
char *output;
uint32_t anchors;
int32_t layer; /* enum zwlr_layer_shell_v1_layer or -1 if unset */
+ enum zwlr_layer_surface_v1_keyboard_interactivity keyboard_focus;
/* Colors */
uint32_t button_text;
@@ -69,11 +72,18 @@ struct pointer {
int y;
};
+struct keyboard {
+ struct wl_keyboard *keyboard;
+ struct xkb_keymap *keymap;
+ struct xkb_state *state;
+};
+
struct seat {
struct wl_seat *wl_seat;
uint32_t wl_name;
struct nag *nag;
struct pointer pointer;
+ struct keyboard keyboard;
struct wl_list link; /* nag.seats */
};
@@ -130,6 +140,7 @@ struct nag {
struct conf *conf;
char *message;
struct wl_list buttons;
+ int selected_button;
struct pollfd pollfds[NR_FDS];
struct {
@@ -409,7 +420,8 @@ render_detailed(cairo_t *cairo, struct nag *nag, uint32_t y)
}
static uint32_t
-render_button(cairo_t *cairo, struct nag *nag, struct button *button, int *x)
+render_button(cairo_t *cairo, struct nag *nag, struct button *button,
+ bool selected, int *x)
{
int text_width, text_height;
get_text_size(cairo, nag->conf->font_description, &text_width,
@@ -439,6 +451,14 @@ render_button(cairo_t *cairo, struct nag *nag, struct button *button, int *x)
button->width, button->height);
cairo_fill(cairo);
+ if (selected) {
+ cairo_set_source_u32(cairo, nag->conf->button_border);
+ cairo_set_line_width(cairo, 1);
+ cairo_rectangle(cairo, button->x + 1.5, button->y + 1.5,
+ button->width - 3, button->height - 3);
+ cairo_stroke(cairo);
+ }
+
cairo_set_source_u32(cairo, nag->conf->button_text);
cairo_move_to(cairo, button->x + padding, button->y + padding);
render_text(cairo, nag->conf->font_description, 1, true,
@@ -464,11 +484,13 @@ render_to_cairo(cairo_t *cairo, struct nag *nag)
int x = nag->width - nag->conf->button_margin_right;
x -= nag->conf->button_gap_close;
+ int idx = 0;
struct button *button;
wl_list_for_each(button, &nag->buttons, link) {
- h = render_button(cairo, nag, button, &x);
+ h = render_button(cairo, nag, button, idx == nag->selected_button, &x);
max_height = h > max_height ? h : max_height;
x -= nag->conf->button_gap;
+ idx++;
}
if (nag->details.visible) {
@@ -555,6 +577,15 @@ seat_destroy(struct seat *seat)
if (seat->pointer.pointer) {
wl_pointer_destroy(seat->pointer.pointer);
}
+ if (seat->keyboard.keyboard) {
+ wl_keyboard_destroy(seat->keyboard.keyboard);
+ }
+ if (seat->keyboard.keymap) {
+ xkb_keymap_unref(seat->keyboard.keymap);
+ }
+ if (seat->keyboard.state) {
+ xkb_state_unref(seat->keyboard.state);
+ }
wl_seat_destroy(seat->wl_seat);
wl_list_remove(&seat->link);
free(seat);
@@ -939,12 +970,170 @@ static const struct wl_pointer_listener pointer_listener = {
.axis_discrete = wl_pointer_axis_discrete,
};
+static void
+wl_keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard,
+ uint32_t format, int32_t fd, uint32_t size)
+{
+ struct seat *seat = data;
+
+ if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) {
+ wlr_log(WLR_ERROR, "unreconizable keymap format: %d", format);
+ close(fd);
+ return;
+ }
+
+ char *map_shm = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
+ if (map_shm == MAP_FAILED) {
+ wlr_log_errno(WLR_ERROR, "mmap()");
+ close(fd);
+ return;
+ }
+
+ if (seat->keyboard.keymap) {
+ xkb_keymap_unref(seat->keyboard.keymap);
+ seat->keyboard.keymap = NULL;
+ }
+ if (seat->keyboard.state) {
+ xkb_state_unref(seat->keyboard.state);
+ seat->keyboard.state = NULL;
+ }
+ struct xkb_context *xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
+ seat->keyboard.keymap = xkb_keymap_new_from_string(xkb, map_shm,
+ XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS);
+ if (seat->keyboard.keymap) {
+ seat->keyboard.state = xkb_state_new(seat->keyboard.keymap);
+ } else {
+ wlr_log(WLR_ERROR, "failed to compile keymap");
+ }
+ xkb_context_unref(xkb);
+
+ munmap(map_shm, size);
+ close(fd);
+}
+
+static void
+wl_keyboard_enter(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial,
+ struct wl_surface *surface, struct wl_array *keys)
+{
+}
+
+static void
+wl_keyboard_leave(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial,
+ struct wl_surface *surface)
+{
+}
+
+static void
+wl_keyboard_key(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial,
+ uint32_t time, uint32_t key, uint32_t state)
+{
+ struct seat *seat = data;
+ struct nag *nag = seat->nag;
+
+ if (!seat->keyboard.keymap || !seat->keyboard.state) {
+ wlr_log(WLR_ERROR, "keymap/state unavailable");
+ return;
+ }
+
+ if (state != WL_KEYBOARD_KEY_STATE_PRESSED) {
+ return;
+ }
+
+ key += 8;
+ const xkb_keysym_t *syms;
+ if (!xkb_keymap_key_get_syms_by_level(seat->keyboard.keymap,
+ key, 0, 0, &syms)) {
+ wlr_log(WLR_ERROR, "failed to translate key: %d", key);
+ return;
+ }
+ xkb_mod_mask_t mods = xkb_state_serialize_mods(seat->keyboard.state,
+ XKB_STATE_MODS_EFFECTIVE);
+ xkb_mod_index_t shift_idx = xkb_keymap_mod_get_index(
+ seat->keyboard.keymap, XKB_MOD_NAME_SHIFT);
+ bool shift = shift_idx != XKB_MOD_INVALID && (mods & (1 << shift_idx));
+
+ int nr_buttons = wl_list_length(&nag->buttons);
+
+ switch (syms[0]) {
+ case XKB_KEY_Left:
+ case XKB_KEY_Right:
+ case XKB_KEY_Tab: {
+ if (nr_buttons <= 0) {
+ break;
+ }
+ int direction;
+ if (syms[0] == XKB_KEY_Left || (syms[0] == XKB_KEY_Tab && shift)) {
+ direction = 1;
+ } else {
+ direction = -1;
+ }
+ nag->selected_button += nr_buttons + direction;
+ nag->selected_button %= nr_buttons;
+ render_frame(nag);
+ close_pollfd(&nag->pollfds[FD_TIMER]);
+ break;
+ }
+ case XKB_KEY_Escape:
+ exit_status = LAB_EXIT_CANCELLED;
+ nag->run_display = false;
+ break;
+ case XKB_KEY_Return:
+ case XKB_KEY_KP_Enter: {
+ int idx = 0;
+ struct button *button;
+ wl_list_for_each(button, &nag->buttons, link) {
+ if (idx == nag->selected_button) {
+ button_execute(nag, button);
+ close_pollfd(&nag->pollfds[FD_TIMER]);
+ exit_status = idx;
+ break;
+ }
+ idx++;
+ }
+ break;
+ }
+ }
+}
+
+static void
+wl_keyboard_modifiers(void *data, struct wl_keyboard *wl_keyboard,
+ uint32_t serial, uint32_t mods_depressed, uint32_t mods_latched,
+ uint32_t mods_locked, uint32_t group)
+{
+ struct seat *seat = data;
+
+ if (!seat->keyboard.state) {
+ wlr_log(WLR_ERROR, "xkb state unavailable");
+ return;
+ }
+
+ xkb_state_update_mask(seat->keyboard.state, mods_depressed,
+ mods_latched, mods_locked, 0, 0, group);
+}
+
+static void
+wl_keyboard_repeat_info(void *data, struct wl_keyboard *wl_keyboard,
+ int32_t rate, int32_t delay)
+{
+}
+
+static const struct wl_keyboard_listener keyboard_listener = {
+ .keymap = wl_keyboard_keymap,
+ .enter = wl_keyboard_enter,
+ .leave = wl_keyboard_leave,
+ .key = wl_keyboard_key,
+ .modifiers = wl_keyboard_modifiers,
+ .repeat_info = wl_keyboard_repeat_info,
+};
+
static void
seat_handle_capabilities(void *data, struct wl_seat *wl_seat,
enum wl_seat_capability caps)
{
struct seat *seat = data;
bool cap_pointer = caps & WL_SEAT_CAPABILITY_POINTER;
+ bool cap_keyboard = caps & WL_SEAT_CAPABILITY_KEYBOARD;
+
if (cap_pointer && !seat->pointer.pointer) {
seat->pointer.pointer = wl_seat_get_pointer(wl_seat);
wl_pointer_add_listener(seat->pointer.pointer,
@@ -953,6 +1142,15 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat,
wl_pointer_destroy(seat->pointer.pointer);
seat->pointer.pointer = NULL;
}
+
+ if (cap_keyboard && !seat->keyboard.keyboard) {
+ seat->keyboard.keyboard = wl_seat_get_keyboard(wl_seat);
+ wl_keyboard_add_listener(seat->keyboard.keyboard,
+ &keyboard_listener, seat);
+ } else if (!cap_keyboard && seat->keyboard.keyboard) {
+ wl_keyboard_destroy(seat->keyboard.keyboard);
+ seat->keyboard.keyboard = NULL;
+ }
}
static void
@@ -1075,7 +1273,7 @@ handle_global(void *data, struct wl_registry *registry, uint32_t name,
}
} else if (strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) {
nag->layer_shell = wl_registry_bind(
- registry, name, &zwlr_layer_shell_v1_interface, 1);
+ registry, name, &zwlr_layer_shell_v1_interface, 4);
} else if (strcmp(interface, wp_cursor_shape_manager_v1_interface.name) == 0) {
nag->cursor_shape_manager = wl_registry_bind(
registry, name, &wp_cursor_shape_manager_v1_interface, 1);
@@ -1170,6 +1368,8 @@ nag_setup(struct nag *nag)
&layer_surface_listener, nag);
zwlr_layer_surface_v1_set_anchor(nag->layer_surface,
nag->conf->anchors);
+ zwlr_layer_surface_v1_set_keyboard_interactivity(nag->layer_surface,
+ nag->conf->keyboard_focus);
wl_registry_destroy(registry);
@@ -1233,7 +1433,7 @@ nag_run(struct nag *nag)
wl_display_cancel_read(nag->display);
}
if (nag->pollfds[FD_TIMER].revents & POLLIN) {
- exit_status = LAB_EXIT_TIMEOUT;
+ exit_status = LAB_EXIT_CANCELLED;
break;
}
if (nag->pollfds[FD_SIGNAL].revents & POLLIN) {
@@ -1250,13 +1450,7 @@ conf_init(struct conf *conf)
| ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT
| ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT;
conf->layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP;
- conf->button_background = 0x333333FF;
- conf->details_background = 0x333333FF;
- conf->background = 0x323232FF;
- conf->text = 0xFFFFFFFF;
- conf->button_text = 0xFFFFFFFF;
- conf->button_border = 0x222222FF;
- conf->border_bottom = 0x444444FF;
+ conf->keyboard_focus = ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE;
conf->bar_border_thickness = 2;
conf->message_padding = 8;
conf->details_border_thickness = 3;
@@ -1364,6 +1558,7 @@ nag_parse_options(int argc, char **argv, struct nag *nag,
{"debug", no_argument, NULL, 'd'},
{"edge", required_argument, NULL, 'e'},
{"layer", required_argument, NULL, 'y'},
+ {"keyboard-focus", required_argument, NULL, 'k'},
{"font", required_argument, NULL, 'f'},
{"help", no_argument, NULL, 'h'},
{"detailed-message", no_argument, NULL, 'l'},
@@ -1402,6 +1597,8 @@ nag_parse_options(int argc, char **argv, struct nag *nag,
" -e, --edge top|bottom Set the edge to use.\n"
" -y, --layer overlay|top|bottom|background\n"
" Set the layer to use.\n"
+ " -k, --keyboard-focus none|exclusive|on-demand|\n"
+ " Set the policy for keyboard focus.\n"
" -f, --font Set the font to use.\n"
" -h, --help Show help message and quit.\n"
" -l, --detailed-message Read a detailed message from stdin.\n"
@@ -1433,7 +1630,7 @@ nag_parse_options(int argc, char **argv, struct nag *nag,
optind = 1;
while (1) {
- int c = getopt_long(argc, argv, "B:Z:c:de:y:f:hlL:m:o:s:t:vx", opts, NULL);
+ int c = getopt_long(argc, argv, "B:Z:c:de:y:k:f:hlL:m:o:s:t:vx", opts, NULL);
if (c == -1) {
break;
}
@@ -1487,6 +1684,23 @@ nag_parse_options(int argc, char **argv, struct nag *nag,
return LAB_EXIT_FAILURE;
}
break;
+ case 'k':
+ if (strcmp(optarg, "none") == 0) {
+ conf->keyboard_focus =
+ ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE;
+ } else if (strcmp(optarg, "exclusive") == 0) {
+ conf->keyboard_focus =
+ ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE;
+ } else if (strcmp(optarg, "on-demand") == 0) {
+ conf->keyboard_focus =
+ ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND;
+ } else {
+ fprintf(stderr, "Invalid keyboard focus: %s\n"
+ "Usage: --keyboard-focus none|exclusive|on-demand\n",
+ optarg);
+ return LAB_EXIT_FAILURE;
+ }
+ break;
case 'f': /* Font */
pango_font_description_free(conf->font_description);
conf->font_description = pango_font_description_from_string(optarg);
@@ -1629,6 +1843,14 @@ main(int argc, char **argv)
wl_list_insert(nag.buttons.prev, &nag.details.button_details->link);
}
+ int nr_buttons = wl_list_length(&nag.buttons);
+ if (conf.keyboard_focus && nr_buttons > 0) {
+ /* select the leftmost button */
+ nag.selected_button = nr_buttons - 1;
+ } else {
+ nag.selected_button = -1;
+ }
+
wlr_log(WLR_DEBUG, "Output: %s", nag.conf->output);
wlr_log(WLR_DEBUG, "Anchors: %lu", (unsigned long)nag.conf->anchors);
wlr_log(WLR_DEBUG, "Message: %s", nag.message);
diff --git a/clients/meson.build b/clients/meson.build
index 54b92db8..467bc035 100644
--- a/clients/meson.build
+++ b/clients/meson.build
@@ -49,6 +49,7 @@ executable(
wlroots,
server_protos,
epoll_dep,
+ xkbcommon,
],
include_directories: [labwc_inc],
install: true,
diff --git a/docs/labnag.1.scd b/docs/labnag.1.scd
index c8aa4d09..a2a36c10 100644
--- a/docs/labnag.1.scd
+++ b/docs/labnag.1.scd
@@ -31,6 +31,9 @@ _labnag_ [options...]
*-y, --layer* overlay|top|bottom|background
Set the layer to use.
+*-k, --keyboard-focus none|exclusive|on-demand*
+ Set the policy for keyboard focus.
+
*-f, --font*
Set the font to use.
diff --git a/docs/labwc-config.5.scd b/docs/labwc-config.5.scd
index 21ac2629..00817898 100644
--- a/docs/labwc-config.5.scd
+++ b/docs/labwc-config.5.scd
@@ -285,6 +285,8 @@ this is for compatibility with Openbox.
--button-text-color '%t' \\
--border-bottom-size 1 \\
--button-border-size 3 \\
+ --keyboard-focus on-demand \\
+ --layer overlay \\
--timeout 0
```
@@ -346,7 +348,7 @@ this is for compatibility with Openbox.
```
-**
+**
*show* [yes|no] Draw the OnScreenDisplay when switching between
windows. Default is yes.
@@ -364,6 +366,9 @@ this is for compatibility with Openbox.
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.
+
**
Define window switcher fields when using **.
@@ -397,9 +402,9 @@ this is for compatibility with Openbox.
fields are:
- 'B' - shell type, values [xwayland|xdg-shell]
- 'b' - shell type (short form), values [X|W]
- - 'S' - state of window, values [M|m|F] (3 spaces allocated)
- (maximized, minimized, fullscreen)
- - 's' - state of window (short form), values [M|m|F] (1 space)
+ - 'S' - state of window, values [m|s|M|F] (4 spaces allocated)
+ (minimized, shaded, maximized, fullscreen)
+ - 's' - state of window (short form), values [m|s|M|F] (1 space)
- 'I' - wm-class/app-id
- 'i' - wm-class/app-id trimmed, remove "org." if available
- 'n' - desktop entry/file application name, falls back to
diff --git a/docs/labwc-theme.5.scd b/docs/labwc-theme.5.scd
index fad8a77e..8097615b 100644
--- a/docs/labwc-theme.5.scd
+++ b/docs/labwc-theme.5.scd
@@ -327,6 +327,14 @@ all are supported.
Border width of the selection box in the window switcher in pixels.
Default is 2.
+*osd.window-switcher.style-classic.item.active.border.color*
+ Border color around the selected window switcher item.
+ Default is *osd.label.text.color* with 50% opacity.
+
+*osd.window-switcher.style-classic.item.active.bg.color*
+ Background color of the selected window switcher item.
+ Default is *osd.label.text.color* with 15% opacity.
+
*osd.window-switcher.style-classic.item.icon.size*
Size of the icon in window switcher, in pixels.
If not set, the font size derived from
diff --git a/docs/menu.xml b/docs/menu.xml
index b9dda361..4b4d5dda 100644
--- a/docs/menu.xml
+++ b/docs/menu.xml
@@ -1,5 +1,3 @@
-
-