Skip to content

Commit 118f61f

Browse files
committed
Pane(feat[display_popup]): add display_popup() wrapping tmux display-popup
why: display-popup (3.2+) creates overlay popups for running commands, useful in interactive tmux sessions and automatable via control mode. what: - Add display_popup() with command, close_on_exit (-E), close_on_success (-C), width (-w), height (-h), start_directory (-d) core parameters - Version-gate 3.3+ flags: title (-T), border_lines (-b), border_style (-s), environment (-e) - Test uses ControlMode to create a client, then verifies popup command ran by checking for a marker file side-effect (no mocking)
1 parent c92cea7 commit 118f61f

2 files changed

Lines changed: 154 additions & 0 deletions

File tree

src/libtmux/pane.py

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

1142+
def display_popup(
1143+
self,
1144+
command: str | None = None,
1145+
*,
1146+
close_on_exit: bool | None = None,
1147+
close_on_success: bool | None = None,
1148+
width: int | str | None = None,
1149+
height: int | str | None = None,
1150+
start_directory: StrPath | None = None,
1151+
title: str | None = None,
1152+
border_lines: str | None = None,
1153+
border_style: str | None = None,
1154+
environment: dict[str, str] | None = None,
1155+
) -> None:
1156+
"""Display a popup overlay via ``$ tmux display-popup``.
1157+
1158+
Requires tmux 3.2+ and an attached client. Use
1159+
:class:`~libtmux.test.control_mode.ControlMode` in tests to provide
1160+
a client.
1161+
1162+
Parameters
1163+
----------
1164+
command : str, optional
1165+
Shell command to run in the popup.
1166+
close_on_exit : bool, optional
1167+
Close popup when command exits (``-E`` flag).
1168+
close_on_success : bool, optional
1169+
Close popup only on success exit code (``-C`` flag).
1170+
width : int or str, optional
1171+
Popup width (``-w`` flag).
1172+
height : int or str, optional
1173+
Popup height (``-h`` flag).
1174+
start_directory : str or PathLike, optional
1175+
Working directory (``-d`` flag).
1176+
title : str, optional
1177+
Popup title (``-T`` flag). Requires tmux 3.3+.
1178+
border_lines : str, optional
1179+
Border line style (``-b`` flag). Requires tmux 3.3+.
1180+
border_style : str, optional
1181+
Border style (``-s`` flag). Requires tmux 3.3+.
1182+
environment : dict, optional
1183+
Environment variables (``-e`` flag). Requires tmux 3.3+.
1184+
1185+
.. versionadded:: 0.45
1186+
1187+
Examples
1188+
--------
1189+
>>> with control_mode() as ctl:
1190+
... pane.display_popup(command='true', close_on_exit=True)
1191+
"""
1192+
import warnings
1193+
1194+
from libtmux.common import has_gte_version
1195+
1196+
tmux_args: tuple[str, ...] = ()
1197+
1198+
if close_on_exit:
1199+
tmux_args += ("-E",)
1200+
1201+
if close_on_success:
1202+
tmux_args += ("-C",)
1203+
1204+
if width is not None:
1205+
tmux_args += ("-w", str(width))
1206+
1207+
if height is not None:
1208+
tmux_args += ("-h", str(height))
1209+
1210+
if start_directory is not None:
1211+
start_path = pathlib.Path(start_directory).expanduser()
1212+
tmux_args += ("-d", str(start_path))
1213+
1214+
if title is not None:
1215+
if has_gte_version("3.3", tmux_bin=self.server.tmux_bin):
1216+
tmux_args += ("-T", title)
1217+
else:
1218+
warnings.warn(
1219+
"title requires tmux 3.3+, ignoring",
1220+
stacklevel=2,
1221+
)
1222+
1223+
if border_lines is not None:
1224+
if has_gte_version("3.3", tmux_bin=self.server.tmux_bin):
1225+
tmux_args += ("-b", border_lines)
1226+
else:
1227+
warnings.warn(
1228+
"border_lines requires tmux 3.3+, ignoring",
1229+
stacklevel=2,
1230+
)
1231+
1232+
if border_style is not None:
1233+
if has_gte_version("3.3", tmux_bin=self.server.tmux_bin):
1234+
tmux_args += ("-s", border_style)
1235+
else:
1236+
warnings.warn(
1237+
"border_style requires tmux 3.3+, ignoring",
1238+
stacklevel=2,
1239+
)
1240+
1241+
if environment:
1242+
if has_gte_version("3.3", tmux_bin=self.server.tmux_bin):
1243+
for k, v in environment.items():
1244+
tmux_args += ("-e", f"{k}={v}")
1245+
else:
1246+
warnings.warn(
1247+
"environment requires tmux 3.3+, ignoring",
1248+
stacklevel=2,
1249+
)
1250+
1251+
if command is not None:
1252+
tmux_args += (command,)
1253+
1254+
proc = self.cmd("display-popup", *tmux_args)
1255+
1256+
if proc.stderr:
1257+
raise exc.LibTmuxException(proc.stderr)
1258+
11421259
def paste_buffer(
11431260
self,
11441261
*,

tests/test_pane.py

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

742742

743+
def test_display_popup_runs_command(
744+
control_mode: t.Callable[..., t.Any],
745+
session: Session,
746+
tmp_path: pathlib.Path,
747+
) -> None:
748+
"""Test Pane.display_popup() runs a command — verified by file side-effect."""
749+
marker = tmp_path / "popup_ran.marker"
750+
pane = session.active_window.active_pane
751+
assert pane is not None
752+
753+
with control_mode():
754+
pane.display_popup(command=f"touch {marker}", close_on_exit=True)
755+
756+
retry_until(lambda: marker.exists(), 3, raises=True)
757+
758+
759+
def test_display_popup_with_dimensions(
760+
control_mode: t.Callable[..., t.Any],
761+
session: Session,
762+
tmp_path: pathlib.Path,
763+
) -> None:
764+
"""Test Pane.display_popup() with width and height."""
765+
marker = tmp_path / "popup_sized.marker"
766+
pane = session.active_window.active_pane
767+
assert pane is not None
768+
769+
with control_mode():
770+
pane.display_popup(
771+
command=f"touch {marker}",
772+
close_on_exit=True,
773+
width=40,
774+
height=10,
775+
)
776+
777+
retry_until(lambda: marker.exists(), 3, raises=True)
778+
779+
743780
def test_paste_buffer(session: Session) -> None:
744781
"""Test Pane.paste_buffer() pastes buffer content into pane."""
745782
env = shutil.which("env")

0 commit comments

Comments
 (0)