Skip to content

Commit 4d4b4ce

Browse files
committed
Pane,Window(feat[respawn]): add respawn() wrapping tmux respawn-pane/respawn-window
why: respawn-pane and respawn-window are needed for restarting processes in panes/windows without destroying and recreating them. what: - Add Pane.respawn() with shell, start_directory (-c), environment (-e), kill (-k) parameters wrapping respawn-pane - Add Window.respawn() with same parameters wrapping respawn-window - Add tests verifying respawn with kill on active panes and windows
1 parent 20d81a3 commit 4d4b4ce

4 files changed

Lines changed: 124 additions & 0 deletions

File tree

src/libtmux/pane.py

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

1142+
def respawn(
1143+
self,
1144+
*,
1145+
shell: str | None = None,
1146+
start_directory: StrPath | None = None,
1147+
environment: dict[str, str] | None = None,
1148+
kill: bool | None = None,
1149+
) -> None:
1150+
"""Respawn the pane process via ``$ tmux respawn-pane``.
1151+
1152+
Parameters
1153+
----------
1154+
shell : str, optional
1155+
Shell command to run in the respawned pane.
1156+
start_directory : str or PathLike, optional
1157+
Working directory for the respawned pane (``-c`` flag).
1158+
environment : dict, optional
1159+
Environment variables (``-e`` flag).
1160+
kill : bool, optional
1161+
Kill the current process before respawning (``-k`` flag).
1162+
Required if the pane is still active.
1163+
1164+
Examples
1165+
--------
1166+
>>> pane = window.split(shell='sleep 1m')
1167+
>>> pane.respawn(kill=True, shell='sh')
1168+
"""
1169+
tmux_args: tuple[str, ...] = ()
1170+
1171+
if kill:
1172+
tmux_args += ("-k",)
1173+
1174+
if start_directory is not None:
1175+
start_path = pathlib.Path(start_directory).expanduser()
1176+
tmux_args += ("-c", str(start_path))
1177+
1178+
if environment:
1179+
for k, v in environment.items():
1180+
tmux_args += (f"-e{k}={v}",)
1181+
1182+
if shell:
1183+
tmux_args += (shell,)
1184+
1185+
proc = self.cmd("respawn-pane", *tmux_args)
1186+
1187+
if proc.stderr:
1188+
raise exc.LibTmuxException(proc.stderr)
1189+
11421190
def join(
11431191
self,
11441192
target: str | Pane | Window,

src/libtmux/window.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import dataclasses
1111
import logging
12+
import pathlib
1213
import shlex
1314
import typing as t
1415
import warnings
@@ -495,6 +496,54 @@ def select_layout(
495496

496497
return self
497498

499+
def respawn(
500+
self,
501+
*,
502+
shell: str | None = None,
503+
start_directory: StrPath | None = None,
504+
environment: dict[str, str] | None = None,
505+
kill: bool | None = None,
506+
) -> None:
507+
"""Respawn the window process via ``$ tmux respawn-window``.
508+
509+
Parameters
510+
----------
511+
shell : str, optional
512+
Shell command to run in the respawned window.
513+
start_directory : str or PathLike, optional
514+
Working directory for the respawned window (``-c`` flag).
515+
environment : dict, optional
516+
Environment variables (``-e`` flag).
517+
kill : bool, optional
518+
Kill the current process before respawning (``-k`` flag).
519+
Required if the window is still active.
520+
521+
Examples
522+
--------
523+
>>> window = session.new_window(window_name='respawn_test')
524+
>>> window.respawn(kill=True, shell='sh')
525+
"""
526+
tmux_args: tuple[str, ...] = ()
527+
528+
if kill:
529+
tmux_args += ("-k",)
530+
531+
if start_directory is not None:
532+
start_path = pathlib.Path(start_directory).expanduser()
533+
tmux_args += ("-c", str(start_path))
534+
535+
if environment:
536+
for k, v in environment.items():
537+
tmux_args += (f"-e{k}={v}",)
538+
539+
if shell:
540+
tmux_args += (shell,)
541+
542+
proc = self.cmd("respawn-window", *tmux_args)
543+
544+
if proc.stderr:
545+
raise exc.LibTmuxException(proc.stderr)
546+
498547
def swap(
499548
self,
500549
target: str | Window,

tests/test_pane.py

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

742742

743+
def test_respawn_pane_kill(session: Session) -> None:
744+
"""Test Pane.respawn() with kill flag on active pane."""
745+
window = session.new_window(window_name="test_respawn")
746+
pane = window.active_pane
747+
assert pane is not None
748+
749+
# Respawn the active pane with kill
750+
pane.respawn(kill=True, shell="sh")
751+
752+
# Pane should still exist and be alive
753+
pane.refresh()
754+
assert pane in window.panes
755+
756+
743757
def test_join_pane(session: Session) -> None:
744758
"""Test Pane.join() roundtrip with break_pane."""
745759
window = session.new_window(window_name="test_join")

tests/test_window.py

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

823823

824+
def test_respawn_window(session: Session) -> None:
825+
"""Test Window.respawn() with kill flag."""
826+
window = session.new_window(window_name="test_respawn_w")
827+
828+
# Respawn the window with kill
829+
window.respawn(kill=True, shell="sh")
830+
831+
# Window should still exist
832+
window.refresh()
833+
session.refresh()
834+
assert window.window_id in [w.window_id for w in session.windows]
835+
836+
824837
def test_swap_window(session: Session) -> None:
825838
"""Test Window.swap() swaps two windows."""
826839
w1 = session.new_window(window_name="swap_w1")

0 commit comments

Comments
 (0)