Skip to content

Commit fb3ec79

Browse files
committed
Workspace(fix[synchronize-panes]): Restore sync state on setup failure
why: Window setup temporarily disables tmux pane synchronization so pane setup commands do not broadcast across panes. If pane creation or layout application fails, that temporary state must still be restored so partial windows and existing/global tmux state are not left with an unintended local override. what: - Restore synchronize-panes from a build-level finally block when pane setup aborts - Preserve and restore pane-local synchronize-panes overrides while setup is isolated - Cover raw options, inherited global state, options_after, and plugin-set synchronization state in regression tests
1 parent 0ad7d16 commit fb3ec79

2 files changed

Lines changed: 331 additions & 27 deletions

File tree

src/tmuxp/workspace/builder/classic.py

Lines changed: 79 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import typing as t
1010

1111
from libtmux._internal.query_list import ObjectDoesNotExist
12+
from libtmux.constants import OptionScope
1213
from libtmux.pane import Pane
1314
from libtmux.server import Server
1415
from libtmux.session import Session
@@ -32,6 +33,7 @@
3233
SYNCHRONIZE_PANES_OPTION = "synchronize-panes"
3334
SYNCHRONIZE_PANES_FINAL_OPTION = "_synchronize_panes"
3435
SYNCHRONIZE_PANES_FORCED_OFF_OPTION = "_synchronize_panes_forced_off"
36+
SYNCHRONIZE_PANES_PANE_RESTORE_OPTION = "_synchronize_panes_pane_restore"
3537
SYNCHRONIZE_PANES_RESTORE_OPTION = "_synchronize_panes_restore"
3638
_MISSING = object()
3739

@@ -619,17 +621,24 @@ def build(self, session: Session | None = None, append: bool = False) -> None:
619621
self.prepare_window_synchronize_panes(window, window_config)
620622

621623
focus_pane = None
622-
for pane, pane_config in self.iter_create_panes(window, window_config):
623-
assert isinstance(pane, Pane)
624-
pane = pane
624+
try:
625+
for pane, pane_config in self.iter_create_panes(window, window_config):
626+
assert isinstance(pane, Pane)
627+
pane = pane
625628

626-
if pane_config.get("focus"):
627-
focus_pane = pane
629+
if pane_config.get("focus"):
630+
focus_pane = pane
628631

629-
if window_config.get("focus"):
630-
focus = window
632+
if window_config.get("focus"):
633+
focus = window
631634

632-
self.config_after_window(window, window_config)
635+
self.config_after_window(window, window_config)
636+
finally:
637+
self.restore_window_synchronize_panes(
638+
window,
639+
window_config,
640+
apply_options_after=False,
641+
)
633642

634643
for plugin in self.plugins:
635644
plugin.after_window_finished(window)
@@ -763,7 +772,7 @@ def prepare_window_synchronize_panes(
763772
Examples
764773
--------
765774
>>> cfg = {}
766-
>>> builder = WorkspaceBuilder(
775+
>>> builder = ClassicWorkspaceBuilder(
767776
... session_config={"session_name": session.name, "windows": []},
768777
... server=session.server,
769778
... )
@@ -788,17 +797,38 @@ def prepare_window_synchronize_panes(
788797
window.set_option(SYNCHRONIZE_PANES_OPTION, False)
789798
window_config[SYNCHRONIZE_PANES_FORCED_OFF_OPTION] = True
790799

800+
pane_restores = []
801+
for pane in window.panes:
802+
local_pane_sync = pane.show_option(
803+
SYNCHRONIZE_PANES_OPTION,
804+
scope=OptionScope.Pane,
805+
)
806+
if local_pane_sync is True:
807+
pane_restores.append((pane, local_pane_sync))
808+
pane.set_option(
809+
SYNCHRONIZE_PANES_OPTION,
810+
False,
811+
scope=OptionScope.Pane,
812+
ignore_errors=True,
813+
)
814+
815+
if pane_restores:
816+
window_config[SYNCHRONIZE_PANES_PANE_RESTORE_OPTION] = pane_restores
817+
window_config[SYNCHRONIZE_PANES_FORCED_OFF_OPTION] = True
818+
791819
def restore_window_synchronize_panes(
792820
self,
793821
window: Window,
794822
window_config: dict[str, t.Any],
823+
*,
824+
apply_options_after: bool = True,
795825
) -> None:
796826
"""Restore the configured final tmux pane synchronization state.
797827
798828
Examples
799829
--------
800830
>>> cfg = {"options_after": {"synchronize-panes": "off"}}
801-
>>> builder = WorkspaceBuilder(
831+
>>> builder = ClassicWorkspaceBuilder(
802832
... session_config={"session_name": session.name, "windows": []},
803833
... server=session.server,
804834
... )
@@ -809,26 +839,49 @@ def restore_window_synchronize_panes(
809839
False
810840
>>> _ = scratch.kill()
811841
"""
812-
after_sync = _get_window_option_value(
813-
window_config,
814-
"options_after",
815-
SYNCHRONIZE_PANES_OPTION,
816-
)
817-
if after_sync is not _MISSING:
818-
window.set_option(SYNCHRONIZE_PANES_OPTION, after_sync)
819-
return
820-
821-
if not window_config.get(SYNCHRONIZE_PANES_FORCED_OFF_OPTION):
822-
return
842+
after_sync = _MISSING
843+
if apply_options_after:
844+
after_sync = _get_window_option_value(
845+
window_config,
846+
"options_after",
847+
SYNCHRONIZE_PANES_OPTION,
848+
)
823849

824-
restore_sync = window_config.get(
850+
pane_restores = window_config.pop(
851+
SYNCHRONIZE_PANES_PANE_RESTORE_OPTION,
852+
[],
853+
)
854+
forced_off = window_config.pop(SYNCHRONIZE_PANES_FORCED_OFF_OPTION, False)
855+
restore_sync = window_config.pop(
825856
SYNCHRONIZE_PANES_RESTORE_OPTION,
826857
_MISSING,
827858
)
828-
if restore_sync is _MISSING or restore_sync is None:
829-
window.unset_option(SYNCHRONIZE_PANES_OPTION, ignore_errors=True)
830-
else:
831-
window.set_option(SYNCHRONIZE_PANES_OPTION, restore_sync)
859+
860+
if after_sync is not _MISSING:
861+
window.set_option(SYNCHRONIZE_PANES_OPTION, t.cast(int | str, after_sync))
862+
elif forced_off and restore_sync is not _MISSING:
863+
if restore_sync is None:
864+
window.unset_option(SYNCHRONIZE_PANES_OPTION, ignore_errors=True)
865+
else:
866+
window.set_option(
867+
SYNCHRONIZE_PANES_OPTION,
868+
t.cast(int | str, restore_sync),
869+
)
870+
871+
for pane, restore_pane_sync in pane_restores:
872+
if restore_pane_sync is _MISSING or restore_pane_sync is None:
873+
pane.unset_option(
874+
SYNCHRONIZE_PANES_OPTION,
875+
scope=OptionScope.Pane,
876+
ignore_errors=True,
877+
)
878+
else:
879+
pane.set_option(
880+
SYNCHRONIZE_PANES_OPTION,
881+
t.cast(int | str, restore_pane_sync),
882+
scope=OptionScope.Pane,
883+
ignore_errors=True,
884+
)
832885

833886
def iter_create_panes(
834887
self,

0 commit comments

Comments
 (0)