Skip to content

Commit cf661bf

Browse files
committed
Pane(feat[send_keys]): add reset, copy-mode, repeat, hex, format, client, key-name flags
why: send-keys has many useful flags (reset terminal, hex input, repeat count, format expansion, copy-mode commands) that were not exposed in the Python API. what: - Add reset (-R), copy_mode_cmd (-X), repeat (-N), expand_formats (-F), hex_keys (-H), target_client (-c, 3.4+), key_name (-K, 3.4+) parameters - Version-gate target_client and key_name with has_gte_version("3.4") - Add SendKeysCase NamedTuple parametrized tests for all new flags
1 parent e822d30 commit cf661bf

2 files changed

Lines changed: 174 additions & 3 deletions

File tree

src/libtmux/pane.py

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,13 @@ def send_keys(
426426
enter: bool | None = True,
427427
suppress_history: bool | None = False,
428428
literal: bool | None = False,
429+
reset: bool | None = None,
430+
copy_mode_cmd: str | None = None,
431+
repeat: int | None = None,
432+
expand_formats: bool | None = None,
433+
hex_keys: bool | None = None,
434+
target_client: str | None = None,
435+
key_name: bool | None = None,
429436
) -> None:
430437
r"""``$ tmux send-keys`` to the pane.
431438
@@ -446,6 +453,29 @@ def send_keys(
446453
Default changed from True to False.
447454
literal : bool, optional
448455
Send keys literally, default False.
456+
reset : bool, optional
457+
Reset terminal state before sending keys (``-R`` flag).
458+
copy_mode_cmd : str, optional
459+
Send a command to copy mode instead of keys (``-X`` flag).
460+
When set, *cmd* is ignored.
461+
repeat : int, optional
462+
Repeat count for the key (``-N`` flag).
463+
expand_formats : bool, optional
464+
Expand tmux format strings in keys (``-F`` flag).
465+
466+
.. versionadded:: 0.45
467+
hex_keys : bool, optional
468+
Send keys as hex values (``-H`` flag).
469+
470+
.. versionadded:: 0.45
471+
target_client : str, optional
472+
Specify a target client (``-c`` flag). Requires tmux 3.4+.
473+
474+
.. versionadded:: 0.45
475+
key_name : bool, optional
476+
Handle keys as key names (``-K`` flag). Requires tmux 3.4+.
477+
478+
.. versionadded:: 0.45
449479
450480
Examples
451481
--------
@@ -463,14 +493,54 @@ def send_keys(
463493
Hello world
464494
$
465495
"""
496+
import warnings
497+
498+
from libtmux.common import has_gte_version
499+
466500
prefix = " " if suppress_history else ""
467501

502+
tmux_args: tuple[str, ...] = ()
503+
504+
if reset:
505+
tmux_args += ("-R",)
506+
507+
if expand_formats:
508+
tmux_args += ("-F",)
509+
510+
if hex_keys:
511+
tmux_args += ("-H",)
512+
513+
if key_name:
514+
if has_gte_version("3.4", tmux_bin=self.server.tmux_bin):
515+
tmux_args += ("-K",)
516+
else:
517+
warnings.warn(
518+
"key_name requires tmux 3.4+, ignoring",
519+
stacklevel=2,
520+
)
521+
468522
if literal:
469-
self.cmd("send-keys", "-l", prefix + cmd)
523+
tmux_args += ("-l",)
524+
525+
if repeat is not None:
526+
tmux_args += ("-N", str(repeat))
527+
528+
if target_client is not None:
529+
if has_gte_version("3.4", tmux_bin=self.server.tmux_bin):
530+
tmux_args += ("-c", target_client)
531+
else:
532+
warnings.warn(
533+
"target_client requires tmux 3.4+, ignoring",
534+
stacklevel=2,
535+
)
536+
537+
if copy_mode_cmd is not None:
538+
tmux_args += ("-X",)
539+
self.cmd("send-keys", *tmux_args, copy_mode_cmd)
470540
else:
471-
self.cmd("send-keys", prefix + cmd)
541+
self.cmd("send-keys", *tmux_args, prefix + cmd)
472542

473-
if enter:
543+
if enter and copy_mode_cmd is None:
474544
self.enter()
475545

476546
@t.overload

tests/test_pane.py

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

1010
import pytest
1111

12+
from libtmux.common import has_gte_version
1213
from libtmux.constants import PaneDirection, ResizeAdjustmentDirection
1314
from libtmux.test.retry import retry_until
1415

@@ -443,3 +444,103 @@ def test_split_start_directory_pathlib(
443444
actual_path = str(pathlib.Path(new_pane.pane_current_path).resolve())
444445
expected_path = str(user_path.resolve())
445446
assert actual_path == expected_path
447+
448+
449+
class SendKeysCase(t.NamedTuple):
450+
"""Test case for send_keys() flag variations."""
451+
452+
test_id: str
453+
key: str
454+
kwargs: dict[str, t.Any]
455+
expected_in_capture: str | None
456+
not_expected_in_capture: str | None
457+
min_tmux_version: str | None
458+
459+
460+
SEND_KEYS_CASES: list[SendKeysCase] = [
461+
SendKeysCase(
462+
test_id="reset_terminal",
463+
key="",
464+
kwargs={"reset": True, "enter": False},
465+
expected_in_capture=None,
466+
not_expected_in_capture=None,
467+
min_tmux_version=None,
468+
),
469+
SendKeysCase(
470+
test_id="repeat_count",
471+
key="a",
472+
kwargs={"repeat": 3, "literal": True, "enter": False},
473+
expected_in_capture="aaa",
474+
not_expected_in_capture=None,
475+
min_tmux_version=None,
476+
),
477+
SendKeysCase(
478+
test_id="hex_key_A",
479+
key="41",
480+
kwargs={"hex_keys": True, "enter": False},
481+
expected_in_capture="A",
482+
not_expected_in_capture=None,
483+
min_tmux_version=None,
484+
),
485+
SendKeysCase(
486+
test_id="expand_formats",
487+
key="a",
488+
kwargs={"expand_formats": True, "repeat": 2, "enter": False},
489+
expected_in_capture="aa",
490+
not_expected_in_capture=None,
491+
min_tmux_version=None,
492+
),
493+
SendKeysCase(
494+
test_id="key_name_flag",
495+
key="a",
496+
kwargs={"key_name": True, "enter": False},
497+
expected_in_capture=None,
498+
not_expected_in_capture=None,
499+
min_tmux_version="3.4",
500+
),
501+
]
502+
503+
504+
@pytest.mark.parametrize(
505+
list(SendKeysCase._fields),
506+
SEND_KEYS_CASES,
507+
ids=[c.test_id for c in SEND_KEYS_CASES],
508+
)
509+
def test_send_keys_flags(
510+
test_id: str,
511+
key: str,
512+
kwargs: dict[str, t.Any],
513+
expected_in_capture: str | None,
514+
not_expected_in_capture: str | None,
515+
min_tmux_version: str | None,
516+
session: Session,
517+
) -> None:
518+
"""Test send_keys() with various flag combinations."""
519+
if min_tmux_version and not has_gte_version(min_tmux_version):
520+
pytest.skip(f"Requires tmux {min_tmux_version}+")
521+
522+
env = shutil.which("env")
523+
assert env is not None, "Cannot find usable `env` in PATH."
524+
525+
window = session.new_window(
526+
window_name=f"sk_{test_id[:15]}",
527+
window_shell=f"{env} PS1='$ ' sh",
528+
)
529+
pane = window.active_pane
530+
assert pane is not None
531+
532+
retry_until(lambda: "$" in "\n".join(pane.capture_pane()), 2, raises=True)
533+
534+
pane.send_keys(key, **kwargs)
535+
536+
if expected_in_capture is not None:
537+
retry_until(
538+
lambda: expected_in_capture in "\n".join(pane.capture_pane()),
539+
3,
540+
raises=True,
541+
)
542+
543+
if not_expected_in_capture is not None:
544+
# Give a brief moment then verify absence
545+
contents = "\n".join(pane.capture_pane())
546+
assert not_expected_in_capture not in contents

0 commit comments

Comments
 (0)