Skip to content

Commit 20d81a3

Browse files
committed
Pane(feat[join]): add join() wrapping tmux join-pane
why: join-pane is the inverse of break-pane, needed for programmatically merging panes between windows. what: - Add join() method with vertical (-v/-h), detach (-d), full_window (-f), size (-l), before (-b) parameters - Accept Pane, Window, or string target ID - Use server.cmd with explicit -s/-t to avoid auto-target conflicts - Add roundtrip test (break + join) and horizontal join test
1 parent 992ec48 commit 20d81a3

2 files changed

Lines changed: 113 additions & 0 deletions

File tree

src/libtmux/pane.py

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

1142+
def join(
1143+
self,
1144+
target: str | Pane | Window,
1145+
*,
1146+
vertical: bool = True,
1147+
detach: bool = True,
1148+
full_window: bool | None = None,
1149+
size: str | int | None = None,
1150+
before: bool | None = None,
1151+
) -> None:
1152+
"""Join this pane into another window/pane via ``$ tmux join-pane``.
1153+
1154+
This is the inverse of :meth:`break_pane`.
1155+
1156+
Parameters
1157+
----------
1158+
target : str, Pane, or Window
1159+
Target pane or window to join into.
1160+
vertical : bool, optional
1161+
Join vertically (``-v`` flag), default True. Set to False for
1162+
horizontal (``-h``).
1163+
detach : bool, optional
1164+
Do not switch to the target window (``-d`` flag), default True.
1165+
full_window : bool, optional
1166+
Join spanning the full window width/height (``-f`` flag).
1167+
size : str or int, optional
1168+
Size for the joined pane (``-l`` flag).
1169+
before : bool, optional
1170+
Place the pane before the target (``-b`` flag).
1171+
1172+
Examples
1173+
--------
1174+
>>> pane_to_join = window.split(shell='sleep 1m')
1175+
>>> new_window = pane_to_join.break_pane()
1176+
>>> pane_to_join.join(window)
1177+
"""
1178+
tmux_args: tuple[str, ...] = ()
1179+
1180+
if vertical:
1181+
tmux_args += ("-v",)
1182+
else:
1183+
tmux_args += ("-h",)
1184+
1185+
if detach:
1186+
tmux_args += ("-d",)
1187+
1188+
if full_window:
1189+
tmux_args += ("-f",)
1190+
1191+
if size is not None:
1192+
tmux_args += (f"-l{size}",)
1193+
1194+
if before:
1195+
tmux_args += ("-b",)
1196+
1197+
# Determine target ID
1198+
from libtmux.window import Window
1199+
1200+
if isinstance(target, Pane):
1201+
target_id = str(target.pane_id)
1202+
elif isinstance(target, Window):
1203+
target_id = str(target.window_id)
1204+
else:
1205+
target_id = target
1206+
1207+
tmux_args += ("-s", str(self.pane_id), "-t", target_id)
1208+
1209+
# Use server.cmd to avoid auto-adding -t from self.cmd
1210+
proc = self.server.cmd("join-pane", *tmux_args)
1211+
1212+
if proc.stderr:
1213+
raise exc.LibTmuxException(proc.stderr)
1214+
11421215
def break_pane(
11431216
self,
11441217
*,

tests/test_pane.py

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

742742

743+
def test_join_pane(session: Session) -> None:
744+
"""Test Pane.join() roundtrip with break_pane."""
745+
window = session.new_window(window_name="test_join")
746+
pane = window.active_pane
747+
assert pane is not None
748+
749+
# Create a second pane and break it out
750+
new_pane = pane.split(shell="sleep 1m")
751+
assert len(window.panes) == 2
752+
753+
new_window = new_pane.break_pane()
754+
window.refresh()
755+
assert len(window.panes) == 1
756+
757+
# Join the pane back
758+
new_pane.join(window)
759+
window.refresh()
760+
assert len(window.panes) == 2
761+
762+
# The new window should be gone (only had one pane)
763+
session.refresh()
764+
window_ids = [w.window_id for w in session.windows]
765+
assert new_window.window_id not in window_ids
766+
767+
768+
def test_join_pane_horizontal(session: Session) -> None:
769+
"""Test Pane.join() with horizontal split."""
770+
window = session.new_window(window_name="test_join_h")
771+
window.resize(height=40, width=80)
772+
pane = window.active_pane
773+
assert pane is not None
774+
775+
new_pane = pane.split(shell="sleep 1m")
776+
new_pane.break_pane()
777+
778+
new_pane.join(window, vertical=False)
779+
window.refresh()
780+
assert len(window.panes) == 2
781+
782+
743783
def test_break_pane_basic(session: Session) -> None:
744784
"""Test Pane.break_pane() creates a new window."""
745785
window = session.new_window(window_name="test_break")

0 commit comments

Comments
 (0)