Repository: espanso/espanso (language: rust)

Issue:
A white blob appears on starting Espanso
**Describe the bug**
A white rectangular unresponsive box appears on the top left of the screen whenever espanso is running. Additionally, an entry titled __unknown__ appears in the dock.

**To Reproduce**
Steps to reproduce the behavior:
1. Open a Terminal and run `espanso start`.

**Expected behavior**
There should be no such boxes.

**Screenshots**
![image](https://github.com/espanso/espanso/assets/113207560/cf9831ff-202a-4d84-bb61-97ec72dfe61f)


**Logs**
```
21:07:39 [daemon(206289)] [INFO] reading configs from: "/home/arch/.config/espanso"
21:07:39 [daemon(206289)] [INFO] reading packages from: "/home/arch/.config/espanso/match/packages"
21:07:39 [daemon(206289)] [INFO] using runtime dir: "/home/arch/.cache/espanso"
21:07:39 [daemon(206289)] [INFO] system info: Fedora Linux v39 - kernel: 6.7.5-200.fc39.x86_64
21:07:39 [daemon(206289)] [INFO] watching for changes in path: "/home/arch/.config/espanso"
21:07:39 [daemon(206289)] [INFO] espanso version: 2.2.1
21:07:39 [daemon(206289)] [INFO] spawning the worker process...
21:07:39 [daemon(206289)] [INFO] binded to IPC unix socket: /home/arch/.cache/espanso/espansodaemonv2.sock
21:07:39 [daemon(206289)] [WARN] keyboard layout watcher couldn't determine active layout.
21:07:39 [worker(206299)] [INFO] reading configs from: "/home/arch/.config/espanso"
21:07:39 [worker(206299)] [INFO] reading packages from: "/home/arch/.config/espanso/match/packages"
21:07:39 [worker(206299)] [INFO] using runtime dir: "/home/arch/.cache/espanso"
21:07:39 [worker(206299)] [INFO] system info: Fedora Linux v39 - kernel: 6.7.5-200.fc39.x86_64
21:07:39 [worker(206299)] [INFO] binded to IPC unix socket: /home/arch/.cache/espanso/espansoworkerv2.sock
21:07:39 [worker(206299)] [INFO] using WaylandAppInfoProvider
21:07:39 [worker(206299)] [INFO] detection module will use this keyboard layout: [R=, M=, L=in, V=eng, O=]
21:07:39 [worker(206299)] [INFO] monitoring the status of the daemon process
21:07:39 [worker(206299)] [INFO] using EVDEVSource
21:07:39 [worker(206299)] [INFO] Querying modifier status...
21:07:40 [worker(206299)] [INFO] inject module will use this keyboard layout: [R=, M=, L=in, V=eng, O=]
21:07:40 [worker(206299)] [INFO] using EVDEVInjector
21:07:40 [worker(206299)] [INFO] using WaylandFallbackClipboard
```

**Setup information**
- OS: Fedora Workstation 39
- Version: 2.2.1

[espanso-detect] Espanso Sync Tool window not rendering correctly on wayland
**Describe the bug**
The window created by espanso-detect fails to render correctly on Wayland. The window is created but the Espanso logo and text is not shown. Instead, the window is filled with a rainbow gradient. 

**To Reproduce**
Steps to reproduce the behavior:
1. Run espanso worker or espanso service start

**Expected behavior**
The espanso sync tool window should display the logo and text correctly. 

**Screenshots**

![image](https://github.com/espanso/espanso/assets/13283822/12c1dcbd-2753-4520-8d7c-ed45c747ae81)

**Logs**
Relevant logs are from Wayland compositor. 

```
[3375819.298] wl_callback@3.done(89687)
[3375819.312]  -> wl_registry@2.bind(9, "zwp_primary_selection_device_manager_v1", 1, new id [unknown]@3)
[3375819.340]  -> zwp_primary_selection_device_manager_v1@3.get_device(new id zwp_primary_selection_device_v1@14, wl_seat@8)
[3375819.380]  -> wl_compositor@5.create_surface(new id wl_surface@15)
[3375819.395]  -> wl_registry@2.bind(13, "xdg_wm_base", 2, new id [unknown]@16)
[3375819.437]  -> wl_shm@4.create_pool(new id wl_shm_pool@17, fd 20, 4096)
[3375819.477]  -> xdg_wm_base@16.get_xdg_surface(new id xdg_surface@18, wl_surface@15)
[3375819.490]  -> xdg_surface@18.get_toplevel(new id xdg_toplevel@19)
[3375819.499]  -> wl_surface@15.commit()
[3375819.511]  -> xdg_toplevel@19.set_min_size(2, 1)
[3375819.519]  -> xdg_surface@18.set_window_geometry(0, 0, 1, 1)
[3375819.530]  -> wl_compositor@5.create_surface(new id wl_surface@20)
[3375819.539]  -> wl_seat@8.get_pointer(new id wl_pointer@21)
[3375819.554]  -> zxdg_decoration_manager_v1@9.get_toplevel_decoration(new id zxdg_toplevel_decoration_v1@22, xdg_toplevel@19)
[3375819.568]  -> xdg_toplevel@19.set_title("Espanso Sync Tool")
[3375819.587]  -> wl_shm@4.create_pool(new id wl_shm_pool@23, fd 22, 4096)
[3375819.605]  -> wl_seat@8.get_keyboard(new id wl_keyboard@24)

```

**Setup information**
- OS: Arch Linux 6.6.4 / Hyprland v.0.33.1
- Version:  2.2.0 built from source with latest fix for sctk from dev branch.  



Discussion / hints:
![image](https://github.com/espanso/espanso/assets/113207560/14e22fc0-9b5e-4bc5-87e9-687a21efcb0a)

Here's the entry in the dock
This is a well-documented Wayland issue.
#1826, #1722, #1619
Update: the wayland surface created is also unresponsive and cannot be closed via normal means (espanso process must be killed). 

More detailed wayland logs below. It looks like the destroy method is called on both the xdg_toplevel object as well as the xdg_surface associated with the sync tool window.  However, wl_surface@15a seems to remain mapped. 

Take this analysis with a grain of salt, as I am far from an expert on wayland.

```
 0.0014 A: → wl_compositor@5a.create_surface(id=new wl_surface@15a)
 0.0014 A: → wl_registry@2a.bind(13, 'xdg_wm_base', 2, new xdg_wm_base@16a)
 0.0015 A: → wl_shm@4a.create_pool(id=new wl_shm_pool@17a, fd=fd 20, size=4096)
 0.0015 A: → xdg_wm_base@16a.get_xdg_surface(id=new xdg_surface@18a, surface=wl_surface@15a)
 0.0015 A: → xdg_surface@18a.get_toplevel(id=new xdg_toplevel@19a)
 0.0015 A: → wl_surface@15a.commit()
 0.0015 A: → xdg_toplevel@19a.set_min_size(width=2, height=1)
 0.0015 A: → xdg_surface@18a.set_window_geometry(x=0, y=0, width=1, height=1)
 0.0015 A: → wl_compositor@5a.create_surface(id=new wl_surface@20a)
 0.0015 A: → wl_seat@8a.get_pointer(id=new wl_pointer@21a)
 0.0015 A: → zxdg_decoration_manager_v1@9a.get_toplevel_decoration(id=new zxdg_toplevel_decoration_v1@22a, toplevel=xdg_toplevel@19a)
 0.0016 A: → xdg_toplevel@19a.set_title(title='Espanso Sync Tool')
 0.0016 A: → wl_shm@4a.create_pool(id=new wl_shm_pool@23a, fd=fd 22, size=4096)
 0.0016 A: → wl_seat@8a.get_keyboard(id=new wl_keyboard@24a)
 0.0083 A: wl_keyboard@24a.keymap(format=1:xkb_v1, fd=fd 20, size=70369) ↲
 0.0108 A: wl_keyboard@24a.repeat_info(rate=25, delay=600) ↲
 0.0108 A: xdg_toplevel@19a.configure(width=0, height=0, states=Unknown: 'array[0]') ↲
 0.0108 A: zxdg_toplevel_decoration_v1@22a.configure(mode=2:server_side) ↲
 0.0108 A: xdg_surface@18a.configure(serial=31322) ↲
 0.0109 A: → xdg_surface@18a.ack_configure(serial=31322)
 0.0109 A: → wl_shm_pool@23a.create_buffer(id=new wl_buffer@25a, offset=0, width=1, height=1, stride=4, format=0:argb8888)
 0.0109 A: → wl_surface@15a.attach(buffer=wl_buffer@25a, x=0, y=0)
 0.0109 A: → wl_surface@15a.damage_buffer(x=0, y=0, width=1, height=1)
 0.0109 A: → wl_surface@15a.commit()
 0.0121 A: wl_surface@15a.enter(output=wl_output@11a) ↲
 0.0121 A: wl_buffer@25a.release() ↲
 0.0121 A: → wl_buffer@25a.destroy()
 0.0121 A: xdg_toplevel@19a.configure(width=835, height=321, states=Unknown: 'array[24]') ↲
 0.0121 A: xdg_surface@18a.configure(serial=31323) ↲
 0.0121 A: → xdg_surface@18a.ack_configure(serial=31323)
 0.0121 A: → xdg_surface@18a.set_window_geometry(x=0, y=0, width=835, height=321)
 0.0121 A: → wl_shm_pool@23a.resize(size=1072144)
 0.0122 A: → wl_shm_pool@23a.create_buffer(id=new wl_buffer@26a, offset=0, width=835, height=321, stride=3340, format=0:argb8888)
 0.0154 A: → wl_surface@15a.attach(buffer=wl_buffer@26a, x=0, y=0)
 0.0154 A: → wl_surface@15a.damage_buffer(x=0, y=0, width=835, height=321)
 0.0154 A: → wl_surface@15a.commit()
 0.0164 A: wl_display@1a.delete_id(id=25) -- wl_buffer@25a.destroyed after 0.0055s ↲
 0.0164 A: wl_buffer@26a.release() ↲
 0.0164 A: → wl_buffer@26a.destroy()
 0.0164 A: → wl_shm_pool@23a.destroy()
 0.0164 A: → zxdg_toplevel_decoration_v1@22a.destroy()
 0.0164 A: → xdg_toplevel@19a.destroy()
 0.0164 A: → xdg_surface@18a.destroy()
 0.0164 A: → wl_pointer@21a.release()
 0.0164 A: → wl_surface@20a.destroy()
 0.0165 A: → wl_shm_pool@17a.destroy()
 ```
No help,  but I am also seeing this,  I have been moving the window to desktop 10, as a work around.
I am experiencing the same issue.
Same, and here is a bit more information:

- this happens on machines with Intel GPU as well as NVIDIA
  (I initially thought this was an NVIDIA issue since NVIDIA's Wayland
  support is known to be... suboptimal 😅)

- it looks like the regression happened between 2.2.0 and 2.2.1 
  (I have the rainbow window on 2.2.1, but not on 2.2.0)
>     * this happens on machines with Intel GPU as well as NVIDIA
>       (I initially thought this was an NVIDIA issue since NVIDIA's Wayland
>       support is known to be... suboptimal 😅)

I'm also seeing it with amdgpu.

same for me with amd as well
> * it looks like the regression happened between 2.2.0 and 2.2.1
>       (I have the rainbow window on 2.2.1, but not on 2.2.0)

Downgrading did _not_ change anything, same issue still. Looking at the diff between those versions that is as expected, as only Windows related code changed.
The regression was introduced in 2.20 due to upgrade of smithay-client-toolkit (sctk) dependency from v0.15.4 to v0.16.1 in espanso-detect on wayland. Prior to this upgrade, espanso was crashing on launch under wayland due to a mismatch between protocol versions offered by the server and those used in the client library.

I have done some digging on this and as far as I can tell there is no protocol error occuring (this would cause the server to kill the connection with client). When the xdg_toplevel is destroyed, the associated wl_surface should also be destroyed but this does not appear to be occuring.  

Bumping the sctk dependency again _may_ fix this behaviour, but there are breaking changes to the API in both 17.0 and the latest version.
Are there any workaround for this?
> Are there any workaround for this?

The PR #1849 contains a fix which seem to resolve the issue. No binary/direct download available. But there is a great description how to build this PR. Otherwise, downgrading might be a workaround as well. @KAGEYAM4 
On Archlinux, KDE Plasma6/wayland.

'Espanso Sync tool' stays on Task manager. It can't be quit.
![espanso-sync-tool](https://github.com/espanso/espanso/assets/5688288/f5868837-59cd-4c2a-a1b2-3f2b1aac2d3e)

And when the system is going to shut down, from time to time, it fails to quit espanso sync tool.

> On Archlinux, KDE Plasma6/wayland.
> 
> 'Espanso Sync tool' stays on Task manager. It can't be quit. ![espanso-sync-tool](https://private-user-images.githubusercontent.com/5688288/311995285-f5868837-59cd-4c2a-a1b2-3f2b1aac2d3e.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTAyMzI3OTAsIm5iZiI6MTcxMDIzMjQ5MCwicGF0aCI6Ii81Njg4Mjg4LzMxMTk5NTI4NS1mNTg2ODgzNy01OWNkLTRjMmEtYTFiMi0zZjJiMWFhYzJkM2UucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MDMxMiUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDAzMTJUMDgzNDUwWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9Y2Y3ZGU2MmRhOTA5OTFlZWI5YmYxMWZiMTFhMjIyNzViYzUxNTQ2NmIwM2IwZjNjY2Y4ZGE5YTUxOTQ5NGJlMyZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmYWN0b3JfaWQ9MCZrZXlfaWQ9MCZyZXBvX2lkPTAifQ.9EyZUXCRtIKzqVVCi801kJbu2iUZV7ZWju6BkTcTHEQ)
> 
> And when the system is going to shut down, from time to time, it fails to quit espanso sync tool.

I've experienced the "fails to quit espanso syc tool" on shutdown or reboot after upgrading to Plasma 6. Didn't happen with Plasma 5

Fix patch:
diff --git a/espanso-detect/Cargo.toml b/espanso-detect/Cargo.toml
index a933282f1..f57fa4625 100644
--- a/espanso-detect/Cargo.toml
+++ b/espanso-detect/Cargo.toml
@@ -30,7 +30,7 @@ widestring = "0.4.3"
 [target.'cfg(target_os="linux")'.dependencies]
 libc = "0.2.85"
 scopeguard = "1.1.0"
-sctk = { package = "smithay-client-toolkit", version = "0.16.1", optional = true }
+sctk = { package = "smithay-client-toolkit", version = "0.18.1", optional = true }
 
 [build-dependencies]
 cc = "1.0.73"
diff --git a/espanso-detect/src/evdev/sync/wayland.rs b/espanso-detect/src/evdev/sync/wayland.rs
index f48af4e84..aed1d4d21 100644
--- a/espanso-detect/src/evdev/sync/wayland.rs
+++ b/espanso-detect/src/evdev/sync/wayland.rs
@@ -1,203 +1,367 @@
-// This module was implemented starting from this wonderful example:
-// https://github.com/Smithay/client-toolkit/blob/master/examples/kbd_input.rs
+// Majority of code below reproduced from https://github.com/Smithay/client-toolkit/blob/master/examples/simple_window.rs
 
-use std::cell::RefCell;
-use std::cmp::min;
-use std::rc::Rc;
+use super::ModifiersState;
+use anyhow::Result;
+use log::debug;
+use std::{convert::TryInto, time::Duration};
 
-use anyhow::{Context, Result};
-use log::error;
-use sctk::reexports::calloop;
-use sctk::reexports::client::protocol::{wl_keyboard, wl_shm, wl_surface};
-use sctk::seat::keyboard::{map_keyboard_repeat, Event as KbEvent, RepeatKind};
-use sctk::shm::AutoMemPool;
-use sctk::window::{Event as WEvent, FallbackFrame};
-
-sctk::default_environment!(EspansoModifiersSync, desktop);
+use sctk::activation::RequestData;
+use sctk::reexports::calloop::EventLoop;
+use sctk::reexports::calloop_wayland_source::WaylandSource;
+use sctk::reexports::client::{
+  globals::registry_queue_init,
+  protocol::{wl_keyboard, wl_output, wl_seat, wl_shm, wl_surface},
+  Connection, QueueHandle,
+};
+use sctk::{
+  activation::{ActivationHandler, ActivationState},
+  compositor::{CompositorHandler, CompositorState},
+  delegate_activation, delegate_compositor, delegate_keyboard, delegate_output, delegate_registry,
+  delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window,
+  output::{OutputHandler, OutputState},
+  registry::{ProvidesRegistryState, RegistryState},
+  registry_handlers,
+  seat::{
+    keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers},
+    Capability, SeatHandler, SeatState,
+  },
+  shell::{
+    xdg::{
+      window::{Window, WindowConfigure, WindowDecorations, WindowHandler},
+      XdgShell,
+    },
+    WaylandSurface,
+  },
+  shm::{
+    slot::{Buffer, SlotPool},
+    Shm, ShmHandler,
+  },
+};
 
 pub fn get_modifiers_state() -> Result<Option<super::ModifiersState>> {
-  let (env, display, queue) = sctk::new_default_environment!(EspansoModifiersSync, desktop)
-    .context("Unable to connect to a Wayland compositor")?;
-
-  let result = Rc::new(RefCell::new(None));
-
-  /*
-   * Prepare a calloop event loop to handle key repetion
-   */
-  // Here `Option<WEvent>` is the type of a global value that will be shared by
-  // all callbacks invoked by the event loop.
-  let mut event_loop = calloop::EventLoop::<Option<WEvent>>::try_new().unwrap();
-
-  /*
-   * Create a buffer with window contents
-   */
-
-  let mut dimensions = (1u32, 1u32);
-
-  /*
-   * Init wayland objects
-   */
-
-  let surface = env.create_surface().detach();
-
-  let mut window = env
-    .create_window::<FallbackFrame, _>(surface, None, dimensions, move |evt, mut dispatch_data| {
-      let next_action = dispatch_data.get::<Option<WEvent>>().unwrap();
-      // Keep last event in priority order : Close > Configure > Refresh
-      let replace = matches!(
-        (&evt, &*next_action),
-        (_, &None | &Some(WEvent::Refresh))
-          | (&WEvent::Configure { .. }, &Some(WEvent::Configure { .. }))
-          | (&WEvent::Close, _)
-      );
-      if replace {
-        *next_action = Some(evt);
-      }
-    })
-    .context("Failed to create a window !")?;
-
-  window.set_title("Espanso Sync Tool".to_string());
-
-  let mut pool = env
-    .create_auto_pool()
-    .context("Failed to create a memory pool !")?;
-
-  /*
-   * Keyboard initialization
-   */
-
-  let mut seats = Vec::<(String, Option<wl_keyboard::WlKeyboard>)>::new();
-
-  // first process already existing seats
-  for seat in env.get_all_seats() {
-    if let Some((has_kbd, name)) = sctk::seat::with_seat_data(&seat, |seat_data| {
-      (
-        seat_data.has_keyboard && !seat_data.defunct,
-        seat_data.name.clone(),
-      )
-    }) {
-      if has_kbd {
-        let result_clone = result.clone();
-        match map_keyboard_repeat(
-          event_loop.handle(),
-          &seat,
-          None,
-          RepeatKind::System,
-          move |event, _, _| keyboard_event_handler(event, &result_clone),
-        ) {
-          Ok(kbd) => {
-            seats.push((name, Some(kbd)));
-          }
-          Err(e) => {
-            error!("Failed to map keyboard on seat {} : {:?}.", name, e);
-            seats.push((name, None));
-          }
-        }
-      } else {
-        seats.push((name, None));
-      }
-    }
-  }
+  let conn = Connection::connect_to_env().unwrap();
+
+  // Enumerate the list of globals to get the protocols the server implements.
+  let (globals, event_queue) = registry_queue_init(&conn).unwrap();
+
+  let qh = event_queue.handle();
+
+  let mut event_loop: EventLoop<SimpleWindow> =
+    EventLoop::try_new().expect("Failed to initialize the event loop!");
 
-  // then setup a listener for changes
   let loop_handle = event_loop.handle();
-  let result_clone = result.clone();
-  let _seat_listener = env.listen_for_seats(move |seat, seat_data, _| {
-    let result_clone = result_clone.clone();
-    // find the seat in the vec of seats, or insert it if it is unknown
-    let idx = seats.iter().position(|(name, _)| name == &seat_data.name);
-    let idx = idx.unwrap_or_else(|| {
-      seats.push((seat_data.name.clone(), None));
-      seats.len() - 1
-    });
 
-    let (_, ref mut opt_kbd) = &mut seats[idx];
-    // we should map a keyboard if the seat has the capability & is not defunct
-    if seat_data.has_keyboard && !seat_data.defunct {
-      if opt_kbd.is_none() {
-        // we should initalize a keyboard
-        match map_keyboard_repeat(
-          loop_handle.clone(),
-          &seat,
-          None,
-          RepeatKind::System,
-          move |event, _, _| keyboard_event_handler(event, &result_clone),
-        ) {
-          Ok(kbd) => {
-            *opt_kbd = Some(kbd);
-          }
-          Err(e) => {
-            eprintln!(
-              "Failed to map keyboard on seat {} : {:?}.",
-              seat_data.name, e
-            );
-          }
-        }
-      }
-    } else if let Some(kbd) = opt_kbd.take() {
-      // the keyboard has been removed, cleanup
-      kbd.release();
-    }
-  });
+  WaylandSource::new(conn.clone(), event_queue)
+    .insert(loop_handle)
+    .unwrap();
+
+  // The compositor (not to be confused with the server which is commonly called the compositor) allows
+  // configuring surfaces to be presented.
+  let compositor = CompositorState::bind(&globals, &qh).expect("wl_compositor not available");
+  // For desktop platforms, the XDG shell is the standard protocol for creating desktop windows.
+  let xdg_shell = XdgShell::bind(&globals, &qh).expect("xdg shell is not available");
+  // Since we are not using the GPU in this example, we use wl_shm to allow software rendering to a buffer
+  // we share with the compositor process.
+  let shm = Shm::bind(&globals, &qh).expect("wl shm is not available.");
+  // If the compositor supports xdg-activation it probably wants us to use it to get focus
+  let xdg_activation = ActivationState::bind(&globals, &qh).ok();
 
-  if !env.get_shell().unwrap().needs_configure() {
-    // initial draw to bootstrap on wl_shell
-    redraw(&mut pool, window.surface(), dimensions).expect("Failed to draw");
-    window.refresh();
+  // A window is created from a surface.
+  let surface = compositor.create_surface(&qh);
+  // And then we can create the window.
+  let window = xdg_shell.create_window(surface, WindowDecorations::RequestServer, &qh);
+  // Configure the window, this may include hints to the compositor about the desired minimum size of the
+  // window, app id for WM identification, the window title, etc.
+  window.set_title("Espanso Sync Tool");
+  // GitHub does not let projects use the `org.github` domain but the `io.github` domain is fine.
+  window.set_app_id("Espanso.SyncTool");
+  window.set_min_size(Some((256, 256)));
+
+  // In order for the window to be mapped, we need to perform an initial commit with no attached buffer.
+  // For more info, see WaylandSurface::commit
+  //
+  // The compositor will respond with an initial configure that we can then use to present to the window with
+  // the correct options.
+  window.commit();
+
+  // To request focus, we first need to request a token
+  if let Some(activation) = xdg_activation.as_ref() {
+    activation.request_token(
+      &qh,
+      RequestData {
+        seat_and_serial: None,
+        surface: Some(window.wl_surface().clone()),
+        app_id: Some(String::from("Espanso.SyncTool")),
+      },
+    );
   }
 
-  sctk::WaylandSource::new(queue)
-    .quick_insert(event_loop.handle())
-    .unwrap();
+  // We don't know how large the window will be yet, so lets assume the minimum size we suggested for the
+  // initial memory allocation.
+  let pool = SlotPool::new(256 * 256 * 4, &shm).expect("Failed to create pool");
 
-  let mut next_action = None;
+  let mut simple_window = SimpleWindow {
+    // Seats and outputs may be hotplugged at runtime, therefore we need to setup a registry state to
+    // listen for seats and outputs.
+    registry_state: RegistryState::new(&globals),
+    seat_state: SeatState::new(&globals, &qh),
+    output_state: OutputState::new(&globals, &qh),
+    shm,
+    xdg_activation,
 
-  loop {
-    match next_action.take() {
-      Some(WEvent::Close) => break,
-      Some(WEvent::Refresh) => {
-        window.refresh();
-        window.surface().commit();
-      }
-      Some(WEvent::Configure {
-        new_size,
-        states: _,
-      }) => {
-        if let Some((w, h)) = new_size {
-          window.resize(w, h);
-          dimensions = (w, h);
-        }
-        window.refresh();
-        redraw(&mut pool, window.surface(), dimensions).expect("Failed to draw");
-      }
-      None => {
-        let result_clone = result.clone();
-        let result_ref = result_clone.borrow();
+    exit: false,
+    first_configure: true,
+    pool,
+    width: 256,
+    height: 256,
+    shift: None,
+    buffer: None,
+    window,
+    keyboard: None,
+    keyboard_focus: false,
+    modifiers: None,
+  };
 
-        if let Some(result) = &*result_ref {
-          return Ok(Some(*result));
-        }
-      }
-    }
+  conn.flush().unwrap();
 
-    // always flush the connection before going to sleep waiting for events
-    display.flush().unwrap();
+  loop {
+    if simple_window.modifiers.is_some() {
+      debug!("{:?}", simple_window.modifiers);
+      return Ok(simple_window.modifiers);
+    }
 
     event_loop
-      .dispatch(Some(std::time::Duration::from_millis(10)), &mut next_action)
+      .dispatch(Duration::from_millis(16), &mut simple_window)
       .unwrap();
   }
+  // We don't draw immediately, the configure will notify us when to first draw.
+}
+
+struct SimpleWindow {
+  registry_state: RegistryState,
+  seat_state: SeatState,
+  output_state: OutputState,
+  shm: Shm,
+  xdg_activation: Option<ActivationState>,
 
-  Ok(None)
+  exit: bool,
+  first_configure: bool,
+  pool: SlotPool,
+  width: u32,
+  height: u32,
+  shift: Option<u32>,
+  buffer: Option<Buffer>,
+  window: Window,
+  keyboard: Option<wl_keyboard::WlKeyboard>,
+  keyboard_focus: bool,
+  modifiers: Option<ModifiersState>,
 }
 
-fn keyboard_event_handler(
-  event: KbEvent,
-  result_clone: &Rc<RefCell<Option<super::ModifiersState>>>,
-) {
-  if let KbEvent::Modifiers { modifiers } = event {
-    let mut result_mut = (**result_clone).borrow_mut();
-    *result_mut = Some(super::ModifiersState {
+impl CompositorHandler for SimpleWindow {
+  fn scale_factor_changed(
+    &mut self,
+    _conn: &Connection,
+    _qh: &QueueHandle<Self>,
+    _surface: &wl_surface::WlSurface,
+    _new_factor: i32,
+  ) {
+    // Not needed for this example.
+  }
+
+  fn transform_changed(
+    &mut self,
+    _conn: &Connection,
+    _qh: &QueueHandle<Self>,
+    _surface: &wl_surface::WlSurface,
+    _new_transform: wl_output::Transform,
+  ) {
+    // Not needed for this example.
+  }
+
+  fn frame(
+    &mut self,
+    conn: &Connection,
+    qh: &QueueHandle<Self>,
+    _surface: &wl_surface::WlSurface,
+    _time: u32,
+  ) {
+    self.draw(conn, qh);
+  }
+}
+
+impl OutputHandler for SimpleWindow {
+  fn output_state(&mut self) -> &mut OutputState {
+    &mut self.output_state
+  }
+
+  fn new_output(
+    &mut self,
+    _conn: &Connection,
+    _qh: &QueueHandle<Self>,
+    _output: wl_output::WlOutput,
+  ) {
+  }
+
+  fn update_output(
+    &mut self,
+    _conn: &Connection,
+    _qh: &QueueHandle<Self>,
+    _output: wl_output::WlOutput,
+  ) {
+  }
+
+  fn output_destroyed(
+    &mut self,
+    _conn: &Connection,
+    _qh: &QueueHandle<Self>,
+    _output: wl_output::WlOutput,
+  ) {
+  }
+}
+
+impl WindowHandler for SimpleWindow {
+  fn request_close(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &Window) {
+    self.exit = true;
+  }
+
+  fn configure(
+    &mut self,
+    conn: &Connection,
+    qh: &QueueHandle<Self>,
+    _window: &Window,
+    configure: WindowConfigure,
+    _serial: u32,
+  ) {
+    debug!("Window configured to: {:?}", configure);
+
+    self.buffer = None;
+    self.width = configure.new_size.0.map_or(256, std::num::NonZeroU32::get);
+    self.height = configure.new_size.1.map_or(256, std::num::NonZeroU32::get);
+
+    // Initiate the first draw.
+    if self.first_configure {
+      self.first_configure = false;
+      self.draw(conn, qh);
+    }
+  }
+}
+
+impl ActivationHandler for SimpleWindow {
+  type RequestData = RequestData;
+
+  fn new_token(&mut self, token: String, _data: &Self::RequestData) {
+    self
+      .xdg_activation
+      .as_ref()
+      .unwrap()
+      .activate::<SimpleWindow>(self.window.wl_surface(), token);
+  }
+}
+
+impl SeatHandler for SimpleWindow {
+  fn seat_state(&mut self) -> &mut SeatState {
+    &mut self.seat_state
+  }
+
+  fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
+
+  fn new_capability(
+    &mut self,
+    _conn: &Connection,
+    qh: &QueueHandle<Self>,
+    seat: wl_seat::WlSeat,
+    capability: Capability,
+  ) {
+    if capability == Capability::Keyboard && self.keyboard.is_none() {
+      debug!("Set keyboard capability");
+      let keyboard = self
+        .seat_state
+        .get_keyboard(qh, &seat, None)
+        .expect("Failed to create keyboard");
+
+      self.keyboard = Some(keyboard);
+    }
+  }
+
+  fn remove_capability(
+    &mut self,
+    _conn: &Connection,
+    _: &QueueHandle<Self>,
+    _: wl_seat::WlSeat,
+    capability: Capability,
+  ) {
+    if capability == Capability::Keyboard && self.keyboard.is_some() {
+      debug!("Unset keyboard capability");
+      self.keyboard.take().unwrap().release();
+    }
+  }
+
+  fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
+}
+
+impl KeyboardHandler for SimpleWindow {
+  fn enter(
+    &mut self,
+    _: &Connection,
+    _: &QueueHandle<Self>,
+    _: &wl_keyboard::WlKeyboard,
+    surface: &wl_surface::WlSurface,
+    _: u32,
+    _: &[u32],
+    keysyms: &[Keysym],
+  ) {
+    if self.window.wl_surface() == surface {
+      debug!("Keyboard focus on window with pressed syms: {keysyms:?}");
+      self.keyboard_focus = true;
+    }
+  }
+
+  fn leave(
+    &mut self,
+    _: &Connection,
+    _: &QueueHandle<Self>,
+    _: &wl_keyboard::WlKeyboard,
+    surface: &wl_surface::WlSurface,
+    _: u32,
+  ) {
+    if self.window.wl_surface() == surface {
+      debug!("Release keyboard focus on window");
+      self.keyboard_focus = false;
+    }
+  }
+
+  fn press_key(
+    &mut self,
+    _conn: &Connection,
+    _qh: &QueueHandle<Self>,
+    _: &wl_keyboard::WlKeyboard,
+    _: u32,
+    event: KeyEvent,
+  ) {
+    debug!("Key press: {event:?}");
+  }
+
+  fn release_key(
+    &mut self,
+    _: &Connection,
+    _: &QueueHandle<Self>,
+    _: &wl_keyboard::WlKeyboard,
+    _: u32,
+    event: KeyEvent,
+  ) {
+    debug!("Key release: {event:?}");
+  }
+
+  fn update_modifiers(
+    &mut self,
+    _: &Connection,
+    _: &QueueHandle<Self>,
+    _: &wl_keyboard::WlKeyboard,
+    _serial: u32,
+    modifiers: Modifiers,
+  ) {
+    debug!("Update modifiers: {modifiers:?}");
+    self.modifiers = Some(ModifiersState {
       ctrl: modifiers.ctrl,
       alt: modifiers.alt,
       shift: modifiers.shift,
@@ -208,36 +372,113 @@ fn keyboard_event_handler(
   }
 }
 
+impl ShmHandler for SimpleWindow {
+  fn shm_state(&mut self) -> &mut Shm {
+    &mut self.shm
+  }
+}
+
 #[allow(clippy::many_single_char_names)]
-fn redraw(
-  pool: &mut AutoMemPool,
-  surface: &wl_surface::WlSurface,
-  (buf_x, buf_y): (u32, u32),
-) -> Result<(), ::std::io::Error> {
-  let (canvas, new_buffer) = pool.buffer(
-    buf_x as i32,
-    buf_y as i32,
-    4 * buf_x as i32,
-    wl_shm::Format::Argb8888,
-  )?;
-  for (i, dst_pixel) in canvas.chunks_exact_mut(4).enumerate() {
-    let x = i as u32 % buf_x;
-    let y = i as u32 / buf_x;
-    let r: u32 = min(((buf_x - x) * 0xFF) / buf_x, ((buf_y - y) * 0xFF) / buf_y);
-    let g: u32 = min((x * 0xFF) / buf_x, ((buf_y - y) * 0xFF) / buf_y);
-    let b: u32 = min(((buf_x - x) * 0xFF) / buf_x, (y * 0xFF) / buf_y);
-    let pixel: [u8; 4] = ((0xFF << 24) + (r << 16) + (g << 8) + b).to_ne_bytes();
-    dst_pixel[0] = pixel[0];
-    dst_pixel[1] = pixel[1];
-    dst_pixel[2] = pixel[2];
-    dst_pixel[3] = pixel[3];
-  }
-  surface.attach(Some(&new_buffer), 0, 0);
-  if surface.as_ref().version() >= 4 {
-    surface.damage_buffer(0, 0, buf_x as i32, buf_y as i32);
-  } else {
-    surface.damage(0, 0, buf_x as i32, buf_y as i32);
-  }
-  surface.commit();
-  Ok(())
+#[allow(clippy::single_match_else)]
+impl SimpleWindow {
+  pub fn draw(&mut self, _conn: &Connection, qh: &QueueHandle<Self>) {
+    let width = self.width;
+    let height = self.height;
+    let stride = self.width as i32 * 4;
+
+    let buffer = self.buffer.get_or_insert_with(|| {
+      self
+        .pool
+        .create_buffer(
+          width as i32,
+          height as i32,
+          stride,
+          wl_shm::Format::Argb8888,
+        )
+        .expect("create buffer")
+        .0
+    });
+
+    let canvas = match self.pool.canvas(buffer) {
+      Some(canvas) => canvas,
+      None => {
+        // This should be rare, but if the compositor has not released the previous
+        // buffer, we need double-buffering.
+        let (second_buffer, canvas) = self
+          .pool
+          .create_buffer(
+            self.width as i32,
+            self.height as i32,
+            stride,
+            wl_shm::Format::Argb8888,
+          )
+          .expect("create buffer");
+        *buffer = second_buffer;
+        canvas
+      }
+    };
+
+    // Draw to the window:
+    {
+      let shift = self.shift.unwrap_or(0);
+      canvas
+        .chunks_exact_mut(4)
+        .enumerate()
+        .for_each(|(index, chunk)| {
+          let x = ((index + shift as usize) % width as usize) as u32;
+          let y = (index / width as usize) as u32;
+
+          let a = 0xFF;
+          let r = u32::min(((width - x) * 0xFF) / width, ((height - y) * 0xFF) / height);
+          let g = u32::min((x * 0xFF) / width, ((height - y) * 0xFF) / height);
+          let b = u32::min(((width - x) * 0xFF) / width, (y * 0xFF) / height);
+          let color = (a << 24) + (r << 16) + (g << 8) + b;
+
+          let array: &mut [u8; 4] = chunk.try_into().unwrap();
+          *array = color.to_le_bytes();
+        });
+
+      if let Some(shift) = &mut self.shift {
+        *shift = (*shift + 1) % width;
+      }
+    }
+
+    // Damage the entire window
+    self
+      .window
+      .wl_surface()
+      .damage_buffer(0, 0, self.width as i32, self.height as i32);
+
+    // Request our next frame
+    self
+      .window
+      .wl_surface()
+      .frame(qh, self.window.wl_surface().clone());
+
+    // Attach and commit to present.
+    buffer
+      .attach_to(self.window.wl_surface())
+      .expect("buffer attach");
+    self.window.commit();
+  }
+}
+
+delegate_compositor!(SimpleWindow);
+delegate_output!(SimpleWindow);
+delegate_shm!(SimpleWindow);
+
+delegate_seat!(SimpleWindow);
+delegate_keyboard!(SimpleWindow);
+
+delegate_xdg_shell!(SimpleWindow);
+delegate_xdg_window!(SimpleWindow);
+delegate_activation!(SimpleWindow);
+
+delegate_registry!(SimpleWindow);
+
+impl ProvidesRegistryState for SimpleWindow {
+  fn registry(&mut self) -> &mut RegistryState {
+    &mut self.registry_state
+  }
+  registry_handlers![OutputState, SeatState,];
 }


Test patch: