Skip to content

Commit 210333a

Browse files
committed
feat(builder): allow environments for windows and panes
This fixes #832.
1 parent b7e7a79 commit 210333a

File tree

7 files changed

+197
-13
lines changed

7 files changed

+197
-13
lines changed

CHANGES

+22-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,28 @@ $ pipx install --suffix=@next 'tmuxp' --pip-args '\--pre' --force
1717

1818
## tmuxp 1.19.x (unreleased)
1919

20-
- Notes on upcoming releases will be added here
20+
### What's new
21+
22+
- #845: allow to configure window and pane specific environment variables
23+
24+
Having a setup like:
25+
```yaml
26+
session_name: env-demo
27+
environment:
28+
DATABASE_URL: "sqlite3:///default.db"
29+
windows:
30+
- window_name: dev
31+
environment:
32+
DATABASE_URL: "sqlite3:///dev-1.db"
33+
panes:
34+
- pane
35+
- environment:
36+
DATABASE_URL: "sqlite3:///dev-2.db"
37+
```
38+
will result in a window with two panes. In the first pane `$DATABASE_URL` is
39+
`sqlite3:///dev-1.db`, while in the second pane it is `sqlite3://dev-2.db`.
40+
Any freshly created window gets `sqlite3:///default.db` as this is what was
41+
defined for the session.
2142

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

docs/configuration/examples.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,9 @@ please make a ticket on the [issue tracker][issue tracker].
262262

263263
## Environment variables
264264

265-
tmuxp will set session environment variables.
265+
tmuxp will set session, window and pane environment variables. Note that
266+
setting environment variables for windows and panes requires tmuxp 1.19 or
267+
newer.
266268

267269
````{tab} YAML
268270

examples/session-environment.json

+24-7
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,32 @@
22
"environment": {
33
"EDITOR": "/usr/bin/vim",
44
"DJANGO_SETTINGS_MODULE": "my_app.settings.local",
5-
"SERVER_PORT": "8009",
6-
},
5+
"SERVER_PORT": "8009"
6+
},
77
"windows": [
88
{
99
"panes": [
10-
"./manage.py runserver 0.0.0.0:${SERVER_PORT}"
11-
],
10+
"./manage.py runserver 0.0.0.0:${SERVER_PORT}"
11+
],
1212
"window_name": "Django project"
13-
},
14-
],
13+
},
14+
{
15+
"environment": {
16+
"DJANGO_SETTINGS_MODULE": "my_app.settings.local",
17+
"SERVER_PORT": "8010"
18+
},
19+
"panes": [
20+
"./manage.py runserver 0.0.0.0:${SERVER_PORT}",
21+
{
22+
"environment": {
23+
"DJANGO_SETTINGS_MODULE": "my_app.settings.local-testing",
24+
"SERVER_PORT": "8011"
25+
},
26+
"shell_command": "./manage.py runserver 0.0.0.0:${SERVER_PORT}"
27+
}
28+
],
29+
"window_name": "Another Django project"
30+
}
31+
],
1532
"session_name": "Environment variables test"
16-
}
33+
}

examples/session-environment.yaml

+10
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,13 @@ windows:
77
- window_name: Django project
88
panes:
99
- ./manage.py runserver 0.0.0.0:${SERVER_PORT}
10+
- window_name: Another Django project
11+
environment:
12+
DJANGO_SETTINGS_MODULE: my_app.settings.local
13+
SERVER_PORT: "8010"
14+
panes:
15+
- ./manage.py runserver 0.0.0.0:${SERVER_PORT}
16+
- environment:
17+
DJANGO_SETTINGS_MODULE: my_app.settings.local-testing
18+
SERVER_PORT: "8011"
19+
shell_command: ./manage.py runserver 0.0.0.0:${SERVER_PORT}

src/tmuxp/workspace/builder.py

+33
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import logging
88
import time
99

10+
from libtmux.common import has_lt_version
1011
from libtmux.exc import TmuxSessionExists
1112
from libtmux.pane import Pane
1213
from libtmux.server import Server
@@ -346,12 +347,31 @@ def iter_create_windows(self, session, append=False):
346347
except (KeyError, IndexError):
347348
pass
348349

350+
environment = panes[0].get("environment", wconf.get("environment"))
351+
if environment and has_lt_version("3.0"):
352+
# Falling back to use the environment of the first pane for the window
353+
# creation is nice but yields misleading error messages.
354+
pane_env = panes[0].get("environment")
355+
win_env = wconf.get("environment")
356+
if pane_env and win_env:
357+
target = "panes and windows"
358+
elif pane_env:
359+
target = "panes"
360+
else:
361+
target = "windows"
362+
logging.warning(
363+
f"Cannot set environment for new {target}. "
364+
"You need tmux 3.0 or newer for this."
365+
)
366+
environment = None
367+
349368
w = session.new_window(
350369
window_name=window_name,
351370
start_directory=sd,
352371
attach=False, # do not move to the new window
353372
window_index=wconf.get("window_index", ""),
354373
window_shell=ws,
374+
environment=environment,
355375
)
356376

357377
if is_first_window_pass: # if first window, use window 1
@@ -418,11 +438,24 @@ def get_pane_shell():
418438
else:
419439
return None
420440

441+
environment = pconf.get("environment", wconf.get("environment"))
442+
if environment and has_lt_version("3.0"):
443+
# Just issue a warning when the environment comes from the pane
444+
# configuration as a warning for the window was already issued when
445+
# the window was created.
446+
if pconf.get("environment"):
447+
logging.warning(
448+
"Cannot set environment for new panes. "
449+
"You need tmux 3.0 or newer for this."
450+
)
451+
environment = None
452+
421453
p = w.split_window(
422454
attach=True,
423455
start_directory=get_pane_start_directory(),
424456
shell=get_pane_shell(),
425457
target=p.id,
458+
environment=environment,
426459
)
427460

428461
assert isinstance(p, Pane)
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
11
session_name: test env vars
2-
start_directory: '~'
2+
start_directory: "~"
33
environment:
4-
FOO: BAR
4+
FOO: SESSION
55
PATH: /tmp
66
windows:
7-
- window_name: editor
7+
- window_name: no_overrides
88
panes:
99
- pane
10+
- window_name: window_overrides
11+
environment:
12+
FOO: WINDOW
13+
panes:
14+
- pane
15+
- window_name: pane_overrides
16+
panes:
17+
- environment:
18+
FOO: PANE
19+
- window_name: both_overrides
20+
environment:
21+
FOO: WINDOW
22+
panes:
23+
- pane
24+
- environment:
25+
FOO: PANE
26+
# This test case it just needed for warnings issued in old versions of tmux.
27+
- window_name: both_overrides_on_first_pane
28+
environment:
29+
FOO: WINDOW
30+
panes:
31+
- environment:
32+
FOO: PANE

tests/workspace/test_builder.py

+79-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import libtmux
1111
from libtmux.common import has_gte_version, has_lt_version
12+
from libtmux.session import Session
1213
from libtmux.test import retry_until, temp_session
1314
from libtmux.window import Window
1415
from tmuxp import exc
@@ -331,18 +332,95 @@ def f():
331332
assert w.name != "top"
332333

333334

335+
@pytest.mark.skipif(
336+
has_lt_version("3.0"),
337+
reason="needs -e flag for new-window and split-window introduced in tmux 3.0",
338+
)
334339
def test_environment_variables(session):
335340
workspace = ConfigReader._from_file(
336341
test_utils.get_workspace_file("workspace/builder/environment_vars.yaml")
337342
)
338343
workspace = loader.expand(workspace)
339344

345+
builder = WorkspaceBuilder(sconf=workspace)
346+
builder.build(session)
347+
# Give slow shells some time to settle as otherwise tests might fail.
348+
time.sleep(0.3)
349+
350+
assert session.getenv("FOO") == "SESSION"
351+
assert session.getenv("PATH") == "/tmp"
352+
353+
no_overrides_win = session.windows[0]
354+
pane = no_overrides_win.panes[0]
355+
pane.send_keys("echo $FOO")
356+
assert pane.capture_pane()[1] == "SESSION"
357+
358+
window_overrides_win = session.windows[1]
359+
pane = window_overrides_win.panes[0]
360+
pane.send_keys("echo $FOO")
361+
assert pane.capture_pane()[1] == "WINDOW"
362+
363+
pane_overrides_win = session.windows[2]
364+
pane = pane_overrides_win.panes[0]
365+
pane.send_keys("echo $FOO")
366+
assert pane.capture_pane()[1] == "PANE"
367+
368+
both_overrides_win = session.windows[3]
369+
pane = both_overrides_win.panes[0]
370+
pane.send_keys("echo $FOO")
371+
assert pane.capture_pane()[1] == "WINDOW"
372+
pane = both_overrides_win.panes[1]
373+
pane.send_keys("echo $FOO")
374+
assert pane.capture_pane()[1] == "PANE"
375+
376+
377+
@pytest.mark.skipif(
378+
has_gte_version("3.0"),
379+
reason="warnings are not needed for tmux >= 3.0",
380+
)
381+
def test_environment_variables_logs(session: Session, caplog: pytest.LogCaptureFixture):
382+
workspace = ConfigReader._from_file(
383+
test_utils.get_workspace_file("workspace/builder/environment_vars.yaml")
384+
)
385+
workspace = loader.expand(workspace)
386+
340387
builder = WorkspaceBuilder(sconf=workspace)
341388
builder.build(session)
342389

343-
assert session.getenv("FOO") == "BAR"
390+
# environment on sessions should work as this is done using set-environment
391+
# on the session itself
392+
assert session.getenv("FOO") == "SESSION"
344393
assert session.getenv("PATH") == "/tmp"
345394

395+
assert (
396+
sum(
397+
1
398+
for record in caplog.records
399+
if "Cannot set environment for new windows." in record.msg
400+
)
401+
# From window_overrides and both_overrides, but not
402+
# both_overrides_in_first_pane.
403+
== 2
404+
), "Warning on creating windows missing"
405+
assert (
406+
sum(
407+
1
408+
for record in caplog.records
409+
if "Cannot set environment for new panes." in record.msg
410+
)
411+
# From pane_overrides and both_overrides, but not both_overrides_in_first_pane.
412+
== 2
413+
), "Warning on creating panes missing"
414+
assert (
415+
sum(
416+
1
417+
for record in caplog.records
418+
if 'Cannot set environment for new panes and windows.' in record.msg
419+
)
420+
# From both_overrides_in_first_pane.
421+
== 1
422+
)
423+
346424

347425
def test_automatic_rename_option(session):
348426
"""With option automatic-rename: on."""

0 commit comments

Comments
 (0)