Skip to content

feat(builder): allow environments for windows and panes #845

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<!-- Maintainers, insert changes / features for the next release here -->

Expand Down
4 changes: 3 additions & 1 deletion docs/configuration/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 24 additions & 7 deletions examples/session-environment.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
10 changes: 10 additions & 0 deletions examples/session-environment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
33 changes: 33 additions & 0 deletions src/tmuxp/workspace/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
29 changes: 26 additions & 3 deletions tests/fixtures/workspace/builder/environment_vars.yaml
Original file line number Diff line number Diff line change
@@ -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
80 changes: 79 additions & 1 deletion tests/workspace/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down