diff --git a/CHANGES b/CHANGES index 86d5af26f..df5f059fb 100644 --- a/CHANGES +++ b/CHANGES @@ -14,6 +14,11 @@ $ pip install --user --upgrade --pre libtmux +### Features + +- `Window.split_window()` and `Session.new_window()` now support an optional + dictionary of environmental variables, via (#453), credit @zappolowski. + ## libtmux 0.15.10 (2022-11-05) _There will be more improvements over the coming weeks and months to shore up diff --git a/src/libtmux/session.py b/src/libtmux/session.py index f74c6e2a3..37cd8c122 100644 --- a/src/libtmux/session.py +++ b/src/libtmux/session.py @@ -19,6 +19,7 @@ TmuxRelationalObject, WindowDict, handle_option_error, + has_gte_version, has_version, session_check_name, ) @@ -202,6 +203,7 @@ def new_window( attach: bool = True, window_index: str = "", window_shell: t.Optional[str] = None, + environment: t.Optional[t.Dict[str, str]] = None, ) -> Window: """ Return :class:`Window` from ``$ tmux new-window``. @@ -227,6 +229,9 @@ def new_window( When this command exits the window will close. This feature is useful for long-running processes where the closing of the window upon completion is desired. + environment: dict, optional + Environmental variables for new window. tmux 3.0+ only. Passthrough to + ``-e``. Returns ------- @@ -259,6 +264,15 @@ def new_window( % (self.id, window_index), ) + if environment: + if has_gte_version("3.0"): + for k, v in environment.items(): + window_args += (f"-e{k}={v}",) + else: + logger.warning( + "Cannot set up environment as tmux 3.0 or newer is required." + ) + if window_shell: window_args += (window_shell,) diff --git a/src/libtmux/window.py b/src/libtmux/window.py index 434dca17c..0075b0187 100644 --- a/src/libtmux/window.py +++ b/src/libtmux/window.py @@ -9,7 +9,7 @@ import shlex import typing as t -from libtmux.common import tmux_cmd +from libtmux.common import has_gte_version, tmux_cmd from libtmux.pane import Pane from . import exc, formats @@ -442,6 +442,7 @@ def split_window( vertical: bool = True, shell: t.Optional[str] = None, percent: t.Optional[int] = None, + environment: t.Optional[t.Dict[str, str]] = None, ) -> Pane: """ Split window and return the created :class:`Pane`. @@ -468,6 +469,8 @@ def split_window( window upon completion is desired. percent: int, optional percentage to occupy with respect to current window + environment: dict, optional + Environmental variables for new pane. tmux 3.0+ only. Passthrough to ``-e``. Returns ------- @@ -520,6 +523,15 @@ def split_window( if not attach: tmux_args += ("-d",) + if environment: + if has_gte_version("3.0"): + for k, v in environment.items(): + tmux_args += (f"-e{k}={v}",) + else: + logger.warning( + "Cannot set up environment as tmux 3.0 or newer is required." + ) + if shell: tmux_args += (shell,) diff --git a/tests/test_session.py b/tests/test_session.py index c23d17c61..b3db613cb 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -1,11 +1,12 @@ """Test for libtmux Session object.""" import logging +import shutil import typing as t import pytest from libtmux import exc -from libtmux.common import has_gte_version +from libtmux.common import has_gte_version, has_lt_version from libtmux.pane import Pane from libtmux.server import Server from libtmux.session import Session @@ -257,3 +258,57 @@ def test_cmd_inserts_sesion_id(session: Session) -> None: assert "-t" in cmd.cmd assert current_session_id in cmd.cmd assert cmd.cmd[-1] == last_arg + + +@pytest.mark.skipif( + has_lt_version("3.0"), + reason="needs -e flag for new-window which was introduced in 3.0", +) +@pytest.mark.parametrize( + "environment", + [ + {"ENV_VAR": "window"}, + {"ENV_VAR_1": "window_1", "ENV_VAR_2": "window_2"}, + ], +) +def test_new_window_with_environment( + session: Session, + environment: t.Dict[str, str], +) -> None: + env = shutil.which("env") + assert env is not None, "Cannot find usable `env` in PATH." + + window = session.new_window( + attach=True, + window_name="window_with_environment", + window_shell=f"{env} PS1='$ ' sh", + environment=environment, + ) + pane = window.attached_pane + assert pane is not None + for k, v in environment.items(): + pane.send_keys(f"echo ${k}") + assert pane.capture_pane()[-2] == v + + +@pytest.mark.skipif( + has_gte_version("3.0"), + reason="3.0 has the -e flag on new-window", +) +def test_new_window_with_environment_logs_warning_for_old_tmux( + session: Session, + caplog: pytest.LogCaptureFixture, +) -> None: + env = shutil.which("env") + assert env is not None, "Cannot find usable `env` in PATH." + + session.new_window( + attach=True, + window_name="window_with_environment", + window_shell=f"{env} PS1='$ ' sh", + environment={"ENV_VAR": "window"}, + ) + + assert any( + "Cannot set up environment" in record.msg for record in caplog.records + ), "Warning missing" diff --git a/tests/test_window.py b/tests/test_window.py index 4c10c9c2a..02fb40552 100644 --- a/tests/test_window.py +++ b/tests/test_window.py @@ -1,5 +1,7 @@ """Test for libtmux Window object.""" import logging +import shutil +import time import typing as t import pytest @@ -310,3 +312,56 @@ def test_empty_window_name(session: Session) -> None: "#{==:#{session_name}," + session.name + "}", ) assert "''" in cmd.stdout + + +@pytest.mark.skipif( + has_lt_version("3.0"), + reason="needs -e flag for split-window which was introduced in 3.0", +) +@pytest.mark.parametrize( + "environment", + [ + {"ENV_VAR": "pane"}, + {"ENV_VAR_1": "pane_1", "ENV_VAR_2": "pane_2"}, + ], +) +def test_split_window_with_environment( + session: Session, + environment: t.Dict[str, str], +) -> None: + env = shutil.which("env") + assert env is not None, "Cannot find usable `env` in Path." + + window = session.new_window(window_name="split_window_with_environment") + pane = window.split_window( + shell=f"{env} PS1='$ ' sh", + environment=environment, + ) + assert pane is not None + # wait a bit for the prompt to be ready as the test gets flaky otherwise + time.sleep(0.05) + for k, v in environment.items(): + pane.send_keys(f"echo ${k}") + assert pane.capture_pane()[-2] == v + + +@pytest.mark.skipif( + has_gte_version("3.0"), + reason="3.0 has the -e flag on split-window", +) +def test_split_window_with_environment_logs_warning_for_old_tmux( + session: Session, + caplog: pytest.LogCaptureFixture, +) -> None: + env = shutil.which("env") + assert env is not None, "Cannot find usable `env` in Path." + + window = session.new_window(window_name="split_window_with_environment") + window.split_window( + shell=f"{env} PS1='$ ' sh", + environment={"ENV_VAR": "pane"}, + ) + + assert any( + "Cannot set up environment" in record.msg for record in caplog.records + ), "Warning missing"