Files
nixpkgs/pkgs/development/python-modules/libtmux/0003-Popup-test-Attach-a-terminal-client.patch
Otavio Salvador 3d5072d457 python3Packages.libtmux: 0.61.0 -> 0.62.0
Changelog: https://github.com/tmux-python/libtmux/blob/v0.62.0/CHANGES

Backport the tmux 3.8 fixes from upstream PR #758 so the full test suite passes with tmux 3.8:

- decode the new hooks tmux 3.8 reports from show-hooks, which otherwise break Hooks
- run the display-popup tests through a terminal client, as control-mode clients no longer
  execute popup commands
- account for tmux 3.8 counting borders in floating pane geometry

Upstream PR: https://github.com/tmux-python/libtmux/pull/758

Assisted-by: Claude Code (Claude Opus 5.5)
2026-10-11 21:27:44 -03:00

192 lines
6.3 KiB
Diff

From 2466d5ec8b8e185c71839196aaac8cf59133ff75 Mon Sep 17 00:00:00 2001
From: Tony Narlock <tony@git-pull.com>
Date: Sun, 13 Sep 2026 10:33:12 -0500
Subject: [PATCH] Popup(test): Attach a terminal client
why: Control clients do not execute popup commands on tmux master.
what:
- Exercise popup flags and completion through a real terminal client
- Bound process cleanup and close PTYs on setup or wait failure
- State the control-client limitation in its module documentation
Upstream: https://github.com/tmux-python/libtmux/pull/758/commits/2466d5ec8b8e185c71839196aaac8cf59133ff75
---
src/libtmux/_internal/control_mode.py | 4 +-
tests/test_pane.py | 97 +++++++++++++++++++++------
2 files changed, 80 insertions(+), 21 deletions(-)
diff --git a/src/libtmux/_internal/control_mode.py b/src/libtmux/_internal/control_mode.py
index 6c1d230..1d73271 100644
--- a/src/libtmux/_internal/control_mode.py
+++ b/src/libtmux/_internal/control_mode.py
@@ -1,8 +1,8 @@
"""Control-mode client context manager for tmux testing.
Provides a context manager that spawns a ``tmux -C attach-session``
-subprocess, creating a real tmux client that satisfies commands
-requiring an attached client (e.g. ``display-popup``, ``detach-client``).
+subprocess, creating a real tmux client for commands such as
+``detach-client``. Popups require a terminal client to run their commands.
"""
from __future__ import annotations
diff --git a/tests/test_pane.py b/tests/test_pane.py
index 416032c..4865203 100644
--- a/tests/test_pane.py
+++ b/tests/test_pane.py
@@ -2,9 +2,17 @@
from __future__ import annotations
+import contextlib
+import fcntl
import logging
+import os
import pathlib
+import select
import shutil
+import struct
+import subprocess
+import termios
+import threading
import typing as t
import pytest
@@ -1082,6 +1090,65 @@ DISPLAY_POPUP_CASES: list[DisplayPopupCase] = [
]
+@pytest.fixture
+def terminal_client(server: Server, session: Session) -> t.Iterator[str]:
+ """Attach a terminal client for popup execution."""
+ socket_path = server.cmd("display-message", "-p", "#{socket_path}").stdout[0]
+ with contextlib.ExitStack() as cleanup:
+ master, slave = os.openpty()
+ cleanup.callback(os.close, master)
+ cleanup.callback(os.close, slave)
+ stop = threading.Event()
+ client: subprocess.Popen[bytes] | None = None
+
+ def drain() -> None:
+ while not stop.is_set():
+ if select.select([master], [], [], 0.1)[0]:
+ try:
+ if not os.read(master, 65536):
+ return
+ except OSError:
+ return
+
+ try:
+ fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack("HHHH", 30, 100, 0, 0))
+ client = subprocess.Popen(
+ [
+ server.tmux_bin or "tmux",
+ "-S",
+ socket_path,
+ "attach-session",
+ "-t",
+ str(session.session_id),
+ ],
+ stdin=slave,
+ stdout=slave,
+ stderr=slave,
+ env={**os.environ, "TERM": "xterm-256color"},
+ start_new_session=True,
+ )
+ reader = threading.Thread(target=drain, daemon=True)
+ reader.start()
+ cleanup.callback(reader.join, timeout=1)
+ cleanup.callback(stop.set)
+ client_name = os.ttyname(slave)
+
+ def attached() -> bool:
+ clients = server.cmd("list-clients", "-F", "#{client_name}").stdout
+ return client_name in clients
+
+ retry_until(attached, 3, raises=True)
+ yield client_name
+ finally:
+ if client is not None:
+ client.terminate()
+ try:
+ client.wait(timeout=3)
+ except subprocess.TimeoutExpired:
+ client.kill()
+ client.wait(timeout=3)
+
+
@pytest.mark.parametrize(
list(DisplayPopupCase._fields),
DISPLAY_POPUP_CASES,
@@ -1091,7 +1158,7 @@ def test_display_popup_flags(
test_id: str,
kwargs: dict[str, t.Any],
min_tmux_version: str | None,
- control_mode: t.Callable[..., t.Any],
+ terminal_client: str,
session: Session,
tmp_path: pathlib.Path,
) -> None:
@@ -1110,14 +1177,13 @@ def test_display_popup_flags(
call_kwargs = {"command": f"touch {marker}", "close_on_exit": True, **kwargs}
- with control_mode():
- pane.display_popup(**call_kwargs)
+ pane.display_popup(**call_kwargs)
retry_until(lambda: marker.exists(), 3, raises=True)
def test_display_popup_close_on_success(
- control_mode: t.Callable[..., t.Any],
+ terminal_client: str,
session: Session,
tmp_path: pathlib.Path,
) -> None:
@@ -1126,8 +1192,7 @@ def test_display_popup_close_on_success(
pane = session.active_window.active_pane
assert pane is not None
- with control_mode():
- pane.display_popup(command=f"touch {marker}", close_on_success=True)
+ pane.display_popup(command=f"touch {marker}", close_on_success=True)
retry_until(lambda: marker.exists(), 3, raises=True)
@@ -1157,26 +1222,20 @@ def test_display_popup_close_existing(
def test_display_popup_target_client(
- control_mode: t.Callable[..., t.Any],
+ terminal_client: str,
session: Session,
tmp_path: pathlib.Path,
) -> None:
- """Test Pane.display_popup(target_client=...) emits ``-c <client>``.
-
- ``-c`` has been on ``display-popup`` since tmux 3.2a, so no version
- guard is needed. The popup itself is invisible without a TTY-backed
- client; this is a smoke test for the flag-passing path.
- """
+ """Run the popup command on the specified terminal client."""
pane = session.active_window.active_pane
assert pane is not None
marker = tmp_path / "popup_target_client.marker"
- with control_mode() as ctl:
- pane.display_popup(
- command=f"touch {marker}",
- close_on_exit=True,
- target_client=ctl.client_name,
- )
+ pane.display_popup(
+ command=f"touch {marker}",
+ close_on_exit=True,
+ target_client=terminal_client,
+ )
retry_until(lambda: marker.exists(), 3, raises=True)