Repository: qtile/qtile

Issue:
Pass on ignored key events to clients
Currently, you can make a keyboard shortcut layout specific by calling .when(layout_name) on it. 

However, qtile still swallows the keypress even when the layout does not match (it call self.root.grab_key for all registered key codes). It would be nice if the keypress could be passed on to the focused client if no handlers handle it.

This would make it possible to rebind commonly used keys for specific layouts that are used with particular applications, without affecting other layouts/applications.

Two possible implementations (I'm no Xlib expert so I could be talking nonsense):

1) Ungrab layout specifc keys codes whenever a layout that oes not match is used

2) If no handler claims the key press, resend it to the client application, in a manner similar to xte.



Discussion:
Note, it is probbaly possible to do this with the layout_change hook and mapKey()/unmapKey(), but this should be handled automatyically, given that the when() functionality already exists.

Here's the hook based implementation, it adds some extra shortcuts when the layout is changed to monad:

``` python
@hook.subscribe.layout_change
def set_monad_keys(layout, group):
    if group.qtile is not None:
        is_monad = layout and layout.name == 'monadtall'
        for key in monad_keys:
            (group.qtile.mapKey if is_monad else group.qtile.unmapKey)(key)
```


Resolving patch:
diff --git a/libqtile/backend/wayland/keyboard.py b/libqtile/backend/wayland/keyboard.py
--- a/libqtile/backend/wayland/keyboard.py
+++ b/libqtile/backend/wayland/keyboard.py
@@ -105,7 +105,7 @@ def _on_key(self, _listener, event: KeyboardKeyEvent):
             mods = self.keyboard.modifier
             for keysym in keysyms:
                 if (keysym, mods) in self.grabbed_keys:
-                    self.qtile.process_key_event(keysym, mods)
-                    return
+                    if self.qtile.process_key_event(keysym, mods):
+                        return
 
         self.seat.keyboard_notify_key(event)
diff --git a/libqtile/backend/x11/core.py b/libqtile/backend/x11/core.py
--- a/libqtile/backend/x11/core.py
+++ b/libqtile/backend/x11/core.py
@@ -462,7 +462,7 @@ def grab_key(self, key: Union[config.Key, config.KeyChord]) -> Tuple[int, int]:
                     modmask | amask,
                     code,
                     xcffib.xproto.GrabMode.Async,
-                    xcffib.xproto.GrabMode.Async,
+                    xcffib.xproto.GrabMode.Sync,
                 )
         return keysym, modmask & self._valid_mask
 
@@ -592,11 +592,22 @@ def handle_ClientMessage(self, event) -> None:  # noqa: N802
             except IndexError:
                 logger.info("Invalid Desktop Index: %s" % index)
 
-    def handle_KeyPress(self, event) -> None:  # noqa: N802
+    def handle_KeyPress(self, event, *, simulated=False) -> None:  # noqa: N802
         assert self.qtile is not None
 
         keysym = self.conn.code_to_syms[event.detail][0]
-        self.qtile.process_key_event(keysym, event.state & self._valid_mask)
+        handled = self.qtile.process_key_event(keysym, event.state & self._valid_mask)
+
+        if simulated:
+            # Simulated key press are currently for internal use only
+            return
+
+        if handled:
+            # Swallow the event
+            self.conn.conn.core.AllowEvents(xcffib.xproto.Allow.SyncKeyboard, event.time)
+        else:
+            # Forward the event to any focussed client
+            self.conn.conn.core.AllowEvents(xcffib.xproto.Allow.ReplayKeyboard, event.time)
 
     def handle_ButtonPress(self, event) -> None:  # noqa: N802
         assert self.qtile is not None
@@ -730,7 +741,7 @@ class DummyEv:
         d = DummyEv()
         d.detail = self.conn.keysym_to_keycode(keysym)[0]
         d.state = modmasks
-        self.handle_KeyPress(d)
+        self.handle_KeyPress(d, simulated=True)
 
     def focus_by_click(self, e, window=None):
         """Bring a window to the front
diff --git a/libqtile/core/manager.py b/libqtile/core/manager.py
--- a/libqtile/core/manager.py
+++ b/libqtile/core/manager.py
@@ -336,12 +336,13 @@ def paint_screen(
     ) -> None:
         self.core.painter.paint(screen, image_path, mode)
 
-    def process_key_event(self, keysym: int, mask: int) -> None:
+    def process_key_event(self, keysym: int, mask: int) -> bool:
         key = self.keys_map.get((keysym, mask), None)
         if key is None:
             logger.info("Ignoring unknown keysym: {keysym}, mask: {mask}".format(keysym=keysym, mask=mask))
-            return
+            return False
 
+        handled = False
         if isinstance(key, KeyChord):
             self.grab_chord(key)
         else:
@@ -352,9 +353,11 @@ def process_key_event(self, keysym: int, mask: int) -> None:
                     )
                     if status in (interface.ERROR, interface.EXCEPTION):
                         logger.error("KB command error %s: %s" % (cmd.name, val))
+                    handled = True
             if self.chord_stack and (self.chord_stack[-1].mode == "" or key.key == "Escape"):
                 self.cmd_ungrab_chord()
-            return
+                handled = True
+        return handled
 
     def grab_keys(self) -> None:
         """Re-grab all of the keys configured in the key map


Test patch: