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
84 changes: 74 additions & 10 deletions src/tmuxp/workspace/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import typing as t

from libtmux._internal.query_list import ObjectDoesNotExist
from libtmux.options import handle_option_error
from libtmux.pane import Pane
from libtmux.server import Server
from libtmux.session import Session
Expand All @@ -19,15 +20,15 @@
from tmuxp.util import get_current_pane, run_before_script

if t.TYPE_CHECKING:
from collections.abc import Iterator
from collections.abc import Iterator, Mapping

logger = logging.getLogger(__name__)


def _wait_for_pane_ready(
pane: Pane,
timeout: float = 2.0,
interval: float = 0.05,
interval: float = 0.01,
) -> bool:
"""Wait for pane shell to draw its prompt.

Expand Down Expand Up @@ -407,6 +408,57 @@ def session_exists(self, session_name: str) -> bool:
return False
return True

def _bulk_set_options(
self,
items: Mapping[str, int | str | bool],
*,
target: str | None,
scope_flag: str,
) -> None:
"""Apply ``set-option`` for each (key, value) pair.

Mirrors :meth:`libtmux.options.OptionsMixin.set_option`'s
``True/False -> "on"/"off"`` convention so behaviour matches a
plain loop of ``set_option`` calls. Errors propagate as the
same ``OptionError`` subclasses ``handle_option_error``
produces.

Currently issues one ``set-option`` round-trip per item. The
helper's API (mapping + scope flag + optional target) is
deliberately batch-shaped so the body can swap to a single
pipelined dispatch (e.g. ``Server.batch()``) once libtmux
exposes one, without touching the call sites in ``build`` and
``iter_create_windows``.

Parameters
----------
items : mapping
Option name -> value pairs.
target : str, optional
Target identifier (session_id / window_id) for ``-t``;
pass ``None`` for global options where ``-g`` already
names the scope.
scope_flag : str
``"-s"``, ``"-g"``, or ``"-w"``. Selects the option scope.
"""
if not items:
return
server = self.server
assert server is not None
for key, raw_val in items.items():
if raw_val is True:
val: int | str = "on"
elif raw_val is False:
val = "off"
else:
val = raw_val
if target is not None:
cmd = server.cmd("set-option", scope_flag, "-t", target, key, val)
else:
cmd = server.cmd("set-option", scope_flag, key, val)
if cmd.stderr:
handle_option_error(cmd.stderr[0])

def build(self, session: Session | None = None, append: bool = False) -> None:
"""Build tmux workspace in session.

Expand Down Expand Up @@ -527,12 +579,18 @@ def build(self, session: Session | None = None, append: bool = False) -> None:
self.on_build_event({"event": "before_script_done"})

if "options" in self.session_config:
for option, value in self.session_config["options"].items():
self.session.set_option(option, value)
self._bulk_set_options(
self.session_config["options"],
target=self.session.session_id,
scope_flag="-s",
)

if "global_options" in self.session_config:
for option, value in self.session_config["global_options"].items():
self.session.set_option(option, value, global_=True)
self._bulk_set_options(
self.session_config["global_options"],
target=None,
scope_flag="-g",
)

if "environment" in self.session_config:
for option, value in self.session_config["environment"].items():
Expand Down Expand Up @@ -671,8 +729,11 @@ def iter_create_windows(
window_config["options"],
dict,
):
for key, val in window_config["options"].items():
window.set_option(key, val)
self._bulk_set_options(
window_config["options"],
target=window.window_id,
scope_flag="-w",
)

if window_config.get("focus"):
window.select()
Expand Down Expand Up @@ -841,8 +902,11 @@ def config_after_window(
window_config["options_after"],
dict,
):
for key, val in window_config["options_after"].items():
window.set_option(key, val)
self._bulk_set_options(
window_config["options_after"],
target=window.window_id,
scope_flag="-w",
)

def find_current_attached_session(self) -> Session:
"""Return current attached session."""
Expand Down
45 changes: 45 additions & 0 deletions tests/workspace/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,51 @@ def f() -> bool:
), "Synchronized command did not execute properly"


def test_bulk_set_options_propagates_unknown_option_error(
session: Session,
) -> None:
"""Bad options surface as ``OptionError``."""
workspace = {
"session_name": "bulk-set-options-bad",
"options": {"this-option-does-not-exist": "value"},
"windows": [{"window_name": "main", "panes": [{"shell_command": []}]}],
}
builder = WorkspaceBuilder(session_config=workspace, server=session.server)
with pytest.raises(libtmux.exc.OptionError):
builder.build(session=session)


def test_bulk_set_options_applies_session_window_and_options_after(
session: Session,
) -> None:
"""Session, window, and options_after loops all land via the helper."""
workspace = {
"session_name": "bulk-set-options-good",
"options": {"default-shell": "/bin/sh"},
"global_options": {"repeat-time": 491},
"windows": [
{
"window_name": "main",
"options": {"main-pane-height": 7},
"options_after": {"synchronize-panes": "on"},
"panes": [{"shell_command": []}, {"shell_command": []}],
},
],
}
builder = WorkspaceBuilder(session_config=workspace, server=session.server)
builder.build(session=session)

sess = builder.session
win = sess.active_window
default_shell = sess.show_option("default-shell")
assert isinstance(default_shell, str)
assert "/bin/sh" in default_shell
assert sess.show_option("repeat-time", global_=True) == 491
assert win.show_option("main-pane-height") == 7
sync = win.cmd("show-option", "-v", "synchronize-panes").stdout
assert sync == ["on"]


def test_window_shell(
session: Session,
) -> None:
Expand Down