Skip to content

Commit 663569f

Browse files
committed
Pane,Window(feat[swap]): add swap() wrapping tmux swap-pane and swap-window
why: swap-pane and swap-window are core layout manipulation commands needed for programmatic pane and window reordering. what: - Add Pane.swap() with target, detach (-d), move_up (-U), move_down (-D), keep_zoom (-Z) parameters wrapping swap-pane - Add Window.swap() with target, detach (-d) parameters wrapping swap-window - Add tests verifying pane indices and window indices swap correctly
1 parent 7df463a commit 663569f

4 files changed

Lines changed: 142 additions & 0 deletions

File tree

src/libtmux/pane.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,6 +1139,61 @@ def enter(self) -> Pane:
11391139
self.cmd("send-keys", "Enter")
11401140
return self
11411141

1142+
def swap(
1143+
self,
1144+
target: str | Pane,
1145+
*,
1146+
detach: bool | None = None,
1147+
move_up: bool | None = None,
1148+
move_down: bool | None = None,
1149+
keep_zoom: bool | None = None,
1150+
) -> None:
1151+
"""Swap this pane with another via ``$ tmux swap-pane``.
1152+
1153+
Parameters
1154+
----------
1155+
target : str or Pane
1156+
Target pane to swap with. Can be a pane ID string or Pane object.
1157+
detach : bool, optional
1158+
Do not change the active pane (``-d`` flag).
1159+
move_up : bool, optional
1160+
Swap with the pane above (``-U`` flag). Overrides *target*.
1161+
move_down : bool, optional
1162+
Swap with the pane below (``-D`` flag). Overrides *target*.
1163+
keep_zoom : bool, optional
1164+
Keep the window zoomed if it was zoomed (``-Z`` flag).
1165+
1166+
Examples
1167+
--------
1168+
>>> pane1 = window.active_pane
1169+
>>> pane2 = window.split()
1170+
>>> pane1_id, pane2_id = pane1.pane_id, pane2.pane_id
1171+
>>> pane1.swap(pane2)
1172+
>>> pane1.refresh()
1173+
>>> pane2.refresh()
1174+
"""
1175+
tmux_args: tuple[str, ...] = ()
1176+
1177+
if detach:
1178+
tmux_args += ("-d",)
1179+
1180+
if move_up:
1181+
tmux_args += ("-U",)
1182+
1183+
if move_down:
1184+
tmux_args += ("-D",)
1185+
1186+
if keep_zoom:
1187+
tmux_args += ("-Z",)
1188+
1189+
target_id = target.pane_id if isinstance(target, Pane) else target
1190+
tmux_args += ("-s", str(target_id))
1191+
1192+
proc = self.cmd("swap-pane", *tmux_args)
1193+
1194+
if proc.stderr:
1195+
raise exc.LibTmuxException(proc.stderr)
1196+
11421197
def clear_history(self, *, clear_pane: bool | None = None) -> None:
11431198
"""Clear pane history buffer via ``$ tmux clear-history``.
11441199

src/libtmux/window.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,48 @@ def select_layout(
495495

496496
return self
497497

498+
def swap(
499+
self,
500+
target: str | Window,
501+
*,
502+
detach: bool | None = None,
503+
) -> None:
504+
"""Swap this window with another via ``$ tmux swap-window``.
505+
506+
Parameters
507+
----------
508+
target : str or Window
509+
Target window to swap with. Can be a window ID string or Window.
510+
detach : bool, optional
511+
Do not change the active window (``-d`` flag).
512+
513+
Examples
514+
--------
515+
>>> w1 = session.new_window(window_name='swap_a')
516+
>>> w2 = session.new_window(window_name='swap_b')
517+
>>> w1_idx = w1.window_index
518+
>>> w2_idx = w2.window_index
519+
>>> w1.swap(w2)
520+
>>> w1.refresh()
521+
>>> w2.refresh()
522+
>>> w1.window_index == w2_idx
523+
True
524+
>>> w2.window_index == w1_idx
525+
True
526+
"""
527+
tmux_args: tuple[str, ...] = ()
528+
529+
if detach:
530+
tmux_args += ("-d",)
531+
532+
target_id = target.window_id if isinstance(target, Window) else target
533+
tmux_args += ("-s", str(target_id))
534+
535+
proc = self.cmd("swap-window", *tmux_args)
536+
537+
if proc.stderr:
538+
raise exc.LibTmuxException(proc.stderr)
539+
498540
def rename_window(self, new_name: str) -> Window:
499541
"""Rename window.
500542

tests/test_pane.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,35 @@ def test_split_percentage_size_mutual_exclusion(session: Session) -> None:
740740
pane.split(size=10, percentage=50)
741741

742742

743+
def test_swap_pane(session: Session) -> None:
744+
"""Test Pane.swap() swaps two panes."""
745+
window = session.new_window(window_name="test_swap_pane")
746+
window.resize(height=40, width=80)
747+
pane1 = window.active_pane
748+
assert pane1 is not None
749+
pane2 = pane1.split()
750+
751+
pane1_id = pane1.pane_id
752+
pane2_id = pane2.pane_id
753+
754+
# Record initial indices
755+
pane1.refresh()
756+
pane2.refresh()
757+
pane1_idx = pane1.pane_index
758+
pane2_idx = pane2.pane_index
759+
760+
# Swap
761+
pane1.swap(pane2)
762+
763+
# Verify indices swapped
764+
pane1.refresh()
765+
pane2.refresh()
766+
assert pane1.pane_index == pane2_idx
767+
assert pane2.pane_index == pane1_idx
768+
assert pane1.pane_id == pane1_id
769+
assert pane2.pane_id == pane2_id
770+
771+
743772
def test_clear_history(session: Session) -> None:
744773
"""Test Pane.clear_history()."""
745774
env = shutil.which("env")

tests/test_window.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,22 @@ def test_select_layout_mutual_exclusion(session: Session) -> None:
821821
window.select_layout("tiled", spread=True)
822822

823823

824+
def test_swap_window(session: Session) -> None:
825+
"""Test Window.swap() swaps two windows."""
826+
w1 = session.new_window(window_name="swap_w1")
827+
w2 = session.new_window(window_name="swap_w2")
828+
829+
w1_idx = w1.window_index
830+
w2_idx = w2.window_index
831+
832+
w1.swap(w2)
833+
834+
w1.refresh()
835+
w2.refresh()
836+
assert w1.window_index == w2_idx
837+
assert w2.window_index == w1_idx
838+
839+
824840
def test_move_window_kill_target(session: Session) -> None:
825841
"""Test Window.move_window() with kill_target flag."""
826842
session.new_window(window_name="move_w1")

0 commit comments

Comments
 (0)