Skip to content

Commit 0b7f25c

Browse files
committed
Window,Server(feat): add link, unlink, and wait_for commands
why: link-window, unlink-window, and wait-for are needed for sharing windows across sessions and for synchronization between tmux commands. what: - Add Window.link() wrapping link-window with target_session, target_index, kill_existing (-k), after (-a), before (-b), detach (-d) parameters - Add Window.unlink() wrapping unlink-window with kill_if_last (-k) parameter - Add Server.wait_for() wrapping wait-for with lock (-L), unlock (-U), set_flag (-S) parameters - Add tests for link/unlink roundtrip and wait-for set_flag
1 parent 097f5ba commit 0b7f25c

4 files changed

Lines changed: 173 additions & 0 deletions

File tree

src/libtmux/server.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,51 @@ def run_shell(
483483
return None
484484
return proc.stdout
485485

486+
def wait_for(
487+
self,
488+
channel: str,
489+
*,
490+
lock: bool | None = None,
491+
unlock: bool | None = None,
492+
set_flag: bool | None = None,
493+
) -> None:
494+
"""Wait for, signal, or lock a channel via ``$ tmux wait-for``.
495+
496+
Parameters
497+
----------
498+
channel : str
499+
Channel name.
500+
lock : bool, optional
501+
Lock the channel (``-L`` flag).
502+
unlock : bool, optional
503+
Unlock the channel (``-U`` flag).
504+
set_flag : bool, optional
505+
Set the channel flag and wake waiters (``-S`` flag).
506+
507+
Examples
508+
--------
509+
>>> server.new_session(session_name='wait_test')
510+
Session(...)
511+
>>> server.wait_for('test_channel', set_flag=True)
512+
"""
513+
tmux_args: tuple[str, ...] = ()
514+
515+
if lock:
516+
tmux_args += ("-L",)
517+
518+
if unlock:
519+
tmux_args += ("-U",)
520+
521+
if set_flag:
522+
tmux_args += ("-S",)
523+
524+
tmux_args += (channel,)
525+
526+
proc = self.cmd("wait-for", *tmux_args)
527+
528+
if proc.stderr:
529+
raise exc.LibTmuxException(proc.stderr)
530+
486531
def switch_client(self, target_session: str) -> None:
487532
"""Switch tmux client.
488533

src/libtmux/window.py

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

497497
return self
498498

499+
def link(
500+
self,
501+
target_session: str | Session,
502+
*,
503+
target_index: str | None = None,
504+
kill_existing: bool | None = None,
505+
after: bool | None = None,
506+
before: bool | None = None,
507+
detach: bool | None = None,
508+
) -> None:
509+
"""Link this window to another session via ``$ tmux link-window``.
510+
511+
Parameters
512+
----------
513+
target_session : str or Session
514+
Target session to link the window to.
515+
target_index : str, optional
516+
Target window index in the destination session.
517+
kill_existing : bool, optional
518+
Kill target window if it exists (``-k`` flag).
519+
after : bool, optional
520+
Insert after the target window (``-a`` flag).
521+
before : bool, optional
522+
Insert before the target window (``-b`` flag).
523+
detach : bool, optional
524+
Do not make the linked window active (``-d`` flag).
525+
526+
Examples
527+
--------
528+
>>> w = session.new_window(window_name='link_test')
529+
>>> s2 = server.new_session(session_name='link_target')
530+
>>> w.link(s2)
531+
"""
532+
tmux_args: tuple[str, ...] = ()
533+
534+
if kill_existing:
535+
tmux_args += ("-k",)
536+
537+
if after:
538+
tmux_args += ("-a",)
539+
540+
if before:
541+
tmux_args += ("-b",)
542+
543+
if detach:
544+
tmux_args += ("-d",)
545+
546+
# Source: this window
547+
tmux_args += ("-s", f"{self.session_id}:{self.window_index}")
548+
549+
# Target: destination session[:index]
550+
from libtmux.session import Session
551+
552+
session_id = (
553+
target_session.session_id
554+
if isinstance(target_session, Session)
555+
else target_session
556+
)
557+
target = f"{session_id}:{target_index}" if target_index else str(session_id)
558+
tmux_args += ("-t", target)
559+
560+
proc = self.server.cmd("link-window", *tmux_args)
561+
562+
if proc.stderr:
563+
raise exc.LibTmuxException(proc.stderr)
564+
565+
def unlink(self, *, kill_if_last: bool | None = None) -> None:
566+
"""Unlink this window from the current session via ``$ tmux unlink-window``.
567+
568+
Parameters
569+
----------
570+
kill_if_last : bool, optional
571+
Kill the window if it is the last window in the session (``-k``).
572+
573+
Examples
574+
--------
575+
>>> w = session.new_window(window_name='unlink_test')
576+
>>> s2 = server.new_session(session_name='unlink_s2')
577+
>>> w.link(s2)
578+
>>> linked = [x for x in s2.windows if x.window_name == 'unlink_test']
579+
>>> linked[0].unlink()
580+
"""
581+
tmux_args: tuple[str, ...] = ()
582+
583+
if kill_if_last:
584+
tmux_args += ("-k",)
585+
586+
proc = self.cmd("unlink-window", *tmux_args)
587+
588+
if proc.stderr:
589+
raise exc.LibTmuxException(proc.stderr)
590+
499591
def rotate(
500592
self,
501593
*,

tests/test_server.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,13 @@ def test_tmux_bin_invalid_path_raise_if_dead() -> None:
460460
s.raise_if_dead()
461461

462462

463+
def test_wait_for_set_flag(server: Server) -> None:
464+
"""Test Server.wait_for() with set_flag."""
465+
server.new_session(session_name="wait_test")
466+
# Just set the flag — should not block or error
467+
server.wait_for("test_channel_set", set_flag=True)
468+
469+
463470
def test_run_shell_basic(server: Server) -> None:
464471
"""Test Server.run_shell() executes command and returns output."""
465472
server.new_session(session_name="run_shell_test")

tests/test_window.py

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

823823

824+
def test_link_unlink_window(server: Server, session: Session) -> None:
825+
"""Test Window.link() and Window.unlink()."""
826+
# Create a second session
827+
s2 = server.new_session(session_name="link_target")
828+
829+
# Create a window in the first session
830+
w = session.new_window(window_name="link_test")
831+
832+
# Link it to s2
833+
w.link(s2, detach=True)
834+
835+
# Verify window appears in s2
836+
s2.refresh()
837+
s2_window_names = [win.window_name for win in s2.windows]
838+
assert "link_test" in s2_window_names
839+
840+
# Unlink from s2 — select a different window first
841+
linked_windows = [win for win in s2.windows if win.window_name == "link_test"]
842+
assert len(linked_windows) > 0
843+
844+
# We need another window in s2 before unlinking the last one
845+
linked_windows[0].unlink()
846+
847+
# Verify it's gone from s2
848+
s2.refresh()
849+
s2_window_names = [win.window_name for win in s2.windows]
850+
assert "link_test" not in s2_window_names
851+
852+
824853
def test_rotate_window(session: Session) -> None:
825854
"""Test Window.rotate() rotates pane positions."""
826855
window = session.new_window(window_name="test_rotate")

0 commit comments

Comments
 (0)