From 908a73da8228a9e0ce0dab1dbeaa18b795724cf9 Mon Sep 17 00:00:00 2001 From: thanatos Date: Sat, 21 Feb 2026 16:53:19 +0100 Subject: [PATCH 01/63] Corrected README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 30ffea3..d5f2098 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # dwl - dwm for Wayland -Join us on our [Discord server] -Or Matrix: [#dwl-official:matrix.org] +Join us on our [Discord server] +Or Matrix: [#dwl-official:matrix.org] Or on our IRC channel: [#dwl on Libera Chat] dwl is a compact, hackable compositor for [Wayland] based on [wlroots]. It is From 9b76ee8e90b6ad3600b4370aca36be1f6b12b1b7 Mon Sep 17 00:00:00 2001 From: thanatos Date: Sat, 21 Feb 2026 19:50:17 +0100 Subject: [PATCH 02/63] Updated README to clarify branch roles This change effectively reverts DWL to the prior development model, where development of DWL itself happens against the current wlroots release, and changes in anticipation of the next wlroots release are made on a separate branch. --- README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d5f2098..c81efd0 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,13 @@ with each release on the [release] page ### Development branch [main] Active development progresses on the `main` branch. The `main` branch is built -against a late (and often changing) git commit of wlroots. While the adventurous -are welcome to use `main`, it is a rocky road. Using `main` requires that the -user be willing to chase git commits of wlroots. Testing development pull -requests may involve merging unmerged pull requests in [wlroots]' git repository -and/or git commits of wayland. +against the latest release of [wlroots]. PRs should target this branch unless they +depend on functionality that is not in the current release of `wlroots`. + +### Preview branch [wlroots-next] +The `wlroots-next` branch is built against the git version of [wlroots], which +is unstable and changes frequently. PRs requiring functionality from the git +version of `wlroots` should target this branch. ### Building dwl dwl has the following dependencies: @@ -203,6 +205,7 @@ inspiration, and to the various contributors to the project, including: [dwl-patches]: https://codeberg.org/dwl/dwl-patches [list of useful resources on our wiki]: https://codeberg.org/dwl/dwl/wiki/Home#migrating-from-x [main]: https://codeberg.org/dwl/dwl/src/branch/main +[wlroots-next]: https://codeberg.org/dwl/dwl/src/branch/wlroots-next [release]: https://codeberg.org/dwl/dwl/releases [runit]: http://smarden.org/runit/faq.html#userservices [s6]: https://skarnet.org/software/s6/ From cb4cc3ae8ff2bdc7e41ae7ab6e888b0c3b176b16 Mon Sep 17 00:00:00 2001 From: Diego Viola Date: Wed, 25 Feb 2026 02:38:09 -0300 Subject: [PATCH 03/63] Fix typos --- dwl.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dwl.c b/dwl.c index 320910d..101a45f 100644 --- a/dwl.c +++ b/dwl.c @@ -586,7 +586,7 @@ arrangelayers(Monitor *m) arrange(m); } - /* Arrange non-exlusive surfaces from top->bottom */ + /* Arrange non-exclusive surfaces from top->bottom */ for (i = 3; i >= 0; i--) arrangelayer(m, &m->layers[i], &usable_area, 0); @@ -2134,7 +2134,7 @@ powermgrsetmode(struct wl_listener *listener, void *data) if (!m) return; - m->gamma_lut_changed = 1; /* Reapply gamma LUT when re-enabling the ouput */ + m->gamma_lut_changed = 1; /* Reapply gamma LUT when re-enabling the output */ wlr_output_state_set_enabled(&state, event->mode); wlr_output_commit_state(m->wlr_output, &state); @@ -2456,7 +2456,7 @@ setup(void) wlr_log_init(log_level, NULL); /* The Wayland display is managed by libwayland. It handles accepting - * clients from the Unix socket, manging Wayland globals, and so on. */ + * clients from the Unix socket, managing Wayland globals, and so on. */ dpy = wl_display_create(); event_loop = wl_display_get_event_loop(dpy); From a2d03cf6188350005dbdaac12f3b0fbc4d60c567 Mon Sep 17 00:00:00 2001 From: Diego Viola Date: Wed, 25 Feb 2026 02:26:38 -0300 Subject: [PATCH 04/63] Remove trailing whitespace from README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c81efd0..3a3b9bd 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ depend on functionality that is not in the current release of `wlroots`. The `wlroots-next` branch is built against the git version of [wlroots], which is unstable and changes frequently. PRs requiring functionality from the git version of `wlroots` should target this branch. - + ### Building dwl dwl has the following dependencies: - libinput @@ -213,4 +213,4 @@ inspiration, and to the various contributors to the project, including: [wiki]: https://codeberg.org/dwl/dwl/wiki/Home#compatible-status-bars [Discord server]: https://discord.gg/jJxZnrGPWN [Wayland]: https://wayland.freedesktop.org/ -[#dwl-official:matrix.org]: https://matrix.to/#/#dwl-official:matrix.org \ No newline at end of file +[#dwl-official:matrix.org]: https://matrix.to/#/#dwl-official:matrix.org From f4dfdabd0bdd632d0b137ede22ff1beb73122efb Mon Sep 17 00:00:00 2001 From: Alex Denes Date: Fri, 10 Apr 2026 15:34:36 +0000 Subject: [PATCH 05/63] NULL out decoration on destroy --- dwl.c | 1 + 1 file changed, 1 insertion(+) diff --git a/dwl.c b/dwl.c index 101a45f..bf3e58c 100644 --- a/dwl.c +++ b/dwl.c @@ -1242,6 +1242,7 @@ void destroydecoration(struct wl_listener *listener, void *data) { Client *c = wl_container_of(listener, c, destroy_decoration); + c->decoration = NULL; wl_list_remove(&c->destroy_decoration.link); wl_list_remove(&c->set_decoration_mode.link); From 04279f28e06f6948acf6d4960834165f6d268080 Mon Sep 17 00:00:00 2001 From: Alex Denes Date: Sun, 6 Sep 2026 13:48:13 +0000 Subject: [PATCH 06/63] Remove decoration event listeners on destroynotify When destroynotify doesn't clean up these listeners it causes a crash once the cleanup of the decoration is triggered Thanks to kennylevinsen - https://codeberg.org/dwl/dwl/issues/1205#issuecomment-22424401 --- dwl.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dwl.c b/dwl.c index bf3e58c..df9b2ba 100644 --- a/dwl.c +++ b/dwl.c @@ -1348,6 +1348,10 @@ destroynotify(struct wl_listener *listener, void *data) wl_list_remove(&c->map.link); wl_list_remove(&c->unmap.link); wl_list_remove(&c->maximize.link); + if (c->decoration) { + wl_list_remove(&c->set_decoration_mode.link); + wl_list_remove(&c->destroy_decoration.link); + } } free(c); } From 4847f97678f705352a8ffa3995c69b5bb2226207 Mon Sep 17 00:00:00 2001 From: klim Date: Mon, 20 Jul 2026 11:26:54 +0200 Subject: [PATCH 07/63] Clear surface->data on unmap to fix idle-inhibitor use-after-free mapnotify() stores the client's scene tree in client_surface(c)->data; checkidleinhibitor() later reads it back as a wlr_scene_tree and calls wlr_scene_node_coords() on it. unmapnotify() frees the scene tree with wlr_scene_node_destroy() but leaves surface->data dangling, so the next arrange() -> checkidleinhibitor() dereferences freed memory and crashes (SIGSEGV) whenever an idle inhibitor outlives the unmap. Reset the pointer to NULL on unmap so the existing !tree guard handles it. --- dwl.c | 1 + 1 file changed, 1 insertion(+) diff --git a/dwl.c b/dwl.c index df9b2ba..2e63c8c 100644 --- a/dwl.c +++ b/dwl.c @@ -2839,6 +2839,7 @@ unmapnotify(struct wl_listener *listener, void *data) } wlr_scene_node_destroy(&c->scene->node); + client_surface(c)->data = NULL; printstatus(); motionnotify(0, NULL, 0, 0, 0, 0); } From babccfe6dcf3f5d591fe5f1f7ca1171b03ada68e Mon Sep 17 00:00:00 2001 From: save196 Date: Sat, 4 Apr 2026 11:34:38 +0300 Subject: [PATCH 08/63] Preserve asleep disabled outputs in output config --- dwl.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dwl.c b/dwl.c index 2e63c8c..f0aa4cf 100644 --- a/dwl.c +++ b/dwl.c @@ -2862,10 +2862,13 @@ updatemons(struct wl_listener *listener, void *data) /* First remove from the layout the disabled monitors */ wl_list_for_each(m, &mons, link) { - if (m->wlr_output->enabled || m->asleep) + if (m->wlr_output->enabled) continue; config_head = wlr_output_configuration_head_v1_create(config, m->wlr_output); config_head->state.enabled = 0; + if (m->asleep) + continue; + /* Remove this output from the layout to avoid cursor enter inside it */ wlr_output_layout_remove(output_layout, m->wlr_output); closemon(m); From 49ca43451486c23776ada284023a2e8b7501913f Mon Sep 17 00:00:00 2001 From: Thanatos Date: Thu, 15 Jan 2026 18:05:18 -0700 Subject: [PATCH 09/63] Bumped to 0.9 for wlroots-0.20.0. Updated cursor image buffer handling for xwayland --- config.mk | 6 +++--- dwl.c | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/config.mk b/config.mk index eb08a05..b1d782a 100644 --- a/config.mk +++ b/config.mk @@ -1,4 +1,4 @@ -_VERSION = 0.8-dev +_VERSION = 0.9-dev VERSION = `git describe --tags --dirty 2>/dev/null || echo $(_VERSION)` PKG_CONFIG = pkg-config @@ -8,8 +8,8 @@ PREFIX = /usr/local MANDIR = $(PREFIX)/share/man DATADIR = $(PREFIX)/share -WLR_INCS = `$(PKG_CONFIG) --cflags wlroots-0.19` -WLR_LIBS = `$(PKG_CONFIG) --libs wlroots-0.19` +WLR_INCS = `$(PKG_CONFIG) --cflags wlroots-0.20` +WLR_LIBS = `$(PKG_CONFIG) --libs wlroots-0.20` # Allow using an alternative wlroots installation # This has to have all the includes required by wlroots, e.g: diff --git a/dwl.c b/dwl.c index f0aa4cf..60e1e89 100644 --- a/dwl.c +++ b/dwl.c @@ -69,6 +69,7 @@ #include #endif +#include "xdg-shell-protocol.h" #include "util.h" /* macros */ @@ -3187,9 +3188,7 @@ xwaylandready(struct wl_listener *listener, void *data) /* Set the default XWayland cursor to match the rest of dwl. */ if ((xcursor = wlr_xcursor_manager_get_xcursor(cursor_mgr, "default", 1))) - wlr_xwayland_set_cursor(xwayland, - xcursor->images[0]->buffer, xcursor->images[0]->width * 4, - xcursor->images[0]->width, xcursor->images[0]->height, + wlr_xwayland_set_cursor(xwayland, wlr_xcursor_image_get_buffer(xcursor->images[0]), xcursor->images[0]->hotspot_x, xcursor->images[0]->hotspot_y); } #endif From 132606e1330d8b6faf32077873b114c28df4f8fc Mon Sep 17 00:00:00 2001 From: A Frederick Christensen Date: Sun, 22 Feb 2026 18:56:17 -0600 Subject: [PATCH 10/63] add support for ext-image-copy-capture-v1 and ext-image-capture-source-v1 (wlroots!4545) --- Makefile | 4 ++++ dwl.c | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/Makefile b/Makefile index 578194f..3981fbb 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,7 @@ all: dwl dwl: dwl.o util.o $(CC) dwl.o util.o $(DWLCFLAGS) $(LDFLAGS) $(LDLIBS) -o $@ dwl.o: dwl.c client.h config.h config.mk cursor-shape-v1-protocol.h \ + ext-image-copy-capture-v1-protocol.h \ pointer-constraints-unstable-v1-protocol.h wlr-layer-shell-unstable-v1-protocol.h \ wlr-output-power-management-unstable-v1-protocol.h xdg-shell-protocol.h util.o: util.c util.h @@ -33,6 +34,9 @@ WAYLAND_PROTOCOLS = `$(PKG_CONFIG) --variable=pkgdatadir wayland-protocols` cursor-shape-v1-protocol.h: $(WAYLAND_SCANNER) enum-header \ $(WAYLAND_PROTOCOLS)/staging/cursor-shape/cursor-shape-v1.xml $@ +ext-image-copy-capture-v1-protocol.h: + $(WAYLAND_SCANNER) enum-header \ + $(WAYLAND_PROTOCOLS)/staging/ext-image-copy-capture/ext-image-copy-capture-v1.xml $@ pointer-constraints-unstable-v1-protocol.h: $(WAYLAND_SCANNER) enum-header \ $(WAYLAND_PROTOCOLS)/unstable/pointer-constraints/pointer-constraints-unstable-v1.xml $@ diff --git a/dwl.c b/dwl.c index 60e1e89..e3b1175 100644 --- a/dwl.c +++ b/dwl.c @@ -25,6 +25,8 @@ #include #include #include +#include +#include #include #include #include @@ -2524,6 +2526,8 @@ setup(void) wlr_data_device_manager_create(dpy); wlr_export_dmabuf_manager_v1_create(dpy); wlr_screencopy_manager_v1_create(dpy); + wlr_ext_image_copy_capture_manager_v1_create(dpy, 1); + wlr_ext_output_image_capture_source_manager_v1_create(dpy, 1); wlr_data_control_manager_v1_create(dpy); wlr_ext_data_control_manager_v1_create(dpy, 1); wlr_primary_selection_v1_device_manager_create(dpy); From ee788f0149f0c78939f8f441cbe3d7a86a1d2264 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Tue, 8 Sep 2026 11:44:33 +0200 Subject: [PATCH 11/63] update wlroots version in commented variables --- config.mk | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config.mk b/config.mk index b1d782a..ab5a3fd 100644 --- a/config.mk +++ b/config.mk @@ -17,12 +17,12 @@ WLR_LIBS = `$(PKG_CONFIG) --libs wlroots-0.20` #WLR_INCS = -I/usr/include/pixman-1 -I/usr/include/elogind -I/usr/include/libdrm \ # -I$(PWD)/wlroots/include # Set -rpath to avoid using the wrong library. -#WLR_LIBS = -Wl,-rpath,$(PWD)/wlroots/build -L$(PWD)/wlroots/build -lwlroots-0.19 +#WLR_LIBS = -Wl,-rpath,$(PWD)/wlroots/build -L$(PWD)/wlroots/build -lwlroots-0.20 -# Assuming you ran "meson setup --prefix ${PWD}/0.19 build && ninja -C build install" +# Assuming you ran "meson setup --prefix ${PWD}/0.20 build && ninja -C build install" #WLR_INCS = -I/usr/include/pixman-1 -I/usr/include/elogind -I/usr/include/libdrm \ -# -I$(PWD)/wlroots/0.19/include/wlroots-0.19 -#WLR_LIBS = -Wl,-rpath,$(PWD)/wlroots/0.19/lib64 -L$(PWD)/wlroots/0.19/lib64 -lwlroots-0.19 +# -I$(PWD)/wlroots/0.20/include/wlroots-0.20 +#WLR_LIBS = -Wl,-rpath,$(PWD)/wlroots/0.20/lib64 -L$(PWD)/wlroots/0.20/lib64 -lwlroots-0.20 XWAYLAND = XLIBS = From 70c33c015ab41b65c50b35688d73037925ec5198 Mon Sep 17 00:00:00 2001 From: julmajustus Date: Sat, 23 May 2026 01:22:25 +0300 Subject: [PATCH 12/63] Remove rendermon pending resize frame skip This is no longer needed as wlroots => 0.19 handles this internally via scene-level surface state tracking and linux-drm-syncobj explicit sync. The manual commit skip now causes the c->resize to be stuck on non-zero state which leads to choppy resizing experience. --- dwl.c | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/dwl.c b/dwl.c index e3b1175..9808ef9 100644 --- a/dwl.c +++ b/dwl.c @@ -2162,24 +2162,15 @@ rendermon(struct wl_listener *listener, void *data) /* This function is called every time an output is ready to display a frame, * generally at the output's refresh rate (e.g. 60Hz). */ Monitor *m = wl_container_of(listener, m, frame); - Client *c; - struct wlr_output_state pending = {0}; struct timespec now; - /* Render if no XDG clients have an outstanding resize and are visible on - * this monitor. */ - wl_list_for_each(c, &clients, link) { - if (c->resize && !c->isfloating && client_is_rendered_on_mon(c, m) && !client_is_stopped(c)) - goto skip; + if (!wlr_scene_output_needs_frame(m->scene_output)) { + return; } wlr_scene_output_commit(m->scene_output, NULL); - -skip: - /* Let clients know a frame has been rendered */ clock_gettime(CLOCK_MONOTONIC, &now); wlr_scene_output_send_frame_done(m->scene_output, &now); - wlr_output_state_finish(&pending); } void From 21be76ac80220a1f7a8e0efc416345f4b32e7253 Mon Sep 17 00:00:00 2001 From: A Frederick Christensen Date: Tue, 8 Sep 2026 11:33:59 -0500 Subject: [PATCH 13/63] Formally not lead developers in README User @guidocella joins dwl as a lead developer --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 3a3b9bd..6c372c1 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,10 @@ inspiration, and to the various contributors to the project, including: and for helping to keep the project running - Stivvo for output management and fullscreen support, and patch maintenance +Also, thanks to our current lead developers: + +- @thanatos +- @guidocella [wlroots]: https://gitlab.freedesktop.org/wlroots [dwm]: https://dwm.suckless.org/ From 7fb20244c698717519bd4993d11d0f1bb71071a7 Mon Sep 17 00:00:00 2001 From: Siva Mahadevan Date: Sat, 28 Feb 2026 18:49:59 -0500 Subject: [PATCH 14/63] move util.c to util.h This allows single-source compilation and whole-program optimization using something like: make CFLAGS='-flto -ffunction-sections -fdata-sections -Wl,--gc-sections' Furthermore, move more compositor-agnostic utility functions/macros to util.h, similar to the original dwm sources. --- Makefile | 14 ++++--------- README.md | 2 +- dwl.c | 4 ---- util.c | 51 ----------------------------------------------- util.h | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 5 files changed, 61 insertions(+), 69 deletions(-) delete mode 100644 util.c diff --git a/Makefile b/Makefile index 3981fbb..599581a 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ include config.mk # flags for compiling DWLCPPFLAGS = -I. -DWLR_USE_UNSTABLE -D_POSIX_C_SOURCE=200809L \ - -DVERSION=\"$(VERSION)\" $(XWAYLAND) + -DVERSION=\"$(VERSION)\" $(XWAYLAND) $(CPPFLAGS) DWLDEVCFLAGS = -g -Wpedantic -Wall -Wextra -Wdeclaration-after-statement \ -Wno-unused-parameter -Wshadow -Wunused-macros -Werror=strict-prototypes \ -Werror=implicit -Werror=return-type -Werror=incompatible-pointer-types \ @@ -17,13 +17,11 @@ DWLCFLAGS = `$(PKG_CONFIG) --cflags $(PKGS)` $(WLR_INCS) $(DWLCPPFLAGS) $(DWLDEV LDLIBS = `$(PKG_CONFIG) --libs $(PKGS)` $(WLR_LIBS) -lm $(LIBS) all: dwl -dwl: dwl.o util.o - $(CC) dwl.o util.o $(DWLCFLAGS) $(LDFLAGS) $(LDLIBS) -o $@ -dwl.o: dwl.c client.h config.h config.mk cursor-shape-v1-protocol.h \ +dwl: dwl.c client.h config.h util.h config.mk cursor-shape-v1-protocol.h \ ext-image-copy-capture-v1-protocol.h \ pointer-constraints-unstable-v1-protocol.h wlr-layer-shell-unstable-v1-protocol.h \ wlr-output-power-management-unstable-v1-protocol.h xdg-shell-protocol.h -util.o: util.c util.h + $(CC) dwl.c $(DWLCFLAGS) $(LDFLAGS) $(LDLIBS) -o $@ # wayland-scanner is a tool which generates C headers and rigging for Wayland # protocols, which are specified in XML. wlroots requires you to rig these up @@ -58,7 +56,7 @@ clean: dist: clean mkdir -p dwl-$(VERSION) cp -R LICENSE* Makefile CHANGELOG.md README.md client.h config.def.h \ - config.mk protocols dwl.1 dwl.c util.c util.h dwl.desktop \ + config.mk protocols dwl.1 dwl.c util.h dwl.desktop \ dwl-$(VERSION) tar -caf dwl-$(VERSION).tar.gz dwl-$(VERSION) rm -rf dwl-$(VERSION) @@ -77,7 +75,3 @@ install: dwl uninstall: rm -f $(DESTDIR)$(PREFIX)/bin/dwl $(DESTDIR)$(MANDIR)/man1/dwl.1 \ $(DESTDIR)$(DATADIR)/wayland-sessions/dwl.desktop - -.SUFFIXES: .c .o -.c.o: - $(CC) $(CPPFLAGS) $(DWLCFLAGS) -o $@ -c $< diff --git a/README.md b/README.md index 6c372c1..d811a40 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ primarily in terms of functionality, and secondarily in terms of philosophy. Like [dwm], dwl is: - Easy to understand, hack on, and extend with patches -- One C source file (or a very small number) configurable via `config.h` +- One C source file configurable via `config.h` - Tied to as few external dependencies as possible ## Getting Started: diff --git a/dwl.c b/dwl.c index 9808ef9..29961dc 100644 --- a/dwl.c +++ b/dwl.c @@ -75,12 +75,8 @@ #include "util.h" /* macros */ -#define MAX(A, B) ((A) > (B) ? (A) : (B)) -#define MIN(A, B) ((A) < (B) ? (A) : (B)) #define CLEANMASK(mask) (mask & ~WLR_MODIFIER_CAPS) #define VISIBLEON(C, M) ((M) && (C)->mon == (M) && ((C)->tags & (M)->tagset[(M)->seltags])) -#define LENGTH(X) (sizeof X / sizeof X[0]) -#define END(A) ((A) + LENGTH(A)) #define TAGMASK ((1u << TAGCOUNT) - 1) #define LISTEN(E, L, H) wl_signal_add((E), ((L)->notify = (H), (L))) #define LISTEN_STATIC(E, H) do { struct wl_listener *_l = ecalloc(1, sizeof(*_l)); _l->notify = (H); wl_signal_add((E), _l); } while (0) diff --git a/util.c b/util.c deleted file mode 100644 index b925987..0000000 --- a/util.c +++ /dev/null @@ -1,51 +0,0 @@ -/* See LICENSE.dwm file for copyright and license details. */ -#include -#include -#include -#include -#include - -#include "util.h" - -void -die(const char *fmt, ...) { - va_list ap; - - va_start(ap, fmt); - vfprintf(stderr, fmt, ap); - va_end(ap); - - if (fmt[0] && fmt[strlen(fmt)-1] == ':') { - fputc(' ', stderr); - perror(NULL); - } else { - fputc('\n', stderr); - } - - exit(1); -} - -void * -ecalloc(size_t nmemb, size_t size) -{ - void *p; - - if (!(p = calloc(nmemb, size))) - die("calloc:"); - return p; -} - -int -fd_set_nonblock(int fd) { - int flags = fcntl(fd, F_GETFL); - if (flags < 0) { - perror("fcntl(F_GETFL):"); - return -1; - } - if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) { - perror("fcntl(F_SETFL):"); - return -1; - } - - return 0; -} diff --git a/util.h b/util.h index 226980d..40cfbe4 100644 --- a/util.h +++ b/util.h @@ -1,5 +1,58 @@ /* See LICENSE.dwm file for copyright and license details. */ +#include +#include +#include +#include +#include -void die(const char *fmt, ...); -void *ecalloc(size_t nmemb, size_t size); -int fd_set_nonblock(int fd); +#define MAX(A, B) ((A) > (B) ? (A) : (B)) +#define MIN(A, B) ((A) < (B) ? (A) : (B)) +#define LENGTH(X) (sizeof X / sizeof X[0]) +#define END(A) ((A) + LENGTH(A)) + +static void die(const char *fmt, ...); +static void *ecalloc(size_t nmemb, size_t size); +static int fd_set_nonblock(int fd); + +void +die(const char *fmt, ...) { + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + if (fmt[0] && fmt[strlen(fmt)-1] == ':') { + fputc(' ', stderr); + perror(NULL); + } else { + fputc('\n', stderr); + } + + exit(1); +} + +void * +ecalloc(size_t nmemb, size_t size) +{ + void *p; + + if (!(p = calloc(nmemb, size))) + die("calloc:"); + return p; +} + +int +fd_set_nonblock(int fd) { + int flags = fcntl(fd, F_GETFL); + if (flags < 0) { + perror("fcntl(F_GETFL):"); + return -1; + } + if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) { + perror("fcntl(F_SETFL):"); + return -1; + } + + return 0; +} From f6800e95ee3f2009c2bd0d8178d24ae961c18988 Mon Sep 17 00:00:00 2001 From: Andrea Chiavazza Date: Thu, 8 May 2025 22:18:56 +0100 Subject: [PATCH 15/63] fix some key binding events being passed to clients --- client.h | 15 +++++++++++---- dwl.c | 32 +++++++++++++++++++++++--------- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/client.h b/client.h index d9f90bb..75f1c27 100644 --- a/client.h +++ b/client.h @@ -291,11 +291,18 @@ client_is_unmanaged(Client *c) static inline void client_notify_enter(struct wlr_surface *s, struct wlr_keyboard *kb) { - if (kb) - wlr_seat_keyboard_notify_enter(seat, s, kb->keycodes, - kb->num_keycodes, &kb->modifiers); - else + uint32_t filtered[WLR_KEYBOARD_KEYS_CAP]; + size_t size = 0; + if (!kb) { wlr_seat_keyboard_notify_enter(seat, s, NULL, 0, NULL); + return; + } + for (size_t i = 0; i < kb->num_keycodes; i++) { + uint32_t key = kb->keycodes[i]; + if (!consumed[key]) + filtered[size++] = key; + } + wlr_seat_keyboard_notify_enter(seat, s, filtered, size, &kb->modifiers); } static inline void diff --git a/dwl.c b/dwl.c index 29961dc..9637d2e 100644 --- a/dwl.c +++ b/dwl.c @@ -291,7 +291,7 @@ static void gpureset(struct wl_listener *listener, void *data); static void handlesig(int signo); static void incnmaster(const Arg *arg); static void inputdevice(struct wl_listener *listener, void *data); -static int keybinding(uint32_t mods, xkb_keysym_t sym); +static const Key *keybinding(uint32_t mods, xkb_keysym_t sym); static void keypress(struct wl_listener *listener, void *data); static void keypressmod(struct wl_listener *listener, void *data); static int keyrepeat(void *data); @@ -405,6 +405,7 @@ static struct wlr_output_layout *output_layout; static struct wlr_box sgeom; static struct wl_list mons; static Monitor *selmon; +static int consumed[KEY_MAX + 1]; /* global event handlers */ static struct wl_listener cursor_axis = {.notify = axisnotify}; @@ -1610,7 +1611,7 @@ inputdevice(struct wl_listener *listener, void *data) wlr_seat_set_capabilities(seat, caps); } -int +const Key * keybinding(uint32_t mods, xkb_keysym_t sym) { /* @@ -1623,11 +1624,10 @@ keybinding(uint32_t mods, xkb_keysym_t sym) if (CLEANMASK(mods) == CLEANMASK(k->mod) && xkb_keysym_to_lower(sym) == xkb_keysym_to_lower(k->keysym) && k->func) { - k->func(&k->arg); - return 1; + return k; } } - return 0; + return NULL; } void @@ -1653,8 +1653,14 @@ keypress(struct wl_listener *listener, void *data) /* On _press_ if there is no active screen locker, * attempt to process a compositor keybinding. */ if (!locked && event->state == WL_KEYBOARD_KEY_STATE_PRESSED) { - for (i = 0; i < nsyms; i++) - handled = keybinding(mods, syms[i]) || handled; + for (i = 0; i < nsyms; i++) { + const Key *key = keybinding(mods, syms[i]); + if (key) { + consumed[event->keycode] = 1; + key->func(&key->arg); + handled = 1; + } + } } if (handled && group->wlr_group->keyboard.repeat_info.delay > 0) { @@ -1671,6 +1677,11 @@ keypress(struct wl_listener *listener, void *data) if (handled) return; + if (consumed[event->keycode]) { + if (event->state == WL_KEYBOARD_KEY_STATE_RELEASED) + consumed[event->keycode] = 0; + return; /* Don't pass to the client the release event of a handled key-press */ + } wlr_seat_set_keyboard(seat, &group->wlr_group->keyboard); /* Pass unhandled keycodes along to the client. */ wlr_seat_keyboard_notify_key(seat, event->time_msec, @@ -1701,8 +1712,11 @@ keyrepeat(void *data) wl_event_source_timer_update(group->key_repeat_source, 1000 / group->wlr_group->keyboard.repeat_info.rate); - for (i = 0; i < group->nsyms; i++) - keybinding(group->mods, group->keysyms[i]); + for (i = 0; i < group->nsyms; i++) { + const Key *key = keybinding(group->mods, group->keysyms[i]); + if (key) + key->func(&key->arg); + } return 0; } From 2f3fe0671df9fd9aee34acc6ef2471a76b4cece6 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Thu, 10 Sep 2026 22:27:09 +0200 Subject: [PATCH 16/63] add IME support Based on mangowm, which is based on labwc, which is based on sway, so this should be covered by LICENSE.sway. Fixes https://codeberg.org/dwl/dwl/issues/6 Fixes https://codeberg.org/dwl/dwl/issues/710 Fixes https://codeberg.org/dwl/dwl/issues/1176 Fixes https://codeberg.org/dwl/dwl/issues/1185 Co-authored-by: DreamMaoMao <2523610504@qq.com> --- Makefile | 4 +- README.md | 3 - dwl.c | 24 +- ime.h | 639 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 662 insertions(+), 8 deletions(-) create mode 100644 ime.h diff --git a/Makefile b/Makefile index 599581a..8304245 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ DWLCFLAGS = `$(PKG_CONFIG) --cflags $(PKGS)` $(WLR_INCS) $(DWLCPPFLAGS) $(DWLDEV LDLIBS = `$(PKG_CONFIG) --libs $(PKGS)` $(WLR_LIBS) -lm $(LIBS) all: dwl -dwl: dwl.c client.h config.h util.h config.mk cursor-shape-v1-protocol.h \ +dwl: dwl.c client.h config.h ime.h util.h config.mk cursor-shape-v1-protocol.h \ ext-image-copy-capture-v1-protocol.h \ pointer-constraints-unstable-v1-protocol.h wlr-layer-shell-unstable-v1-protocol.h \ wlr-output-power-management-unstable-v1-protocol.h xdg-shell-protocol.h @@ -56,7 +56,7 @@ clean: dist: clean mkdir -p dwl-$(VERSION) cp -R LICENSE* Makefile CHANGELOG.md README.md client.h config.def.h \ - config.mk protocols dwl.1 dwl.c util.h dwl.desktop \ + config.mk protocols dwl.1 dwl.c ime.h util.h dwl.desktop \ dwl-$(VERSION) tar -caf dwl-$(VERSION).tar.gz dwl-$(VERSION) rm -rf dwl-$(VERSION) diff --git a/README.md b/README.md index d811a40..b8cddb4 100644 --- a/README.md +++ b/README.md @@ -166,9 +166,6 @@ indiscriminately. We will try to keep the code as small as possible. Features under consideration (possibly as patches) are: - Protocols made trivial by wlroots -- Implement the text-input and input-method protocols to support IME once ibus - implements input-method v2 (see https://github.com/ibus/ibus/pull/2256 and - https://codeberg.org/dwl/dwl/pulls/235) Feature *non-goals* for the main codebase include: diff --git a/dwl.c b/dwl.c index 9637d2e..1e7b9e9 100644 --- a/dwl.c +++ b/dwl.c @@ -84,7 +84,7 @@ /* enums */ enum { CurNormal, CurPressed, CurMove, CurResize }; /* cursor */ enum { XDGShell, LayerShell, X11 }; /* client types */ -enum { LyrBg, LyrBottom, LyrTile, LyrFloat, LyrTop, LyrFS, LyrOverlay, LyrBlock, NUM_LAYERS }; /* scene layers */ +enum { LyrBg, LyrBottom, LyrTile, LyrFloat, LyrTop, LyrFS, LyrOverlay, LyrIMPopup, LyrBlock, NUM_LAYERS }; /* scene layers */ typedef union { int i; @@ -150,6 +150,7 @@ typedef struct { typedef struct { struct wlr_keyboard_group *wlr_group; + struct wlr_keyboard *virtual_keyboard; int nsyms; const xkb_keysym_t *keysyms; /* invalid if nsyms == 0 */ @@ -456,6 +457,8 @@ static struct wlr_xwayland *xwayland; /* attempt to encapsulate suck into one file */ #include "client.h" +#include "ime.h" + /* function implementations */ void applybounds(Client *c, struct wlr_box *bbox) @@ -714,6 +717,8 @@ cleanup(void) destroykeyboardgroup(&kb_group->destroy, NULL); + input_method_relay_finish(input_method_relay); + /* If it's not destroyed manually, it will cause a use-after-free of wlr_seat. * Destroy it until it's fixed on the wlroots side */ wlr_backend_destroy(backend); @@ -1465,6 +1470,7 @@ focusclient(Client *c, int lift) if (!c) { /* With no client, all we have left is to clear focus */ + input_method_relay_set_focus(input_method_relay, NULL); wlr_seat_keyboard_notify_clear_focus(seat); return; } @@ -1472,6 +1478,8 @@ focusclient(Client *c, int lift) /* Change cursor surface */ motionnotify(0, NULL, 0, 0, 0, 0); + input_method_relay_set_focus(input_method_relay, client_surface(c)); + /* Have a client, so focus its top-level wlr_surface */ client_notify_enter(client_surface(c), wlr_seat_get_keyboard(seat)); @@ -1674,7 +1682,7 @@ keypress(struct wl_listener *listener, void *data) wl_event_source_timer_update(group->key_repeat_source, 0); } - if (handled) + if (handled || input_method_keyboard_grab_forward_key(group, event)) return; if (consumed[event->keycode]) { @@ -1695,6 +1703,9 @@ keypressmod(struct wl_listener *listener, void *data) * pressed. We simply communicate this to the client. */ KeyboardGroup *group = wl_container_of(listener, group, modifiers); + if (input_method_keyboard_grab_forward_modifiers(group)) + return; + wlr_seat_set_keyboard(seat, &group->wlr_group->keyboard); /* Send modifiers to the client. */ wlr_seat_keyboard_notify_modifiers(seat, @@ -2659,6 +2670,11 @@ setup(void) wl_signal_add(&output_mgr->events.apply, &output_mgr_apply); wl_signal_add(&output_mgr->events.test, &output_mgr_test); + input_method_manager = wlr_input_method_manager_v2_create(dpy); + text_input_manager = wlr_text_input_manager_v3_create(dpy); + input_method_relay = ecalloc(1, sizeof(*input_method_relay)); + input_method_relay = input_method_relay_create(); + /* Make sure XWayland clients don't connect to the parent X server, * e.g when running in the x11 backend or the wayland backend and the * compositor has Xwayland support */ @@ -3002,6 +3018,7 @@ virtualkeyboard(struct wl_listener *listener, void *data) struct wlr_virtual_keyboard_v1 *kb = data; /* virtual keyboards shouldn't share keyboard group */ KeyboardGroup *group = createkeyboardgroup(); + group->virtual_keyboard = &kb->keyboard; /* Set the keymap to match the group keymap */ wlr_keyboard_set_keymap(&kb->keyboard, group->wlr_group->keyboard.keymap); LISTEN(&kb->keyboard.base.events.destroy, &group->destroy, destroykeyboardgroup); @@ -3039,7 +3056,8 @@ xytonode(double x, double y, struct wlr_surface **psurface, int layer; for (layer = NUM_LAYERS - 1; !surface && layer >= 0; layer--) { - if (!(node = wlr_scene_node_at(&layers[layer]->node, x, y, nx, ny))) + if (layer == LyrIMPopup || + !(node = wlr_scene_node_at(&layers[layer]->node, x, y, nx, ny))) continue; if (node->type == WLR_SCENE_NODE_BUFFER) diff --git a/ime.h b/ime.h new file mode 100644 index 0000000..f537a2d --- /dev/null +++ b/ime.h @@ -0,0 +1,639 @@ +#include +#include + +struct input_method_relay { + struct wl_list text_inputs; /* struct text_input.link */ + struct wlr_input_method_v2 *input_method; + + struct wlr_surface *focused_surface; + struct text_input *active_text_input; + + struct wl_list popups; /* input_method_popup.link */ + struct wlr_scene_tree *popup_tree; + + struct wl_listener new_text_input; + struct wl_listener new_input_method; + + struct wl_listener input_method_commit; + struct wl_listener input_method_grab_keyboard; + struct wl_listener input_method_destroy; + struct wl_listener input_method_new_popup_surface; + + struct wl_listener keyboard_grab_destroy; + struct wl_listener focused_surface_destroy; +}; + +struct input_method_popup { + struct wlr_input_popup_surface_v2 *popup_surface; + struct wlr_scene_tree *tree; + struct wlr_scene_tree *scene_surface; + struct input_method_relay *relay; + struct wl_list link; /* input_method_relay.popups */ + + struct wl_listener destroy; + struct wl_listener commit; +}; + +struct text_input { + struct input_method_relay *relay; + struct wlr_text_input_v3 *input; + struct wl_list link; + + struct wl_listener enable; + struct wl_listener commit; + struct wl_listener disable; + struct wl_listener destroy; +}; + +struct wlr_input_method_manager_v2 *input_method_manager; +struct wlr_text_input_manager_v3 *text_input_manager; +struct input_method_relay *input_method_relay; + +/* functions used in dwl.c */ +static int input_method_keyboard_grab_forward_key( + KeyboardGroup *keyboard, struct wlr_keyboard_key_event *event); +static int input_method_keyboard_grab_forward_modifiers(KeyboardGroup *keyboard); +static struct input_method_relay *input_method_relay_create(void); +static void input_method_relay_finish(struct input_method_relay *relay); +static void input_method_relay_set_focus(struct input_method_relay *relay, + struct wlr_surface *surface); + +/* internal functions */ +static struct text_input *get_active_text_input(struct input_method_relay *relay); +static struct wlr_input_method_keyboard_grab_v2 *get_keyboard_grab(KeyboardGroup *keyboard); +static int keyboard_is_emulated_by_input_method(struct wlr_keyboard *keyboard, + struct wlr_input_method_v2 *input_method); +static void handle_focused_surface_destroy(struct wl_listener *listener, void *data); +static void handle_input_method_commit(struct wl_listener *listener, void *data); +static void handle_input_method_destroy(struct wl_listener *listener, void *data); +static void handle_input_method_grab_keyboard(struct wl_listener *listener, void *data); +static void handle_input_method_new_popup_surface(struct wl_listener *listener, void *data); +static void handle_keyboard_grab_destroy(struct wl_listener *listener, void *data); +static void handle_new_input_method(struct wl_listener *listener, void *data); +static void handle_new_text_input(struct wl_listener *listener, void *data); +static void handle_popup_surface_commit(struct wl_listener *listener, void *data); +static void handle_popup_surface_destroy(struct wl_listener *listener, void *data); +static void handle_text_input_commit(struct wl_listener *listener, void *data); +static void handle_text_input_destroy(struct wl_listener *listener, void *data); +static void handle_text_input_disable(struct wl_listener *listener, void *data); +static void handle_text_input_enable(struct wl_listener *listener, void *data); +static Monitor *output_from_wlr_output(struct wlr_output *wlr_output); +static Monitor *output_nearest_to(int lx, int ly); +static void send_state_to_input_method(struct input_method_relay *relay); +static void update_active_text_input(struct input_method_relay *relay); +static void update_popup_position(struct input_method_popup *popup); +static void update_popups_position(struct input_method_relay *relay); +static void update_text_inputs_focused_surface(struct input_method_relay *relay); + +Monitor * +output_from_wlr_output(struct wlr_output *wlr_output) +{ + Monitor *m = NULL; + wl_list_for_each(m, &mons, link) { + if (m->wlr_output == wlr_output) + return m; + } + return NULL; +} + +Monitor * +output_nearest_to(int lx, int ly) +{ + double closest_x, closest_y; + wlr_output_layout_closest_point(output_layout, NULL, lx, ly, &closest_x, + &closest_y); + + return output_from_wlr_output( + wlr_output_layout_output_at(output_layout, closest_x, closest_y)); +} + +int +keyboard_is_emulated_by_input_method(struct wlr_keyboard *keyboard, + struct wlr_input_method_v2 *input_method) +{ + struct wlr_virtual_keyboard_v1 *virtual_keyboard; + + if (!keyboard || !input_method) + return 0; + + virtual_keyboard = wlr_input_device_get_virtual_keyboard(&keyboard->base); + + return virtual_keyboard && + wl_resource_get_client(virtual_keyboard->resource) == + wl_resource_get_client(input_method->resource); +} + +struct wlr_input_method_keyboard_grab_v2 * +get_keyboard_grab(KeyboardGroup *keyboard) +{ + struct wlr_input_method_v2 *input_method = input_method_relay->input_method; + + if (!input_method || !input_method->keyboard_grab || keyboard != kb_group || + keyboard_is_emulated_by_input_method(keyboard->virtual_keyboard, + input_method)) + return NULL; + + return input_method->keyboard_grab; +} + +int +input_method_keyboard_grab_forward_modifiers(KeyboardGroup *keyboard) +{ + struct wlr_input_method_keyboard_grab_v2 *keyboard_grab = + get_keyboard_grab(keyboard); + + struct wlr_keyboard_modifiers *modifiers = + &keyboard->wlr_group->keyboard.modifiers; + + if (keyboard_grab) { + wlr_input_method_keyboard_grab_v2_set_keyboard( + keyboard_grab, &keyboard->wlr_group->keyboard); + wlr_input_method_keyboard_grab_v2_send_modifiers(keyboard_grab, modifiers); + return 1; + } else { + return 0; + } +} + +int +input_method_keyboard_grab_forward_key( + KeyboardGroup *keyboard, struct wlr_keyboard_key_event *event) +{ + struct wlr_input_method_keyboard_grab_v2 *keyboard_grab = + get_keyboard_grab(keyboard); + if (keyboard_grab) { + wlr_input_method_keyboard_grab_v2_set_keyboard( + keyboard_grab, &keyboard->wlr_group->keyboard); + wlr_input_method_keyboard_grab_v2_send_key(keyboard_grab, event->time_msec, + event->keycode, event->state); + return 1; + } else { + return 0; + } +} + +struct text_input * +get_active_text_input(struct input_method_relay *relay) +{ + struct text_input *text_input; + + if (!relay->input_method) + return NULL; + + wl_list_for_each(text_input, &relay->text_inputs, link) { + if (text_input->input->focused_surface && + text_input->input->current_enabled) + return text_input; + } + return NULL; +} + +void +update_active_text_input(struct input_method_relay *relay) +{ + struct text_input *active_text_input = get_active_text_input(relay); + + if (relay->input_method && relay->active_text_input != active_text_input) { + if (active_text_input) + wlr_input_method_v2_send_activate(relay->input_method); + else + wlr_input_method_v2_send_deactivate(relay->input_method); + wlr_input_method_v2_send_done(relay->input_method); + } + + relay->active_text_input = active_text_input; +} + +void +update_text_inputs_focused_surface(struct input_method_relay *relay) +{ + struct text_input *text_input; + wl_list_for_each(text_input, &relay->text_inputs, link) { + struct wlr_text_input_v3 *input = text_input->input; + + struct wlr_surface *new_focused_surface; + if (relay->input_method && relay->focused_surface && + wl_resource_get_client(input->resource) == + wl_resource_get_client(relay->focused_surface->resource)) { + new_focused_surface = relay->focused_surface; + } else { + new_focused_surface = NULL; + } + + if (input->focused_surface == new_focused_surface) + continue; + if (input->focused_surface) + wlr_text_input_v3_send_leave(input); + if (new_focused_surface) + wlr_text_input_v3_send_enter(input, new_focused_surface); + } +} + +void +update_popup_position(struct input_method_popup *popup) +{ + struct input_method_relay *relay = popup->relay; + struct text_input *text_input = relay->active_text_input; + struct wlr_box cursor_rect; + struct wlr_xdg_surface *xdg_surface; + struct wlr_layer_surface_v1 *layer_surface; + struct wlr_scene_tree *tree; + int lx, ly; + Monitor *output; + struct wlr_box output_box; + struct wlr_xdg_positioner_rules positioner_rules; + struct wlr_box popup_box; + + if (!text_input || !relay->focused_surface || + !popup->popup_surface->surface->mapped) + return; + + xdg_surface = wlr_xdg_surface_try_from_wlr_surface(relay->focused_surface); + layer_surface = wlr_layer_surface_v1_try_from_wlr_surface(relay->focused_surface); + + if ((text_input->input->current.features & + WLR_TEXT_INPUT_V3_FEATURE_CURSOR_RECTANGLE) && + (xdg_surface || layer_surface)) { + cursor_rect = text_input->input->current.cursor_rectangle; + + tree = relay->focused_surface->data; + wlr_scene_node_coords(&tree->node, &lx, &ly); + cursor_rect.x += lx; + cursor_rect.y += ly; + + if (xdg_surface) { + cursor_rect.x -= xdg_surface->geometry.x; + cursor_rect.y -= xdg_surface->geometry.y; + } + } else { + cursor_rect = (struct wlr_box){0}; + } + + output = output_nearest_to(cursor_rect.x, cursor_rect.y); + if (!output || !output->wlr_output->enabled) + return; + + wlr_output_layout_get_box(output_layout, output->wlr_output, &output_box); + + positioner_rules = (struct wlr_xdg_positioner_rules) { + .anchor_rect = cursor_rect, + .anchor = XDG_POSITIONER_ANCHOR_BOTTOM_LEFT, + .gravity = XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT, + .size = + { + .width = popup->popup_surface->surface->current.width, + .height = popup->popup_surface->surface->current.height, + }, + .constraint_adjustment = XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y | + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X, + }; + + wlr_xdg_positioner_rules_get_geometry(&positioner_rules, &popup_box); + wlr_xdg_positioner_rules_unconstrain_box(&positioner_rules, &output_box, &popup_box); + + wlr_scene_node_set_position(&popup->tree->node, popup_box.x, popup_box.y); + wlr_scene_node_raise_to_top(&relay->popup_tree->node); + + wlr_input_popup_surface_v2_send_text_input_rectangle( + popup->popup_surface, &(struct wlr_box){ + .x = cursor_rect.x - popup_box.x, + .y = cursor_rect.y - popup_box.y, + .width = cursor_rect.width, + .height = cursor_rect.height, + }); +} + +void +update_popups_position(struct input_method_relay *relay) +{ + struct input_method_popup *popup; + wl_list_for_each(popup, &relay->popups, link) { + update_popup_position(popup); + } +} + +void +handle_input_method_commit(struct wl_listener *listener, + void *data) +{ + struct input_method_relay *relay = + wl_container_of(listener, relay, input_method_commit); + struct wlr_input_method_v2 *input_method = relay->input_method; + struct text_input *text_input = relay->active_text_input; + + if (!text_input) + return; + + if (input_method->current.preedit.text) { + wlr_text_input_v3_send_preedit_string(text_input->input, + input_method->current.preedit.text, + input_method->current.preedit.cursor_begin, + input_method->current.preedit.cursor_end); + } + if (input_method->current.commit_text) { + wlr_text_input_v3_send_commit_string(text_input->input, + input_method->current.commit_text); + } + if (input_method->current.delete.before_length || + input_method->current.delete.after_length) { + wlr_text_input_v3_send_delete_surrounding_text(text_input->input, + input_method->current.delete.before_length, + input_method->current.delete.after_length); + } + wlr_text_input_v3_send_done(text_input->input); +} + +void +handle_keyboard_grab_destroy(struct wl_listener *listener, + void *data) +{ + struct input_method_relay *relay = + wl_container_of(listener, relay, keyboard_grab_destroy); + struct wlr_input_method_keyboard_grab_v2 *keyboard_grab = + relay->input_method->keyboard_grab; + wl_list_remove(&relay->keyboard_grab_destroy.link); + + if (keyboard_grab->keyboard) { + /* Send modifier state to original client */ + wlr_seat_keyboard_notify_modifiers(keyboard_grab->input_method->seat, + &keyboard_grab->keyboard->modifiers); + } +} + +void +handle_input_method_grab_keyboard(struct wl_listener *listener, + void *data) +{ + struct input_method_relay *relay = + wl_container_of(listener, relay, input_method_grab_keyboard); + struct wlr_input_method_keyboard_grab_v2 *keyboard_grab = data; + + struct wlr_keyboard *active_keyboard = &kb_group->wlr_group->keyboard; + + if (!keyboard_is_emulated_by_input_method(active_keyboard, + relay->input_method)) { + /* Send modifier state to grab */ + wlr_input_method_keyboard_grab_v2_set_keyboard(keyboard_grab, + active_keyboard); + } + + relay->keyboard_grab_destroy.notify = handle_keyboard_grab_destroy; + wl_signal_add(&keyboard_grab->events.destroy, &relay->keyboard_grab_destroy); +} + +void +handle_input_method_destroy(struct wl_listener *listener, + void *data) +{ + struct input_method_relay *relay = + wl_container_of(listener, relay, input_method_destroy); + wl_list_remove(&relay->input_method_commit.link); + wl_list_remove(&relay->input_method_grab_keyboard.link); + wl_list_remove(&relay->input_method_new_popup_surface.link); + wl_list_remove(&relay->input_method_destroy.link); + relay->input_method = NULL; + + update_text_inputs_focused_surface(relay); + update_active_text_input(relay); +} + +void +handle_popup_surface_destroy(struct wl_listener *listener, + void *data) +{ + struct input_method_popup *popup = wl_container_of(listener, popup, destroy); + wlr_scene_node_destroy(&popup->tree->node); + wl_list_remove(&popup->destroy.link); + wl_list_remove(&popup->commit.link); + wl_list_remove(&popup->link); + free(popup); +} + +void +handle_popup_surface_commit(struct wl_listener *listener, + void *data) +{ + struct input_method_popup *popup = wl_container_of(listener, popup, commit); + update_popup_position(popup); +} + +void +handle_input_method_new_popup_surface(struct wl_listener *listener, + void *data) +{ + struct input_method_relay *relay = + wl_container_of(listener, relay, input_method_new_popup_surface); + + struct input_method_popup *popup = ecalloc(1, sizeof(*popup)); + popup->popup_surface = data; + popup->relay = relay; + + popup->destroy.notify = handle_popup_surface_destroy; + wl_signal_add(&popup->popup_surface->events.destroy, &popup->destroy); + + popup->commit.notify = handle_popup_surface_commit; + wl_signal_add(&popup->popup_surface->surface->events.commit, &popup->commit); + + popup->tree = wlr_scene_tree_create(layers[LyrIMPopup]); + popup->scene_surface = wlr_scene_subsurface_tree_create( + popup->tree, popup->popup_surface->surface); + popup->scene_surface->node.data = popup; + + wl_list_insert(&relay->popups, &popup->link); + + update_popup_position(popup); +} + +void +handle_new_input_method(struct wl_listener *listener, void *data) +{ + struct input_method_relay *relay = + wl_container_of(listener, relay, new_input_method); + struct wlr_input_method_v2 *input_method = data; + if (seat != input_method->seat) + return; + + if (relay->input_method) { + wlr_input_method_v2_send_unavailable(input_method); + return; + } + + relay->input_method = input_method; + + relay->input_method_commit.notify = handle_input_method_commit; + wl_signal_add(&relay->input_method->events.commit, + &relay->input_method_commit); + + relay->input_method_grab_keyboard.notify = handle_input_method_grab_keyboard; + wl_signal_add(&relay->input_method->events.grab_keyboard, + &relay->input_method_grab_keyboard); + + relay->input_method_destroy.notify = handle_input_method_destroy; + wl_signal_add(&relay->input_method->events.destroy, + &relay->input_method_destroy); + + relay->input_method_new_popup_surface.notify = + handle_input_method_new_popup_surface; + wl_signal_add(&relay->input_method->events.new_popup_surface, + &relay->input_method_new_popup_surface); + + update_text_inputs_focused_surface(relay); + update_active_text_input(relay); +} + +void +send_state_to_input_method(struct input_method_relay *relay) +{ + struct wlr_input_method_v2 *input_method = relay->input_method; + struct wlr_text_input_v3 *input = relay->active_text_input->input; + + if (input->active_features & WLR_TEXT_INPUT_V3_FEATURE_SURROUNDING_TEXT) { + wlr_input_method_v2_send_surrounding_text(input_method, + input->current.surrounding.text, + input->current.surrounding.cursor, + input->current.surrounding.anchor); + } + wlr_input_method_v2_send_text_change_cause(input_method, + input->current.text_change_cause); + if (input->active_features & WLR_TEXT_INPUT_V3_FEATURE_CONTENT_TYPE) { + wlr_input_method_v2_send_content_type(input_method, + input->current.content_type.hint, + input->current.content_type.purpose); + } + wlr_input_method_v2_send_done(input_method); +} + +void +handle_text_input_enable(struct wl_listener *listener, void *data) +{ + struct text_input *text_input = wl_container_of(listener, text_input, enable); + struct input_method_relay *relay = text_input->relay; + + update_active_text_input(relay); + if (relay->active_text_input == text_input) { + update_popups_position(relay); + send_state_to_input_method(relay); + } + wlr_text_input_v3_send_done(text_input->input); +} + +void +handle_text_input_disable(struct wl_listener *listener, + void *data) +{ + struct text_input *text_input = + wl_container_of(listener, text_input, disable); + update_active_text_input(text_input->relay); +} + +void +handle_text_input_commit(struct wl_listener *listener, void *data) +{ + struct text_input *text_input = wl_container_of(listener, text_input, commit); + struct input_method_relay *relay = text_input->relay; + + if (relay->active_text_input == text_input) { + update_popups_position(relay); + send_state_to_input_method(relay); + } +} + +void +handle_text_input_destroy(struct wl_listener *listener, + void *data) +{ + struct text_input *text_input = + wl_container_of(listener, text_input, destroy); + wl_list_remove(&text_input->enable.link); + wl_list_remove(&text_input->disable.link); + wl_list_remove(&text_input->commit.link); + wl_list_remove(&text_input->destroy.link); + wl_list_remove(&text_input->link); + update_active_text_input(text_input->relay); + free(text_input); +} + +void +handle_new_text_input(struct wl_listener *listener, void *data) +{ + struct input_method_relay *relay = + wl_container_of(listener, relay, new_text_input); + struct wlr_text_input_v3 *wlr_text_input = data; + struct text_input *text_input; + + if (seat != wlr_text_input->seat) + return; + + text_input = ecalloc(1, sizeof(*text_input)); + text_input->input = wlr_text_input; + text_input->relay = relay; + wl_list_insert(&relay->text_inputs, &text_input->link); + + text_input->enable.notify = handle_text_input_enable; + wl_signal_add(&text_input->input->events.enable, &text_input->enable); + + text_input->disable.notify = handle_text_input_disable; + wl_signal_add(&text_input->input->events.disable, &text_input->disable); + + text_input->commit.notify = handle_text_input_commit; + wl_signal_add(&text_input->input->events.commit, &text_input->commit); + + text_input->destroy.notify = handle_text_input_destroy; + wl_signal_add(&text_input->input->events.destroy, &text_input->destroy); + + update_text_inputs_focused_surface(relay); +} + +void +handle_focused_surface_destroy(struct wl_listener *listener, + void *data) +{ + struct input_method_relay *relay = + wl_container_of(listener, relay, focused_surface_destroy); + input_method_relay_set_focus(relay, NULL); +} + +struct input_method_relay * +input_method_relay_create() +{ + struct input_method_relay *relay = ecalloc(1, sizeof(*relay)); + wl_list_init(&relay->text_inputs); + wl_list_init(&relay->popups); + relay->popup_tree = wlr_scene_tree_create(&scene->tree); + + relay->new_text_input.notify = handle_new_text_input; + wl_signal_add(&text_input_manager->events.new_text_input, &relay->new_text_input); + + relay->new_input_method.notify = handle_new_input_method; + wl_signal_add(&input_method_manager->events.new_input_method, + &relay->new_input_method); + + relay->focused_surface_destroy.notify = handle_focused_surface_destroy; + + return relay; +} + +void +input_method_relay_finish(struct input_method_relay *relay) +{ + wl_list_remove(&relay->new_text_input.link); + wl_list_remove(&relay->new_input_method.link); + free(relay); +} + +void +input_method_relay_set_focus(struct input_method_relay *relay, + struct wlr_surface *surface) +{ + if (relay->focused_surface == surface) + return; + + if (relay->focused_surface) + wl_list_remove(&relay->focused_surface_destroy.link); + relay->focused_surface = surface; + if (surface) + wl_signal_add(&surface->events.destroy, &relay->focused_surface_destroy); + + update_text_inputs_focused_surface(relay); + update_active_text_input(relay); +} From 4c54bd811347b22038ec4a0285fe8d4e0622e0f0 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Fri, 11 Sep 2026 23:08:32 +0200 Subject: [PATCH 17/63] implement xytonode() with toplevel_from_wlr_surface() xytonode() does not return the layer surface under the cursor because pnode->data is always NULL. It was not noticed because it is not passed the LayerSurface argument anywhere. But it is noticeable after implementing layer shell on-demand keyboard interactivity because layer shell surfaces are never focused by sloppyfocus. Rather than figuring out how to fix it, just replace its implementation with toplevel_from_wlr_surface() which works and also reduces the SLOC. --- dwl.c | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/dwl.c b/dwl.c index 1e7b9e9..7b8e55a 100644 --- a/dwl.c +++ b/dwl.c @@ -3049,10 +3049,8 @@ void xytonode(double x, double y, struct wlr_surface **psurface, Client **pc, LayerSurface **pl, double *nx, double *ny) { - struct wlr_scene_node *node, *pnode; + struct wlr_scene_node *node; struct wlr_surface *surface = NULL; - Client *c = NULL; - LayerSurface *l = NULL; int layer; for (layer = NUM_LAYERS - 1; !surface && layer >= 0; layer--) { @@ -3063,18 +3061,10 @@ xytonode(double x, double y, struct wlr_surface **psurface, if (node->type == WLR_SCENE_NODE_BUFFER) surface = wlr_scene_surface_try_from_buffer( wlr_scene_buffer_from_node(node))->surface; - /* Walk the tree to find a node that knows the client */ - for (pnode = node; pnode && !c; pnode = &pnode->parent->node) - c = pnode->data; - if (c && c->type == LayerShell) { - c = NULL; - l = pnode->data; - } } if (psurface) *psurface = surface; - if (pc) *pc = c; - if (pl) *pl = l; + toplevel_from_wlr_surface(surface, pc, pl); } void From 0a6dc6d5481917e1bb9fdcee0aa42d3ea5e8d708 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Sat, 12 Sep 2026 08:00:20 +0200 Subject: [PATCH 18/63] add support for layer-shell on-demand keyboard interactivity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supersedes https://codeberg.org/dwl/dwl/pulls/674 Co-authored-by: Leonardo Hernández Hernández Co-authored-by: MayOrMayNotBeACat --- dwl.c | 56 ++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/dwl.c b/dwl.c index 7b8e55a..0d10a22 100644 --- a/dwl.c +++ b/dwl.c @@ -309,7 +309,7 @@ static void moveresize(const Arg *arg); static void outputmgrapply(struct wl_listener *listener, void *data); static void outputmgrapplyortest(struct wlr_output_configuration_v1 *config, int test); static void outputmgrtest(struct wl_listener *listener, void *data); -static void pointerfocus(Client *c, struct wlr_surface *surface, +static void pointerfocus(Client *c, LayerSurface* l, struct wlr_surface *surface, double sx, double sy, uint32_t time); static void printstatus(void); static void powermgrsetmode(struct wl_listener *listener, void *data); @@ -593,10 +593,13 @@ arrangelayers(Monitor *m) for (i = 3; i >= 0; i--) arrangelayer(m, &m->layers[i], &usable_area, 0); - /* Find topmost keyboard interactive layer, if such a layer exists */ + /* Find topmost keyboard interactive layer that has indicated it wants + * exclusive access to the keyboard, if such a layer exists */ for (i = 0; i < (int)LENGTH(layers_above_shell); i++) { wl_list_for_each_reverse(l, &m->layers[layers_above_shell[i]], link) { - if (locked || !l->layer_surface->current.keyboard_interactive || !l->mapped) + if (locked || + l->layer_surface->current.keyboard_interactive != ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE || + !l->mapped) continue; /* Deactivate the focused client. */ focusclient(NULL, 0); @@ -629,6 +632,7 @@ buttonpress(struct wl_listener *listener, void *data) struct wlr_keyboard *keyboard; uint32_t mods; Client *c; + LayerSurface *l; const Button *b; wlr_idle_notifier_v1_notify_activity(idle_notifier, seat); @@ -640,10 +644,15 @@ buttonpress(struct wl_listener *listener, void *data) if (locked) break; - /* Change focus if the button was _pressed_ over a client */ - xytonode(cursor->x, cursor->y, NULL, &c, NULL, NULL, NULL); - if (c && (!client_is_unmanaged(c) || client_wants_focus(c))) + /* Change focus if the button was _pressed_ over a client + or a layer surface with on-demand keyboard interactivity */ + xytonode(cursor->x, cursor->y, NULL, &c, &l, NULL, NULL); + if (c && (!client_is_unmanaged(c) || client_wants_focus(c))) { focusclient(c, 1); + } else if (l && l->layer_surface->current.keyboard_interactive) { + focusclient(NULL, 0); + client_notify_enter(l->layer_surface->surface, wlr_seat_get_keyboard(seat)); + } keyboard = wlr_seat_get_keyboard(seat); mods = keyboard ? wlr_keyboard_get_modifiers(keyboard) : 0; @@ -843,6 +852,11 @@ commitlayersurfacenotify(struct wl_listener *listener, void *data) return; } + if (layer_surface == exclusive_focus + && layer_surface->current.keyboard_interactive != + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE) + exclusive_focus = NULL; + if (layer_surface->current.committed == 0 && l->mapped == layer_surface->surface->mapped) return; l->mapped = layer_surface->surface->mapped; @@ -1454,7 +1468,8 @@ focusclient(Client *c, int lift) * and focus it after the overlay is closed. */ if (old_client_type == LayerShell && wlr_scene_node_coords( &old_l->scene->node, &unused_lx, &unused_ly) - && old_l->layer_surface->current.layer >= ZWLR_LAYER_SHELL_V1_LAYER_TOP) { + && old_l->layer_surface->current.layer >= ZWLR_LAYER_SHELL_V1_LAYER_TOP + && old_l->layer_surface->current.keyboard_interactive == ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE) { return; } else if (old_c && old_c == exclusive_focus && client_wants_focus(old_c)) { return; @@ -1894,20 +1909,20 @@ motionnotify(uint32_t time, struct wlr_input_device *device, double dx, double d { double sx = 0, sy = 0, sx_confined, sy_confined; Client *c = NULL, *w = NULL; - LayerSurface *l = NULL; + LayerSurface *l = NULL, *focusedl = NULL; struct wlr_surface *surface = NULL; struct wlr_pointer_constraint_v1 *constraint; /* Find the client under the pointer and send the event along. */ - xytonode(cursor->x, cursor->y, &surface, &c, NULL, &sx, &sy); + xytonode(cursor->x, cursor->y, &surface, &c, &l, &sx, &sy); if (cursor_mode == CurPressed && !seat->drag && surface != seat->pointer_state.focused_surface - && toplevel_from_wlr_surface(seat->pointer_state.focused_surface, &w, &l) >= 0) { + && toplevel_from_wlr_surface(seat->pointer_state.focused_surface, &w, &focusedl) >= 0) { c = w; surface = seat->pointer_state.focused_surface; - sx = cursor->x - (l ? l->scene->node.x : w->geom.x); - sy = cursor->y - (l ? l->scene->node.y : w->geom.y); + sx = cursor->x - (focusedl ? focusedl->scene->node.x : w->geom.x); + sy = cursor->y - (focusedl ? focusedl->scene->node.y : w->geom.y); } /* time is 0 in internal calls meant to restore pointer focus. */ @@ -1964,7 +1979,7 @@ motionnotify(uint32_t time, struct wlr_input_device *device, double dx, double d if (!surface && !seat->drag) wlr_cursor_set_xcursor(cursor, cursor_mgr, "default"); - pointerfocus(c, surface, sx, sy, time); + pointerfocus(c, l, surface, sx, sy, time); } void @@ -2088,14 +2103,19 @@ outputmgrtest(struct wl_listener *listener, void *data) } void -pointerfocus(Client *c, struct wlr_surface *surface, double sx, double sy, +pointerfocus(Client *c, LayerSurface *l, struct wlr_surface *surface, double sx, double sy, uint32_t time) { struct timespec now; - if (surface != seat->pointer_state.focused_surface && - sloppyfocus && time && c && !client_is_unmanaged(c)) - focusclient(c, 0); + if (surface != seat->pointer_state.focused_surface && sloppyfocus && time) { + if (c && (!client_is_unmanaged(c) || client_wants_focus(c))) { + focusclient(c, 0); + } else if (l && l->layer_surface->current.keyboard_interactive) { + focusclient(NULL, 0); + client_notify_enter(l->layer_surface->surface, wlr_seat_get_keyboard(seat)); + } + } /* If surface is NULL, clear pointer focus */ if (!surface) { @@ -2583,7 +2603,7 @@ setup(void) wl_signal_add(&xdg_shell->events.new_toplevel, &new_xdg_toplevel); wl_signal_add(&xdg_shell->events.new_popup, &new_xdg_popup); - layer_shell = wlr_layer_shell_v1_create(dpy, 3); + layer_shell = wlr_layer_shell_v1_create(dpy, 4); wl_signal_add(&layer_shell->events.new_surface, &new_layer_surface); idle_notifier = wlr_idle_notifier_v1_create(dpy); From 721f70ea8b2c2ce4ce765f020f6653db4278528c Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Fri, 11 Sep 2026 23:28:01 +0200 Subject: [PATCH 19/63] implement ext-foreign-toplevel-list This is used by lswt which lists toplevel ids and titles. keyd-application-mapper will also support it hopefully. This will also be needed to support screenshare of single windows (https://codeberg.org/dwl/dwl/pulls/1157). Co-authored-by: Zuki Air --- dwl.c | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/dwl.c b/dwl.c index 0d10a22..bbd5b98 100644 --- a/dwl.c +++ b/dwl.c @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -119,6 +120,7 @@ typedef struct { struct wlr_xwayland_surface *xwayland; } surface; struct wlr_xdg_toplevel_decoration_v1 *decoration; + struct wlr_ext_foreign_toplevel_handle_v1 *foreign_toplevel_handle; struct wl_listener commit; struct wl_listener map; struct wl_listener maximize; @@ -383,6 +385,7 @@ static struct wlr_virtual_keyboard_manager_v1 *virtual_keyboard_mgr; static struct wlr_virtual_pointer_manager_v1 *virtual_pointer_mgr; static struct wlr_cursor_shape_manager_v1 *cursor_shape_mgr; static struct wlr_output_power_manager_v1 *power_mgr; +static struct wlr_ext_foreign_toplevel_list_v1 *foreign_toplevel_list; static struct wlr_pointer_constraints_v1 *pointer_constraints; static struct wlr_relative_pointer_manager_v1 *relative_pointer_mgr; @@ -1788,6 +1791,11 @@ mapnotify(struct wl_listener *listener, void *data) Monitor *m; int i; + struct wlr_ext_foreign_toplevel_handle_v1_state foreign_toplevel_state = { + .app_id = client_get_appid(c), + .title = client_get_title(c), + }; + /* Create scene tree for this client and its border */ c->scene = client_surface(c)->data = wlr_scene_tree_create(layers[LyrTile]); /* Enabled later by a call to arrange() */ @@ -1827,6 +1835,10 @@ mapnotify(struct wl_listener *listener, void *data) wl_list_insert(&clients, &c->link); wl_list_insert(&fstack, &c->flink); + c->foreign_toplevel_handle = wlr_ext_foreign_toplevel_handle_v1_create( + foreign_toplevel_list, &foreign_toplevel_state); + c->foreign_toplevel_handle->data = c; + /* Set initial monitor, tags, floating status, and focus: * we always consider floating, clients that have parent and thus * we set the same tags and monitor as its parent. @@ -2617,6 +2629,8 @@ setup(void) (float [4]){0.1f, 0.1f, 0.1f, 1.0f}); wlr_scene_node_set_enabled(&locked_bg->node, 0); + foreign_toplevel_list = wlr_ext_foreign_toplevel_list_v1_create(dpy,1); + /* Use decoration protocols to negotiate server-side decorations */ wlr_server_decoration_manager_set_default_mode( wlr_server_decoration_manager_create(dpy), @@ -2880,6 +2894,10 @@ unmapnotify(struct wl_listener *listener, void *data) wl_list_remove(&c->flink); } + if (c->foreign_toplevel_handle) { + wlr_ext_foreign_toplevel_handle_v1_destroy(c->foreign_toplevel_handle); + c->foreign_toplevel_handle = NULL; + } wlr_scene_node_destroy(&c->scene->node); client_surface(c)->data = NULL; printstatus(); @@ -3001,6 +3019,15 @@ updatetitle(struct wl_listener *listener, void *data) Client *c = wl_container_of(listener, c, set_title); if (c == focustop(c->mon)) printstatus(); + + if (c->foreign_toplevel_handle) { + struct wlr_ext_foreign_toplevel_handle_v1_state foreign_toplevel_state = { + .app_id = client_get_appid(c), + .title = client_get_title(c), + }; + wlr_ext_foreign_toplevel_handle_v1_update_state(c->foreign_toplevel_handle, + &foreign_toplevel_state); + } } void From fb4102b5ae1891282c0d35a856e4eaa2118dfd02 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Sat, 12 Sep 2026 10:28:05 +0200 Subject: [PATCH 20/63] support screenshare of single windows Supersedes https://codeberg.org/dwl/dwl/pulls/1157. Co-authored-by: Zuki Air --- dwl.c | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/dwl.c b/dwl.c index bbd5b98..fa8c8ae 100644 --- a/dwl.c +++ b/dwl.c @@ -121,6 +121,12 @@ typedef struct { } surface; struct wlr_xdg_toplevel_decoration_v1 *decoration; struct wlr_ext_foreign_toplevel_handle_v1 *foreign_toplevel_handle; + struct wlr_scene *image_capture_scene; + struct wlr_ext_image_capture_source_v1 *image_capture_source; + union { + struct wlr_scene_tree *image_capture_tree; + struct wlr_scene_surface *image_capture_scene_surface; + } capture; struct wl_listener commit; struct wl_listener map; struct wl_listener maximize; @@ -251,6 +257,7 @@ static void arrangelayer(Monitor *m, struct wl_list *list, static void arrangelayers(Monitor *m); static void axisnotify(struct wl_listener *listener, void *data); static void buttonpress(struct wl_listener *listener, void *data); +static void capturerequest(struct wl_listener *listener, void *data); static void chvt(const Arg *arg); static void checkidleinhibitor(struct wlr_surface *exclude); static void cleanup(void); @@ -386,6 +393,7 @@ static struct wlr_virtual_pointer_manager_v1 *virtual_pointer_mgr; static struct wlr_cursor_shape_manager_v1 *cursor_shape_mgr; static struct wlr_output_power_manager_v1 *power_mgr; static struct wlr_ext_foreign_toplevel_list_v1 *foreign_toplevel_list; +static struct wlr_ext_foreign_toplevel_image_capture_source_manager_v1 *ext_foreign_toplevel_image_capture_source_manager_v1; static struct wlr_pointer_constraints_v1 *pointer_constraints; static struct wlr_relative_pointer_manager_v1 *relative_pointer_mgr; @@ -440,6 +448,7 @@ static struct wl_listener request_set_cursor_shape = {.notify = setcursorshape}; static struct wl_listener request_start_drag = {.notify = requeststartdrag}; static struct wl_listener start_drag = {.notify = startdrag}; static struct wl_listener new_session_lock = {.notify = locksession}; +static struct wl_listener new_foreign_toplevel_capture_request = {.notify = capturerequest}; #ifdef XWAYLAND static void activatex11(struct wl_listener *listener, void *data); @@ -688,6 +697,23 @@ buttonpress(struct wl_listener *listener, void *data) event->time_msec, event->button, event->state); } +void +capturerequest(struct wl_listener *listener, void *data) +{ + struct wlr_ext_foreign_toplevel_image_capture_source_manager_v1_request *request = data; + Client *view = request->toplevel_handle->data; + + if (!view->image_capture_source) { + view->image_capture_source = wlr_ext_image_capture_source_v1_create_with_scene_node( + &view->image_capture_scene->tree.node, event_loop, alloc, drw); + if (!view->image_capture_source) + return; + } + + wlr_ext_foreign_toplevel_image_capture_source_manager_v1_request_accept( + request, view->image_capture_source); +} + void chvt(const Arg *arg) { @@ -800,6 +826,7 @@ cleanuplisteners(void) wl_list_remove(&request_start_drag.link); wl_list_remove(&start_drag.link); wl_list_remove(&new_session_lock.link); + wl_list_remove(&new_foreign_toplevel_capture_request.link); #ifdef XWAYLAND wl_list_remove(&new_xwayland_surface.link); wl_list_remove(&xwayland_ready.link); @@ -1838,6 +1865,14 @@ mapnotify(struct wl_listener *listener, void *data) c->foreign_toplevel_handle = wlr_ext_foreign_toplevel_handle_v1_create( foreign_toplevel_list, &foreign_toplevel_state); c->foreign_toplevel_handle->data = c; + c->image_capture_scene = wlr_scene_create(); + if (c->type == XDGShell) + c->capture.image_capture_tree = wlr_scene_xdg_surface_create(&c->image_capture_scene->tree, c->surface.xdg); +#ifdef XWAYLAND + else + c->capture.image_capture_scene_surface = wlr_scene_surface_create(&c->image_capture_scene->tree, c->surface.xwayland->surface); +#endif + /* Set initial monitor, tags, floating status, and focus: * we always consider floating, clients that have parent and thus @@ -2630,6 +2665,10 @@ setup(void) wlr_scene_node_set_enabled(&locked_bg->node, 0); foreign_toplevel_list = wlr_ext_foreign_toplevel_list_v1_create(dpy,1); + ext_foreign_toplevel_image_capture_source_manager_v1 = + wlr_ext_foreign_toplevel_image_capture_source_manager_v1_create(dpy, 1); + wl_signal_add(&ext_foreign_toplevel_image_capture_source_manager_v1->events.new_request, + &new_foreign_toplevel_capture_request); /* Use decoration protocols to negotiate server-side decorations */ wlr_server_decoration_manager_set_default_mode( @@ -2898,6 +2937,14 @@ unmapnotify(struct wl_listener *listener, void *data) wlr_ext_foreign_toplevel_handle_v1_destroy(c->foreign_toplevel_handle); c->foreign_toplevel_handle = NULL; } +#ifdef XWAYLAND + if (c->type != XDGShell && c->capture.image_capture_scene_surface) { + wlr_scene_node_destroy(&c->capture.image_capture_scene_surface->buffer->node); + c->capture.image_capture_scene_surface = NULL; + } +#endif + + wlr_scene_node_destroy(&c->image_capture_scene->tree.node); wlr_scene_node_destroy(&c->scene->node); client_surface(c)->data = NULL; printstatus(); From 7ba335063f492aa62bcdd8d34a4f3451f5fa3671 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Sun, 13 Sep 2026 08:24:13 +0200 Subject: [PATCH 21/63] don't crash when clicking an empty area If l is not initialized, accessing l->layer_surface->current.keyboard_interactive causes a segmentation fault. This was hard to see because c above was never initialized but never caused a crash, and dwl also didn't crash building with ASan. Also initialize c to be safe. Fixes 4c54bd8. Fixes https://codeberg.org/dwl/dwl/issues/1225. --- dwl.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dwl.c b/dwl.c index fa8c8ae..8d32c08 100644 --- a/dwl.c +++ b/dwl.c @@ -643,8 +643,8 @@ buttonpress(struct wl_listener *listener, void *data) struct wlr_pointer_button_event *event = data; struct wlr_keyboard *keyboard; uint32_t mods; - Client *c; - LayerSurface *l; + Client *c = NULL; + LayerSurface *l = NULL; const Button *b; wlr_idle_notifier_v1_notify_activity(idle_notifier, seat); From 99cdc9a825ea54ead1415bea301996aef2e6af75 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Sun, 13 Sep 2026 08:38:07 +0200 Subject: [PATCH 22/63] set seat capabilities for virtual keyboards and pointers wlr_seat_set_capabilities was never called without a real input device. Fixes https://codeberg.org/dwl/dwl/issues/1155. Co-authored-by: brinkpw --- dwl.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dwl.c b/dwl.c index 8d32c08..0f36e58 100644 --- a/dwl.c +++ b/dwl.c @@ -3119,6 +3119,7 @@ virtualkeyboard(struct wl_listener *listener, void *data) /* Add the new keyboard to the group */ wlr_keyboard_group_add_keyboard(group->wlr_group, &kb->keyboard); + wlr_seat_set_capabilities(seat, seat->capabilities | WL_SEAT_CAPABILITY_KEYBOARD); } void @@ -3130,6 +3131,7 @@ virtualpointer(struct wl_listener *listener, void *data) wlr_cursor_attach_input_device(cursor, device); if (event->suggested_output) wlr_cursor_map_input_to_output(cursor, device, event->suggested_output); + wlr_seat_set_capabilities(seat, seat->capabilities | WL_SEAT_CAPABILITY_POINTER); } Monitor * From e8bf70d6391015b374549458b716ccd4d93ce3b0 Mon Sep 17 00:00:00 2001 From: Peter Hofmann Date: Sun, 13 Sep 2026 18:14:47 +0200 Subject: [PATCH 23/63] implement Axis mouse bindings Co-authored-by: Guido Cella --- config.def.h | 5 +++++ dwl.c | 29 +++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/config.def.h b/config.def.h index 8a6eda0..572984b 100644 --- a/config.def.h +++ b/config.def.h @@ -170,3 +170,8 @@ static const Button buttons[] = { { MODKEY, BTN_MIDDLE, togglefloating, {0} }, { MODKEY, BTN_RIGHT, moveresize, {.ui = CurResize} }, }; + +static const Axis axes[] = { + { MODKEY, AxisUp, spawn, SHCMD("volume-up_EXAMPLE") }, + { MODKEY, AxisDown, spawn, SHCMD("volume-down_EXAMPLE") }, +}; diff --git a/dwl.c b/dwl.c index 0f36e58..9e293b8 100644 --- a/dwl.c +++ b/dwl.c @@ -86,6 +86,7 @@ enum { CurNormal, CurPressed, CurMove, CurResize }; /* cursor */ enum { XDGShell, LayerShell, X11 }; /* client types */ enum { LyrBg, LyrBottom, LyrTile, LyrFloat, LyrTop, LyrFS, LyrOverlay, LyrIMPopup, LyrBlock, NUM_LAYERS }; /* scene layers */ +enum { AxisUp, AxisRight, AxisDown, AxisLeft }; typedef union { int i; @@ -94,6 +95,13 @@ typedef union { const void *v; } Arg; +typedef struct { + unsigned int mod; + unsigned int dir; + void (*func)(const Arg *); + const Arg arg; +} Axis; + typedef struct { unsigned int mod; unsigned int button; @@ -628,9 +636,26 @@ axisnotify(struct wl_listener *listener, void *data) /* This event is forwarded by the cursor when a pointer emits an axis event, * for example when you move the scroll wheel. */ struct wlr_pointer_axis_event *event = data; + const Axis *a; + unsigned int adir; + struct wlr_keyboard *keyboard; + uint32_t mods; + wlr_idle_notifier_v1_notify_activity(idle_notifier, seat); - /* TODO: allow usage of scroll wheel for mousebindings, it can be implemented - * by checking the event's orientation and the delta of the event */ + + if (event->orientation == WL_POINTER_AXIS_VERTICAL_SCROLL) + adir = event->delta > 0 ? AxisDown : AxisUp; + else + adir = event->delta > 0 ? AxisRight : AxisLeft; + keyboard = wlr_seat_get_keyboard(seat); + mods = keyboard ? wlr_keyboard_get_modifiers(keyboard) : 0; + for (a = axes; a < END(axes); a++) { + if (CLEANMASK(mods) == CLEANMASK(a->mod) && adir == a->dir && a->func) { + a->func(&a->arg); + return; + } + } + /* Notify the client with pointer focus of the axis event. */ wlr_seat_pointer_notify_axis(seat, event->time_msec, event->orientation, event->delta, From 8ad2c920db7e2f1cb99709a8e46c968c5f526dc8 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Sun, 13 Sep 2026 19:06:54 +0200 Subject: [PATCH 24/63] use the right cursor after move and resize Fixes https://codeberg.org/dwl/dwl/issues/1162 Co-authored-by: DreamMaoMao <2523610504@qq.com> --- dwl.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dwl.c b/dwl.c index 9e293b8..7bffb83 100644 --- a/dwl.c +++ b/dwl.c @@ -703,10 +703,11 @@ buttonpress(struct wl_listener *listener, void *data) break; case WL_POINTER_BUTTON_STATE_RELEASED: /* If you released any buttons, we exit interactive move/resize mode. */ - /* TODO: should reset to the pointer focus's current setcursor */ if (!locked && cursor_mode != CurNormal && cursor_mode != CurPressed) { - wlr_cursor_set_xcursor(cursor, cursor_mgr, "default"); cursor_mode = CurNormal; + /* Update the cursor */ + wlr_seat_pointer_clear_focus(seat); + motionnotify(0, NULL, 0, 0, 0, 0); /* Drop the window off on its new monitor */ selmon = xytomon(cursor->x, cursor->y); setmon(grabc, selmon, 0); From b1c101515a579b91bd36a3e98c4642255bbb599d Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Sun, 13 Sep 2026 19:34:27 +0200 Subject: [PATCH 25/63] remove zero flickering lie This isn't true since dwl switched to the scene API in 2021 (1b38801). The issue is tracked at https://codeberg.org/dwl/dwl/issues/552. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index b8cddb4..fd72785 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,6 @@ given the base on which it is built. Implemented default features are: - Support screen lockers via ext-session-lock-v1 protocol - Various Wayland protocols - XWayland support as provided by wlroots (can be enabled in `config.mk`) -- Zero flickering - Wayland users naturally expect that "every frame is perfect" - Layer shell popups (used by Waybar) - Damage tracking provided by scenegraph API From 5fd19feaff54209bcb3a12017040a80ada4e32b1 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Sun, 13 Sep 2026 21:14:10 +0200 Subject: [PATCH 26/63] support monitors above and below in focusmon and tagmon Fixes https://codeberg.org/dwl/dwl/issues/1105 --- dwl.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dwl.c b/dwl.c index 7bffb83..f902ace 100644 --- a/dwl.c +++ b/dwl.c @@ -1474,7 +1474,8 @@ dirtomon(enum wlr_direction dir) dir, selmon->wlr_output, selmon->m.x, selmon->m.y))) return next->data; if ((next = wlr_output_layout_farthest_output(output_layout, - dir ^ (WLR_DIRECTION_LEFT|WLR_DIRECTION_RIGHT), + dir ^ (WLR_DIRECTION_LEFT | WLR_DIRECTION_RIGHT | + WLR_DIRECTION_UP | WLR_DIRECTION_DOWN), selmon->wlr_output, selmon->m.x, selmon->m.y))) return next->data; return selmon; From 8f52e96be48a6fab5a2d31d595733cbdf1c0b1a8 Mon Sep 17 00:00:00 2001 From: sewn Date: Mon, 1 Apr 2024 16:02:45 +0300 Subject: [PATCH 27/63] replicate dwm behavior for snap resize --- config.def.h | 1 + dwl.c | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/config.def.h b/config.def.h index 572984b..7ba41e0 100644 --- a/config.def.h +++ b/config.def.h @@ -7,6 +7,7 @@ static const int sloppyfocus = 1; /* focus follows mouse */ static const int bypass_surface_visibility = 0; /* 1 means idle inhibitors will disable idle tracking even if it's surface isn't visible */ static const unsigned int borderpx = 1; /* border pixel of windows */ +static const unsigned int snap = 32; /* snap pixel */ static const float rootcolor[] = COLOR(0x222222ff); static const float bordercolor[] = COLOR(0x444444ff); static const float focuscolor[] = COLOR(0x005577ff); diff --git a/dwl.c b/dwl.c index f902ace..23bb3f6 100644 --- a/dwl.c +++ b/dwl.c @@ -1981,6 +1981,7 @@ void motionnotify(uint32_t time, struct wlr_input_device *device, double dx, double dy, double dx_unaccel, double dy_unaccel) { + int nx, ny; double sx = 0, sy = 0, sx_confined, sy_confined; Client *c = NULL, *w = NULL; LayerSurface *l = NULL, *focusedl = NULL; @@ -2038,7 +2039,17 @@ motionnotify(uint32_t time, struct wlr_input_device *device, double dx, double d /* If we are currently grabbing the mouse, handle and return */ if (cursor_mode == CurMove) { /* Move the grabbed client to the new position. */ - resize(grabc, (struct wlr_box){.x = (int)round(cursor->x) - grabcx, .y = (int)round(cursor->y) - grabcy, + nx = (int)round(cursor->x) - grabcx; + ny = (int)round(cursor->y) - grabcy; + if (abs(selmon->w.x - nx) < (int)snap) + nx = selmon->w.x; + else if (abs((selmon->w.x + selmon->w.width) - (nx + grabc->geom.width)) < (int)snap) + nx = selmon->w.x + selmon->w.width - grabc->geom.width; + if (abs(selmon->w.y - ny) < (int)snap) + ny = selmon->w.y; + else if (abs((selmon->w.y + selmon->w.height) - (ny + grabc->geom.height)) < (int)snap) + ny = selmon->w.y + selmon->w.height - grabc->geom.height; + resize(grabc, (struct wlr_box){.x = nx, .y = ny, .width = grabc->geom.width, .height = grabc->geom.height}, 1); return; } else if (cursor_mode == CurResize) { From d1ebf3c9855ec643c877a2216eaad21e179cd447 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Mon, 14 Sep 2026 10:42:59 +0200 Subject: [PATCH 28/63] fix firefox opening with rounded corners Fixes https://codeberg.org/dwl/dwl/issues/1169 The c->isfloating check is needed to have the correct floating window size with e.g. alacritty -o window.dimensions.columns=15 window.dimensions.lines=10. --- dwl.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dwl.c b/dwl.c index 23bb3f6..c0c617b 100644 --- a/dwl.c +++ b/dwl.c @@ -951,6 +951,9 @@ commitnotify(struct wl_listener *listener, void *data) if (c->decoration) requestdecorationmode(&c->set_decoration_mode, c->decoration); wlr_xdg_toplevel_set_size(c->surface.xdg->toplevel, 0, 0); + if (!c->isfloating) + client_set_tiled(c, WLR_EDGE_TOP | WLR_EDGE_BOTTOM | WLR_EDGE_LEFT | + WLR_EDGE_RIGHT); return; } @@ -1881,7 +1884,6 @@ mapnotify(struct wl_listener *listener, void *data) } /* Initialize client geometry with room for border */ - client_set_tiled(c, WLR_EDGE_TOP | WLR_EDGE_BOTTOM | WLR_EDGE_LEFT | WLR_EDGE_RIGHT); c->geom.width += 2 * c->bw; c->geom.height += 2 * c->bw; From dc9a860191791df3854635a9ea57510309038b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonardo=20Hern=C3=A1ndez=20Hern=C3=A1ndez?= Date: Mon, 14 Sep 2026 01:48:32 +0200 Subject: [PATCH 29/63] use the new output swapchain manager in outputmgrapplyortest() --- dwl.c | 68 +++++++++++++++++++++++++++++++++++------------------------ 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/dwl.c b/dwl.c index c0c617b..aead8eb 100644 --- a/dwl.c +++ b/dwl.c @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -2129,49 +2131,61 @@ outputmgrapplyortest(struct wlr_output_configuration_v1 *config, int test) * output_layout.change event, not here. */ struct wlr_output_configuration_head_v1 *config_head; - int ok = 1; + size_t states_len, i; + struct wlr_output_swapchain_manager swapchain_manager; + struct wlr_backend_output_state *states = + wlr_output_configuration_v1_build_state(config, &states_len); + int ok = 0; + + if (!states) { + wlr_output_configuration_v1_send_failed(config); + return; + } + + wlr_output_swapchain_manager_init(&swapchain_manager, backend); + + ok = wlr_output_swapchain_manager_prepare(&swapchain_manager, states, states_len); + if (!ok || test) + goto out; + + for (i = 0; i < states_len; i++) { + struct wlr_swapchain *swapchain = wlr_output_swapchain_manager_get_swapchain( + &swapchain_manager, states[i].output); + if (swapchain && !states[i].output->enabled) + wlr_output_state_set_buffer(&states[i].base, wlr_swapchain_acquire(swapchain)); + } + + if (!(ok = wlr_backend_commit(backend, states, states_len))) + goto out; + + wlr_output_swapchain_manager_apply(&swapchain_manager); wl_list_for_each(config_head, &config->heads, link) { struct wlr_output *wlr_output = config_head->state.output; Monitor *m = wlr_output->data; - struct wlr_output_state state; /* Ensure displays previously disabled by wlr-output-power-management-v1 * are properly handled*/ m->asleep = 0; - wlr_output_state_init(&state); - wlr_output_state_set_enabled(&state, config_head->state.enabled); - if (!config_head->state.enabled) - goto apply_or_test; - - if (config_head->state.mode) - wlr_output_state_set_mode(&state, config_head->state.mode); - else - wlr_output_state_set_custom_mode(&state, - config_head->state.custom_mode.width, - config_head->state.custom_mode.height, - config_head->state.custom_mode.refresh); - - wlr_output_state_set_transform(&state, config_head->state.transform); - wlr_output_state_set_scale(&state, config_head->state.scale); - wlr_output_state_set_adaptive_sync_enabled(&state, - config_head->state.adaptive_sync_enabled); - -apply_or_test: - ok &= test ? wlr_output_test_state(wlr_output, &state) - : wlr_output_commit_state(wlr_output, &state); - /* Don't move monitors if position wouldn't change. This avoids * wlroots marking the output as manually configured. * wlr_output_layout_add does not like disabled outputs */ - if (!test && wlr_output->enabled && (m->m.x != config_head->state.x || m->m.y != config_head->state.y)) + if (config_head->state.enabled + && (m->m.x != config_head->state.x || m->m.y != config_head->state.y + || !wlr_output_layout_get(output_layout, wlr_output))) { wlr_output_layout_add(output_layout, wlr_output, config_head->state.x, config_head->state.y); - - wlr_output_state_finish(&state); + } } +out: + wlr_output_swapchain_manager_finish(&swapchain_manager); + for (i = 0; i < states_len; i++) + wlr_output_state_finish(&states[i].base); + + free(states); + if (ok) wlr_output_configuration_v1_send_succeeded(config); else From f9c644f0b07a408e3b6a1389b1ffbd90dcaf4285 Mon Sep 17 00:00:00 2001 From: Andrea Chiavazza Date: Mon, 14 Sep 2026 17:53:35 +0200 Subject: [PATCH 30/63] use modifier-independent key symbols in keybindings Co-authored-by: Guido Cella --- config.def.h | 35 +++++++++++++++++------------------ dwl.c | 44 ++++++++++++++++++-------------------------- util.h | 12 ++++++++++++ 3 files changed, 47 insertions(+), 44 deletions(-) diff --git a/config.def.h b/config.def.h index 7ba41e0..13c5322 100644 --- a/config.def.h +++ b/config.def.h @@ -106,11 +106,11 @@ static const enum libinput_config_tap_button_map button_map = LIBINPUT_CONFIG_TA /* If you want to use the windows key for MODKEY, use WLR_MODIFIER_LOGO */ #define MODKEY WLR_MODIFIER_ALT -#define TAGKEYS(KEY,SKEY,TAG) \ +#define TAGKEYS(KEY,TAG) \ { MODKEY, KEY, view, {.ui = 1 << TAG} }, \ { MODKEY|WLR_MODIFIER_CTRL, KEY, toggleview, {.ui = 1 << TAG} }, \ - { MODKEY|WLR_MODIFIER_SHIFT, SKEY, tag, {.ui = 1 << TAG} }, \ - { MODKEY|WLR_MODIFIER_CTRL|WLR_MODIFIER_SHIFT,SKEY,toggletag, {.ui = 1 << TAG} } + { MODKEY|WLR_MODIFIER_SHIFT, KEY, tag, {.ui = 1 << TAG} }, \ + { MODKEY|WLR_MODIFIER_CTRL|WLR_MODIFIER_SHIFT,KEY,toggletag, {.ui = 1 << TAG} } /* helper for spawning shell commands in the pre dwm-5.0 fashion */ #define SHCMD(cmd) { .v = (const char*[]){ "/bin/sh", "-c", cmd, NULL } } @@ -120,7 +120,6 @@ static const char *termcmd[] = { "foot", NULL }; static const char *menucmd[] = { "wmenu-run", NULL }; static const Key keys[] = { - /* Note that Shift changes certain key codes: 2 -> at, etc. */ /* modifier key function argument */ { MODKEY, XKB_KEY_p, spawn, {.v = menucmd} }, { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_Return, spawn, {.v = termcmd} }, @@ -140,28 +139,28 @@ static const Key keys[] = { { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_space, togglefloating, {0} }, { MODKEY, XKB_KEY_e, togglefullscreen, {0} }, { MODKEY, XKB_KEY_0, view, {.ui = ~0} }, - { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_parenright, tag, {.ui = ~0} }, + { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_0, tag, {.ui = ~0} }, { MODKEY, XKB_KEY_comma, focusmon, {.i = WLR_DIRECTION_LEFT} }, { MODKEY, XKB_KEY_period, focusmon, {.i = WLR_DIRECTION_RIGHT} }, - { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_less, tagmon, {.i = WLR_DIRECTION_LEFT} }, - { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_greater, tagmon, {.i = WLR_DIRECTION_RIGHT} }, - TAGKEYS( XKB_KEY_1, XKB_KEY_exclam, 0), - TAGKEYS( XKB_KEY_2, XKB_KEY_at, 1), - TAGKEYS( XKB_KEY_3, XKB_KEY_numbersign, 2), - TAGKEYS( XKB_KEY_4, XKB_KEY_dollar, 3), - TAGKEYS( XKB_KEY_5, XKB_KEY_percent, 4), - TAGKEYS( XKB_KEY_6, XKB_KEY_asciicircum, 5), - TAGKEYS( XKB_KEY_7, XKB_KEY_ampersand, 6), - TAGKEYS( XKB_KEY_8, XKB_KEY_asterisk, 7), - TAGKEYS( XKB_KEY_9, XKB_KEY_parenleft, 8), + { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_comma, tagmon, {.i = WLR_DIRECTION_LEFT} }, + { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_period, tagmon, {.i = WLR_DIRECTION_RIGHT} }, + TAGKEYS( XKB_KEY_1, 0), + TAGKEYS( XKB_KEY_2, 1), + TAGKEYS( XKB_KEY_3, 2), + TAGKEYS( XKB_KEY_4, 3), + TAGKEYS( XKB_KEY_5, 4), + TAGKEYS( XKB_KEY_6, 5), + TAGKEYS( XKB_KEY_7, 6), + TAGKEYS( XKB_KEY_8, 7), + TAGKEYS( XKB_KEY_9, 8), { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_q, quit, {0} }, /* Ctrl-Alt-Backspace and Ctrl-Alt-Fx used to be handled by X server */ - { WLR_MODIFIER_CTRL|WLR_MODIFIER_ALT,XKB_KEY_Terminate_Server, quit, {0} }, + { WLR_MODIFIER_CTRL|WLR_MODIFIER_ALT,XKB_KEY_BackSpace, quit, {0} }, /* Ctrl-Alt-Fx is used to switch to another VT, if you don't know what a VT is * do not remove them. */ -#define CHVT(n) { WLR_MODIFIER_CTRL|WLR_MODIFIER_ALT,XKB_KEY_XF86Switch_VT_##n, chvt, {.ui = (n)} } +#define CHVT(n) { WLR_MODIFIER_CTRL|WLR_MODIFIER_ALT,XKB_KEY_F##n, chvt, {.ui = (n)} } CHVT(1), CHVT(2), CHVT(3), CHVT(4), CHVT(5), CHVT(6), CHVT(7), CHVT(8), CHVT(9), CHVT(10), CHVT(11), CHVT(12), }; diff --git a/dwl.c b/dwl.c index aead8eb..f3abcf8 100644 --- a/dwl.c +++ b/dwl.c @@ -170,9 +170,8 @@ typedef struct { struct wlr_keyboard_group *wlr_group; struct wlr_keyboard *virtual_keyboard; - int nsyms; - const xkb_keysym_t *keysyms; /* invalid if nsyms == 0 */ - uint32_t mods; /* invalid if nsyms == 0 */ + xkb_keysym_t keysyms[2]; + uint32_t mods; struct wl_event_source *key_repeat_source; struct wl_listener modifiers; @@ -1706,11 +1705,8 @@ keybinding(uint32_t mods, xkb_keysym_t sym) */ const Key *k; for (k = keys; k < END(keys); k++) { - if (CLEANMASK(mods) == CLEANMASK(k->mod) - && xkb_keysym_to_lower(sym) == xkb_keysym_to_lower(k->keysym) - && k->func) { + if (CLEANMASK(mods) == CLEANMASK(k->mod) && sym == k->keysym && k->func) return k; - } } return NULL; } @@ -1718,28 +1714,30 @@ keybinding(uint32_t mods, xkb_keysym_t sym) void keypress(struct wl_listener *listener, void *data) { - int i; /* This event is raised when a key is pressed or released. */ KeyboardGroup *group = wl_container_of(listener, group, key); struct wlr_keyboard_key_event *event = data; /* Translate libinput keycode -> xkbcommon */ uint32_t keycode = event->keycode + 8; - /* Get a list of keysyms based on the keymap for this keyboard */ - const xkb_keysym_t *syms; - int nsyms = xkb_state_key_get_syms( - group->wlr_group->keyboard.xkb_state, keycode, &syms); - + struct wlr_keyboard *kb = &group->wlr_group->keyboard; + xkb_layout_index_t layout = xkb_state_key_get_layout(kb->xkb_state, keycode); int handled = 0; - uint32_t mods = wlr_keyboard_get_modifiers(&group->wlr_group->keyboard); + + // Get the keysyms for level 0 (normal) and level 1 (shifted) + // Only one keysym for each level: + // multiple keysyms per keycode don't really exist in the real world + for (int i = 0; i < 2; i++) + group->keysyms[i] = keymap_get_one_sym_by_level(kb->keymap, keycode, layout, i); + group->mods = wlr_keyboard_get_modifiers(&group->wlr_group->keyboard); wlr_idle_notifier_v1_notify_activity(idle_notifier, seat); /* On _press_ if there is no active screen locker, * attempt to process a compositor keybinding. */ if (!locked && event->state == WL_KEYBOARD_KEY_STATE_PRESSED) { - for (i = 0; i < nsyms; i++) { - const Key *key = keybinding(mods, syms[i]); + for (int i = 0; i < 2; i++) { + const Key *key = keybinding(group->mods, group->keysyms[i]); if (key) { consumed[event->keycode] = 1; key->func(&key->arg); @@ -1748,16 +1746,11 @@ keypress(struct wl_listener *listener, void *data) } } - if (handled && group->wlr_group->keyboard.repeat_info.delay > 0) { - group->mods = mods; - group->keysyms = syms; - group->nsyms = nsyms; + if (handled && group->wlr_group->keyboard.repeat_info.delay > 0) wl_event_source_timer_update(group->key_repeat_source, group->wlr_group->keyboard.repeat_info.delay); - } else { - group->nsyms = 0; + else wl_event_source_timer_update(group->key_repeat_source, 0); - } if (handled || input_method_keyboard_grab_forward_key(group, event)) return; @@ -1793,14 +1786,13 @@ int keyrepeat(void *data) { KeyboardGroup *group = data; - int i; - if (!group->nsyms || group->wlr_group->keyboard.repeat_info.rate <= 0) + if (group->wlr_group->keyboard.repeat_info.rate <= 0) return 0; wl_event_source_timer_update(group->key_repeat_source, 1000 / group->wlr_group->keyboard.repeat_info.rate); - for (i = 0; i < group->nsyms; i++) { + for (int i = 0; i < 2; i++) { const Key *key = keybinding(group->mods, group->keysyms[i]); if (key) key->func(&key->arg); diff --git a/util.h b/util.h index 40cfbe4..4aaf208 100644 --- a/util.h +++ b/util.h @@ -4,6 +4,7 @@ #include #include #include +#include #define MAX(A, B) ((A) > (B) ? (A) : (B)) #define MIN(A, B) ((A) < (B) ? (A) : (B)) @@ -13,6 +14,8 @@ static void die(const char *fmt, ...); static void *ecalloc(size_t nmemb, size_t size); static int fd_set_nonblock(int fd); +xkb_keysym_t keymap_get_one_sym_by_level(struct xkb_keymap *keymap, + xkb_keycode_t key, xkb_layout_index_t layout, xkb_level_index_t level); void die(const char *fmt, ...) { @@ -56,3 +59,12 @@ fd_set_nonblock(int fd) { return 0; } + +xkb_keysym_t +keymap_get_one_sym_by_level(struct xkb_keymap *keymap, xkb_keycode_t key, + xkb_layout_index_t layout, xkb_level_index_t level) +{ + const xkb_keysym_t *syms; + int count = xkb_keymap_key_get_syms_by_level(keymap, key, layout, level, &syms); + return count ? syms[0] : XKB_KEY_NoSymbol; +} From e7f185ed3e0038e490c0b10d6dbe5664c01ae1f0 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Tue, 15 Sep 2026 09:49:05 +0200 Subject: [PATCH 31/63] remove c->resize Unused since 70c33c0. --- dwl.c | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/dwl.c b/dwl.c index f3abcf8..6636a4d 100644 --- a/dwl.c +++ b/dwl.c @@ -156,7 +156,6 @@ typedef struct { unsigned int bw; uint32_t tags; int isfloating, isurgent, isfullscreen; - uint32_t resize; /* configure serial of a pending resize */ } Client; typedef struct { @@ -959,10 +958,6 @@ commitnotify(struct wl_listener *listener, void *data) } resize(c, c->geom, (c->isfloating && !c->isfullscreen)); - - /* mark a pending resize as completed */ - if (c->resize && c->resize <= c->surface.xdg->current.configure_serial) - c->resize = 0; } void @@ -2362,9 +2357,7 @@ resize(Client *c, struct wlr_box geo, int interact) wlr_scene_node_set_position(&c->border[2]->node, 0, c->bw); wlr_scene_node_set_position(&c->border[3]->node, c->geom.width - c->bw, c->bw); - /* this is a no-op if size hasn't changed */ - c->resize = client_set_size(c, c->geom.width - 2 * c->bw, - c->geom.height - 2 * c->bw); + client_set_size(c, c->geom.width - 2 * c->bw, c->geom.height - 2 * c->bw); client_get_clip(c, &clip); wlr_scene_subsurface_tree_set_clip(&c->scene_surface->node, &clip); } From fec0699985650cdf8da5571376797c0bc6613a5e Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Tue, 15 Sep 2026 10:23:13 +0200 Subject: [PATCH 32/63] make client_set_size() return void The return value is unused since the previous commit. --- client.h | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/client.h b/client.h index 75f1c27..a915f98 100644 --- a/client.h +++ b/client.h @@ -344,20 +344,19 @@ client_set_scale(struct wlr_surface *s, float scale) wlr_surface_set_preferred_buffer_scale(s, (int32_t)ceilf(scale)); } -static inline uint32_t +static inline void client_set_size(Client *c, uint32_t width, uint32_t height) { #ifdef XWAYLAND if (client_is_x11(c)) { wlr_xwayland_surface_configure(c->surface.xwayland, c->geom.x + c->bw, c->geom.y + c->bw, width, height); - return 0; + return; } #endif - if ((int32_t)width == c->surface.xdg->toplevel->current.width - && (int32_t)height == c->surface.xdg->toplevel->current.height) - return 0; - return wlr_xdg_toplevel_set_size(c->surface.xdg->toplevel, (int32_t)width, (int32_t)height); + if ((int32_t)width != c->surface.xdg->toplevel->current.width + || (int32_t)height != c->surface.xdg->toplevel->current.height) + wlr_xdg_toplevel_set_size(c->surface.xdg->toplevel, (int32_t)width, (int32_t)height); } static inline void From 498a0e605cb0260f3a1d9c1ffc092b3041337033 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Tue, 15 Sep 2026 10:24:34 +0200 Subject: [PATCH 33/63] don't resize on commit Fixes https://codeberg.org/dwl/dwl/issues/1229 Reverts 32e66f4 This could be called only on the initial_commit, but it was added to fix https://github.com/djpohly/dwl/issues/515 which I can't reproduce after removing it, so let's try to remove it completely. --- dwl.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/dwl.c b/dwl.c index 6636a4d..44d57ab 100644 --- a/dwl.c +++ b/dwl.c @@ -956,8 +956,6 @@ commitnotify(struct wl_listener *listener, void *data) WLR_EDGE_RIGHT); return; } - - resize(c, c->geom, (c->isfloating && !c->isfullscreen)); } void From c824886b8ce6a249c608e3a04bf251e4b32206c1 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Tue, 15 Sep 2026 10:26:30 +0200 Subject: [PATCH 34/63] reduce nesting in commitnotify() --- dwl.c | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/dwl.c b/dwl.c index 44d57ab..bb010d8 100644 --- a/dwl.c +++ b/dwl.c @@ -933,29 +933,28 @@ commitnotify(struct wl_listener *listener, void *data) { Client *c = wl_container_of(listener, c, commit); - if (c->surface.xdg->initial_commit) { - /* - * Get the monitor this client will be rendered on - * Note that if the user set a rule in which the client is placed on - * a different monitor based on its title, this will likely select - * a wrong monitor. - */ - applyrules(c); - if (c->mon) { - client_set_scale(client_surface(c), c->mon->wlr_output->scale); - } - setmon(c, NULL, 0); /* Make sure to reapply rules in mapnotify() */ - - wlr_xdg_toplevel_set_wm_capabilities(c->surface.xdg->toplevel, - WLR_XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN); - if (c->decoration) - requestdecorationmode(&c->set_decoration_mode, c->decoration); - wlr_xdg_toplevel_set_size(c->surface.xdg->toplevel, 0, 0); - if (!c->isfloating) - client_set_tiled(c, WLR_EDGE_TOP | WLR_EDGE_BOTTOM | WLR_EDGE_LEFT | - WLR_EDGE_RIGHT); + if (!c->surface.xdg->initial_commit) return; - } + + /* + * Get the monitor this client will be rendered on + * Note that if the user set a rule in which the client is placed on + * a different monitor based on its title, this will likely select + * a wrong monitor. + */ + applyrules(c); + if (c->mon) + client_set_scale(client_surface(c), c->mon->wlr_output->scale); + setmon(c, NULL, 0); /* Make sure to reapply rules in mapnotify() */ + + wlr_xdg_toplevel_set_wm_capabilities(c->surface.xdg->toplevel, + WLR_XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN); + if (c->decoration) + requestdecorationmode(&c->set_decoration_mode, c->decoration); + wlr_xdg_toplevel_set_size(c->surface.xdg->toplevel, 0, 0); + if (!c->isfloating) + client_set_tiled(c, WLR_EDGE_TOP | WLR_EDGE_BOTTOM | WLR_EDGE_LEFT | + WLR_EDGE_RIGHT); } void From 68c6319e3fdb46aabc47595f85faef25cb20873a Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Tue, 15 Sep 2026 15:24:27 +0200 Subject: [PATCH 35/63] Revert "reduce nesting in commitnotify()" This reverts commit c824886b8ce6a249c608e3a04bf251e4b32206c1. --- dwl.c | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/dwl.c b/dwl.c index bb010d8..44d57ab 100644 --- a/dwl.c +++ b/dwl.c @@ -933,28 +933,29 @@ commitnotify(struct wl_listener *listener, void *data) { Client *c = wl_container_of(listener, c, commit); - if (!c->surface.xdg->initial_commit) + if (c->surface.xdg->initial_commit) { + /* + * Get the monitor this client will be rendered on + * Note that if the user set a rule in which the client is placed on + * a different monitor based on its title, this will likely select + * a wrong monitor. + */ + applyrules(c); + if (c->mon) { + client_set_scale(client_surface(c), c->mon->wlr_output->scale); + } + setmon(c, NULL, 0); /* Make sure to reapply rules in mapnotify() */ + + wlr_xdg_toplevel_set_wm_capabilities(c->surface.xdg->toplevel, + WLR_XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN); + if (c->decoration) + requestdecorationmode(&c->set_decoration_mode, c->decoration); + wlr_xdg_toplevel_set_size(c->surface.xdg->toplevel, 0, 0); + if (!c->isfloating) + client_set_tiled(c, WLR_EDGE_TOP | WLR_EDGE_BOTTOM | WLR_EDGE_LEFT | + WLR_EDGE_RIGHT); return; - - /* - * Get the monitor this client will be rendered on - * Note that if the user set a rule in which the client is placed on - * a different monitor based on its title, this will likely select - * a wrong monitor. - */ - applyrules(c); - if (c->mon) - client_set_scale(client_surface(c), c->mon->wlr_output->scale); - setmon(c, NULL, 0); /* Make sure to reapply rules in mapnotify() */ - - wlr_xdg_toplevel_set_wm_capabilities(c->surface.xdg->toplevel, - WLR_XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN); - if (c->decoration) - requestdecorationmode(&c->set_decoration_mode, c->decoration); - wlr_xdg_toplevel_set_size(c->surface.xdg->toplevel, 0, 0); - if (!c->isfloating) - client_set_tiled(c, WLR_EDGE_TOP | WLR_EDGE_BOTTOM | WLR_EDGE_LEFT | - WLR_EDGE_RIGHT); + } } void From 36c80aff0344b279197acb98435c77c2aff03366 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Tue, 15 Sep 2026 15:24:34 +0200 Subject: [PATCH 36/63] Revert "don't resize on commit" This reverts commit 498a0e605cb0260f3a1d9c1ffc092b3041337033. Without resizing on every commit the top left of firefox is cropped. --- dwl.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dwl.c b/dwl.c index 44d57ab..6636a4d 100644 --- a/dwl.c +++ b/dwl.c @@ -956,6 +956,8 @@ commitnotify(struct wl_listener *listener, void *data) WLR_EDGE_RIGHT); return; } + + resize(c, c->geom, (c->isfloating && !c->isfullscreen)); } void From 4de32d2d5c8eec142f9aa8d87467a3b086b02762 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Tue, 15 Sep 2026 20:03:28 +0200 Subject: [PATCH 37/63] Reapply "respect size hints" This reverts commit 31bf1cbaf6418a6fb39e41b21cadc5c20826a663. Fixes https://codeberg.org/dwl/dwl/issues/105 Fixes https://codeberg.org/dwl/dwl/issues/634 Fixes https://codeberg.org/dwl/dwl/issues/1171 --- client.h | 43 ++++++++++++++++++++++++++++++++----------- dwl.c | 16 +++++++++++++--- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/client.h b/client.h index a915f98..5d76b13 100644 --- a/client.h +++ b/client.h @@ -15,6 +15,32 @@ client_is_x11(Client *c) return 0; } +static inline void +client_get_size_hints(Client *c, struct wlr_box *max, struct wlr_box *min) +{ + struct wlr_xdg_toplevel_state *state; + +#ifdef XWAYLAND + if (client_is_x11(c)) { + xcb_size_hints_t *size_hints = c->surface.xwayland->size_hints; + if (size_hints) { + max->width = size_hints->max_width; + max->height = size_hints->max_height; + min->width = size_hints->min_width; + min->height = size_hints->min_height; + } + return; + } +#endif + + state = &c->surface.xdg->toplevel->current; + max->width = state->max_width; + max->height = state->max_height; + min->width = state->min_width; + min->height = state->min_height; +} + + static inline struct wlr_surface * client_surface(Client *c) { @@ -206,13 +232,12 @@ client_get_title(Client *c) static inline int client_is_float_type(Client *c) { - struct wlr_xdg_toplevel *toplevel; - struct wlr_xdg_toplevel_state state; + struct wlr_box min = {0}, max = {0}; + client_get_size_hints(c, &max, &min); #ifdef XWAYLAND if (client_is_x11(c)) { struct wlr_xwayland_surface *surface = c->surface.xwayland; - xcb_size_hints_t *size_hints = surface->size_hints; if (surface->modal) return 1; @@ -223,17 +248,13 @@ client_is_float_type(Client *c) return 1; } - return size_hints && size_hints->min_width > 0 && size_hints->min_height > 0 - && (size_hints->max_width == size_hints->min_width - || size_hints->max_height == size_hints->min_height); + return min.width && min.height && + (min.width == max.width || min.height == max.height); } #endif - toplevel = c->surface.xdg->toplevel; - state = toplevel->current; - return toplevel->parent || (state.min_width != 0 && state.min_height != 0 - && (state.min_width == state.max_width - || state.min_height == state.max_height)); + return c->surface.xdg->toplevel->parent || (min.width && min.height && + (min.width == max.width || min.height == max.height)); } static inline int diff --git a/dwl.c b/dwl.c index 6636a4d..f9ea272 100644 --- a/dwl.c +++ b/dwl.c @@ -3,6 +3,7 @@ */ #include #include +#include #include #include #include @@ -483,9 +484,18 @@ static struct wlr_xwayland *xwayland; void applybounds(Client *c, struct wlr_box *bbox) { - /* set minimum possible */ - c->geom.width = MAX(1 + 2 * (int)c->bw, c->geom.width); - c->geom.height = MAX(1 + 2 * (int)c->bw, c->geom.height); + if (!c->isfullscreen) { + struct wlr_box min = {0}, max = {0}; + client_get_size_hints(c, &max, &min); + c->geom.width = MAX(min.width + 2 * (int)c->bw, c->geom.width); + c->geom.height = MAX(min.height + 2 * (int)c->bw, c->geom.height); + /* Some clients set their max size to INT_MAX, which does not violate the + * protocol but it's unnecesary, as they can set their max size to zero. */ + if (max.width && 2 * (int)c->bw <= INT_MAX - max.width) /* Checks for overflow */ + c->geom.width = MIN(max.width + 2 * (int)c->bw, c->geom.width); + if (max.height && 2 * (int)c->bw <= INT_MAX - max.height) /* Checks for overflow */ + c->geom.height = MIN(max.height + 2 * (int)c->bw, c->geom.height); + } if (c->geom.x >= bbox->x + bbox->width) c->geom.x = bbox->x + bbox->width - c->geom.width; From 0f8630282d11e21b694dcd4716a3935a4ead6fa5 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Tue, 15 Sep 2026 20:57:28 +0200 Subject: [PATCH 38/63] Revert "use modifier-independent key symbols in keybindings" This reverts commit f9c644f0b07a408e3b6a1389b1ffbd90dcaf4285. --- config.def.h | 35 ++++++++++++++++++----------------- dwl.c | 44 ++++++++++++++++++++++++++------------------ util.h | 12 ------------ 3 files changed, 44 insertions(+), 47 deletions(-) diff --git a/config.def.h b/config.def.h index 13c5322..7ba41e0 100644 --- a/config.def.h +++ b/config.def.h @@ -106,11 +106,11 @@ static const enum libinput_config_tap_button_map button_map = LIBINPUT_CONFIG_TA /* If you want to use the windows key for MODKEY, use WLR_MODIFIER_LOGO */ #define MODKEY WLR_MODIFIER_ALT -#define TAGKEYS(KEY,TAG) \ +#define TAGKEYS(KEY,SKEY,TAG) \ { MODKEY, KEY, view, {.ui = 1 << TAG} }, \ { MODKEY|WLR_MODIFIER_CTRL, KEY, toggleview, {.ui = 1 << TAG} }, \ - { MODKEY|WLR_MODIFIER_SHIFT, KEY, tag, {.ui = 1 << TAG} }, \ - { MODKEY|WLR_MODIFIER_CTRL|WLR_MODIFIER_SHIFT,KEY,toggletag, {.ui = 1 << TAG} } + { MODKEY|WLR_MODIFIER_SHIFT, SKEY, tag, {.ui = 1 << TAG} }, \ + { MODKEY|WLR_MODIFIER_CTRL|WLR_MODIFIER_SHIFT,SKEY,toggletag, {.ui = 1 << TAG} } /* helper for spawning shell commands in the pre dwm-5.0 fashion */ #define SHCMD(cmd) { .v = (const char*[]){ "/bin/sh", "-c", cmd, NULL } } @@ -120,6 +120,7 @@ static const char *termcmd[] = { "foot", NULL }; static const char *menucmd[] = { "wmenu-run", NULL }; static const Key keys[] = { + /* Note that Shift changes certain key codes: 2 -> at, etc. */ /* modifier key function argument */ { MODKEY, XKB_KEY_p, spawn, {.v = menucmd} }, { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_Return, spawn, {.v = termcmd} }, @@ -139,28 +140,28 @@ static const Key keys[] = { { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_space, togglefloating, {0} }, { MODKEY, XKB_KEY_e, togglefullscreen, {0} }, { MODKEY, XKB_KEY_0, view, {.ui = ~0} }, - { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_0, tag, {.ui = ~0} }, + { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_parenright, tag, {.ui = ~0} }, { MODKEY, XKB_KEY_comma, focusmon, {.i = WLR_DIRECTION_LEFT} }, { MODKEY, XKB_KEY_period, focusmon, {.i = WLR_DIRECTION_RIGHT} }, - { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_comma, tagmon, {.i = WLR_DIRECTION_LEFT} }, - { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_period, tagmon, {.i = WLR_DIRECTION_RIGHT} }, - TAGKEYS( XKB_KEY_1, 0), - TAGKEYS( XKB_KEY_2, 1), - TAGKEYS( XKB_KEY_3, 2), - TAGKEYS( XKB_KEY_4, 3), - TAGKEYS( XKB_KEY_5, 4), - TAGKEYS( XKB_KEY_6, 5), - TAGKEYS( XKB_KEY_7, 6), - TAGKEYS( XKB_KEY_8, 7), - TAGKEYS( XKB_KEY_9, 8), + { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_less, tagmon, {.i = WLR_DIRECTION_LEFT} }, + { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_greater, tagmon, {.i = WLR_DIRECTION_RIGHT} }, + TAGKEYS( XKB_KEY_1, XKB_KEY_exclam, 0), + TAGKEYS( XKB_KEY_2, XKB_KEY_at, 1), + TAGKEYS( XKB_KEY_3, XKB_KEY_numbersign, 2), + TAGKEYS( XKB_KEY_4, XKB_KEY_dollar, 3), + TAGKEYS( XKB_KEY_5, XKB_KEY_percent, 4), + TAGKEYS( XKB_KEY_6, XKB_KEY_asciicircum, 5), + TAGKEYS( XKB_KEY_7, XKB_KEY_ampersand, 6), + TAGKEYS( XKB_KEY_8, XKB_KEY_asterisk, 7), + TAGKEYS( XKB_KEY_9, XKB_KEY_parenleft, 8), { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_q, quit, {0} }, /* Ctrl-Alt-Backspace and Ctrl-Alt-Fx used to be handled by X server */ - { WLR_MODIFIER_CTRL|WLR_MODIFIER_ALT,XKB_KEY_BackSpace, quit, {0} }, + { WLR_MODIFIER_CTRL|WLR_MODIFIER_ALT,XKB_KEY_Terminate_Server, quit, {0} }, /* Ctrl-Alt-Fx is used to switch to another VT, if you don't know what a VT is * do not remove them. */ -#define CHVT(n) { WLR_MODIFIER_CTRL|WLR_MODIFIER_ALT,XKB_KEY_F##n, chvt, {.ui = (n)} } +#define CHVT(n) { WLR_MODIFIER_CTRL|WLR_MODIFIER_ALT,XKB_KEY_XF86Switch_VT_##n, chvt, {.ui = (n)} } CHVT(1), CHVT(2), CHVT(3), CHVT(4), CHVT(5), CHVT(6), CHVT(7), CHVT(8), CHVT(9), CHVT(10), CHVT(11), CHVT(12), }; diff --git a/dwl.c b/dwl.c index f9ea272..7994e41 100644 --- a/dwl.c +++ b/dwl.c @@ -170,8 +170,9 @@ typedef struct { struct wlr_keyboard_group *wlr_group; struct wlr_keyboard *virtual_keyboard; - xkb_keysym_t keysyms[2]; - uint32_t mods; + int nsyms; + const xkb_keysym_t *keysyms; /* invalid if nsyms == 0 */ + uint32_t mods; /* invalid if nsyms == 0 */ struct wl_event_source *key_repeat_source; struct wl_listener modifiers; @@ -1710,8 +1711,11 @@ keybinding(uint32_t mods, xkb_keysym_t sym) */ const Key *k; for (k = keys; k < END(keys); k++) { - if (CLEANMASK(mods) == CLEANMASK(k->mod) && sym == k->keysym && k->func) + if (CLEANMASK(mods) == CLEANMASK(k->mod) + && xkb_keysym_to_lower(sym) == xkb_keysym_to_lower(k->keysym) + && k->func) { return k; + } } return NULL; } @@ -1719,30 +1723,28 @@ keybinding(uint32_t mods, xkb_keysym_t sym) void keypress(struct wl_listener *listener, void *data) { + int i; /* This event is raised when a key is pressed or released. */ KeyboardGroup *group = wl_container_of(listener, group, key); struct wlr_keyboard_key_event *event = data; /* Translate libinput keycode -> xkbcommon */ uint32_t keycode = event->keycode + 8; - struct wlr_keyboard *kb = &group->wlr_group->keyboard; - xkb_layout_index_t layout = xkb_state_key_get_layout(kb->xkb_state, keycode); - int handled = 0; + /* Get a list of keysyms based on the keymap for this keyboard */ + const xkb_keysym_t *syms; + int nsyms = xkb_state_key_get_syms( + group->wlr_group->keyboard.xkb_state, keycode, &syms); - // Get the keysyms for level 0 (normal) and level 1 (shifted) - // Only one keysym for each level: - // multiple keysyms per keycode don't really exist in the real world - for (int i = 0; i < 2; i++) - group->keysyms[i] = keymap_get_one_sym_by_level(kb->keymap, keycode, layout, i); - group->mods = wlr_keyboard_get_modifiers(&group->wlr_group->keyboard); + int handled = 0; + uint32_t mods = wlr_keyboard_get_modifiers(&group->wlr_group->keyboard); wlr_idle_notifier_v1_notify_activity(idle_notifier, seat); /* On _press_ if there is no active screen locker, * attempt to process a compositor keybinding. */ if (!locked && event->state == WL_KEYBOARD_KEY_STATE_PRESSED) { - for (int i = 0; i < 2; i++) { - const Key *key = keybinding(group->mods, group->keysyms[i]); + for (i = 0; i < nsyms; i++) { + const Key *key = keybinding(mods, syms[i]); if (key) { consumed[event->keycode] = 1; key->func(&key->arg); @@ -1751,11 +1753,16 @@ keypress(struct wl_listener *listener, void *data) } } - if (handled && group->wlr_group->keyboard.repeat_info.delay > 0) + if (handled && group->wlr_group->keyboard.repeat_info.delay > 0) { + group->mods = mods; + group->keysyms = syms; + group->nsyms = nsyms; wl_event_source_timer_update(group->key_repeat_source, group->wlr_group->keyboard.repeat_info.delay); - else + } else { + group->nsyms = 0; wl_event_source_timer_update(group->key_repeat_source, 0); + } if (handled || input_method_keyboard_grab_forward_key(group, event)) return; @@ -1791,13 +1798,14 @@ int keyrepeat(void *data) { KeyboardGroup *group = data; - if (group->wlr_group->keyboard.repeat_info.rate <= 0) + int i; + if (!group->nsyms || group->wlr_group->keyboard.repeat_info.rate <= 0) return 0; wl_event_source_timer_update(group->key_repeat_source, 1000 / group->wlr_group->keyboard.repeat_info.rate); - for (int i = 0; i < 2; i++) { + for (i = 0; i < group->nsyms; i++) { const Key *key = keybinding(group->mods, group->keysyms[i]); if (key) key->func(&key->arg); diff --git a/util.h b/util.h index 4aaf208..40cfbe4 100644 --- a/util.h +++ b/util.h @@ -4,7 +4,6 @@ #include #include #include -#include #define MAX(A, B) ((A) > (B) ? (A) : (B)) #define MIN(A, B) ((A) < (B) ? (A) : (B)) @@ -14,8 +13,6 @@ static void die(const char *fmt, ...); static void *ecalloc(size_t nmemb, size_t size); static int fd_set_nonblock(int fd); -xkb_keysym_t keymap_get_one_sym_by_level(struct xkb_keymap *keymap, - xkb_keycode_t key, xkb_layout_index_t layout, xkb_level_index_t level); void die(const char *fmt, ...) { @@ -59,12 +56,3 @@ fd_set_nonblock(int fd) { return 0; } - -xkb_keysym_t -keymap_get_one_sym_by_level(struct xkb_keymap *keymap, xkb_keycode_t key, - xkb_layout_index_t layout, xkb_level_index_t level) -{ - const xkb_keysym_t *syms; - int count = xkb_keymap_key_get_syms_by_level(keymap, key, layout, level, &syms); - return count ? syms[0] : XKB_KEY_NoSymbol; -} From bab3fdadf31fcd56fc0f4562b7fc3d28022cea39 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Tue, 15 Sep 2026 21:21:23 +0200 Subject: [PATCH 39/63] don't run keybindings twice when the shift keysym is the same For keys like F1-F12 the keysym with the shift is the same as without. Avoid running keybindings bound to these keys twice. --- dwl.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dwl.c b/dwl.c index 7994e41..d18d1f8 100644 --- a/dwl.c +++ b/dwl.c @@ -1749,6 +1749,7 @@ keypress(struct wl_listener *listener, void *data) consumed[event->keycode] = 1; key->func(&key->arg); handled = 1; + break; } } } @@ -1807,8 +1808,10 @@ keyrepeat(void *data) for (i = 0; i < group->nsyms; i++) { const Key *key = keybinding(group->mods, group->keysyms[i]); - if (key) + if (key) { key->func(&key->arg); + break; + } } return 0; From aa0c1659d30ef5c8bb98bc4de1e90562cc609beb Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Tue, 15 Sep 2026 21:23:47 +0200 Subject: [PATCH 40/63] Reapply "use modifier-independent key symbols in keybindings" This reverts commit 0f8630282d11e21b694dcd4716a3935a4ead6fa5. --- config.def.h | 35 +++++++++++++++++------------------ dwl.c | 44 ++++++++++++++++++-------------------------- util.h | 12 ++++++++++++ 3 files changed, 47 insertions(+), 44 deletions(-) diff --git a/config.def.h b/config.def.h index 7ba41e0..13c5322 100644 --- a/config.def.h +++ b/config.def.h @@ -106,11 +106,11 @@ static const enum libinput_config_tap_button_map button_map = LIBINPUT_CONFIG_TA /* If you want to use the windows key for MODKEY, use WLR_MODIFIER_LOGO */ #define MODKEY WLR_MODIFIER_ALT -#define TAGKEYS(KEY,SKEY,TAG) \ +#define TAGKEYS(KEY,TAG) \ { MODKEY, KEY, view, {.ui = 1 << TAG} }, \ { MODKEY|WLR_MODIFIER_CTRL, KEY, toggleview, {.ui = 1 << TAG} }, \ - { MODKEY|WLR_MODIFIER_SHIFT, SKEY, tag, {.ui = 1 << TAG} }, \ - { MODKEY|WLR_MODIFIER_CTRL|WLR_MODIFIER_SHIFT,SKEY,toggletag, {.ui = 1 << TAG} } + { MODKEY|WLR_MODIFIER_SHIFT, KEY, tag, {.ui = 1 << TAG} }, \ + { MODKEY|WLR_MODIFIER_CTRL|WLR_MODIFIER_SHIFT,KEY,toggletag, {.ui = 1 << TAG} } /* helper for spawning shell commands in the pre dwm-5.0 fashion */ #define SHCMD(cmd) { .v = (const char*[]){ "/bin/sh", "-c", cmd, NULL } } @@ -120,7 +120,6 @@ static const char *termcmd[] = { "foot", NULL }; static const char *menucmd[] = { "wmenu-run", NULL }; static const Key keys[] = { - /* Note that Shift changes certain key codes: 2 -> at, etc. */ /* modifier key function argument */ { MODKEY, XKB_KEY_p, spawn, {.v = menucmd} }, { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_Return, spawn, {.v = termcmd} }, @@ -140,28 +139,28 @@ static const Key keys[] = { { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_space, togglefloating, {0} }, { MODKEY, XKB_KEY_e, togglefullscreen, {0} }, { MODKEY, XKB_KEY_0, view, {.ui = ~0} }, - { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_parenright, tag, {.ui = ~0} }, + { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_0, tag, {.ui = ~0} }, { MODKEY, XKB_KEY_comma, focusmon, {.i = WLR_DIRECTION_LEFT} }, { MODKEY, XKB_KEY_period, focusmon, {.i = WLR_DIRECTION_RIGHT} }, - { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_less, tagmon, {.i = WLR_DIRECTION_LEFT} }, - { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_greater, tagmon, {.i = WLR_DIRECTION_RIGHT} }, - TAGKEYS( XKB_KEY_1, XKB_KEY_exclam, 0), - TAGKEYS( XKB_KEY_2, XKB_KEY_at, 1), - TAGKEYS( XKB_KEY_3, XKB_KEY_numbersign, 2), - TAGKEYS( XKB_KEY_4, XKB_KEY_dollar, 3), - TAGKEYS( XKB_KEY_5, XKB_KEY_percent, 4), - TAGKEYS( XKB_KEY_6, XKB_KEY_asciicircum, 5), - TAGKEYS( XKB_KEY_7, XKB_KEY_ampersand, 6), - TAGKEYS( XKB_KEY_8, XKB_KEY_asterisk, 7), - TAGKEYS( XKB_KEY_9, XKB_KEY_parenleft, 8), + { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_comma, tagmon, {.i = WLR_DIRECTION_LEFT} }, + { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_period, tagmon, {.i = WLR_DIRECTION_RIGHT} }, + TAGKEYS( XKB_KEY_1, 0), + TAGKEYS( XKB_KEY_2, 1), + TAGKEYS( XKB_KEY_3, 2), + TAGKEYS( XKB_KEY_4, 3), + TAGKEYS( XKB_KEY_5, 4), + TAGKEYS( XKB_KEY_6, 5), + TAGKEYS( XKB_KEY_7, 6), + TAGKEYS( XKB_KEY_8, 7), + TAGKEYS( XKB_KEY_9, 8), { MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_q, quit, {0} }, /* Ctrl-Alt-Backspace and Ctrl-Alt-Fx used to be handled by X server */ - { WLR_MODIFIER_CTRL|WLR_MODIFIER_ALT,XKB_KEY_Terminate_Server, quit, {0} }, + { WLR_MODIFIER_CTRL|WLR_MODIFIER_ALT,XKB_KEY_BackSpace, quit, {0} }, /* Ctrl-Alt-Fx is used to switch to another VT, if you don't know what a VT is * do not remove them. */ -#define CHVT(n) { WLR_MODIFIER_CTRL|WLR_MODIFIER_ALT,XKB_KEY_XF86Switch_VT_##n, chvt, {.ui = (n)} } +#define CHVT(n) { WLR_MODIFIER_CTRL|WLR_MODIFIER_ALT,XKB_KEY_F##n, chvt, {.ui = (n)} } CHVT(1), CHVT(2), CHVT(3), CHVT(4), CHVT(5), CHVT(6), CHVT(7), CHVT(8), CHVT(9), CHVT(10), CHVT(11), CHVT(12), }; diff --git a/dwl.c b/dwl.c index d18d1f8..8dbb4a0 100644 --- a/dwl.c +++ b/dwl.c @@ -170,9 +170,8 @@ typedef struct { struct wlr_keyboard_group *wlr_group; struct wlr_keyboard *virtual_keyboard; - int nsyms; - const xkb_keysym_t *keysyms; /* invalid if nsyms == 0 */ - uint32_t mods; /* invalid if nsyms == 0 */ + xkb_keysym_t keysyms[2]; + uint32_t mods; struct wl_event_source *key_repeat_source; struct wl_listener modifiers; @@ -1711,11 +1710,8 @@ keybinding(uint32_t mods, xkb_keysym_t sym) */ const Key *k; for (k = keys; k < END(keys); k++) { - if (CLEANMASK(mods) == CLEANMASK(k->mod) - && xkb_keysym_to_lower(sym) == xkb_keysym_to_lower(k->keysym) - && k->func) { + if (CLEANMASK(mods) == CLEANMASK(k->mod) && sym == k->keysym && k->func) return k; - } } return NULL; } @@ -1723,28 +1719,30 @@ keybinding(uint32_t mods, xkb_keysym_t sym) void keypress(struct wl_listener *listener, void *data) { - int i; /* This event is raised when a key is pressed or released. */ KeyboardGroup *group = wl_container_of(listener, group, key); struct wlr_keyboard_key_event *event = data; /* Translate libinput keycode -> xkbcommon */ uint32_t keycode = event->keycode + 8; - /* Get a list of keysyms based on the keymap for this keyboard */ - const xkb_keysym_t *syms; - int nsyms = xkb_state_key_get_syms( - group->wlr_group->keyboard.xkb_state, keycode, &syms); - + struct wlr_keyboard *kb = &group->wlr_group->keyboard; + xkb_layout_index_t layout = xkb_state_key_get_layout(kb->xkb_state, keycode); int handled = 0; - uint32_t mods = wlr_keyboard_get_modifiers(&group->wlr_group->keyboard); + + // Get the keysyms for level 0 (normal) and level 1 (shifted) + // Only one keysym for each level: + // multiple keysyms per keycode don't really exist in the real world + for (int i = 0; i < 2; i++) + group->keysyms[i] = keymap_get_one_sym_by_level(kb->keymap, keycode, layout, i); + group->mods = wlr_keyboard_get_modifiers(&group->wlr_group->keyboard); wlr_idle_notifier_v1_notify_activity(idle_notifier, seat); /* On _press_ if there is no active screen locker, * attempt to process a compositor keybinding. */ if (!locked && event->state == WL_KEYBOARD_KEY_STATE_PRESSED) { - for (i = 0; i < nsyms; i++) { - const Key *key = keybinding(mods, syms[i]); + for (int i = 0; i < 2; i++) { + const Key *key = keybinding(group->mods, group->keysyms[i]); if (key) { consumed[event->keycode] = 1; key->func(&key->arg); @@ -1754,16 +1752,11 @@ keypress(struct wl_listener *listener, void *data) } } - if (handled && group->wlr_group->keyboard.repeat_info.delay > 0) { - group->mods = mods; - group->keysyms = syms; - group->nsyms = nsyms; + if (handled && group->wlr_group->keyboard.repeat_info.delay > 0) wl_event_source_timer_update(group->key_repeat_source, group->wlr_group->keyboard.repeat_info.delay); - } else { - group->nsyms = 0; + else wl_event_source_timer_update(group->key_repeat_source, 0); - } if (handled || input_method_keyboard_grab_forward_key(group, event)) return; @@ -1799,14 +1792,13 @@ int keyrepeat(void *data) { KeyboardGroup *group = data; - int i; - if (!group->nsyms || group->wlr_group->keyboard.repeat_info.rate <= 0) + if (group->wlr_group->keyboard.repeat_info.rate <= 0) return 0; wl_event_source_timer_update(group->key_repeat_source, 1000 / group->wlr_group->keyboard.repeat_info.rate); - for (i = 0; i < group->nsyms; i++) { + for (int i = 0; i < 2; i++) { const Key *key = keybinding(group->mods, group->keysyms[i]); if (key) { key->func(&key->arg); diff --git a/util.h b/util.h index 40cfbe4..4aaf208 100644 --- a/util.h +++ b/util.h @@ -4,6 +4,7 @@ #include #include #include +#include #define MAX(A, B) ((A) > (B) ? (A) : (B)) #define MIN(A, B) ((A) < (B) ? (A) : (B)) @@ -13,6 +14,8 @@ static void die(const char *fmt, ...); static void *ecalloc(size_t nmemb, size_t size); static int fd_set_nonblock(int fd); +xkb_keysym_t keymap_get_one_sym_by_level(struct xkb_keymap *keymap, + xkb_keycode_t key, xkb_layout_index_t layout, xkb_level_index_t level); void die(const char *fmt, ...) { @@ -56,3 +59,12 @@ fd_set_nonblock(int fd) { return 0; } + +xkb_keysym_t +keymap_get_one_sym_by_level(struct xkb_keymap *keymap, xkb_keycode_t key, + xkb_layout_index_t layout, xkb_level_index_t level) +{ + const xkb_keysym_t *syms; + int count = xkb_keymap_key_get_syms_by_level(keymap, key, layout, level, &syms); + return count ? syms[0] : XKB_KEY_NoSymbol; +} From f6e3a2826ea910dfb501d451194ed48ce3527167 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Tue, 15 Sep 2026 21:47:00 +0200 Subject: [PATCH 41/63] deactivate the old client when the new one wants focus I see no issue with winecfg after removing this condition, so I assume it was fixed upstream. --- dwl.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dwl.c b/dwl.c index 8dbb4a0..190235d 100644 --- a/dwl.c +++ b/dwl.c @@ -1539,9 +1539,7 @@ focusclient(Client *c, int lift) return; } else if (old_c && old_c == exclusive_focus && client_wants_focus(old_c)) { return; - /* Don't deactivate old client if the new one wants focus, as this causes issues with winecfg - * and probably other clients */ - } else if (old_c && !client_is_unmanaged(old_c) && (!c || !client_wants_focus(c))) { + } else if (old_c && !client_is_unmanaged(old_c)) { client_set_border_color(old_c, bordercolor); client_activate_surface(old, 0); From 60138353379f59a3724416d52560778fff60db58 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Tue, 15 Sep 2026 22:17:24 +0200 Subject: [PATCH 42/63] keep the focused border color of the old client in certain cases When focusing an unmanaged client (like dmenu) or a layer shell surface (like wmenu) keep the focused border color of the regular client, but still deactivate it. This is consistent with dwm and sway. This was already done for unmanaged clients but not for layer shell surfaces. This is not perfect because you can press MODKEY+j/k or move the cursor while a program like dmenu is focused and have 2 clients with focused border color after closing dmenu. But this doesn't work well even in sway where if you change focus while dmenu is open you can longer focus it, and after closing a layer shell surface with on-demand keyboard interactivity all toplevels have the focused border color, so this will do for now. --- dwl.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dwl.c b/dwl.c index 190235d..63996a2 100644 --- a/dwl.c +++ b/dwl.c @@ -1540,7 +1540,8 @@ focusclient(Client *c, int lift) } else if (old_c && old_c == exclusive_focus && client_wants_focus(old_c)) { return; } else if (old_c && !client_is_unmanaged(old_c)) { - client_set_border_color(old_c, bordercolor); + if (c && !client_is_unmanaged(c)) + client_set_border_color(old_c, bordercolor); client_activate_surface(old, 0); } From 433c325fb2a1d90b36206925fc429e354e248c99 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Tue, 15 Sep 2026 23:29:30 +0200 Subject: [PATCH 43/63] fix a use-after-free MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prevent a use-after-free when closing a nested dwl with Ctrl+c. It happened because cleanup() → input_method_relay_finish() frees input_method_relay, and then closemon() → focusclient() → input_method_relay_set_focus() uses relay->surface. --- dwl.c | 1 + ime.h | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/dwl.c b/dwl.c index 63996a2..a2418d0 100644 --- a/dwl.c +++ b/dwl.c @@ -792,6 +792,7 @@ cleanup(void) destroykeyboardgroup(&kb_group->destroy, NULL); input_method_relay_finish(input_method_relay); + input_method_relay = NULL; /* If it's not destroyed manually, it will cause a use-after-free of wlr_seat. * Destroy it until it's fixed on the wlroots side */ diff --git a/ime.h b/ime.h index f537a2d..b9591f5 100644 --- a/ime.h +++ b/ime.h @@ -625,7 +625,7 @@ void input_method_relay_set_focus(struct input_method_relay *relay, struct wlr_surface *surface) { - if (relay->focused_surface == surface) + if (!relay || relay->focused_surface == surface) return; if (relay->focused_surface) From 1dae5ba3ed4d786c3e47cc9abe35f5f2e0936497 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Wed, 16 Sep 2026 11:27:58 +0200 Subject: [PATCH 44/63] size hints can be -1 --- client.h | 4 ++-- dwl.c | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client.h b/client.h index 5d76b13..dc0615e 100644 --- a/client.h +++ b/client.h @@ -248,12 +248,12 @@ client_is_float_type(Client *c) return 1; } - return min.width && min.height && + return min.width > 0 && min.height > 0 && (min.width == max.width || min.height == max.height); } #endif - return c->surface.xdg->toplevel->parent || (min.width && min.height && + return c->surface.xdg->toplevel->parent || (min.width > 0 && min.height > 0 && (min.width == max.width || min.height == max.height)); } diff --git a/dwl.c b/dwl.c index a2418d0..156660f 100644 --- a/dwl.c +++ b/dwl.c @@ -491,9 +491,9 @@ applybounds(Client *c, struct wlr_box *bbox) c->geom.height = MAX(min.height + 2 * (int)c->bw, c->geom.height); /* Some clients set their max size to INT_MAX, which does not violate the * protocol but it's unnecesary, as they can set their max size to zero. */ - if (max.width && 2 * (int)c->bw <= INT_MAX - max.width) /* Checks for overflow */ + if (max.width > 0 && 2 * (int)c->bw <= INT_MAX - max.width) /* Checks for overflow */ c->geom.width = MIN(max.width + 2 * (int)c->bw, c->geom.width); - if (max.height && 2 * (int)c->bw <= INT_MAX - max.height) /* Checks for overflow */ + if (max.height > 0 && 2 * (int)c->bw <= INT_MAX - max.height) /* Checks for overflow */ c->geom.height = MIN(max.height + 2 * (int)c->bw, c->geom.height); } From 1fe1b4e66e264998883aa991772362d3a3fad1d5 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Wed, 16 Sep 2026 12:47:52 +0200 Subject: [PATCH 45/63] show popups in fullscreen Xwayland apps The dialogs opened by clicking items in Xwyland applications' toolbars are unmanaged clients, so placing them in LyrFloat drew them below their source client in LyrFS when it is fullscreened. Add a new dedicated layer for unamanged clients to fix it. Don't put them in LyrFS to prevent the regular fullscreen client from hiding unmanaged ones, e.g. by opening dmenu and then clicking the regular client. Fixes https://codeberg.org/dwl/dwl/issues/556 --- dwl.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dwl.c b/dwl.c index 156660f..f7bda0b 100644 --- a/dwl.c +++ b/dwl.c @@ -88,7 +88,8 @@ /* enums */ enum { CurNormal, CurPressed, CurMove, CurResize }; /* cursor */ enum { XDGShell, LayerShell, X11 }; /* client types */ -enum { LyrBg, LyrBottom, LyrTile, LyrFloat, LyrTop, LyrFS, LyrOverlay, LyrIMPopup, LyrBlock, NUM_LAYERS }; /* scene layers */ +enum { LyrBg, LyrBottom, LyrTile, LyrFloat, LyrTop, LyrFS, LyrUnmanaged, LyrOverlay, + LyrIMPopup, LyrBlock, NUM_LAYERS }; /* scene layers */ enum { AxisUp, AxisRight, AxisDown, AxisLeft }; typedef union { @@ -1869,7 +1870,7 @@ mapnotify(struct wl_listener *listener, void *data) /* Handle unmanaged clients first so we can return prior create borders */ if (client_is_unmanaged(c)) { /* Unmanaged clients always are floating */ - wlr_scene_node_reparent(&c->scene->node, layers[LyrFloat]); + wlr_scene_node_reparent(&c->scene->node, layers[LyrUnmanaged]); wlr_scene_node_set_position(&c->scene->node, c->geom.x, c->geom.y); client_set_size(c, c->geom.width, c->geom.height); if (client_wants_focus(c)) { From b3223b15e98425f44f5772e94f1522c6aa50bc3d Mon Sep 17 00:00:00 2001 From: save196 Date: Tue, 15 Sep 2026 17:32:06 +0000 Subject: [PATCH 46/63] fix capturing hidden toplevels --- dwl.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/dwl.c b/dwl.c index f7bda0b..70c76a3 100644 --- a/dwl.c +++ b/dwl.c @@ -549,7 +549,9 @@ arrange(Monitor *m) wl_list_for_each(c, &clients, link) { if (c->mon == m) { wlr_scene_node_set_enabled(&c->scene->node, VISIBLEON(c, m)); - client_set_suspended(c, !VISIBLEON(c, m)); + client_set_suspended(c, !VISIBLEON(c, m) && + (!c->image_capture_source || + wl_list_empty(&c->image_capture_source->resources))); } } @@ -749,6 +751,8 @@ capturerequest(struct wl_listener *listener, void *data) wlr_ext_foreign_toplevel_image_capture_source_manager_v1_request_accept( request, view->image_capture_source); + + client_set_suspended(view, 0); } void @@ -2998,6 +3002,8 @@ unmapnotify(struct wl_listener *listener, void *data) #endif wlr_scene_node_destroy(&c->image_capture_scene->tree.node); + c->image_capture_scene = NULL; + c->image_capture_source = NULL; wlr_scene_node_destroy(&c->scene->node); client_surface(c)->data = NULL; printstatus(); From e45186254497acec608bb6182cc2312c9eebcb75 Mon Sep 17 00:00:00 2001 From: save196 Date: Tue, 15 Sep 2026 18:10:48 +0000 Subject: [PATCH 47/63] follow the geometry of unmanaged clients and fix formatting --- dwl.c | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/dwl.c b/dwl.c index 70c76a3..c51e0d5 100644 --- a/dwl.c +++ b/dwl.c @@ -153,6 +153,7 @@ typedef struct { struct wl_listener associate; struct wl_listener dissociate; struct wl_listener configure; + struct wl_listener set_geometry; struct wl_listener set_hints; #endif unsigned int bw; @@ -466,6 +467,7 @@ static void associatex11(struct wl_listener *listener, void *data); static void configurex11(struct wl_listener *listener, void *data); static void createnotifyx11(struct wl_listener *listener, void *data); static void dissociatex11(struct wl_listener *listener, void *data); +static void setgeometryx11(struct wl_listener *listener, void *data); static void sethints(struct wl_listener *listener, void *data); static void xwaylandready(struct wl_listener *listener, void *data); static struct wl_listener new_xwayland_surface = {.notify = createnotifyx11}; @@ -549,9 +551,9 @@ arrange(Monitor *m) wl_list_for_each(c, &clients, link) { if (c->mon == m) { wlr_scene_node_set_enabled(&c->scene->node, VISIBLEON(c, m)); - client_set_suspended(c, !VISIBLEON(c, m) && - (!c->image_capture_source || - wl_list_empty(&c->image_capture_source->resources))); + client_set_suspended(c, !VISIBLEON(c, m) + && (!c->image_capture_source + || wl_list_empty(&c->image_capture_source->resources))); } } @@ -1430,6 +1432,7 @@ destroynotify(struct wl_listener *listener, void *data) wl_list_remove(&c->associate.link); wl_list_remove(&c->configure.link); wl_list_remove(&c->dissociate.link); + wl_list_remove(&c->set_geometry.link); wl_list_remove(&c->set_hints.link); } else #endif @@ -1876,7 +1879,6 @@ mapnotify(struct wl_listener *listener, void *data) /* Unmanaged clients always are floating */ wlr_scene_node_reparent(&c->scene->node, layers[LyrUnmanaged]); wlr_scene_node_set_position(&c->scene->node, c->geom.x, c->geom.y); - client_set_size(c, c->geom.width, c->geom.height); if (client_wants_focus(c)) { focusclient(c, 1); exclusive_focus = c; @@ -3280,13 +3282,8 @@ configurex11(struct wl_listener *listener, void *data) { Client *c = wl_container_of(listener, c, configure); struct wlr_xwayland_surface_configure_event *event = data; - if (!client_surface(c) || !client_surface(c)->mapped) { - wlr_xwayland_surface_configure(c->surface.xwayland, - event->x, event->y, event->width, event->height); - return; - } - if (client_is_unmanaged(c)) { - wlr_scene_node_set_position(&c->scene->node, event->x, event->y); + if (!client_surface(c) || !client_surface(c)->mapped + || client_is_unmanaged(c)) { wlr_xwayland_surface_configure(c->surface.xwayland, event->x, event->y, event->width, event->height); return; @@ -3319,6 +3316,7 @@ createnotifyx11(struct wl_listener *listener, void *data) LISTEN(&xsurface->events.request_activate, &c->activate, activatex11); LISTEN(&xsurface->events.request_configure, &c->configure, configurex11); LISTEN(&xsurface->events.request_fullscreen, &c->fullscreen, fullscreennotify); + LISTEN(&xsurface->events.set_geometry, &c->set_geometry, setgeometryx11); LISTEN(&xsurface->events.set_hints, &c->set_hints, sethints); LISTEN(&xsurface->events.set_title, &c->set_title, updatetitle); } @@ -3331,6 +3329,19 @@ dissociatex11(struct wl_listener *listener, void *data) wl_list_remove(&c->unmap.link); } +void +setgeometryx11(struct wl_listener *listener, void *data) +{ + Client *c = wl_container_of(listener, c, set_geometry); + + if (!client_is_unmanaged(c) || !client_surface(c) + || !client_surface(c)->mapped) + return; + + client_get_geometry(c, &c->geom); + wlr_scene_node_set_position(&c->scene->node, c->geom.x, c->geom.y); +} + void sethints(struct wl_listener *listener, void *data) { From 669b26d0a9acdb59cc649b81f046ec843e7ef190 Mon Sep 17 00:00:00 2001 From: save196 Date: Tue, 15 Sep 2026 18:14:29 +0000 Subject: [PATCH 48/63] let the focus leave an unmanaged surface Unmanaged clients spawned by a parent window, like menus, would not close if the user changed workspace or focused another client while the menu was selected, because the parent was never told that it lost the focus. This remembers the last managed client and deactivates it on a focus change, so that it closes the menu itself. This also fixes a regression introduced by f6e3a28, where menus would close immediately after being opened. --- dwl.c | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/dwl.c b/dwl.c index c51e0d5..0e90c8f 100644 --- a/dwl.c +++ b/dwl.c @@ -377,6 +377,7 @@ static void zoom(const Arg *arg); static pid_t child_pid = -1; static int locked; static void *exclusive_focus; +static Client *focused_client; static struct wl_display *dpy; static struct wl_event_loop *event_loop; static struct wlr_backend *backend; @@ -1523,6 +1524,9 @@ focusclient(Client *c, int lift) wlr_xdg_popup_destroy(popup); } + if (old_c && old_c == exclusive_focus && client_wants_focus(old_c)) + exclusive_focus = NULL; + /* Put the new client atop the focus stack and select its monitor */ if (c && !client_is_unmanaged(c)) { wl_list_remove(&c->flink); @@ -1536,24 +1540,23 @@ focusclient(Client *c, int lift) client_set_border_color(c, focuscolor); } - /* Deactivate old client if focus is changing */ - if (old && (!c || client_surface(c) != old)) { - /* If an overlay is focused, don't focus or activate the client, - * but only update its position in fstack to render its border with focuscolor - * and focus it after the overlay is closed. */ - if (old_client_type == LayerShell && wlr_scene_node_coords( - &old_l->scene->node, &unused_lx, &unused_ly) - && old_l->layer_surface->current.layer >= ZWLR_LAYER_SHELL_V1_LAYER_TOP - && old_l->layer_surface->current.keyboard_interactive == ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE) { - return; - } else if (old_c && old_c == exclusive_focus && client_wants_focus(old_c)) { - return; - } else if (old_c && !client_is_unmanaged(old_c)) { - if (c && !client_is_unmanaged(c)) - client_set_border_color(old_c, bordercolor); + /* If an overlay is focused, don't focus or activate the client, + * but only update its position in fstack to render its border with focuscolor + * and focus it after the overlay is closed. */ + if (old && (!c || client_surface(c) != old) + && old_client_type == LayerShell && wlr_scene_node_coords( + &old_l->scene->node, &unused_lx, &unused_ly) + && old_l->layer_surface->current.layer >= ZWLR_LAYER_SHELL_V1_LAYER_TOP + && old_l->layer_surface->current.keyboard_interactive == ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE) + return; - client_activate_surface(old, 0); - } + if (focused_client && focused_client != c && !(c && client_is_unmanaged(c))) { + struct wlr_surface *s = client_surface(focused_client); + if (c) + client_set_border_color(focused_client, bordercolor); + if (s && s->mapped) + client_activate_surface(s, 0); + focused_client = NULL; } printstatus(); @@ -1574,6 +1577,9 @@ focusclient(Client *c, int lift) /* Activate the new client */ client_activate_surface(client_surface(c), 1); + + if (!client_is_unmanaged(c)) + focused_client = c; } void @@ -2980,6 +2986,8 @@ unmapnotify(struct wl_listener *listener, void *data) cursor_mode = CurNormal; grabc = NULL; } + if (c == focused_client) + focused_client = NULL; if (client_is_unmanaged(c)) { if (c == exclusive_focus) { From 45054a34a4f4817c7a87576147fdbbc8272af0a0 Mon Sep 17 00:00:00 2001 From: save196 Date: Thu, 17 Sep 2026 14:59:35 +0000 Subject: [PATCH 49/63] always apply a minimum size of 1x1 Resizing with the mouse can make the size negative, which makes dwl crash, and the minimum is 1 because 0 makes some X11 clients crash. --- dwl.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dwl.c b/dwl.c index 0e90c8f..dada453 100644 --- a/dwl.c +++ b/dwl.c @@ -488,6 +488,10 @@ static struct wlr_xwayland *xwayland; void applybounds(Client *c, struct wlr_box *bbox) { + /* set minimum client size to 1 */ + c->geom.width = MAX(1 + 2 * (int)c->bw, c->geom.width); + c->geom.height = MAX(1 + 2 * (int)c->bw, c->geom.height); + if (!c->isfullscreen) { struct wlr_box min = {0}, max = {0}; client_get_size_hints(c, &max, &min); From d16036e6902a7587d2c93b860b72b5e7e544a3be Mon Sep 17 00:00:00 2001 From: julmajustus Date: Fri, 18 Sep 2026 22:02:03 +0300 Subject: [PATCH 50/63] Fix input_method_relay double allocation - input_method_relay is allocated inside input_method_relay_create(), which leads the extra allocation to leak memory. --- dwl.c | 1 - 1 file changed, 1 deletion(-) diff --git a/dwl.c b/dwl.c index dada453..4cfd0fd 100644 --- a/dwl.c +++ b/dwl.c @@ -2814,7 +2814,6 @@ setup(void) input_method_manager = wlr_input_method_manager_v2_create(dpy); text_input_manager = wlr_text_input_manager_v3_create(dpy); - input_method_relay = ecalloc(1, sizeof(*input_method_relay)); input_method_relay = input_method_relay_create(); /* Make sure XWayland clients don't connect to the parent X server, From 49837e985ef8823e6f23137a4d83be99f27fffca Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Sun, 20 Sep 2026 11:04:40 +0200 Subject: [PATCH 51/63] change unreleased to 0.8 and update it --- CHANGELOG.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07c9ee4..9626255 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,13 @@ # Changelog -* [Unreleased](#unreleased) +* [0.8](#0.8) * [0.7](#0.7) * [0.6](#0.6) * [0.5](#0.5) -## Unreleased +## 0.8 + ### Added * Support for the linux-drm-syncobj-v1 protocol ([wlroots!4715][wlroots!4715], [#685][685]) @@ -17,15 +18,12 @@ [646]: https://codeberg.org/dwl/dwl/pulls/646 -### Changed -### Deprecated -### Removed ### Fixed * Crash when a client is created while all outputs are disabled. -### Security -### Contributors + +Most changes in this release are not listed. See the commits for details. ## 0.7 From 98bea2c27f99c549023fb394bfd983a439d8ccb9 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Sun, 20 Sep 2026 11:53:29 +0200 Subject: [PATCH 52/63] add version 0.9 to the changelog --- CHANGELOG.md | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9626255..bace46b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,96 @@ # Changelog +* [0.9](#0.9) * [0.8](#0.8) * [0.7](#0.7) * [0.6](#0.6) * [0.5](#0.5) +## 0.9 + +### Added + +* Add axis mouse bindings ([#652][652]) +* Add support for the ext-data-control protocol ([#1184][1184]) +* Add support for the ext-foreign-toplevel-list protocol ([#1223][1223]) +* Add support for the ext-image-copy-capture and ext-image-capture-source protocols (screensharing) ([#1218][1218], [#1223][1223], [#1224][1224]) +* Add support for version 4 of the layer shell protocol (on-demand keyboard interactivity) ([#674][674]) +* Add support for wlr-text-input and wlr-input-method protocols (IME) ([#1219][1219]) +* Snap windows to edges when moving or resizing them ([#608][608]) + +[1184]: https://codeberg.org/dwl/dwl/pulls/1184 +[1218]: https://codeberg.org/dwl/dwl/pulls/1218 +[1219]: https://codeberg.org/dwl/dwl/pulls/1219 +[1223]: https://codeberg.org/dwl/dwl/pulls/1223 +[1224]: https://codeberg.org/dwl/dwl/pulls/1224 +[608]: https://codeberg.org/dwl/dwl/pulls/608 +[652]: https://codeberg.org/dwl/dwl/pulls/652 +[674]: https://codeberg.org/dwl/dwl/pulls/674 + + +### Changed + +* Update wlroots to version 0.20 ([#1218][1218]) +* Use modifier-independent key symbols in config.h keybindings ([#1075][1075]) + +[1075]: https://codeberg.org/dwl/dwl/pulls/1075 +[1218]: https://codeberg.org/dwl/dwl/pulls/1218 + + +### Fixed + +* Apply a minimum window size of 1x1 ([#1231][1231]) +* Close Xwayland submenus when changing tag or focus ([#1233][1233]) +* Don't make Firefox draw rounded corners when it is opened ([#d1ebf3c][d1ebf3c]) +* Don't send key release events to the wrong client ([#1108][1108]) +* Fix a crash when closing mpv ([#1220][1220]) +* Fix a crash when destroying an idle-inhibitor ([#1214][1214]) +* Fix choppy resizing on newer wlroots ([#1280][1280]) +* Fix the position of certain sub-menus in Xwayland applications ([#1232][1232]) +* Keep the focused border color on the regular client when opening a layer shell surface like wmenu ([#6013835][6013835]) +* Let keybindings focus and tag monitors above and below ([#5fd19fe][5fd19fe]) +* Preserve asleep disabled outputs in output config ([#1198][1198]) +* Respect size hints ([#4de32d2][4de32d2]) +* Set seat capabilities for virtual keyboards and pointers when there is no other input device ([#99cdc9a][99cdc9a]) +* Show popups in fullscreen Xwayland applications ([#1fe1b4e][1fe1b4e]) +* Show the correct cursor after moving and resizing ([#8ad2c92][8ad2c92]) + +[1108]: https://codeberg.org/dwl/dwl/pulls/1108 +[1198]: https://codeberg.org/dwl/dwl/pulls/1198 +[1214]: https://codeberg.org/dwl/dwl/pulls/1214 +[1220]: https://codeberg.org/dwl/dwl/pulls/1220 +[1231]: https://codeberg.org/dwl/dwl/pulls/1231 +[1232]: https://codeberg.org/dwl/dwl/pulls/1232 +[1233]: https://codeberg.org/dwl/dwl/pulls/1233 +[1280]: https://codeberg.org/dwl/dwl/pulls/1280 +[1fe1b4e]: https://codeberg.org/dwl/dwl/commit/1fe1b4e +[4de32d2]: https://codeberg.org/dwl/dwl/commit/4de32d2 +[5fd19fe]: https://codeberg.org/dwl/dwl/commit/5fd19fe +[6013835]: https://codeberg.org/dwl/dwl/commit/6013835 +[8ad2c92]: https://codeberg.org/dwl/dwl/commit/8ad2c92 +[99cdc9a]: https://codeberg.org/dwl/dwl/commit/99cdc9a +[d1ebf3c]: https://codeberg.org/dwl/dwl/commit/d1ebf3c + + +### Contributors + + +* A Frederick Christensen +* Alex Denes +* Andrea Chiavazza +* Diego Viola +* Guido Cella +* Leonardo Hernández Hernández +* Peter Hofmann +* Siva Mahadevan +* julmajustus +* klim +* save196 +* sewn +* thanatos + + ## 0.8 ### Added From cc2325052710e1814a5cc12f7b73fb9e4645f708 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Sun, 20 Sep 2026 12:20:09 +0200 Subject: [PATCH 53/63] bump version to 0.10-dev --- config.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.mk b/config.mk index ab5a3fd..0da079c 100644 --- a/config.mk +++ b/config.mk @@ -1,4 +1,4 @@ -_VERSION = 0.9-dev +_VERSION = 0.10-dev VERSION = `git describe --tags --dirty 2>/dev/null || echo $(_VERSION)` PKG_CONFIG = pkg-config From 2a567195e61a1d3478686e6c30c8014f5d8d29b1 Mon Sep 17 00:00:00 2001 From: save196 Date: Tue, 15 Sep 2026 17:33:36 +0000 Subject: [PATCH 54/63] don't use the xdg window geometry offset as a position It is the offset of the visible bounds inside the surface, which is not zero for clients that pad their surface or place subsurfaces outside it, so floating windows were mapped offset by their client side decorations. --- client.h | 1 + 1 file changed, 1 insertion(+) diff --git a/client.h b/client.h index dc0615e..eb1bb47 100644 --- a/client.h +++ b/client.h @@ -189,6 +189,7 @@ client_get_geometry(Client *c, struct wlr_box *geom) } #endif *geom = c->surface.xdg->geometry; + geom->x = geom->y = 0; } static inline Client * From 40b0ae76ae65882360ac9d86a65ef0f1e57e4a19 Mon Sep 17 00:00:00 2001 From: save196 Date: Sun, 20 Sep 2026 21:37:16 +0300 Subject: [PATCH 55/63] bind axis to nothing by default --- config.def.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config.def.h b/config.def.h index 13c5322..a4019d3 100644 --- a/config.def.h +++ b/config.def.h @@ -172,6 +172,9 @@ static const Button buttons[] = { }; static const Axis axes[] = { + /* example of volume control: { MODKEY, AxisUp, spawn, SHCMD("volume-up_EXAMPLE") }, - { MODKEY, AxisDown, spawn, SHCMD("volume-down_EXAMPLE") }, + { MODKEY, AxisDown, spawn, SHCMD("volume-down_EXAMPLE") }, */ + { 0, 0, NULL, {0} }, + /* does nothing, but the array cannot be empty */ }; From fa400c5d3d1a118389da8d3bfbda7c04f81e5f9f Mon Sep 17 00:00:00 2001 From: save196 Date: Sun, 20 Sep 2026 22:04:38 +0000 Subject: [PATCH 56/63] don't run axis bindings when locked --- dwl.c | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/dwl.c b/dwl.c index 4cfd0fd..12f8c8f 100644 --- a/dwl.c +++ b/dwl.c @@ -663,16 +663,18 @@ axisnotify(struct wl_listener *listener, void *data) wlr_idle_notifier_v1_notify_activity(idle_notifier, seat); - if (event->orientation == WL_POINTER_AXIS_VERTICAL_SCROLL) - adir = event->delta > 0 ? AxisDown : AxisUp; - else - adir = event->delta > 0 ? AxisRight : AxisLeft; - keyboard = wlr_seat_get_keyboard(seat); - mods = keyboard ? wlr_keyboard_get_modifiers(keyboard) : 0; - for (a = axes; a < END(axes); a++) { - if (CLEANMASK(mods) == CLEANMASK(a->mod) && adir == a->dir && a->func) { - a->func(&a->arg); - return; + if (!locked) { + if (event->orientation == WL_POINTER_AXIS_VERTICAL_SCROLL) + adir = event->delta > 0 ? AxisDown : AxisUp; + else + adir = event->delta > 0 ? AxisRight : AxisLeft; + keyboard = wlr_seat_get_keyboard(seat); + mods = keyboard ? wlr_keyboard_get_modifiers(keyboard) : 0; + for (a = axes; a < END(axes); a++) { + if (CLEANMASK(mods) == CLEANMASK(a->mod) && adir == a->dir && a->func) { + a->func(&a->arg); + return; + } } } From b83881f881a92f3bed579d9951b6ad50091b0977 Mon Sep 17 00:00:00 2001 From: save196 Date: Tue, 15 Sep 2026 17:35:19 +0000 Subject: [PATCH 57/63] don't send pointer events to clients while locked --- dwl.c | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/dwl.c b/dwl.c index 12f8c8f..15b0bb8 100644 --- a/dwl.c +++ b/dwl.c @@ -211,6 +211,7 @@ struct Monitor { struct wl_listener frame; struct wl_listener destroy; struct wl_listener request_state; + struct wl_listener commit_lock_surface; struct wl_listener destroy_lock_surface; struct wlr_session_lock_surface_v1 *lock_surface; struct wlr_box m; /* monitor area, layout-relative */ @@ -276,6 +277,7 @@ static void cleanupmon(struct wl_listener *listener, void *data); static void cleanuplisteners(void); static void closemon(Monitor *m); static void commitlayersurfacenotify(struct wl_listener *listener, void *data); +static void commitlocksurface(struct wl_listener *listener, void *data); static void commitnotify(struct wl_listener *listener, void *data); static void commitpopup(struct wl_listener *listener, void *data); static void createdecoration(struct wl_listener *listener, void *data); @@ -953,6 +955,20 @@ commitlayersurfacenotify(struct wl_listener *listener, void *data) arrangelayers(l->mon); } +void +commitlocksurface(struct wl_listener *listener, void *data) +{ + Monitor *m = wl_container_of(listener, m, commit_lock_surface); + + if (!m->lock_surface->surface->mapped) + return; + + wl_list_remove(&m->commit_lock_surface.link); + wl_list_init(&m->commit_lock_surface.link); + + motionnotify(0, NULL, 0, 0, 0, 0); +} + void commitnotify(struct wl_listener *listener, void *data) { @@ -1130,6 +1146,7 @@ createlocksurface(struct wl_listener *listener, void *data) wlr_session_lock_surface_v1_configure(lock_surface, m->m.width, m->m.height); LISTEN(&lock_surface->events.destroy, &m->destroy_lock_surface, destroylocksurface); + LISTEN(&lock_surface->surface->events.commit, &m->commit_lock_surface, commitlocksurface); if (m == selmon) client_notify_enter(lock_surface->surface, wlr_seat_get_keyboard(seat)); @@ -1411,6 +1428,7 @@ destroylocksurface(struct wl_listener *listener, void *data) m->lock_surface = NULL; wl_list_remove(&m->destroy_lock_surface.link); + wl_list_remove(&m->commit_lock_surface.link); if (lock_surface->surface != seat->keyboard_state.focused_surface) return; @@ -1854,6 +1872,12 @@ locksession(struct wl_listener *listener, void *data) cur_lock = lock->lock = session_lock; locked = 1; + /* Cancel any interactive move/resize and take the pointer focus away + * from the clients below the lock screen */ + cursor_mode = CurNormal; + grabc = NULL; + motionnotify(0, NULL, 0, 0, 0, 0); + LISTEN(&session_lock->events.new_surface, &lock->new_surface, createlocksurface); LISTEN(&session_lock->events.destroy, &lock->destroy, destroysessionlock); LISTEN(&session_lock->events.unlock, &lock->unlock, unlocksession); @@ -3223,7 +3247,7 @@ xytonode(double x, double y, struct wlr_surface **psurface, struct wlr_surface *surface = NULL; int layer; - for (layer = NUM_LAYERS - 1; !surface && layer >= 0; layer--) { + for (layer = NUM_LAYERS - 1; !surface && layer >= (locked ? LyrBlock : 0); layer--) { if (layer == LyrIMPopup || !(node = wlr_scene_node_at(&layers[layer]->node, x, y, nx, ny))) continue; From f22c223cbfc7fd4e87354a6c2fad8088cb08558a Mon Sep 17 00:00:00 2001 From: save196 Date: Mon, 21 Sep 2026 17:01:54 +0300 Subject: [PATCH 58/63] don't run keybindings on keyrepeat while locked --- dwl.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dwl.c b/dwl.c index 15b0bb8..d009ef4 100644 --- a/dwl.c +++ b/dwl.c @@ -1830,7 +1830,7 @@ int keyrepeat(void *data) { KeyboardGroup *group = data; - if (group->wlr_group->keyboard.repeat_info.rate <= 0) + if (locked || group->wlr_group->keyboard.repeat_info.rate <= 0) return 0; wl_event_source_timer_update(group->key_repeat_source, From 242ffd7a7521a74fc9edf68bcc7e6a3cd75ec860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Desgualdo=20Pereira?= Date: Mon, 21 Sep 2026 14:35:09 -0300 Subject: [PATCH 59/63] client: do not drop configure events based on current committed size --- client.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client.h b/client.h index eb1bb47..a6fe207 100644 --- a/client.h +++ b/client.h @@ -376,9 +376,7 @@ client_set_size(Client *c, uint32_t width, uint32_t height) return; } #endif - if ((int32_t)width != c->surface.xdg->toplevel->current.width - || (int32_t)height != c->surface.xdg->toplevel->current.height) - wlr_xdg_toplevel_set_size(c->surface.xdg->toplevel, (int32_t)width, (int32_t)height); + wlr_xdg_toplevel_set_size(c->surface.xdg->toplevel, (int32_t)width, (int32_t)height); } static inline void From a514a928e4e80b7e2c6c1aced790abf7f7061975 Mon Sep 17 00:00:00 2001 From: aloires Date: Tue, 22 Sep 2026 12:58:36 +0800 Subject: [PATCH 60/63] focusclient: set input method focus for layer surfaces Focusing a layer surface only set the keyboard focus, not the text-input focus, so layer-shell clients (launchers, menus, ...) never received a text-input enter event and IME input didn't work. Pass the layer surface to focusclient() and set the input method focus there, like we already do for regular clients. --- dwl.c | 60 +++++++++++++++++++++++++++++++---------------------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/dwl.c b/dwl.c index d009ef4..7af9465 100644 --- a/dwl.c +++ b/dwl.c @@ -305,7 +305,7 @@ static void destroypointerconstraint(struct wl_listener *listener, void *data); static void destroysessionlock(struct wl_listener *listener, void *data); static void destroykeyboardgroup(struct wl_listener *listener, void *data); static Monitor *dirtomon(enum wlr_direction dir); -static void focusclient(Client *c, int lift); +static void focusclient(Client *c, LayerSurface *l, int lift); static void focusmon(const Arg *arg); static void focusstack(const Arg *arg); static Client *focustop(Monitor *m); @@ -644,9 +644,8 @@ arrangelayers(Monitor *m) !l->mapped) continue; /* Deactivate the focused client. */ - focusclient(NULL, 0); + focusclient(NULL, l, 0); exclusive_focus = l; - client_notify_enter(l->layer_surface->surface, wlr_seat_get_keyboard(seat)); return; } } @@ -709,10 +708,9 @@ buttonpress(struct wl_listener *listener, void *data) or a layer surface with on-demand keyboard interactivity */ xytonode(cursor->x, cursor->y, NULL, &c, &l, NULL, NULL); if (c && (!client_is_unmanaged(c) || client_wants_focus(c))) { - focusclient(c, 1); + focusclient(c, NULL, 1); } else if (l && l->layer_surface->current.keyboard_interactive) { - focusclient(NULL, 0); - client_notify_enter(l->layer_surface->surface, wlr_seat_get_keyboard(seat)); + focusclient(NULL, l, 0); } keyboard = wlr_seat_get_keyboard(seat); @@ -911,7 +909,7 @@ closemon(Monitor *m) if (c->mon == m) setmon(c, selmon, c->tags); } - focusclient(focustop(selmon), 1); + focusclient(focustop(selmon), NULL, 1); printstatus(); } @@ -1368,7 +1366,7 @@ void destroydragicon(struct wl_listener *listener, void *data) { /* Focus enter isn't sent during drag, so refocus the focused node. */ - focusclient(focustop(selmon), 1); + focusclient(focustop(selmon), NULL, 1); motionnotify(0, NULL, 0, 0, 0, 0); wl_list_remove(&listener->link); free(listener); @@ -1407,7 +1405,7 @@ destroylock(SessionLock *lock, int unlock) wlr_scene_node_set_enabled(&locked_bg->node, 0); - focusclient(focustop(selmon), 0); + focusclient(focustop(selmon), NULL, 0); motionnotify(0, NULL, 0, 0, 0, 0); destroy: @@ -1437,7 +1435,7 @@ destroylocksurface(struct wl_listener *listener, void *data) surface = wl_container_of(cur_lock->surfaces.next, surface, link); client_notify_enter(surface->surface, wlr_seat_get_keyboard(seat)); } else if (!locked) { - focusclient(focustop(selmon), 1); + focusclient(focustop(selmon), NULL, 1); } else { wlr_seat_keyboard_clear_focus(seat); } @@ -1525,7 +1523,7 @@ dirtomon(enum wlr_direction dir) } void -focusclient(Client *c, int lift) +focusclient(Client *c, LayerSurface *l, int lift) { struct wlr_surface *old = seat->keyboard_state.focused_surface; int unused_lx, unused_ly, old_client_type; @@ -1567,7 +1565,7 @@ focusclient(Client *c, int lift) /* If an overlay is focused, don't focus or activate the client, * but only update its position in fstack to render its border with focuscolor * and focus it after the overlay is closed. */ - if (old && (!c || client_surface(c) != old) + if (!l && old && (!c || client_surface(c) != old) && old_client_type == LayerShell && wlr_scene_node_coords( &old_l->scene->node, &unused_lx, &unused_ly) && old_l->layer_surface->current.layer >= ZWLR_LAYER_SHELL_V1_LAYER_TOP @@ -1584,6 +1582,13 @@ focusclient(Client *c, int lift) } printstatus(); + if (l) { + /* Focus a layer surface and let the input method follow it */ + input_method_relay_set_focus(input_method_relay, l->layer_surface->surface); + client_notify_enter(l->layer_surface->surface, wlr_seat_get_keyboard(seat)); + return; + } + if (!c) { /* With no client, all we have left is to clear focus */ input_method_relay_set_focus(input_method_relay, NULL); @@ -1615,7 +1620,7 @@ focusmon(const Arg *arg) selmon = dirtomon(arg->i); while (!selmon->wlr_output->enabled && i++ < nmons); } - focusclient(focustop(selmon), 1); + focusclient(focustop(selmon), NULL, 1); } void @@ -1641,7 +1646,7 @@ focusstack(const Arg *arg) } } /* If only one client is visible on selmon, then c == sel */ - focusclient(c, 1); + focusclient(c, NULL, 1); } /* We probably should change the name of this: it sounds like it @@ -1866,7 +1871,7 @@ locksession(struct wl_listener *listener, void *data) return; } lock = session_lock->data = ecalloc(1, sizeof(*lock)); - focusclient(NULL, 0); + focusclient(NULL, NULL, 0); lock->scene = wlr_scene_tree_create(layers[LyrBlock]); cur_lock = lock->lock = session_lock; @@ -1916,7 +1921,7 @@ mapnotify(struct wl_listener *listener, void *data) wlr_scene_node_reparent(&c->scene->node, layers[LyrUnmanaged]); wlr_scene_node_set_position(&c->scene->node, c->geom.x, c->geom.y); if (client_wants_focus(c)) { - focusclient(c, 1); + focusclient(c, NULL, 1); exclusive_focus = c; } goto unset_fullscreen; @@ -2254,10 +2259,9 @@ pointerfocus(Client *c, LayerSurface *l, struct wlr_surface *surface, double sx, if (surface != seat->pointer_state.focused_surface && sloppyfocus && time) { if (c && (!client_is_unmanaged(c) || client_wants_focus(c))) { - focusclient(c, 0); + focusclient(c, NULL, 0); } else if (l && l->layer_surface->current.keyboard_interactive) { - focusclient(NULL, 0); - client_notify_enter(l->layer_surface->surface, wlr_seat_get_keyboard(seat)); + focusclient(NULL, l, 0); } } @@ -2600,7 +2604,7 @@ setmon(Client *c, Monitor *m, uint32_t newtags) setfullscreen(c, c->isfullscreen); /* This will call arrange(c->mon) */ setfloating(c, c->isfloating); } - focusclient(focustop(selmon), 1); + focusclient(focustop(selmon), NULL, 1); } void @@ -2892,7 +2896,7 @@ tag(const Arg *arg) return; sel->tags = arg->ui & TAGMASK; - focusclient(focustop(selmon), 1); + focusclient(focustop(selmon), NULL, 1); arrange(selmon); printstatus(); } @@ -2965,7 +2969,7 @@ toggletag(const Arg *arg) return; sel->tags = newtags; - focusclient(focustop(selmon), 1); + focusclient(focustop(selmon), NULL, 1); arrange(selmon); printstatus(); } @@ -2978,7 +2982,7 @@ toggleview(const Arg *arg) return; selmon->tagset[selmon->seltags] = newtagset; - focusclient(focustop(selmon), 1); + focusclient(focustop(selmon), NULL, 1); arrange(selmon); printstatus(); } @@ -3002,7 +3006,7 @@ unmaplayersurfacenotify(struct wl_listener *listener, void *data) if (l->layer_surface->output && (l->mon = l->layer_surface->output->data)) arrangelayers(l->mon); if (l->layer_surface->surface == seat->keyboard_state.focused_surface) - focusclient(focustop(selmon), 1); + focusclient(focustop(selmon), NULL, 1); motionnotify(0, NULL, 0, 0, 0, 0); } @@ -3021,7 +3025,7 @@ unmapnotify(struct wl_listener *listener, void *data) if (client_is_unmanaged(c)) { if (c == exclusive_focus) { exclusive_focus = NULL; - focusclient(focustop(selmon), 1); + focusclient(focustop(selmon), NULL, 1); } } else { wl_list_remove(&c->link); @@ -3140,7 +3144,7 @@ updatemons(struct wl_listener *listener, void *data) if (!c->mon && client_surface(c)->mapped) setmon(c, selmon, c->tags); } - focusclient(focustop(selmon), 1); + focusclient(focustop(selmon), NULL, 1); if (selmon->lock_surface) { client_notify_enter(selmon->lock_surface->surface, wlr_seat_get_keyboard(seat)); @@ -3199,7 +3203,7 @@ view(const Arg *arg) selmon->seltags ^= 1; /* toggle sel tagset */ if (arg->ui & TAGMASK) selmon->tagset[selmon->seltags] = arg->ui & TAGMASK; - focusclient(focustop(selmon), 1); + focusclient(focustop(selmon), NULL, 1); arrange(selmon); printstatus(); } @@ -3290,7 +3294,7 @@ zoom(const Arg *arg) wl_list_remove(&sel->link); wl_list_insert(&clients, &sel->link); - focusclient(sel, 1); + focusclient(sel, NULL, 1); arrange(selmon); } From 87029e21c0fcc80121d79d77ff2682f15c98ee5f Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Tue, 22 Sep 2026 14:53:00 +0200 Subject: [PATCH 61/63] remove exclusive_focus variable This is not needed because focusclient() already calls toplevel_from_wlr_surface(seat->keyboard_state.focused_surface) and can check if that should have exclusive focus. When it was added there was no function to get the focused layer shell surface in this way. --- dwl.c | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/dwl.c b/dwl.c index 7af9465..ca97c30 100644 --- a/dwl.c +++ b/dwl.c @@ -378,7 +378,6 @@ static void zoom(const Arg *arg); /* variables */ static pid_t child_pid = -1; static int locked; -static void *exclusive_focus; static Client *focused_client; static struct wl_display *dpy; static struct wl_event_loop *event_loop; @@ -645,7 +644,6 @@ arrangelayers(Monitor *m) continue; /* Deactivate the focused client. */ focusclient(NULL, l, 0); - exclusive_focus = l; return; } } @@ -933,11 +931,6 @@ commitlayersurfacenotify(struct wl_listener *listener, void *data) return; } - if (layer_surface == exclusive_focus - && layer_surface->current.keyboard_interactive != - ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE) - exclusive_focus = NULL; - if (layer_surface->current.committed == 0 && l->mapped == layer_surface->surface->mapped) return; l->mapped = layer_surface->surface->mapped; @@ -1546,9 +1539,6 @@ focusclient(Client *c, LayerSurface *l, int lift) wlr_xdg_popup_destroy(popup); } - if (old_c && old_c == exclusive_focus && client_wants_focus(old_c)) - exclusive_focus = NULL; - /* Put the new client atop the focus stack and select its monitor */ if (c && !client_is_unmanaged(c)) { wl_list_remove(&c->flink); @@ -1558,7 +1548,9 @@ focusclient(Client *c, LayerSurface *l, int lift) /* Don't change border color if there is an exclusive focus or we are * handling a drag operation */ - if (!exclusive_focus && !seat->drag) + if (!(old_l && old_l->layer_surface->current.keyboard_interactive == ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE) + && !(old_c && client_wants_focus(old_c)) + && !seat->drag) client_set_border_color(c, focuscolor); } @@ -1920,10 +1912,8 @@ mapnotify(struct wl_listener *listener, void *data) /* Unmanaged clients always are floating */ wlr_scene_node_reparent(&c->scene->node, layers[LyrUnmanaged]); wlr_scene_node_set_position(&c->scene->node, c->geom.x, c->geom.y); - if (client_wants_focus(c)) { + if (client_wants_focus(c)) focusclient(c, NULL, 1); - exclusive_focus = c; - } goto unset_fullscreen; } @@ -3001,8 +2991,6 @@ unmaplayersurfacenotify(struct wl_listener *listener, void *data) l->mapped = 0; wlr_scene_node_set_enabled(&l->scene->node, 0); - if (l == exclusive_focus) - exclusive_focus = NULL; if (l->layer_surface->output && (l->mon = l->layer_surface->output->data)) arrangelayers(l->mon); if (l->layer_surface->surface == seat->keyboard_state.focused_surface) @@ -3022,15 +3010,12 @@ unmapnotify(struct wl_listener *listener, void *data) if (c == focused_client) focused_client = NULL; - if (client_is_unmanaged(c)) { - if (c == exclusive_focus) { - exclusive_focus = NULL; - focusclient(focustop(selmon), NULL, 1); - } - } else { + if (!client_is_unmanaged(c)) { wl_list_remove(&c->link); setmon(c, NULL, 0); wl_list_remove(&c->flink); + } else if (client_wants_focus(c)) { + focusclient(focustop(selmon), NULL, 1); } if (c->foreign_toplevel_handle) { From a75ca796305e7a12bfbe89a57d31d92b7f35d601 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Tue, 22 Sep 2026 20:42:48 +0200 Subject: [PATCH 62/63] add missing void Needed to compile on OpenBSD. --- ime.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ime.h b/ime.h index b9591f5..454a044 100644 --- a/ime.h +++ b/ime.h @@ -594,7 +594,7 @@ handle_focused_surface_destroy(struct wl_listener *listener, } struct input_method_relay * -input_method_relay_create() +input_method_relay_create(void) { struct input_method_relay *relay = ecalloc(1, sizeof(*relay)); wl_list_init(&relay->text_inputs); From f1675a480067b956a551785148af545f80cb6306 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Wed, 23 Sep 2026 09:03:08 +0200 Subject: [PATCH 63/63] Revert "client: do not drop configure events based on current committed size" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 242ffd7a7521a74fc9edf68bcc7e6a3cd75ec860. Removing this conditon can cause a xdg_toplevel.configure ↔ wl_surface.commit loop that uses 100% CPU. Fixes https://codeberg.org/dwl/dwl/issues/1245 Fixes https://codeberg.org/dwl/dwl/issues/1246 --- client.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client.h b/client.h index a6fe207..eb1bb47 100644 --- a/client.h +++ b/client.h @@ -376,7 +376,9 @@ client_set_size(Client *c, uint32_t width, uint32_t height) return; } #endif - wlr_xdg_toplevel_set_size(c->surface.xdg->toplevel, (int32_t)width, (int32_t)height); + if ((int32_t)width != c->surface.xdg->toplevel->current.width + || (int32_t)height != c->surface.xdg->toplevel->current.height) + wlr_xdg_toplevel_set_size(c->surface.xdg->toplevel, (int32_t)width, (int32_t)height); } static inline void