Repository: qtile/qtile

Issue:
[ Feature ] Allow users to toggle cursor on and off under both X and Wayland
### The issue:

I think a missing feature of qtile that would be very useful to most users would be an option to be able to toggle the cursor on and off under both X and wayland. Something similar to unclutter but allowing the user to toggle it via scripts or keybindings.

### Required:

- [X] I have searched past issues to see if this bug has already been reported.


Discussion:

Resolving patch:
diff --git a/libqtile/backend/wayland/core.py b/libqtile/backend/wayland/core.py
--- a/libqtile/backend/wayland/core.py
+++ b/libqtile/backend/wayland/core.py
@@ -168,6 +168,7 @@ def __init__(self) -> None:
         self.add_listener(self.cursor.button_event, self._on_cursor_button)
         self.add_listener(self.cursor.motion_event, self._on_cursor_motion)
         self.add_listener(self.cursor.motion_absolute_event, self._on_cursor_motion_absolute)
+        self._cursor_state = wlrq.CursorState()
 
         # set up shell
         self.xdg_shell = XdgShell(self.display)
@@ -351,7 +352,11 @@ def _on_request_cursor(
         self, _listener: Listener, event: seat.PointerRequestSetCursorEvent
     ) -> None:
         logger.debug("Signal: seat request_set_cursor_event")
-        self.cursor.set_surface(event.surface, event.hotspot)
+        self._cursor_state.surface = event.surface
+        self._cursor_state.hotspot = event.hotspot
+
+        if not self._cursor_state.hidden:
+            self.cursor.set_surface(event.surface, event.hotspot)
 
     def _on_new_xdg_surface(self, _listener: Listener, surface: XdgSurface) -> None:
         logger.debug("Signal: xdg_shell new_surface_event")
@@ -1025,3 +1030,18 @@ def cmd_change_vt(self, vt: int) -> bool:
         if not success:
             logger.warning("Could not change VT to: %s", vt)
         return success
+
+    def cmd_hide_cursor(self) -> None:
+        """Hide the cursor."""
+        if not self._cursor_state.hidden:
+            self.cursor.set_surface(None, self._cursor_state.hotspot)
+            self._cursor_state.hidden = True
+
+    def cmd_unhide_cursor(self) -> None:
+        """Unhide the cursor."""
+        if self._cursor_state.hidden:
+            self.cursor.set_surface(
+                self._cursor_state.surface,
+                self._cursor_state.hotspot,
+            )
+            self._cursor_state.hidden = False
diff --git a/libqtile/backend/wayland/wlrq.py b/libqtile/backend/wayland/wlrq.py
--- a/libqtile/backend/wayland/wlrq.py
+++ b/libqtile/backend/wayland/wlrq.py
@@ -22,6 +22,7 @@
 
 import functools
 import operator
+from dataclasses import dataclass
 from typing import TYPE_CHECKING
 
 import cairocffi
@@ -37,7 +38,7 @@
 
     from pywayland.server import Signal
     from wlroots import xwayland
-    from wlroots.wlr_types import data_device_manager
+    from wlroots.wlr_types import Surface, data_device_manager
 
     from libqtile.backend.wayland.core import Core
     from libqtile.backend.wayland.output import Output
@@ -256,3 +257,15 @@ def get_xwayland_atoms(xwayland: xwayland.XWayland) -> dict[int, str]:
         atoms[xwayland.get_atom(atom)] = name
 
     return atoms
+
+
+@dataclass()
+class CursorState:
+    """
+    The surface and hotspot state of the cursor. This is tracked directly by the core so
+    that the cursor can be hidden and later restored to this state at will.
+    """
+
+    surface: Surface | None = None
+    hotspot: tuple[int, int] = (0, 0)
+    hidden: bool = False


Test patch: