Skip to content

Commit 097f5ba

Browse files
committed
Session,Window(feat): add last_window, next_window, previous_window, rotate
why: Window navigation commands (last/next/previous) and pane rotation are commonly needed for programmatic window management workflows. what: - Add Session.last_window() wrapping last-window - Add Session.next_window() wrapping next-window - Add Session.previous_window() wrapping previous-window - Add Window.rotate() wrapping rotate-window with direction_up (-U), keep_zoom (-Z) parameters - Add tests for all navigation commands and rotation
1 parent 372b57e commit 097f5ba

4 files changed

Lines changed: 174 additions & 0 deletions

File tree

src/libtmux/session.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,76 @@ def cmd(
245245
Commands (tmux-like)
246246
"""
247247

248+
def last_window(self) -> Window:
249+
"""Select the last (previously selected) window.
250+
251+
Wraps ``$ tmux last-window``.
252+
253+
Returns
254+
-------
255+
:class:`Window`
256+
The newly active window.
257+
258+
Examples
259+
--------
260+
>>> w1 = session.new_window(window_name='lw_a')
261+
>>> w2 = session.new_window(window_name='lw_b', attach=True)
262+
>>> session.last_window()
263+
Window(...)
264+
"""
265+
proc = self.cmd("last-window")
266+
267+
if proc.stderr:
268+
raise exc.LibTmuxException(proc.stderr)
269+
270+
return self.active_window
271+
272+
def next_window(self) -> Window:
273+
"""Select the next window.
274+
275+
Wraps ``$ tmux next-window``.
276+
277+
Returns
278+
-------
279+
:class:`Window`
280+
The newly active window.
281+
282+
Examples
283+
--------
284+
>>> w = session.new_window(window_name='nw_test')
285+
>>> session.next_window()
286+
Window(...)
287+
"""
288+
proc = self.cmd("next-window")
289+
290+
if proc.stderr:
291+
raise exc.LibTmuxException(proc.stderr)
292+
293+
return self.active_window
294+
295+
def previous_window(self) -> Window:
296+
"""Select the previous window.
297+
298+
Wraps ``$ tmux previous-window``.
299+
300+
Returns
301+
-------
302+
:class:`Window`
303+
The newly active window.
304+
305+
Examples
306+
--------
307+
>>> w = session.new_window(window_name='pw_test')
308+
>>> session.previous_window()
309+
Window(...)
310+
"""
311+
proc = self.cmd("previous-window")
312+
313+
if proc.stderr:
314+
raise exc.LibTmuxException(proc.stderr)
315+
316+
return self.active_window
317+
248318
def select_window(self, target_window: str | int) -> Window:
249319
"""Select window and return the selected window.
250320

src/libtmux/window.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,50 @@ def select_layout(
496496

497497
return self
498498

499+
def rotate(
500+
self,
501+
*,
502+
direction_up: bool | None = None,
503+
keep_zoom: bool | None = None,
504+
) -> Window:
505+
"""Rotate pane positions in the window via ``$ tmux rotate-window``.
506+
507+
Parameters
508+
----------
509+
direction_up : bool, optional
510+
Rotate upward (``-U`` flag). Default is downward (``-D``).
511+
keep_zoom : bool, optional
512+
Keep the window zoomed if zoomed (``-Z`` flag).
513+
514+
Returns
515+
-------
516+
:class:`Window`
517+
Self, for method chaining.
518+
519+
Examples
520+
--------
521+
>>> pane1 = window.active_pane
522+
>>> pane2 = window.split()
523+
>>> window.rotate()
524+
Window(...)
525+
"""
526+
tmux_args: tuple[str, ...] = ()
527+
528+
if direction_up:
529+
tmux_args += ("-U",)
530+
else:
531+
tmux_args += ("-D",)
532+
533+
if keep_zoom:
534+
tmux_args += ("-Z",)
535+
536+
proc = self.cmd("rotate-window", *tmux_args)
537+
538+
if proc.stderr:
539+
raise exc.LibTmuxException(proc.stderr)
540+
541+
return self
542+
499543
def respawn(
500544
self,
501545
*,

tests/test_session.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,42 @@ def patched_cmd(cmd_name: str, *args: t.Any, **kwargs: t.Any) -> tmux_cmd:
576576
test_session.attach()
577577

578578

579+
def test_last_window(session: Session) -> None:
580+
"""Test Session.last_window() selects previous window."""
581+
w1 = session.new_window(window_name="last_a", attach=True)
582+
w2 = session.new_window(window_name="last_b", attach=True)
583+
session.refresh()
584+
assert session.active_window.window_id == w2.window_id
585+
586+
result = session.last_window()
587+
assert result.window_id == w1.window_id
588+
589+
590+
def test_next_window(session: Session) -> None:
591+
"""Test Session.next_window() selects next window."""
592+
w1 = session.new_window(window_name="next_a", attach=True)
593+
session.new_window(window_name="next_b", attach=False)
594+
595+
# Active is w1, next should go to next_b
596+
session.refresh()
597+
assert session.active_window.window_id == w1.window_id
598+
599+
result = session.next_window()
600+
# Should have moved to a different window
601+
assert result.window_id != w1.window_id
602+
603+
604+
def test_previous_window(session: Session) -> None:
605+
"""Test Session.previous_window() selects previous window."""
606+
w1 = session.new_window(window_name="prev_a", attach=True)
607+
w2 = session.new_window(window_name="prev_b", attach=True)
608+
session.refresh()
609+
assert session.active_window.window_id == w2.window_id
610+
611+
result = session.previous_window()
612+
assert result.window_id == w1.window_id
613+
614+
579615
def test_new_window_kill_existing(session: Session) -> None:
580616
"""Test Session.new_window() with kill_existing flag."""
581617
# Create a window at a specific index

tests/test_window.py

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

823823

824+
def test_rotate_window(session: Session) -> None:
825+
"""Test Window.rotate() rotates pane positions."""
826+
window = session.new_window(window_name="test_rotate")
827+
window.resize(height=40, width=80)
828+
pane1 = window.active_pane
829+
assert pane1 is not None
830+
pane2 = pane1.split()
831+
pane3 = pane2.split()
832+
833+
pane1.refresh()
834+
pane2.refresh()
835+
pane3.refresh()
836+
idx_before = (pane1.pane_index, pane2.pane_index, pane3.pane_index)
837+
838+
window.rotate()
839+
840+
pane1.refresh()
841+
pane2.refresh()
842+
pane3.refresh()
843+
idx_after = (pane1.pane_index, pane2.pane_index, pane3.pane_index)
844+
845+
assert idx_before != idx_after
846+
847+
824848
def test_respawn_window(session: Session) -> None:
825849
"""Test Window.respawn() with kill flag."""
826850
window = session.new_window(window_name="test_respawn_w")

0 commit comments

Comments
 (0)