Skip to content

Commit cd2526a

Browse files
committed
Load(fix[progress]): Honor panel opt-in
why: Restoring native before_script streaming made progress-lines a no-op. Explicit panel requests should still capture script output, while default loads should leave TTY-aware scripts attached to the terminal. what: - Capture before_script output only for explicit nonzero progress-lines - Keep default and zero-line modes on native script output - Update CLI docs, env docs, changelog, and load tests
1 parent cdaff59 commit cd2526a

5 files changed

Lines changed: 154 additions & 15 deletions

File tree

CHANGES

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ tmuxp 1.67.0 makes {ref}`tmuxp-load` visibly track the workspace build it is per
120120

121121
The {ref}`tmuxp-load` command now shows an animated progress display while it builds a session. Built-in formats cover terse, window-focused, pane-focused, and verbose views, while `--progress-format` and `TMUXP_PROGRESS_FORMAT` allow a custom display.
122122

123-
`--progress-lines` and `TMUXP_PROGRESS_LINES` control how much `before_script` output appears in the panel, and `--no-progress` or `TMUXP_PROGRESS=0` restores quiet output.
123+
`--progress-lines` and `TMUXP_PROGRESS_LINES` capture `before_script` output in the panel when requested, and `--no-progress` or `TMUXP_PROGRESS=0` restores quiet output.
124124

125125
## tmuxp 1.66.0 (2026-03-08)
126126

docs/cli/load.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,21 +218,21 @@ $ tmuxp load --progress-format "{session} {bar} {overall_percent}%" myproject
218218

219219
### Panel lines
220220

221-
The spinner shows script output in a panel below the spinner line. Control the panel height with `--progress-lines`:
221+
By default, `before_script` runs with its normal terminal output before the spinner appears. Use `--progress-lines` to capture that output in a panel below the spinner line:
222222

223-
Hide the panel entirely (script output goes to stdout):
223+
Keep native script output and hide the panel:
224224

225225
```console
226226
$ tmuxp load --progress-lines 0 myproject
227227
```
228228

229-
Show unlimited lines (capped to terminal height):
229+
Capture unlimited lines (capped to terminal height):
230230

231231
```console
232232
$ tmuxp load --progress-lines -1 myproject
233233
```
234234

235-
Set a custom height (default is 3):
235+
Capture five lines:
236236

237237
```console
238238
$ tmuxp load --progress-lines 5 myproject

docs/configuration/environmental-variables.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,15 @@ Equivalent to the `--progress-format` CLI flag.
6060

6161
## `TMUXP_PROGRESS_LINES`
6262

63-
Number of script-output lines shown in the spinner panel. Defaults to `3`.
63+
Capture `before_script` output in the spinner panel with this many lines. By default, scripts keep their normal terminal output.
6464

65-
Set to `0` to hide the panel entirely (script output goes to stdout):
65+
Set to `0` to keep native script output and hide the panel:
6666

6767
```console
6868
$ TMUXP_PROGRESS_LINES=0 tmuxp load myproject
6969
```
7070

71-
Set to `-1` for unlimited lines (capped to terminal height):
71+
Set to `-1` to capture unlimited lines (capped to terminal height):
7272

7373
```console
7474
$ TMUXP_PROGRESS_LINES=-1 tmuxp load myproject

src/tmuxp/cli/load.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -478,9 +478,9 @@ def load_workspace(
478478
progress_format : str, optional
479479
Spinner format preset name or custom format string with tokens.
480480
panel_lines : int, optional
481-
Number of script-output lines shown in the spinner panel.
482-
Defaults to the :class:`~tmuxp.cli._progress.Spinner` default (3).
483-
Override via ``TMUXP_PROGRESS_LINES`` environment variable.
481+
Nonzero values capture ``before_script`` output in the spinner panel.
482+
By default, scripts keep their normal terminal output. Override via
483+
``TMUXP_PROGRESS_LINES`` environment variable.
484484
no_progress : bool
485485
Disable the progress spinner entirely. Default False.
486486
Also disabled when ``TMUXP_PROGRESS=0``.
@@ -658,6 +658,7 @@ def load_workspace(
658658
else:
659659
_panel_lines_env_int = None
660660
_panel_lines = panel_lines if panel_lines is not None else _panel_lines_env_int
661+
_panel_lines_explicit = panel_lines is not None or _panel_lines_env_int is not None
661662
_private_path = str(PrivatePath(workspace_file))
662663
_spinner = Spinner(
663664
message=(
@@ -670,6 +671,9 @@ def load_workspace(
670671
)
671672
_success_emitted = False
672673
_has_before_script = "before_script" in expanded_workspace
674+
_capture_script_output = (
675+
_has_before_script and _panel_lines_explicit and _panel_lines != 0
676+
)
673677

674678
def _emit_success() -> None:
675679
nonlocal _success_emitted
@@ -680,12 +684,14 @@ def _emit_success() -> None:
680684

681685
def _on_build_event(event: dict[str, t.Any]) -> None:
682686
spinner.on_build_event(event)
683-
if event.get("event") == "before_script_done":
687+
if event.get("event") == "before_script_done" and not _capture_script_output:
684688
spinner.start()
685689

686690
spinner = _spinner
687691
with _silence_stream_handlers():
688-
if not _has_before_script:
692+
if _capture_script_output:
693+
builder.on_script_output = spinner.add_output_line
694+
if not _has_before_script or _capture_script_output:
689695
spinner.start()
690696
builder.on_build_event = _on_build_event
691697
try:
@@ -809,8 +815,8 @@ def create_load_subparser(parser: argparse.ArgumentParser) -> argparse.ArgumentP
809815
type=int,
810816
default=None,
811817
help=(
812-
"Number of script-output lines shown in the spinner panel (default: 3). "
813-
"0 hides the panel entirely (script output goes to stdout). "
818+
"Capture before_script output in the spinner panel with N lines. "
819+
"0 keeps native script output and hides the panel. "
814820
"-1 shows unlimited lines (capped to terminal height). "
815821
"Env: TMUXP_PROGRESS_LINES"
816822
),

tests/cli/test_load.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -967,6 +967,139 @@ def fake_dispatch_build(
967967
]
968968

969969

970+
class ProgressLinesCaptureFixture(t.NamedTuple):
971+
"""Test fixture for explicit progress-lines capture mode."""
972+
973+
test_id: str
974+
panel_lines: int | None
975+
env_value: str | None
976+
expected_capture: bool
977+
978+
979+
PROGRESS_LINES_CAPTURE_FIXTURES: list[ProgressLinesCaptureFixture] = [
980+
ProgressLinesCaptureFixture(
981+
test_id="cli_panel_lines",
982+
panel_lines=5,
983+
env_value=None,
984+
expected_capture=True,
985+
),
986+
ProgressLinesCaptureFixture(
987+
test_id="env_panel_lines",
988+
panel_lines=None,
989+
env_value="4",
990+
expected_capture=True,
991+
),
992+
ProgressLinesCaptureFixture(
993+
test_id="cli_zero_lines",
994+
panel_lines=0,
995+
env_value=None,
996+
expected_capture=False,
997+
),
998+
ProgressLinesCaptureFixture(
999+
test_id="env_zero_lines",
1000+
panel_lines=None,
1001+
env_value="0",
1002+
expected_capture=False,
1003+
),
1004+
]
1005+
1006+
1007+
@pytest.mark.parametrize(
1008+
list(ProgressLinesCaptureFixture._fields),
1009+
PROGRESS_LINES_CAPTURE_FIXTURES,
1010+
ids=[f.test_id for f in PROGRESS_LINES_CAPTURE_FIXTURES],
1011+
)
1012+
def test_load_workspace_handles_explicit_before_script_progress_lines(
1013+
server: Server,
1014+
tmp_path: pathlib.Path,
1015+
monkeypatch: pytest.MonkeyPatch,
1016+
test_id: str,
1017+
panel_lines: int | None,
1018+
env_value: str | None,
1019+
expected_capture: bool,
1020+
) -> None:
1021+
"""Explicit progress-lines controls before_script output capture."""
1022+
import yaml
1023+
1024+
from tmuxp.cli._colors import ColorMode, Colors
1025+
1026+
calls: list[str] = []
1027+
captured_script_callback: list[t.Callable[[str], None] | None] = []
1028+
1029+
class FakeSpinner:
1030+
def __init__(self, *_args: t.Any, **_kwargs: t.Any) -> None:
1031+
pass
1032+
1033+
def start(self) -> None:
1034+
calls.append("start")
1035+
1036+
def stop(self) -> None:
1037+
calls.append("stop")
1038+
1039+
def success(self) -> None:
1040+
calls.append("success")
1041+
1042+
def add_output_line(self, line: str) -> None:
1043+
calls.append(f"add_output_line:{line}")
1044+
1045+
def on_build_event(self, event: dict[str, t.Any]) -> None:
1046+
calls.append(str(event["event"]))
1047+
1048+
def fake_dispatch_build(
1049+
builder: WorkspaceBuilder,
1050+
*_args: t.Any,
1051+
**_kwargs: t.Any,
1052+
) -> None:
1053+
captured_script_callback.append(builder.on_script_output)
1054+
assert builder.on_build_event is not None
1055+
1056+
builder.on_build_event({"event": "before_script_started"})
1057+
if builder.on_script_output is not None:
1058+
builder.on_script_output("before line")
1059+
builder.on_build_event({"event": "before_script_done"})
1060+
1061+
config = {
1062+
"session_name": f"before-script-progress-{test_id}",
1063+
"before_script": "echo before",
1064+
"windows": [{"window_name": "main"}],
1065+
}
1066+
config_file = tmp_path / f"{test_id}.yaml"
1067+
config_file.write_text(yaml.dump(config))
1068+
1069+
monkeypatch.delenv("TMUX", raising=False)
1070+
if env_value is None:
1071+
monkeypatch.delenv("TMUXP_PROGRESS_LINES", raising=False)
1072+
else:
1073+
monkeypatch.setenv("TMUXP_PROGRESS_LINES", env_value)
1074+
monkeypatch.setattr("tmuxp.cli.load.Spinner", FakeSpinner)
1075+
monkeypatch.setattr("tmuxp.cli.load._dispatch_build", fake_dispatch_build)
1076+
1077+
result = load_workspace(
1078+
str(config_file),
1079+
socket_name=server.socket_name,
1080+
cli_colors=Colors(ColorMode.NEVER),
1081+
panel_lines=panel_lines,
1082+
)
1083+
1084+
assert result is None
1085+
assert (captured_script_callback[0] is not None) is expected_capture
1086+
if expected_capture:
1087+
assert calls == [
1088+
"start",
1089+
"before_script_started",
1090+
"add_output_line:before line",
1091+
"before_script_done",
1092+
"stop",
1093+
]
1094+
else:
1095+
assert calls == [
1096+
"before_script_started",
1097+
"before_script_done",
1098+
"start",
1099+
"stop",
1100+
]
1101+
1102+
9701103
def test_load_masks_home_in_spinner_message(monkeypatch: pytest.MonkeyPatch) -> None:
9711104
"""Spinner message should mask home directory via PrivatePath."""
9721105
monkeypatch.setattr(pathlib.Path, "home", lambda: pathlib.Path("/home/testuser"))

0 commit comments

Comments
 (0)