diff --git a/CHANGES b/CHANGES index 3261ec59af3..78c152ec0ee 100644 --- a/CHANGES +++ b/CHANGES @@ -17,7 +17,28 @@ $ pipx install --suffix=@next 'tmuxp' --pip-args '\--pre' --force ## tmuxp 1.19.x (unreleased) -- Notes on upcoming releases will be added here +### What's new + +- #845: allow to configure window and pane specific environment variables + + Having a setup like: + ```yaml + session_name: env-demo + environment: + DATABASE_URL: "sqlite3:///default.db" + windows: + - window_name: dev + environment: + DATABASE_URL: "sqlite3:///dev-1.db" + panes: + - pane + - environment: + DATABASE_URL: "sqlite3:///dev-2.db" + ``` + will result in a window with two panes. In the first pane `$DATABASE_URL` is + `sqlite3:///dev-1.db`, while in the second pane it is `sqlite3://dev-2.db`. + Any freshly created window gets `sqlite3:///default.db` as this is what was + defined for the session. diff --git a/docs/configuration/examples.md b/docs/configuration/examples.md index 6677c6d64f8..33c85057962 100644 --- a/docs/configuration/examples.md +++ b/docs/configuration/examples.md @@ -262,7 +262,9 @@ please make a ticket on the [issue tracker][issue tracker]. ## Environment variables -tmuxp will set session environment variables. +tmuxp will set session, window and pane environment variables. Note that +setting environment variables for windows and panes requires tmuxp 1.19 or +newer. ````{tab} YAML diff --git a/examples/session-environment.json b/examples/session-environment.json index d4c5c17cf91..3c31e12ec32 100644 --- a/examples/session-environment.json +++ b/examples/session-environment.json @@ -2,15 +2,32 @@ "environment": { "EDITOR": "/usr/bin/vim", "DJANGO_SETTINGS_MODULE": "my_app.settings.local", - "SERVER_PORT": "8009", - }, + "SERVER_PORT": "8009" + }, "windows": [ { "panes": [ - "./manage.py runserver 0.0.0.0:${SERVER_PORT}" - ], + "./manage.py runserver 0.0.0.0:${SERVER_PORT}" + ], "window_name": "Django project" - }, - ], + }, + { + "environment": { + "DJANGO_SETTINGS_MODULE": "my_app.settings.local", + "SERVER_PORT": "8010" + }, + "panes": [ + "./manage.py runserver 0.0.0.0:${SERVER_PORT}", + { + "environment": { + "DJANGO_SETTINGS_MODULE": "my_app.settings.local-testing", + "SERVER_PORT": "8011" + }, + "shell_command": "./manage.py runserver 0.0.0.0:${SERVER_PORT}" + } + ], + "window_name": "Another Django project" + } + ], "session_name": "Environment variables test" -} +} \ No newline at end of file diff --git a/examples/session-environment.yaml b/examples/session-environment.yaml index 1c766b56e4c..a1091e7a181 100644 --- a/examples/session-environment.yaml +++ b/examples/session-environment.yaml @@ -7,3 +7,13 @@ windows: - window_name: Django project panes: - ./manage.py runserver 0.0.0.0:${SERVER_PORT} + - window_name: Another Django project + environment: + DJANGO_SETTINGS_MODULE: my_app.settings.local + SERVER_PORT: "8010" + panes: + - ./manage.py runserver 0.0.0.0:${SERVER_PORT} + - environment: + DJANGO_SETTINGS_MODULE: my_app.settings.local-testing + SERVER_PORT: "8011" + shell_command: ./manage.py runserver 0.0.0.0:${SERVER_PORT} diff --git a/src/tmuxp/workspace/builder.py b/src/tmuxp/workspace/builder.py index 677ad9f9234..b13bf4caa0e 100644 --- a/src/tmuxp/workspace/builder.py +++ b/src/tmuxp/workspace/builder.py @@ -7,6 +7,7 @@ import logging import time +from libtmux.common import has_lt_version from libtmux.exc import TmuxSessionExists from libtmux.pane import Pane from libtmux.server import Server @@ -346,12 +347,31 @@ def iter_create_windows(self, session, append=False): except (KeyError, IndexError): pass + environment = panes[0].get("environment", wconf.get("environment")) + if environment and has_lt_version("3.0"): + # Falling back to use the environment of the first pane for the window + # creation is nice but yields misleading error messages. + pane_env = panes[0].get("environment") + win_env = wconf.get("environment") + if pane_env and win_env: + target = "panes and windows" + elif pane_env: + target = "panes" + else: + target = "windows" + logging.warning( + f"Cannot set environment for new {target}. " + "You need tmux 3.0 or newer for this." + ) + environment = None + w = session.new_window( window_name=window_name, start_directory=sd, attach=False, # do not move to the new window window_index=wconf.get("window_index", ""), window_shell=ws, + environment=environment, ) if is_first_window_pass: # if first window, use window 1 @@ -418,11 +438,24 @@ def get_pane_shell(): else: return None + environment = pconf.get("environment", wconf.get("environment")) + if environment and has_lt_version("3.0"): + # Just issue a warning when the environment comes from the pane + # configuration as a warning for the window was already issued when + # the window was created. + if pconf.get("environment"): + logging.warning( + "Cannot set environment for new panes. " + "You need tmux 3.0 or newer for this." + ) + environment = None + p = w.split_window( attach=True, start_directory=get_pane_start_directory(), shell=get_pane_shell(), target=p.id, + environment=environment, ) assert isinstance(p, Pane) diff --git a/tests/fixtures/workspace/builder/environment_vars.yaml b/tests/fixtures/workspace/builder/environment_vars.yaml index 1a3e6dddc40..1160dae67c6 100644 --- a/tests/fixtures/workspace/builder/environment_vars.yaml +++ b/tests/fixtures/workspace/builder/environment_vars.yaml @@ -1,9 +1,32 @@ session_name: test env vars -start_directory: '~' +start_directory: "~" environment: - FOO: BAR + FOO: SESSION PATH: /tmp windows: -- window_name: editor +- window_name: no_overrides panes: - pane +- window_name: window_overrides + environment: + FOO: WINDOW + panes: + - pane +- window_name: pane_overrides + panes: + - environment: + FOO: PANE +- window_name: both_overrides + environment: + FOO: WINDOW + panes: + - pane + - environment: + FOO: PANE +# This test case it just needed for warnings issued in old versions of tmux. +- window_name: both_overrides_on_first_pane + environment: + FOO: WINDOW + panes: + - environment: + FOO: PANE diff --git a/tests/workspace/test_builder.py b/tests/workspace/test_builder.py index 4ee513cbc9d..331e24f2aa9 100644 --- a/tests/workspace/test_builder.py +++ b/tests/workspace/test_builder.py @@ -9,6 +9,7 @@ import libtmux from libtmux.common import has_gte_version, has_lt_version +from libtmux.session import Session from libtmux.test import retry_until, temp_session from libtmux.window import Window from tmuxp import exc @@ -331,18 +332,95 @@ def f(): assert w.name != "top" +@pytest.mark.skipif( + has_lt_version("3.0"), + reason="needs -e flag for new-window and split-window introduced in tmux 3.0", +) def test_environment_variables(session): workspace = ConfigReader._from_file( test_utils.get_workspace_file("workspace/builder/environment_vars.yaml") ) workspace = loader.expand(workspace) + builder = WorkspaceBuilder(sconf=workspace) + builder.build(session) + # Give slow shells some time to settle as otherwise tests might fail. + time.sleep(0.3) + + assert session.getenv("FOO") == "SESSION" + assert session.getenv("PATH") == "/tmp" + + no_overrides_win = session.windows[0] + pane = no_overrides_win.panes[0] + pane.send_keys("echo $FOO") + assert pane.capture_pane()[1] == "SESSION" + + window_overrides_win = session.windows[1] + pane = window_overrides_win.panes[0] + pane.send_keys("echo $FOO") + assert pane.capture_pane()[1] == "WINDOW" + + pane_overrides_win = session.windows[2] + pane = pane_overrides_win.panes[0] + pane.send_keys("echo $FOO") + assert pane.capture_pane()[1] == "PANE" + + both_overrides_win = session.windows[3] + pane = both_overrides_win.panes[0] + pane.send_keys("echo $FOO") + assert pane.capture_pane()[1] == "WINDOW" + pane = both_overrides_win.panes[1] + pane.send_keys("echo $FOO") + assert pane.capture_pane()[1] == "PANE" + + +@pytest.mark.skipif( + has_gte_version("3.0"), + reason="warnings are not needed for tmux >= 3.0", +) +def test_environment_variables_logs(session: Session, caplog: pytest.LogCaptureFixture): + workspace = ConfigReader._from_file( + test_utils.get_workspace_file("workspace/builder/environment_vars.yaml") + ) + workspace = loader.expand(workspace) + builder = WorkspaceBuilder(sconf=workspace) builder.build(session) - assert session.getenv("FOO") == "BAR" + # environment on sessions should work as this is done using set-environment + # on the session itself + assert session.getenv("FOO") == "SESSION" assert session.getenv("PATH") == "/tmp" + assert ( + sum( + 1 + for record in caplog.records + if "Cannot set environment for new windows." in record.msg + ) + # From window_overrides and both_overrides, but not + # both_overrides_in_first_pane. + == 2 + ), "Warning on creating windows missing" + assert ( + sum( + 1 + for record in caplog.records + if "Cannot set environment for new panes." in record.msg + ) + # From pane_overrides and both_overrides, but not both_overrides_in_first_pane. + == 2 + ), "Warning on creating panes missing" + assert ( + sum( + 1 + for record in caplog.records + if 'Cannot set environment for new panes and windows.' in record.msg + ) + # From both_overrides_in_first_pane. + == 1 + ) + def test_automatic_rename_option(session): """With option automatic-rename: on."""