Repository: qtile/qtile

Issue:
Spiral layout shuffle methods call rotate
### Issue description

The `shuffle_up` and `shuffle_down` methods of the Spiral layout call the rotate methods of the client list, instead of the shuffle methods like the name implies:

https://github.com/qtile/qtile/blob/fecc37d8b99b8fa20c9e925660fa25c3157c206c/libqtile/layout/spiral.py#L372-L382

Due to this, there's no way to re-order windows in this layout as it is now.

There are solutions to this, like subclassing the layout in your configuration and overwriting the `shuffle_up` and `shuffle_down` methods. Should this be changed though to make the layouts shuffle methods behave as expected or is this intentional? In any case, I see three options here:

- Introduce a breaking change and make these methods call shuffle instead of rotate, which would most likely be more in line with what new users expect these methods to do.
- Keep these methods as they are, but implement new ones which call shuffle.
- Keep everything as it is and have users overwrite the class if they desire different behavior.

### Version

0.29.0

### Backend

X11 (default)

### Config

_No response_

### Logs

_No response_

### Required

- [X] I have searched past issues to see if this bug has already been reported, and it hasn't been.
- [X] I understand that people give their precious time for free, and thus I've done my very best to make this problem as easy as possible to investigate.


Discussion:
I would be inclined to go with the first option as shuffling should just be moving the client not every window. 



Patch:
diff --git a/CHANGELOG b/CHANGELOG
index 4458553e7b..55d03f647a 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,4 +1,6 @@
 Qtile x.xx.x, released xxxx-xx-xx:
+    !!! breaking changes !!!
+        - Spiral layout: `shuffle_up` and `shuffle_down` commands now shuffle the focused window instead of rotating all windows.
     * features
     * bugfixes
 
diff --git a/libqtile/layout/spiral.py b/libqtile/layout/spiral.py
index 9ed38292e5..f28ab5c600 100644
--- a/libqtile/layout/spiral.py
+++ b/libqtile/layout/spiral.py
@@ -372,13 +372,13 @@ def _set_ratio(self, prop: str, value: float):
     @expose_command()
     def shuffle_down(self):
         if self.clients:
-            self.clients.rotate_down()
+            self.clients.shuffle_down()
             self.group.layout_all()
 
     @expose_command()
     def shuffle_up(self):
         if self.clients:
-            self.clients.rotate_up()
+            self.clients.shuffle_up()
             self.group.layout_all()
 
     @expose_command()


Test patch:
diff --git a/test/layouts/test_spiral.py b/test/layouts/test_spiral.py
index c77174db41..0a164093bd 100644
--- a/test/layouts/test_spiral.py
+++ b/test/layouts/test_spiral.py
@@ -202,6 +202,56 @@ def test_spiral_bottom_anticlockwise(manager):
     assert_dimensions(manager, 200, 150, 198, 148)
 
 
+@spiral_config
+def test_spiral_shuffle_up(manager):
+    manager.test_window("one")
+    manager.test_window("two")
+    manager.test_window("three")
+    manager.test_window("four")
+    assert manager.c.layout.info()["clients"] == ["one", "two", "three", "four"]
+    manager.c.layout.shuffle_up()
+    assert manager.c.layout.info()["clients"] == ["one", "two", "four", "three"]
+    manager.c.layout.shuffle_up()
+    assert manager.c.layout.info()["clients"] == ["one", "four", "two", "three"]
+
+
+@spiral_config
+def test_spiral_shuffle_down(manager):
+    manager.test_window("one")
+    manager.test_window("two")
+    manager.test_window("three")
+    manager.test_window("four")
+    # wrap focus back to first window
+    manager.c.layout.down()
+    assert manager.c.layout.info()["clients"] == ["one", "two", "three", "four"]
+    manager.c.layout.shuffle_down()
+    assert manager.c.layout.info()["clients"] == ["two", "one", "three", "four"]
+    manager.c.layout.shuffle_down()
+    assert manager.c.layout.info()["clients"] == ["two", "three", "one", "four"]
+
+
+@spiral_config
+def test_spiral_shuffle_no_wrap_down(manager):
+    # test that shuffling down doesn't wrap around
+    manager.test_window("one")
+    manager.test_window("two")
+    manager.test_window("three")
+    manager.c.layout.shuffle_down()
+    assert manager.c.layout.info()["clients"] == ["one", "two", "three"]
+
+
+@spiral_config
+def test_spiral_shuffle_no_wrap_up(manager):
+    # test that shuffling up doesn't wrap around
+    manager.test_window("one")
+    manager.test_window("two")
+    manager.test_window("three")
+    # wrap focus back to first window
+    manager.c.layout.down()
+    manager.c.layout.shuffle_up()
+    assert manager.c.layout.info()["clients"] == ["one", "two", "three"]
+
+
 @singleborder_disabled_config
 def test_singleborder_disable(manager):
     manager.test_window("one")