Skip to content
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
18 changes: 18 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,24 @@ $ tmuxp@next load yoursession
_Notes on the upcoming release will go here._
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->

### What's new

#### Concurrent workspace builder (opt-in) (#1056)

A new opt-in builder,
{class}`~tmuxp.workspace.builder.concurrent.ConcurrentWorkspaceBuilder`, speeds
up loads by preparing a window's panes together instead of one at a time. Select
it from a workspace file with `workspace_builder: concurrent`. For each window it
creates all the panes up front so their shells warm up together, waits for them
in a single readiness barrier, applies the layout once, then sends each pane's
commands. Windows with several panes — especially with a slow interactive shell
startup — open noticeably quicker, and the resulting session is identical to the
classic builder's. It honors the same `workspace_builder_options.pane_readiness`
policy and falls back to the classic one-pane-at-a-time path for windows whose
later panes depend on an earlier pane's `start_directory` side effects.

See {ref}`custom-workspace-builders` for the guide.

## tmuxp 1.73.0 (2026-06-28)

tmuxp 1.73.0 makes the workspace build step pluggable and tunable. A workspace can now build through a third-party builder selected by registered entry-point name or Python import path, and a new `workspace_builder_options` catalog controls the pane-readiness wait per workspace. The built-in builder stays the default, so existing workspaces keep working — though the new `pane_readiness: auto` default skips the prompt wait on non-zsh shells. See {ref}`custom-workspace-builders` for the guide.
Expand Down
1 change: 1 addition & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def socket_name(request: pytest.FixtureRequest) -> str:
# Modules that actually need tmux fixtures in their doctests
DOCTEST_NEEDS_TMUX = {
"tmuxp.workspace.builder.classic",
"tmuxp.workspace.builder.concurrent",
}


Expand Down
8 changes: 8 additions & 0 deletions docs/internals/api/workspace/builder/concurrent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Concurrent builder - `tmuxp.workspace.builder.concurrent`

```{eval-rst}
.. automodule:: tmuxp.workspace.builder.concurrent
:members:
:show-inheritance:
:undoc-members:
```
7 changes: 7 additions & 0 deletions docs/internals/api/workspace/builder/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ backwards-compatible alias of
The built-in, default builder — `tmuxp.workspace.builder.classic`.
:::

:::{grid-item-card} Concurrent builder
:link: concurrent
:link-type: doc
Opt-in builder that prepares a window's panes together — `tmuxp.workspace.builder.concurrent`.
:::

:::{grid-item-card} Builder protocol
:link: protocol
:link-type: doc
Expand All @@ -35,6 +41,7 @@ Builder selection and trusted import paths — `tmuxp.workspace.builder.registry
:hidden:

classic
concurrent
protocol
registry
```
40 changes: 40 additions & 0 deletions docs/topics/custom-workspace-builders.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,41 @@ covers what `tmuxp load` drives:
The contract is synchronous today. It is shaped so an async builder can be added
later as an additive extension without changing this surface.

## Concurrent builder

tmuxp ships a second built-in builder,
{class}`~tmuxp.workspace.builder.concurrent.ConcurrentWorkspaceBuilder`, that
speeds up loads by observing pane readiness *concurrently* within each window.
Select it by name:

```yaml
session_name: my-session
workspace_builder: concurrent
windows:
- window_name: editor
layout: main-vertical
panes:
- vim
- git status
```

Where the classic builder creates a pane, waits for its prompt, lays out, and
sends its commands one pane at a time, the concurrent builder builds each window
in three phases: it creates all of a window's panes up front so their shells warm
up together, waits for them in a single shared readiness barrier, applies the
layout once, then dispatches each pane's commands. tmux starts each pane's shell
the moment the pane is created, so waiting for the whole set at once observes
that overlap instead of paying for it pane by pane. Windows with several panes
and a slow interactive shell startup open noticeably quicker.

The result is the same session the classic builder produces — same windows,
panes, layout, commands, plugin hooks, and `before_script` behavior. The
concurrent builder honors the same `pane_readiness` policy described below, and a
window whose later panes depend on an earlier pane's `start_directory` side
effects automatically falls back to the classic one-pane-at-a-time path for that
window. If you depend on strict, pane-by-pane command side effects across a
window, prefer the classic builder.

## Pane readiness

tmuxp waits for a pane's shell prompt before dispatching layout and commands,
Expand Down Expand Up @@ -177,6 +212,10 @@ For builders that live in a trusted directory, build the `sys.path` sandbox with
- **Classic builder** — the default. Use it for any workspace that depends on
strict, pane-by-pane side effects (`start_directory`, `shell`, `window_shell`,
pane environment).
- **Concurrent builder** — set `workspace_builder: concurrent` to prepare each
window's panes together for faster loads, while falling back to the classic
path for windows whose later panes depend on an earlier pane's
`start_directory`.
- **Readiness tuning** — set `pane_readiness` to trade prompt-safety for speed
without swapping builders.
- **A custom builder** — when you need behavior the classic builder doesn't
Expand All @@ -186,6 +225,7 @@ For builders that live in a trusted directory, build the `sys.path` sandbox with
## Reference

- {class}`~tmuxp.workspace.builder.classic.ClassicWorkspaceBuilder`
- {class}`~tmuxp.workspace.builder.concurrent.ConcurrentWorkspaceBuilder`
- {class}`~tmuxp.workspace.builder.protocol.WorkspaceBuilderProtocol`
- {func}`~tmuxp.workspace.builder.registry.resolve_builder_class`
- {class}`~tmuxp.workspace.options.PaneReadiness`
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ tmuxp = 'tmuxp:cli.cli'

[project.entry-points."tmuxp.workspace_builders"]
classic = "tmuxp.workspace.builder.classic:ClassicWorkspaceBuilder"
concurrent = "tmuxp.workspace.builder.concurrent:ConcurrentWorkspaceBuilder"

[dependency-groups]
dev = [
Expand Down
2 changes: 2 additions & 0 deletions src/tmuxp/workspace/builder/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
get_default_columns,
get_default_rows,
)
from tmuxp.workspace.builder.concurrent import ConcurrentWorkspaceBuilder
from tmuxp.workspace.builder.protocol import WorkspaceBuilderProtocol
from tmuxp.workspace.builder.registry import (
WORKSPACE_BUILDERS_GROUP,
Expand All @@ -32,6 +33,7 @@
__all__ = [
"WORKSPACE_BUILDERS_GROUP",
"ClassicWorkspaceBuilder",
"ConcurrentWorkspaceBuilder",
"WorkspaceBuilder",
"WorkspaceBuilderProtocol",
"available_builders",
Expand Down
54 changes: 47 additions & 7 deletions src/tmuxp/workspace/builder/classic.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,13 +583,7 @@ def build(self, session: Session | None = None, append: bool = False) -> None:
for plugin in self.plugins:
plugin.on_window_create(window)

focus_pane = None
for pane, pane_config in self.iter_create_panes(window, window_config):
assert isinstance(pane, Pane)
pane = pane

if pane_config.get("focus"):
focus_pane = pane
focus_pane = self._build_window(window, window_config)

if window_config.get("focus"):
focus = window
Expand All @@ -614,6 +608,52 @@ def build(self, session: Session | None = None, append: bool = False) -> None:
if self.on_build_event:
self.on_build_event({"event": "workspace_built"})

def _build_window(
self,
window: Window,
window_config: dict[str, t.Any],
) -> Pane | None:
"""Create, lay out, and run a window's panes; return its focus pane.

The per-window construction seam :meth:`build` drives once per window,
after ``on_window_create`` and before ``config_after_window``. The
classic builder creates panes one at a time through
:meth:`iter_create_panes`. Subclasses override this to change how a
window's panes are built and made ready — see
:class:`~tmuxp.workspace.builder.concurrent.ConcurrentWorkspaceBuilder`,
which prepares a window's panes together.

Parameters
----------
window : :class:`libtmux.Window`
the window to populate
window_config : dict
config section for the window

Returns
-------
:class:`libtmux.Pane` or None
the pane that requested focus, or ``None`` when no pane did;
:meth:`build` selects it once the window is fully configured

Examples
--------
>>> session = server.new_session("build-window-demo")
>>> builder = ClassicWorkspaceBuilder(
... session_config={"session_name": "x", "windows": []},
... server=server,
... )
>>> window_config = {"window_name": "main", "panes": [{"shell_command": []}]}
>>> builder._build_window(session.active_window, window_config) is None
True
"""
focus_pane: Pane | None = None
for pane, pane_config in self.iter_create_panes(window, window_config):
assert isinstance(pane, Pane)
if pane_config.get("focus"):
focus_pane = pane
return focus_pane

def iter_create_windows(
self,
session: Session,
Expand Down
Loading
Loading