Skip to content

Commit b28db9a

Browse files
committed
Server(feat): add confirm_before, command_prompt wrapping interactive tmux commands
why: confirm-before and command-prompt were previously untestable because they block waiting for user input. Using send-keys -K -c (tmux 3.4+) we can inject key events into the client's prompt handler programmatically. what: - Add Server.confirm_before() wrapping confirm-before with -b (always non-blocking), prompt (-p), confirm_key (-c), default_yes (-y), target_client (-t) parameters - Add Server.command_prompt() wrapping command-prompt with -b (always non-blocking), prompt (-p), inputs (-I), target_client (-t) parameters - Both use send-keys -K -c <client> for confirmation/input in tests - ConfirmBeforeCase parametrized tests: confirm_y, default_yes_enter - CommandPromptCase parametrized tests: type_and_submit, prefill_and_submit - All tests version-gated to tmux 3.4+
1 parent be42841 commit b28db9a

2 files changed

Lines changed: 257 additions & 0 deletions

File tree

src/libtmux/server.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,123 @@ def lock_client(self, *, target_client: str | None = None) -> None:
825825
if proc.stderr:
826826
raise exc.LibTmuxException(proc.stderr)
827827

828+
def confirm_before(
829+
self,
830+
command: str,
831+
*,
832+
prompt: str | None = None,
833+
confirm_key: str | None = None,
834+
default_yes: bool | None = None,
835+
target_client: str | None = None,
836+
) -> None:
837+
"""Run a command after confirmation via ``$ tmux confirm-before``.
838+
839+
Always uses ``-b`` (background) to avoid blocking the command queue.
840+
Use ``send-keys -K -c <client>`` to provide the confirmation key.
841+
842+
Requires tmux 3.4+ for ``-b`` flag support.
843+
844+
Parameters
845+
----------
846+
command : str
847+
Tmux command to run after confirmation.
848+
prompt : str, optional
849+
Custom prompt text (``-p`` flag).
850+
confirm_key : str, optional
851+
Key to accept as confirmation (``-c`` flag). Default is ``y``.
852+
default_yes : bool, optional
853+
Make Enter default to yes (``-y`` flag).
854+
target_client : str, optional
855+
Target client (``-t`` flag).
856+
857+
Examples
858+
--------
859+
>>> with control_mode() as ctl:
860+
... server.confirm_before(
861+
... 'set -g @cf_test yes',
862+
... target_client=ctl.client_name,
863+
... )
864+
... _ = server.cmd('send-keys', '-K', '-c', ctl.client_name, 'y')
865+
... server.cmd('show-options', '-gv', '@cf_test').stdout[0]
866+
'yes'
867+
"""
868+
tmux_args: tuple[str, ...] = ("-b",)
869+
870+
if prompt is not None:
871+
tmux_args += ("-p", prompt)
872+
873+
if confirm_key is not None:
874+
tmux_args += ("-c", confirm_key)
875+
876+
if default_yes:
877+
tmux_args += ("-y",)
878+
879+
if target_client is not None:
880+
tmux_args += ("-t", target_client)
881+
882+
tmux_args += (command,)
883+
884+
proc = self.cmd("confirm-before", *tmux_args)
885+
886+
if proc.stderr:
887+
raise exc.LibTmuxException(proc.stderr)
888+
889+
def command_prompt(
890+
self,
891+
template: str,
892+
*,
893+
prompt: str | None = None,
894+
inputs: str | None = None,
895+
target_client: str | None = None,
896+
) -> None:
897+
"""Open a command prompt via ``$ tmux command-prompt``.
898+
899+
Always uses ``-b`` (background) to avoid blocking the command queue.
900+
Use ``send-keys -K -c <client>`` to type into the prompt and submit.
901+
902+
Requires tmux 3.4+ for ``-b`` flag support.
903+
904+
Parameters
905+
----------
906+
template : str
907+
Tmux command template. Use ``%1``, ``%2`` for prompt values.
908+
prompt : str, optional
909+
Custom prompt text (``-p`` flag). Commas separate multiple prompts.
910+
inputs : str, optional
911+
Pre-fill prompt input (``-I`` flag). Commas separate multiple.
912+
target_client : str, optional
913+
Target client (``-t`` flag).
914+
915+
Examples
916+
--------
917+
>>> with control_mode() as ctl:
918+
... server.command_prompt(
919+
... "set -g @cp_test '%1'",
920+
... target_client=ctl.client_name,
921+
... )
922+
... for key in ['h', 'i', 'Enter']:
923+
... _ = server.cmd('send-keys', '-K', '-c', ctl.client_name, key)
924+
... server.cmd('show-options', '-gv', '@cp_test').stdout[0]
925+
'hi'
926+
"""
927+
tmux_args: tuple[str, ...] = ("-b",)
928+
929+
if prompt is not None:
930+
tmux_args += ("-p", prompt)
931+
932+
if inputs is not None:
933+
tmux_args += ("-I", inputs)
934+
935+
if target_client is not None:
936+
tmux_args += ("-t", target_client)
937+
938+
tmux_args += (template,)
939+
940+
proc = self.cmd("command-prompt", *tmux_args)
941+
942+
if proc.stderr:
943+
raise exc.LibTmuxException(proc.stderr)
944+
828945
def start_server(self) -> None:
829946
"""Start the tmux server if not already running.
830947

tests/test_server.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,146 @@ def test_tmux_bin_invalid_path_raise_if_dead() -> None:
461461
s.raise_if_dead()
462462

463463

464+
class ConfirmBeforeCase(t.NamedTuple):
465+
"""Test case for confirm_before()."""
466+
467+
test_id: str
468+
confirm_key: str
469+
use_default_yes: bool
470+
custom_confirm_key: str | None
471+
option_name: str
472+
expected_value: str
473+
min_tmux_version: str | None
474+
475+
476+
CONFIRM_BEFORE_CASES: list[ConfirmBeforeCase] = [
477+
ConfirmBeforeCase(
478+
test_id="confirm_y",
479+
confirm_key="y",
480+
use_default_yes=False,
481+
custom_confirm_key=None,
482+
option_name="@cf_test_y",
483+
expected_value="yes",
484+
min_tmux_version="3.4",
485+
),
486+
ConfirmBeforeCase(
487+
test_id="default_yes_enter",
488+
confirm_key="Enter",
489+
use_default_yes=True,
490+
custom_confirm_key=None,
491+
option_name="@cf_test_enter",
492+
expected_value="yes",
493+
min_tmux_version="3.4",
494+
),
495+
]
496+
497+
498+
@pytest.mark.parametrize(
499+
list(ConfirmBeforeCase._fields),
500+
CONFIRM_BEFORE_CASES,
501+
ids=[c.test_id for c in CONFIRM_BEFORE_CASES],
502+
)
503+
def test_confirm_before(
504+
test_id: str,
505+
confirm_key: str,
506+
use_default_yes: bool,
507+
custom_confirm_key: str | None,
508+
option_name: str,
509+
expected_value: str,
510+
min_tmux_version: str | None,
511+
control_mode: t.Callable[..., t.Any],
512+
server: Server,
513+
) -> None:
514+
"""Test Server.confirm_before() with send-keys -K confirmation."""
515+
from libtmux.common import has_gte_version
516+
517+
if min_tmux_version and not has_gte_version(min_tmux_version):
518+
pytest.skip(f"Requires tmux {min_tmux_version}+")
519+
520+
with control_mode() as ctl:
521+
kwargs: dict[str, t.Any] = {"target_client": ctl.client_name}
522+
if use_default_yes:
523+
kwargs["default_yes"] = True
524+
if custom_confirm_key is not None:
525+
kwargs["confirm_key"] = custom_confirm_key
526+
527+
server.confirm_before(f"set -g {option_name} {expected_value}", **kwargs)
528+
server.cmd("send-keys", "-K", "-c", ctl.client_name, confirm_key)
529+
530+
result = server.cmd("show-options", "-gv", option_name)
531+
assert result.stdout[0] == expected_value
532+
533+
534+
class CommandPromptCase(t.NamedTuple):
535+
"""Test case for command_prompt()."""
536+
537+
test_id: str
538+
template: str
539+
keys: list[str]
540+
inputs: str | None
541+
option_name: str
542+
expected_value: str
543+
min_tmux_version: str | None
544+
545+
546+
COMMAND_PROMPT_CASES: list[CommandPromptCase] = [
547+
CommandPromptCase(
548+
test_id="type_and_submit",
549+
template="set -g @cp_typed '%1'",
550+
keys=["h", "e", "l", "l", "o", "Enter"],
551+
inputs=None,
552+
option_name="@cp_typed",
553+
expected_value="hello",
554+
min_tmux_version="3.4",
555+
),
556+
CommandPromptCase(
557+
test_id="prefill_and_submit",
558+
template="set -g @cp_prefill '%1'",
559+
keys=["Enter"],
560+
inputs="prefilled",
561+
option_name="@cp_prefill",
562+
expected_value="prefilled",
563+
min_tmux_version="3.4",
564+
),
565+
]
566+
567+
568+
@pytest.mark.parametrize(
569+
list(CommandPromptCase._fields),
570+
COMMAND_PROMPT_CASES,
571+
ids=[c.test_id for c in COMMAND_PROMPT_CASES],
572+
)
573+
def test_command_prompt(
574+
test_id: str,
575+
template: str,
576+
keys: list[str],
577+
inputs: str | None,
578+
option_name: str,
579+
expected_value: str,
580+
min_tmux_version: str | None,
581+
control_mode: t.Callable[..., t.Any],
582+
server: Server,
583+
) -> None:
584+
"""Test Server.command_prompt() with send-keys -K input."""
585+
from libtmux.common import has_gte_version
586+
587+
if min_tmux_version and not has_gte_version(min_tmux_version):
588+
pytest.skip(f"Requires tmux {min_tmux_version}+")
589+
590+
with control_mode() as ctl:
591+
kwargs: dict[str, t.Any] = {"target_client": ctl.client_name}
592+
if inputs is not None:
593+
kwargs["inputs"] = inputs
594+
595+
server.command_prompt(template, **kwargs)
596+
597+
for key in keys:
598+
server.cmd("send-keys", "-K", "-c", ctl.client_name, key)
599+
600+
result = server.cmd("show-options", "-gv", option_name)
601+
assert result.stdout[0] == expected_value
602+
603+
464604
def test_lock_server(
465605
control_mode: t.Callable[..., t.Any],
466606
server: Server,

0 commit comments

Comments
 (0)