Skip to content
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

Implements if conditions for pane and window #942

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
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
35 changes: 35 additions & 0 deletions docs/configuration/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,41 @@ newer and tmux 3.0 or newer.

````

## `if` conditions

tmuxp enables one to optionally open windows / panes based on conditions. The `if` conditions can appears in the configuration for window or pane.

````{tab} YAML

```{literalinclude} ../../examples/if-conditions.yaml
:language: yaml

```
````

````{tab} JSON

```{literalinclude} ../../examples/if-conditions.json
:language: json

```

````

In the example, running the example

```console
$ tmuxp load examples/if-conditions.yaml
```

should produce **only** a window with upper and lower split panes (others should have `if` conditions that evaluates to false). This example allows for on-demand pane showing, where

```console
$ show_htop=false tmuxp load examples/if-conditions.yaml
```

will instead suppress the `htop` command pane and resulting in a different behaviour.

## Focusing

tmuxp allows `focus: true` for assuring windows and panes are attached /
Expand Down
53 changes: 53 additions & 0 deletions examples/if-conditions-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
session_name: if conditions test conditions
environment:
foo: 'false'
bar: '1'
F: '0'
MY_VAR: myfoobar
windows:
- window_name: window all false
panes:
- if: ${foo}
shell_command:
- echo pane 1
- if:
shell: '[ 1 -gt 2 ]'
shell_command:
- echo pane 2
- if:
shell_var: ${F}
- window_name: window 2 of 3 true
panes:
- if:
shell: '[ "foo" = "bar" ]'
shell_command:
- echo pane 3
- if:
shell: '[ "hello" != "byte" ]'
shell_command:
- echo pane 4
- if:
python: '2**4 == 16'
shell_command:
- echo pane 5
- window_name: window 2 of 4 true
panes:
- if:
shell_var: 'FALSE'
shell_command:
- echo pane 6
- if:
shell_var: ${bar}
shell_command:
- echo pane 7
- if:
python: import os; not os.path.isdir('/a/very/random/path')
shell_command:
- echo pane 8
- if: ${non_existing_var}
shell_command:
- echo pane 9
- if:
shell: echo ${MY_VAR} | grep -q foo
shell_command:
- echo pane 10
50 changes: 50 additions & 0 deletions examples/if-conditions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"session_name": "if conditions test",
"environment": {
"Foo": "false",
"show_htop": "true"
},
"windows": [
{
"window_name": "window 1 ${ha} $Foo",
"if": "${Foo}",
"panes": [
{
"shell_command": [
"echo \"this shouldn't show up\""
]
},
"echo neither should this $Foo"
]
},
{
"window_name": "window 2",
"panes": [
{
"if": {
"shell": "[ 5 -lt 4 ]"
},
"shell_command": [
"echo the above is a false statement"
]
},
{
"if": {
"python": "import os; os.path.isdir('${PWD}')"
},
"shell_command": [
"echo \"checking for PWD (${PWD}) is a directory in python\"",
"python -m http.server"
]
},
{
"if": "${show_htop}",
"shell_command": [
"echo \"the above is a true statement (by default), but can be disabled on-demand\"",
"htop"
]
}
]
}
]
}
30 changes: 30 additions & 0 deletions examples/if-conditions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
session_name: if conditions test
environment:
Foo: 'false'
show_htop: 'true'
windows:
# the following would not show up as it evaluates to false
- window_name: window 1 ${ha} $Foo
if: ${Foo}
panes:
- shell_command:
- echo "this shouldn't show up"
- echo neither should this $Foo
- window_name: window 2
panes:
# shell expression condition; should not show up
- if:
shell: '[ 5 -lt 4 ]'
shell_command:
- echo the above is a false statement
# python condition
- if:
python: import os; os.path.isdir(os.path.expandvars('${PWD}'))
shell_command:
- echo "checking for PWD (${PWD}) is a directory in python"
- python -m http.server
# display by default, but can be disabled by running `show_htop=false tmuxp load .....`
- if: ${show_htop}
shell_command:
- echo "the above is a true statement (by default), but can be disabled on-demand"
- htop
85 changes: 78 additions & 7 deletions src/tmuxp/workspace/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,76 @@
import logging
import os
import pathlib
import subprocess
import typing as t

logger = logging.getLogger(__name__)


def optional_windows_and_pane(
workspace_dict: t.Dict[str, t.Any],
) -> bool:
"""Determine if a window or pane should be included based on `if` conditions.

The function evaluates the 'if' condition specified in `workspace_dict` to determine inclusion:
- If 'if' key is not present, it defaults to True.
- If 'if' is a string or boolean, it's treated as a shell variable.
- 'if' can be a dictionary containing 'shell', 'shell_var' or 'python' keys with valid expressions.
- 'shell_var' expressions are expanded and checked against true values ('y', 'yes', '1', 'on', 'true', 't').
- 'shell' expressions are evaluated using subprocess
- 'python' expressions are evaluated using `exec()`

Parameters
----------
workspace_dict : Dict
A dictionary containing pane/window configuration data.

Returns
-------
bool
True if the window or pane should be included, False otherwise.
"""
if "if" not in workspace_dict:
return True
if_cond = workspace_dict["if"]
if isinstance(if_cond, (str, bool)):
# treat this as shell variable
if_cond = {"shell_var": if_cond}
if not isinstance(if_cond, dict) or not any(
predicate in if_cond for predicate in ("python", "shell", "shell_var")
):
msg = f"if conditions does not contains valid expression: {if_cond}"
raise ValueError(msg)
if "shell_var" in if_cond:
if expandshell(str(if_cond["shell_var"])).lower() not in {
"y",
"yes",
"1",
"on",
"true",
"t",
}:
return False
if "shell" in if_cond and (
subprocess.run(
expandshell(if_cond["shell"]),
shell=True,
check=False,
).returncode
!= 0
):
return False
if "python" in if_cond:
# assign the result of the last statement from the python snippet
py_statements = if_cond["python"].split(";")
py_statements[-1] = f"ret={py_statements[-1]}"
locals = {}
exec(";".join(py_statements), {}, locals)
if not locals["ret"]:
return False
return True


def expandshell(value: str) -> str:
"""Resolve shell variables based on user's ``$HOME`` and ``env``.

Expand Down Expand Up @@ -114,6 +179,9 @@ def expand(
if any(val.startswith(a) for a in [".", "./"]):
val = str(cwd / val)
workspace_dict["environment"][key] = val
if key not in os.environ:
# using user provided environment variable as default vars
os.environ[key] = val
if "global_options" in workspace_dict:
for key in workspace_dict["global_options"]:
val = workspace_dict["global_options"][key]
Expand Down Expand Up @@ -170,18 +238,21 @@ def expand(

# recurse into window and pane workspace items
if "windows" in workspace_dict:
workspace_dict["windows"] = [
expand(window, parent=workspace_dict)
for window in workspace_dict["windows"]
]
window_dicts = workspace_dict["windows"]
window_dicts = filter(optional_windows_and_pane, window_dicts)
window_dicts = (expand(x, parent=workspace_dict) for x in window_dicts)
# remove windows that has no panels (e.g. due to if conditions)
window_dicts = filter(lambda x: len(x["panes"]), window_dicts)
workspace_dict["windows"] = list(window_dicts)

elif "panes" in workspace_dict:
pane_dicts = workspace_dict["panes"]
for pane_idx, pane_dict in enumerate(pane_dicts):
pane_dicts[pane_idx] = {}
pane_dicts[pane_idx].update(expand_cmd(pane_dict))
workspace_dict["panes"] = [
expand(pane, parent=workspace_dict) for pane in pane_dicts
]
pane_dicts = filter(optional_windows_and_pane, pane_dicts)
pane_dicts = (expand(x, parent=workspace_dict) for x in pane_dicts)
workspace_dict["panes"] = list(pane_dicts)

return workspace_dict

Expand Down
26 changes: 26 additions & 0 deletions tests/workspace/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,32 @@ def test_blank_pane_spawn(
assert len(window4.panes) == 2


def test_if_conditions(
session: Session,
) -> None:
"""Test various ways of spawning panes with conditions from a tmuxp configuration."""
yaml_workspace_file = EXAMPLE_PATH / "if-conditions-test.yaml"
test_config = ConfigReader._from_file(yaml_workspace_file)

test_config = loader.expand(test_config)
builder = WorkspaceBuilder(session_config=test_config, server=session.server)
builder.build(session=session)

assert session == builder.session

with pytest.raises(ObjectDoesNotExist):
window1 = session.windows.get(window_name="window all false")
assert window1 is None

window2 = session.windows.get(window_name="window 2 of 3 true")
assert window2 is not None
assert len(window2.panes) == 2

window3 = session.windows.get(window_name="window 2 of 4 true")
assert window3 is not None
assert len(window3.panes) == 3


def test_start_directory(session: Session, tmp_path: pathlib.Path) -> None:
"""Test workspace builder setting start_directory relative to current directory."""
test_dir = tmp_path / "foo bar"
Expand Down
Loading