From 2e1ac45936fb9dceccbcb22dbda3bc788d879016 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 3 Mar 2024 09:59:46 -0600 Subject: [PATCH 01/29] feat(Pane): Add Pane.split, deprecate Pane.split_window --- src/libtmux/pane.py | 169 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 151 insertions(+), 18 deletions(-) diff --git a/src/libtmux/pane.py b/src/libtmux/pane.py index 608ea9ffb..234c0f2aa 100644 --- a/src/libtmux/pane.py +++ b/src/libtmux/pane.py @@ -7,15 +7,17 @@ import dataclasses import logging +import pathlib import typing as t import warnings from typing import overload -from libtmux.common import has_gte_version, tmux_cmd +from libtmux.common import has_gte_version, has_lt_version, tmux_cmd from libtmux.constants import ( RESIZE_ADJUSTMENT_DIRECTION_FLAG_MAP, ResizeAdjustmentDirection, ) +from libtmux.formats import FORMAT_SEPARATOR from libtmux.neo import Obj, fetch_obj from . import exc @@ -483,47 +485,132 @@ def select_pane(self) -> "Pane": raise exc.PaneNotFound(pane_id=self.pane_id) return pane - def split_window( + def split( self, - attach: bool = False, start_directory: t.Optional[str] = None, + attach: bool = False, vertical: bool = True, shell: t.Optional[str] = None, size: t.Optional[t.Union[str, int]] = None, percent: t.Optional[int] = None, # deprecated environment: t.Optional[t.Dict[str, str]] = None, - ) -> "Pane": # New Pane, not self - """Split window at pane and return newly created :class:`Pane`. + ) -> "Pane": + """Split window and return the created :class:`Pane`. + + Used for splitting window and holding in a python object. Parameters ---------- attach : bool, optional - Attach / select pane after creation. + make new window the current window after creating it, default + True. start_directory : str, optional - specifies the working directory in which the new pane is created. + specifies the working directory in which the new window is created. vertical : bool, optional split vertically + shell : str, optional + execute a command on splitting the window. The pane will close + when the command exits. + + NOTE: When this command exits the pane will close. This feature + is useful for long-running processes where the closing of the + window upon completion is desired. + size: int, optional + Cell/row or percentage to occupy with respect to current window. percent: int, optional - percentage to occupy with respect to current pane + Deprecated in favor of size. Percentage to occupy with respect to current + window. environment: dict, optional Environmental variables for new pane. tmux 3.0+ only. Passthrough to ``-e``. Notes ----- + :term:`tmux(1)` will move window to the new pane if the + ``split-window`` target is off screen. tmux handles the ``-d`` the + same way as ``new-window`` and ``attach`` in + :class:`Session.new_window`. + + By default, this will make the window the pane is created in + active. To remain on the same window and split the pane in another + target window, pass in ``attach=False``. + + .. versionchanged:: 0.28.0 + + ``attach`` default changed from ``True`` to ``False``. + .. deprecated:: 0.28.0 ``percent=25`` deprecated in favor of ``size="25%"``. """ - return self.window.split_window( - target=self.pane_id, - attach=attach, - start_directory=start_directory, - vertical=vertical, - shell=shell, - size=size, - percent=percent, - environment=environment, - ) + tmux_formats = ["#{pane_id}" + FORMAT_SEPARATOR] + + tmux_args: t.Tuple[str, ...] = () + + if vertical: + tmux_args += ("-v",) + else: + tmux_args += ("-h",) + + if size is not None: + if has_lt_version("3.1"): + if isinstance(size, str) and size.endswith("%"): + tmux_args += (f'-p{str(size).rstrip("%")}',) + else: + warnings.warn( + 'Ignored size. Use percent in tmux < 3.1, e.g. "size=50%"', + stacklevel=2, + ) + else: + tmux_args += (f"-l{size}",) + + if percent is not None: + # Deprecated in 3.1 in favor of -l + warnings.warn( + f'Deprecated in favor of size="{str(percent).rstrip("%")}%" ' + + ' ("-l" flag) in tmux 3.1+.', + category=DeprecationWarning, + stacklevel=2, + ) + tmux_args += (f"-p{percent}",) + + tmux_args += ("-P", "-F%s" % "".join(tmux_formats)) # output + + if start_directory is not None: + # as of 2014-02-08 tmux 1.9-dev doesn't expand ~ in new-window -c. + start_path = pathlib.Path(start_directory).expanduser() + tmux_args += (f"-c{start_path}",) + + if not attach: + tmux_args += ("-d",) + + if environment: + if has_gte_version("3.0"): + for k, v in environment.items(): + tmux_args += (f"-e{k}={v}",) + else: + logger.warning( + "Environment flag ignored, tmux 3.0 or newer required.", + ) + + if shell: + tmux_args += (shell,) + + pane_cmd = self.cmd("split-window", *tmux_args) + + # tmux < 1.7. This is added in 1.7. + if pane_cmd.stderr: + if "pane too small" in pane_cmd.stderr: + raise exc.LibTmuxException(pane_cmd.stderr) + + raise exc.LibTmuxException( + pane_cmd.stderr, self.__dict__, self.window.panes + ) + + pane_output = pane_cmd.stdout[0] + + pane_formatters = dict(zip(["pane_id"], pane_output.split(FORMAT_SEPARATOR))) + + return self.from_pane_id(server=self.server, pane_id=pane_formatters["pane_id"]) """ Commands (helpers) @@ -636,6 +723,52 @@ def width(self) -> t.Optional[str]: # # Legacy: Redundant stuff we want to remove # + def split_window( + self, + attach: bool = False, + start_directory: t.Optional[str] = None, + vertical: bool = True, + shell: t.Optional[str] = None, + size: t.Optional[t.Union[str, int]] = None, + percent: t.Optional[int] = None, # deprecated + environment: t.Optional[t.Dict[str, str]] = None, + ) -> "Pane": # New Pane, not self + """Split window at pane and return newly created :class:`Pane`. + + Parameters + ---------- + attach : bool, optional + Attach / select pane after creation. + start_directory : str, optional + specifies the working directory in which the new pane is created. + vertical : bool, optional + split vertically + percent: int, optional + percentage to occupy with respect to current pane + environment: dict, optional + Environmental variables for new pane. tmux 3.0+ only. Passthrough to ``-e``. + + Notes + ----- + .. deprecated:: 0.33 + + Deprecated in favor of :meth:`.split`. + """ + warnings.warn( + "Pane.split_window() is deprecated in favor of Pane.split()", + category=DeprecationWarning, + stacklevel=2, + ) + return self.split( + attach=attach, + start_directory=start_directory, + vertical=vertical, + shell=shell, + size=size, + percent=percent, + environment=environment, + ) + def get(self, key: str, default: t.Optional[t.Any] = None) -> t.Any: """Return key-based lookup. Deprecated by attributes. From cad08018e819c5e6d6a981b78cba431933c439e5 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 3 Mar 2024 10:25:20 -0600 Subject: [PATCH 02/29] tests(Pane): Pane.split() --- tests/test_pane.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/test_pane.py b/tests/test_pane.py index 36137ae77..75d7b4c81 100644 --- a/tests/test_pane.py +++ b/tests/test_pane.py @@ -223,3 +223,36 @@ def test_resize_pane( ) pane_height_expanded = int(pane.pane_height) assert pane_height_before < pane_height_expanded + + +def test_split_pane_size(session: Session) -> None: + """Pane.split().""" + window = session.new_window(window_name="split window size") + window.resize(height=100, width=100) + pane = window.active_pane + assert pane is not None + + if has_gte_version("3.1"): + short_pane = pane.split(size=10) + assert short_pane.pane_height == "10" + + narrow_pane = pane.split(vertical=False, size=10) + assert narrow_pane.pane_width == "10" + + new_pane = pane.split(size="10%") + assert new_pane.pane_height == "8" + + new_pane = short_pane.split(vertical=False, size="10%") + assert new_pane.pane_width == "10" + else: + window_height_before = ( + int(window.window_height) if isinstance(window.window_height, str) else 0 + ) + window_width_before = ( + int(window.window_width) if isinstance(window.window_width, str) else 0 + ) + new_pane = pane.split(size="10%") + assert new_pane.pane_height == str(int(window_height_before * 0.1)) + + new_pane = new_pane.split(vertical=False, size="10%") + assert new_pane.pane_width == str(int(window_width_before * 0.1)) From cd7519d3471bdc9882ab89d8624282b9c96b2099 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 3 Mar 2024 11:40:16 -0600 Subject: [PATCH 03/29] refactor!(Window): Add .split(), deprecate split_window() --- src/libtmux/window.py | 73 +++++++++++++++++++++++++++++++++---------- 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/src/libtmux/window.py b/src/libtmux/window.py index 895fe874f..30b98ea91 100644 --- a/src/libtmux/window.py +++ b/src/libtmux/window.py @@ -197,7 +197,7 @@ def select_pane(self, target_pane: t.Union[str, int]) -> t.Optional["Pane"]: return self.active_pane - def split_window( + def split( self, target: t.Optional[t.Union[int, str]] = None, start_directory: t.Optional[str] = None, @@ -205,7 +205,6 @@ def split_window( vertical: bool = True, shell: t.Optional[str] = None, size: t.Optional[t.Union[str, int]] = None, - percent: t.Optional[int] = None, # deprecated environment: t.Optional[t.Dict[str, str]] = None, ) -> "Pane": """Split window and return the created :class:`Pane`. @@ -232,9 +231,6 @@ def split_window( window upon completion is desired. size: int, optional Cell/row or percentage to occupy with respect to current window. - percent: int, optional - Deprecated in favor of size. Percentage to occupy with respect to current - window. environment: dict, optional Environmental variables for new pane. tmux 3.0+ only. Passthrough to ``-e``. @@ -264,9 +260,10 @@ def split_window( if target is not None: tmux_args += ("-t%s" % target,) else: + active_pane = self.active_pane or self.panes[0] if len(self.panes): tmux_args += ( - f"-t{self.session_id}:{self.window_id}.{self.panes[0].pane_index}", + f"-t{self.session_id}:{self.window_id}.{active_pane.pane_index}", ) else: tmux_args += (f"-t{self.session_id}:{self.window_id}",) @@ -288,16 +285,6 @@ def split_window( else: tmux_args += (f"-l{size}",) - if percent is not None: - # Deprecated in 3.1 in favor of -l - warnings.warn( - f'Deprecated in favor of size="{str(percent).rstrip("%")}%" ' - + ' ("-l" flag) in tmux 3.1+.', - category=DeprecationWarning, - stacklevel=2, - ) - tmux_args += (f"-p{percent}",) - tmux_args += ("-P", "-F%s" % "".join(tmux_formats)) # output if start_directory is not None: @@ -833,6 +820,60 @@ def width(self) -> t.Optional[str]: # # Legacy: Redundant stuff we want to remove # + def split_window( + self, + target: t.Optional[t.Union[int, str]] = None, + start_directory: t.Optional[str] = None, + attach: bool = False, + vertical: bool = True, + shell: t.Optional[str] = None, + size: t.Optional[t.Union[str, int]] = None, + percent: t.Optional[int] = None, # deprecated + environment: t.Optional[t.Dict[str, str]] = None, + ) -> "Pane": + """Split window and return the created :class:`Pane`. + + Notes + ----- + .. deprecated:: 0.33.0 + + Deprecated in favor of :meth:`.split()`. + + .. versionchanged:: 0.28.0 + + ``attach`` default changed from ``True`` to ``False``. + + .. deprecated:: 0.28.0 + + ``percent=25`` deprecated in favor of ``size="25%"``. + """ + warnings.warn( + "Window.split_window() is deprecated in favor of Window.split()", + category=DeprecationWarning, + stacklevel=2, + ) + + if percent is not None: + # Deprecated in 3.1 in favor of -l + warnings.warn( + f'Deprecated in favor of size="{str(percent).rstrip("%")}%" ' + + ' ("-l" flag) in tmux 3.1+.', + category=DeprecationWarning, + stacklevel=2, + ) + if size is None: + size = f"{str(percent).rstrip('%')}%" + + return self.split( + target=target, + start_directory=start_directory, + attach=attach, + vertical=vertical, + shell=shell, + size=size, + environment=environment, + ) + @property def attached_pane(self) -> t.Optional["Pane"]: """Return attached :class:`Pane`. From c8c30bca1ed4671bf6ba84bdaafdcdf41cc8fd5e Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 3 Mar 2024 11:46:38 -0600 Subject: [PATCH 04/29] chore(Window): Window.split_window() -> Window.split() --- README.md | 8 +++--- docs/quickstart.md | 12 ++++----- tests/test_dataclasses.py | 8 +++--- tests/test_pane.py | 8 +++--- tests/test_tmuxobject.py | 4 +-- tests/test_window.py | 54 +++++++++++++++++++-------------------- 6 files changed, 47 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 99b9674a8..eaca41335 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,7 @@ Grab remaining tmux window: ```python >>> window = session.active_window ->>> window.split_window(attach=False) +>>> window.split(attach=False) Pane(%2 Window(@1 1:... Session($1 ...))) ``` @@ -196,14 +196,14 @@ Window(@1 1:libtmuxower, Session($1 ...)) Split window (create a new pane): ```python ->>> pane = window.split_window() ->>> pane = window.split_window(attach=False) +>>> pane = window.split() +>>> pane = window.split(attach=False) >>> pane.select() Pane(%3 Window(@1 1:..., Session($1 ...))) >>> window = session.new_window(attach=False, window_name="test") >>> window Window(@2 2:test, Session($1 ...)) ->>> pane = window.split_window(attach=False) +>>> pane = window.split(attach=False) >>> pane Pane(%5 Window(@2 2:test, Session($1 ...))) ``` diff --git a/docs/quickstart.md b/docs/quickstart.md index db8e42cdb..1c17fa87c 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -149,7 +149,7 @@ Create a pane from a window: '%2' ``` -Raw output directly to a {class}`Pane` (in practice, you'd use {meth}`Window.split_window()`): +Raw output directly to a {class}`Pane` (in practice, you'd use {meth}`Window.split()`): ```python >>> Pane.from_pane_id(pane_id=window.cmd('split-window', '-P', '-F#{pane_id}').stdout[0], server=window.server) @@ -312,10 +312,10 @@ to grab our current window. `window` now has access to all of the objects inside of {class}`Window`. -Let's create a pane, {meth}`Window.split_window`: +Let's create a pane, {meth}`Window.split`: ```python ->>> window.split_window(attach=False) +>>> window.split(attach=False) Pane(%2 Window(@1 ...:..., Session($1 ...))) ``` @@ -341,7 +341,7 @@ You have two ways you can move your cursor to new sessions, windows and panes. For one, arguments such as `attach=False` can be omittted. ```python ->>> pane = window.split_window() +>>> pane = window.split() ``` This gives you the {class}`Pane` along with moving the cursor to a new window. You @@ -349,7 +349,7 @@ can also use the `.select_*` available on the object, in this case the pane has {meth}`Pane.select()`. ```python ->>> pane = window.split_window(attach=False) +>>> pane = window.split(attach=False) ``` ```python @@ -371,7 +371,7 @@ As long as you have the object, or are iterating through a list of them, you can ```python >>> window = session.new_window(attach=False, window_name="test") ->>> pane = window.split_window(attach=False) +>>> pane = window.split(attach=False) >>> pane.send_keys('echo hey', enter=False) ``` diff --git a/tests/test_dataclasses.py b/tests/test_dataclasses.py index 6069aada3..9d164a3ef 100644 --- a/tests/test_dataclasses.py +++ b/tests/test_dataclasses.py @@ -41,8 +41,8 @@ def test_pane( assert __session is not None __window = __session.active_window - __window.split_window() - __pane = __window.split_window() + __window.split() + __pane = __window.split() __window.select_layout("main-vertical") assert __pane is not None @@ -125,12 +125,12 @@ def test_pane( # # Window-level - new_pane = window.split_window() + new_pane = window.split() assert new_pane.pane_id != pane.pane_id assert new_pane.window_id == window.window_id # Pane-level - new_pane_2 = new_pane.split_window() + new_pane_2 = new_pane.split() assert new_pane_2.pane_id != new_pane.pane_id assert new_pane_2.window_id == new_pane.window_id diff --git a/tests/test_pane.py b/tests/test_pane.py index 75d7b4c81..233cd66db 100644 --- a/tests/test_pane.py +++ b/tests/test_pane.py @@ -28,7 +28,7 @@ def test_send_keys(session: Session) -> None: def test_set_height(session: Session) -> None: """Verify Pane.set_height().""" window = session.new_window(window_name="test_set_height") - window.split_window() + window.split() pane1 = window.active_pane assert pane1 is not None pane1_height = pane1.pane_height @@ -42,7 +42,7 @@ def test_set_height(session: Session) -> None: def test_set_width(session: Session) -> None: """Verify Pane.set_width().""" window = session.new_window(window_name="test_set_width") - window.split_window() + window.split() window.select_layout("main-vertical") pane1 = window.active_pane @@ -144,8 +144,8 @@ def test_resize_pane( session.cmd("detach-client", "-s") window = session.active_window - pane = window.split_window(attach=False) - window.split_window(vertical=True, attach=False) + pane = window.split(attach=False) + window.split(vertical=True, attach=False) assert pane is not None diff --git a/tests/test_tmuxobject.py b/tests/test_tmuxobject.py index b5a2d4728..585f7244f 100644 --- a/tests/test_tmuxobject.py +++ b/tests/test_tmuxobject.py @@ -99,7 +99,7 @@ def test_find_where_multiple_infos(server: Server, session: Session) -> None: def test_where(server: Server, session: Session) -> None: """Test self.where() returns matching objects.""" window = session.active_window - window.split_window() # create second pane + window.split() # create second pane for session in server.sessions: session_id = session.session_id @@ -156,7 +156,7 @@ def test_filter(server: Server) -> None: sess = server.new_session("test") window = sess.active_window - window.split_window() # create second pane + window.split() # create second pane for session in server.sessions: session_id = session.session_id diff --git a/tests/test_window.py b/tests/test_window.py index f43efcf57..eeb6caafc 100644 --- a/tests/test_window.py +++ b/tests/test_window.py @@ -72,7 +72,7 @@ def test_fresh_window_data(session: Session) -> None: window = session.active_window assert isinstance(window, Window) assert len(session.active_window.panes) == 1 - window.split_window() + window.split() active_window = session.active_window assert active_window is not None @@ -104,12 +104,12 @@ def test_newest_pane_data(session: Session) -> None: window = session.new_window(window_name="test", attach=True) assert isinstance(window, Window) assert len(window.panes) == 1 - window.split_window(attach=True) + window.split(attach=True) assert len(window.panes) == 2 - # note: the below used to accept -h, removing because split_window now + # note: the below used to accept -h, removing because split now # has attach as its only argument now - window.split_window(attach=True) + window.split(attach=True) assert len(window.panes) == 3 @@ -119,11 +119,11 @@ def test_active_pane(session: Session) -> None: assert isinstance(window.active_pane, Pane) -def test_split_window(session: Session) -> None: - """Window.split_window() splits window, returns new Pane, vertical.""" +def test_split(session: Session) -> None: + """Window.split() splits window, returns new Pane, vertical.""" window_name = "test split window" window = session.new_window(window_name=window_name, attach=True) - pane = window.split_window() + pane = window.split() assert len(window.panes) == 2 assert isinstance(pane, Pane) @@ -134,12 +134,12 @@ def test_split_window(session: Session) -> None: assert float(first_pane.pane_height) <= ((float(window.window_width) + 1) / 2) -def test_split_window_shell(session: Session) -> None: - """Window.split_window() splits window, returns new Pane, vertical.""" +def test_split_shell(session: Session) -> None: + """Window.split() splits window, returns new Pane, vertical.""" window_name = "test split window" cmd = "sleep 1m" window = session.new_window(window_name=window_name, attach=True) - pane = window.split_window(shell=cmd) + pane = window.split(shell=cmd) assert len(window.panes) == 2 assert isinstance(pane, Pane) @@ -156,11 +156,11 @@ def test_split_window_shell(session: Session) -> None: assert pane.pane_start_command == cmd -def test_split_window_horizontal(session: Session) -> None: - """Window.split_window() splits window, returns new Pane, horizontal.""" +def test_split_horizontal(session: Session) -> None: + """Window.split() splits window, returns new Pane, horizontal.""" window_name = "test split window" window = session.new_window(window_name=window_name, attach=True) - pane = window.split_window(vertical=False) + pane = window.split(vertical=False) assert len(window.panes) == 2 assert isinstance(pane, Pane) @@ -189,22 +189,22 @@ def test_split_percentage(session: Session) -> None: assert pane.pane_height == str(int(window_height_before * 0.1)) -def test_split_window_size(session: Session) -> None: - """Window.split_window() respects size.""" +def test_split_size(session: Session) -> None: + """Window.split() respects size.""" window = session.new_window(window_name="split window size") window.resize(height=100, width=100) if has_gte_version("3.1"): - pane = window.split_window(size=10) + pane = window.split(size=10) assert pane.pane_height == "10" - pane = window.split_window(vertical=False, size=10) + pane = window.split(vertical=False, size=10) assert pane.pane_width == "10" - pane = window.split_window(size="10%") + pane = window.split(size="10%") assert pane.pane_height == "8" - pane = window.split_window(vertical=False, size="10%") + pane = window.split(vertical=False, size="10%") assert pane.pane_width == "8" else: window_height_before = ( @@ -213,10 +213,10 @@ def test_split_window_size(session: Session) -> None: window_width_before = ( int(window.window_width) if isinstance(window.window_width, str) else 0 ) - pane = window.split_window(size="10%") + pane = window.split(size="10%") assert pane.pane_height == str(int(window_height_before * 0.1)) - pane = window.split_window(vertical=False, size="10%") + pane = window.split(vertical=False, size="10%") assert pane.pane_width == str(int(window_width_before * 0.1)) @@ -405,7 +405,7 @@ def test_empty_window_name(session: Session) -> None: {"ENV_VAR_1": "pane_1", "ENV_VAR_2": "pane_2"}, ], ) -def test_split_window_with_environment( +def test_split_with_environment( session: Session, environment: t.Dict[str, str], ) -> None: @@ -413,8 +413,8 @@ def test_split_window_with_environment( env = shutil.which("env") assert env is not None, "Cannot find usable `env` in Path." - window = session.new_window(window_name="split_window_with_environment") - pane = window.split_window( + window = session.new_window(window_name="split_with_environment") + pane = window.split( shell=f"{env} PS1='$ ' sh", environment=environment, ) @@ -430,7 +430,7 @@ def test_split_window_with_environment( has_gte_version("3.0"), reason="3.0 has the -e flag on split-window", ) -def test_split_window_with_environment_logs_warning_for_old_tmux( +def test_split_with_environment_logs_warning_for_old_tmux( session: Session, caplog: pytest.LogCaptureFixture, ) -> None: @@ -438,8 +438,8 @@ def test_split_window_with_environment_logs_warning_for_old_tmux( env = shutil.which("env") assert env is not None, "Cannot find usable `env` in Path." - window = session.new_window(window_name="split_window_with_environment") - window.split_window( + window = session.new_window(window_name="split_with_environment") + window.split( shell=f"{env} PS1='$ ' sh", environment={"ENV_VAR": "pane"}, ) From 8290cf323130a2113bede128e0dc2dfc8ca72909 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 3 Mar 2024 11:51:00 -0600 Subject: [PATCH 05/29] tests: Move split_window(percent=*) to legacy tests --- tests/legacy_api/test_window.py | 22 +++++++++++++++++++++- tests/test_window.py | 19 +------------------ 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/tests/legacy_api/test_window.py b/tests/legacy_api/test_window.py index db662fd78..f103bab00 100644 --- a/tests/legacy_api/test_window.py +++ b/tests/legacy_api/test_window.py @@ -8,7 +8,7 @@ import pytest from libtmux import exc -from libtmux.common import has_gte_version, has_lt_version +from libtmux.common import has_gte_version, has_lt_version, has_version from libtmux.pane import Pane from libtmux.server import Server from libtmux.session import Session @@ -158,6 +158,26 @@ def test_split_window_horizontal(session: Session) -> None: assert float(window.panes[0].width) <= ((float(window.width) + 1) / 2) +@pytest.mark.filterwarnings("ignore:.*deprecated in favor of Window.split()") +def test_split_percentage( + session: Session, +) -> None: + """Test deprecated percent param.""" + window = session.new_window(window_name="split window size") + window.resize(height=100, width=100) + window_height_before = ( + int(window.window_height) if isinstance(window.window_height, str) else 0 + ) + if has_version("3.4"): + pytest.skip( + "tmux 3.4 has a split-window bug." + + " See https://github.com/tmux/tmux/pull/3840." + ) + with pytest.warns(match="Deprecated in favor of size.*"): + pane = window.split_window(percent=10) + assert pane.pane_height == str(int(window_height_before * 0.1)) + + @pytest.mark.parametrize( "window_name_before,window_name_after", [("test", "ha ha ha fjewlkjflwef"), ("test", "hello \\ wazzup 0")], diff --git a/tests/test_window.py b/tests/test_window.py index eeb6caafc..8eda0fe0e 100644 --- a/tests/test_window.py +++ b/tests/test_window.py @@ -9,7 +9,7 @@ from libtmux import exc from libtmux._internal.query_list import ObjectDoesNotExist -from libtmux.common import has_gte_version, has_lt_version, has_version +from libtmux.common import has_gte_version, has_lt_version from libtmux.constants import ResizeAdjustmentDirection from libtmux.pane import Pane from libtmux.server import Server @@ -172,23 +172,6 @@ def test_split_horizontal(session: Session) -> None: assert float(first_pane.pane_width) <= ((float(window.window_width) + 1) / 2) -def test_split_percentage(session: Session) -> None: - """Test deprecated percent param.""" - window = session.new_window(window_name="split window size") - window.resize(height=100, width=100) - window_height_before = ( - int(window.window_height) if isinstance(window.window_height, str) else 0 - ) - if has_version("3.4"): - pytest.skip( - "tmux 3.4 has a split-window bug." - + " See https://github.com/tmux/tmux/pull/3840." - ) - with pytest.warns(match="Deprecated in favor of size.*"): - pane = window.split_window(percent=10) - assert pane.pane_height == str(int(window_height_before * 0.1)) - - def test_split_size(session: Session) -> None: """Window.split() respects size.""" window = session.new_window(window_name="split window size") From ee7aac488ab81a90ac015dd8791d2d784bd9a9bc Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 3 Mar 2024 12:50:17 -0600 Subject: [PATCH 06/29] feat(Window): Window.split() to use Pane.split_window() --- src/libtmux/window.py | 87 ++++++------------------------------------- 1 file changed, 11 insertions(+), 76 deletions(-) diff --git a/src/libtmux/window.py b/src/libtmux/window.py index 30b98ea91..11256c437 100644 --- a/src/libtmux/window.py +++ b/src/libtmux/window.py @@ -7,7 +7,6 @@ import dataclasses import logging -import pathlib import shlex import typing as t import warnings @@ -22,8 +21,7 @@ from libtmux.pane import Pane from . import exc -from .common import PaneDict, WindowOptionDict, handle_option_error, has_lt_version -from .formats import FORMAT_SEPARATOR +from .common import PaneDict, WindowOptionDict, handle_option_error if t.TYPE_CHECKING: from .server import Server @@ -207,9 +205,7 @@ def split( size: t.Optional[t.Union[str, int]] = None, environment: t.Optional[t.Dict[str, str]] = None, ) -> "Pane": - """Split window and return the created :class:`Pane`. - - Used for splitting window and holding in a python object. + """Split window on active pane and return the created :class:`Pane`. Parameters ---------- @@ -218,8 +214,6 @@ def split( True. start_directory : str, optional specifies the working directory in which the new window is created. - target : str - ``target_pane`` to split. vertical : str split vertically shell : str, optional @@ -253,74 +247,15 @@ def split( ``percent=25`` deprecated in favor of ``size="25%"``. """ - tmux_formats = ["#{pane_id}" + FORMAT_SEPARATOR] - - tmux_args: t.Tuple[str, ...] = () - - if target is not None: - tmux_args += ("-t%s" % target,) - else: - active_pane = self.active_pane or self.panes[0] - if len(self.panes): - tmux_args += ( - f"-t{self.session_id}:{self.window_id}.{active_pane.pane_index}", - ) - else: - tmux_args += (f"-t{self.session_id}:{self.window_id}",) - - if vertical: - tmux_args += ("-v",) - else: - tmux_args += ("-h",) - - if size is not None: - if has_lt_version("3.1"): - if isinstance(size, str) and size.endswith("%"): - tmux_args += (f'-p{str(size).rstrip("%")}',) - else: - warnings.warn( - 'Ignored size. Use percent in tmux < 3.1, e.g. "size=50%"', - stacklevel=2, - ) - else: - tmux_args += (f"-l{size}",) - - tmux_args += ("-P", "-F%s" % "".join(tmux_formats)) # output - - if start_directory is not None: - # as of 2014-02-08 tmux 1.9-dev doesn't expand ~ in new-window -c. - start_path = pathlib.Path(start_directory).expanduser() - tmux_args += (f"-c{start_path}",) - - if not attach: - tmux_args += ("-d",) - - if environment: - if has_gte_version("3.0"): - for k, v in environment.items(): - tmux_args += (f"-e{k}={v}",) - else: - logger.warning( - "Environment flag ignored, tmux 3.0 or newer required.", - ) - - if shell: - tmux_args += (shell,) - - pane_cmd = self.cmd("split-window", *tmux_args) - - # tmux < 1.7. This is added in 1.7. - if pane_cmd.stderr: - if "pane too small" in pane_cmd.stderr: - raise exc.LibTmuxException(pane_cmd.stderr) - - raise exc.LibTmuxException(pane_cmd.stderr, self.__dict__, self.panes) - - pane_output = pane_cmd.stdout[0] - - pane_formatters = dict(zip(["pane_id"], pane_output.split(FORMAT_SEPARATOR))) - - return Pane.from_pane_id(server=self.server, pane_id=pane_formatters["pane_id"]) + active_pane = self.active_pane or self.panes[0] + return active_pane.split( + start_directory=start_directory, + attach=attach, + vertical=vertical, + shell=shell, + size=size, + environment=environment, + ) def resize( self, From 1829385a60240e883cb53e6ab0754b4351e14806 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 3 Mar 2024 18:37:18 -0600 Subject: [PATCH 07/29] constants(WindowDirection): Add enum and map for new-window directions --- src/libtmux/constants.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/libtmux/constants.py b/src/libtmux/constants.py index d6d52c3e3..525b1bdb3 100644 --- a/src/libtmux/constants.py +++ b/src/libtmux/constants.py @@ -19,3 +19,16 @@ class ResizeAdjustmentDirection(enum.Enum): ResizeAdjustmentDirection.Left: "-L", ResizeAdjustmentDirection.Right: "-R", } + + +class WindowDirection(enum.Enum): + """Used for *adjustment* in :meth:`Session.new_window()`.""" + + Before = "BEFORE" + After = "AFTER" + + +WINDOW_DIRECTION_FLAG_MAP: t.Dict[WindowDirection, str] = { + WindowDirection.Before: "-b", + WindowDirection.After: "-a", +} From b46e7fafc4a5e1154805319352191b2c21d590bb Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 10 Mar 2024 05:17:51 -0500 Subject: [PATCH 08/29] chore(Session.new_window): Avoid redundant use of -t --- src/libtmux/session.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/libtmux/session.py b/src/libtmux/session.py index cd6fa21c9..892186371 100644 --- a/src/libtmux/session.py +++ b/src/libtmux/session.py @@ -590,11 +590,6 @@ def new_window( if window_name is not None and isinstance(window_name, str): window_args += ("-n", window_name) - window_args += ( - # empty string for window_index will use the first one available - f"-t{self.session_id}:{window_index}", - ) - if environment: if has_gte_version("3.0"): for k, v in environment.items(): From 59ac58511ec6c566f8ab13d4970a0ab5c7e2c471 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 9 Mar 2024 14:38:48 -0600 Subject: [PATCH 09/29] feat(Session.new_window): Add direction, target_window --- src/libtmux/session.py | 61 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/libtmux/session.py b/src/libtmux/session.py index 892186371..a85ac8341 100644 --- a/src/libtmux/session.py +++ b/src/libtmux/session.py @@ -13,6 +13,7 @@ from libtmux._internal.query_list import QueryList from libtmux.common import tmux_cmd +from libtmux.constants import WINDOW_DIRECTION_FLAG_MAP, WindowDirection from libtmux.formats import FORMAT_SEPARATOR from libtmux.neo import Obj, fetch_obj, fetch_objs from libtmux.pane import Pane @@ -541,6 +542,8 @@ def new_window( window_index: str = "", window_shell: t.Optional[str] = None, environment: t.Optional[t.Dict[str, str]] = None, + direction: t.Optional[WindowDirection] = None, + target_window: t.Optional[str] = None, ) -> "Window": """Create new window, returns new :class:`Window`. @@ -566,10 +569,52 @@ def new_window( useful for long-running processes where the closing of the window upon completion is desired. + direction : WindowDirection, optional + Insert window before or after target window (tmux 3.2+). + + target_window : str, optional + Used by :meth:`Window.new_window` to specify the target window. + .. versionchanged:: 0.28.0 ``attach`` default changed from ``True`` to ``False``. + See Also + -------- + :meth:`Window.new_window()` + + Examples + -------- + .. :: + >>> import pytest + >>> from libtmux.common import has_lt_version + >>> if has_lt_version('3.2'): + ... pytest.skip('direction doctests require tmux 3.2 or newer') + >>> window_initial = session.new_window(window_name='Example') + >>> window_initial + Window(@... 2:Example, Session($1 libtmux_...)) + >>> window_initial.window_index + '2' + + >>> window_before = session.new_window( + ... window_name='Window before', direction=WindowDirection.Before) + >>> window_initial.refresh() + >>> window_before + Window(@... 1:Window before, Session($1 libtmux_...)) + >>> window_initial + Window(@... 3:Example, Session($1 libtmux_...)) + + >>> window_after = session.new_window( + ... window_name='Window after', direction=WindowDirection.After) + >>> window_initial.refresh() + >>> window_after.refresh() + >>> window_after + Window(@... 3:Window after, Session($1 libtmux_...)) + >>> window_initial + Window(@... 4:Example, Session($1 libtmux_...)) + >>> window_before + Window(@... 1:Window before, Session($1 libtmux_...)) + Returns ------- :class:`Window` @@ -599,6 +644,22 @@ def new_window( "Environment flag ignored, requires tmux 3.0 or newer.", ) + if direction is not None: + if has_gte_version("3.2"): + window_args += (WINDOW_DIRECTION_FLAG_MAP[direction],) + else: + logger.warning( + "Direction flag ignored, requires tmux 3.1 or newer.", + ) + + if target_window: + if has_gte_version("3.2"): + window_args += (f"-t{target_window}",) + else: + logger.warning( + "Window target ignored, requires tmux 3.1 or newer.", + ) + if window_shell: window_args += (window_shell,) From a80e7ebfb115eefb28a28144ddfd94acfdbce908 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 9 Mar 2024 15:12:36 -0600 Subject: [PATCH 10/29] feat(Window): Add `Window.new_window()` --- src/libtmux/window.py | 60 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/libtmux/window.py b/src/libtmux/window.py index 11256c437..32064b092 100644 --- a/src/libtmux/window.py +++ b/src/libtmux/window.py @@ -16,6 +16,7 @@ from libtmux.constants import ( RESIZE_ADJUSTMENT_DIRECTION_FLAG_MAP, ResizeAdjustmentDirection, + WindowDirection, ) from libtmux.neo import Obj, fetch_obj, fetch_objs from libtmux.pane import Pane @@ -627,6 +628,65 @@ def move_window( return self + def new_window( + self, + window_name: t.Optional[str] = None, + start_directory: None = None, + attach: bool = False, + window_index: str = "", + window_shell: t.Optional[str] = None, + environment: t.Optional[t.Dict[str, str]] = None, + direction: t.Optional[WindowDirection] = None, + ) -> "Window": + """Create new window respective of current window's position. + + See Also + -------- + :meth:`Session.new_window()` + + Examples + -------- + .. :: + >>> import pytest + >>> from libtmux.common import has_lt_version + >>> if has_lt_version('3.2'): + ... pytest.skip('This doctest requires tmux 3.2 or newer') + >>> window_initial = session.new_window(window_name='Example') + >>> window_initial + Window(@... 2:Example, Session($1 libtmux_...)) + >>> window_initial.window_index + '2' + + >>> window_before = window_initial.new_window( + ... window_name='Window before', direction=WindowDirection.Before) + >>> window_initial.refresh() + >>> window_before + Window(@... 2:Window before, Session($1 libtmux_...)) + >>> window_initial + Window(@... 3:Example, Session($1 libtmux_...)) + + >>> window_after = window_initial.new_window( + ... window_name='Window after', direction=WindowDirection.After) + >>> window_initial.refresh() + >>> window_after.refresh() + >>> window_after + Window(@... 4:Window after, Session($1 libtmux_...)) + >>> window_initial + Window(@... 3:Example, Session($1 libtmux_...)) + >>> window_before + Window(@... 2:Window before, Session($1 libtmux_...)) + """ + return self.session.new_window( + window_name=window_name, + start_directory=start_directory, + attach=attach, + window_index=window_index, + window_shell=window_shell, + environment=environment, + direction=direction, + target_window=self.window_id, + ) + # # Climbers # From e7886a2f48634e73efca76a143ac097edb44a78a Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 10 Mar 2024 04:52:32 -0500 Subject: [PATCH 11/29] tests(Session): new_window warning for tmux 3.0 and below --- tests/test_session.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_session.py b/tests/test_session.py index 9f6b310e6..8426ddbd6 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -8,6 +8,7 @@ from libtmux import exc from libtmux.common import has_gte_version, has_lt_version +from libtmux.constants import WindowDirection from libtmux.pane import Pane from libtmux.server import Server from libtmux.session import Session @@ -327,3 +328,22 @@ def test_new_window_with_environment_logs_warning_for_old_tmux( assert any( "Environment flag ignored" in record.msg for record in caplog.records ), "Warning missing" + + +@pytest.mark.skipif( + has_gte_version("3.1"), + reason="Only 3.1 has the -a and -b flag on new-window", +) +def test_session_new_window_with_direction_logs_warning_for_old_tmux( + session: Session, + caplog: pytest.LogCaptureFixture, +) -> None: + """Verify new window with direction create a warning if tmux is too old.""" + session.new_window( + window_name="session_window_with_direction", + direction=WindowDirection.After, + ) + + assert any( + "Direction flag ignored" in record.msg for record in caplog.records + ), "Warning missing" From 5a9487f4c79ba43af483c0719e9c26dec8be6cbf Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 10 Mar 2024 04:54:38 -0500 Subject: [PATCH 12/29] tests(Window): new_window warning for tmux 3.0 and below --- tests/test_window.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/test_window.py b/tests/test_window.py index 8eda0fe0e..d26db91fe 100644 --- a/tests/test_window.py +++ b/tests/test_window.py @@ -10,7 +10,7 @@ from libtmux import exc from libtmux._internal.query_list import ObjectDoesNotExist from libtmux.common import has_gte_version, has_lt_version -from libtmux.constants import ResizeAdjustmentDirection +from libtmux.constants import ResizeAdjustmentDirection, WindowDirection from libtmux.pane import Pane from libtmux.server import Server from libtmux.session import Session @@ -510,3 +510,29 @@ def test_resize( ) window_height_expanded = int(window.window_height) assert window_height_before < window_height_expanded + + +@pytest.mark.skipif( + has_gte_version("3.2"), + reason="Only 3.2+ has the -a and -b flag on new-window", +) +def test_new_window_with_direction_logs_warning_for_old_tmux( + session: Session, + caplog: pytest.LogCaptureFixture, +) -> None: + """Verify new window with direction create a warning if tmux is too old.""" + window = session.active_window + window.refresh() + + window.new_window( + window_name="window_with_direction", + direction=WindowDirection.After, + ) + + assert any( + "Window target ignored" in record.msg for record in caplog.records + ), "Warning missing" + + assert any( + "Direction flag ignored" in record.msg for record in caplog.records + ), "Warning missing" From fd0d066e042243f9cd90a391771e60dd162fcf87 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 10 Mar 2024 05:39:20 -0500 Subject: [PATCH 13/29] test(Window.new_window): Test with `direction` --- tests/test_window.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_window.py b/tests/test_window.py index d26db91fe..79f75295b 100644 --- a/tests/test_window.py +++ b/tests/test_window.py @@ -512,6 +512,37 @@ def test_resize( assert window_height_before < window_height_expanded +@pytest.mark.skipif( + has_lt_version("3.2"), + reason="Only 3.2+ has the -a and -b flag on new-window", +) +def test_new_window_with_direction( + session: Session, +) -> None: + """Verify new window with direction.""" + window = session.active_window + window.refresh() + + window_initial = session.new_window(window_name="Example") + assert window_initial.window_index == "2" + + window_before = window_initial.new_window( + window_name="Window before", direction=WindowDirection.Before + ) + window_initial.refresh() + assert window_before.window_index == "2" + assert window_initial.window_index == "3" + + window_after = window_initial.new_window( + window_name="Window after", direction=WindowDirection.After + ) + window_initial.refresh() + window_after.refresh() + assert window_after.window_index == "4" + assert window_initial.window_index == "3" + assert window_before.window_index == "2" + + @pytest.mark.skipif( has_gte_version("3.2"), reason="Only 3.2+ has the -a and -b flag on new-window", From 3c086cf8870012efc4e1ac07746fe7aabcd1d7c1 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 10 Mar 2024 06:00:45 -0500 Subject: [PATCH 14/29] test(Session.new_window): Test with `direction` --- tests/test_session.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_session.py b/tests/test_session.py index 8426ddbd6..aa131702e 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -330,6 +330,37 @@ def test_new_window_with_environment_logs_warning_for_old_tmux( ), "Warning missing" +@pytest.mark.skipif( + has_lt_version("3.2"), + reason="Only 3.2+ has the -a and -b flag on new-window", +) +def test_session_new_window_with_direction( + session: Session, +) -> None: + """Verify new window with direction.""" + window = session.active_window + window.refresh() + + window_initial = session.new_window(window_name="Example") + assert window_initial.window_index == "2" + + window_before = session.new_window( + window_name="Window before", direction=WindowDirection.Before + ) + window_initial.refresh() + assert window_before.window_index == "1" + assert window_initial.window_index == "3" + + window_after = session.new_window( + window_name="Window after", direction=WindowDirection.After + ) + window_initial.refresh() + window_after.refresh() + assert window_after.window_index == "3" + assert window_initial.window_index == "4" + assert window_before.window_index == "1" + + @pytest.mark.skipif( has_gte_version("3.1"), reason="Only 3.1 has the -a and -b flag on new-window", From 76cec6f9506a99a0839fb8021e7e17a0c10787cb Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 10 Mar 2024 07:01:50 -0500 Subject: [PATCH 15/29] constants(PaneDirection): Add enum and map for split-window `direction` --- src/libtmux/constants.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/libtmux/constants.py b/src/libtmux/constants.py index 525b1bdb3..ea8bcab3b 100644 --- a/src/libtmux/constants.py +++ b/src/libtmux/constants.py @@ -32,3 +32,21 @@ class WindowDirection(enum.Enum): WindowDirection.Before: "-b", WindowDirection.After: "-a", } + + +class PaneDirection(enum.Enum): + """Used for *adjustment* in :meth:`Pane.split()`.""" + + Above = "ABOVE" + Below = "BELOW" # default with no args + Right = "RIGHT" + Left = "LEFT" + + +PANE_DIRECTION_FLAG_MAP: t.Dict[PaneDirection, t.List[str]] = { + # -v is assumed, but for explicitness it is passed + PaneDirection.Above: ["-v", "-b"], + PaneDirection.Below: ["-v"], + PaneDirection.Right: ["-h"], + PaneDirection.Left: ["-h", "-b"], +} From a93871f0039089c26a88be40658fcc76a6987413 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 10 Mar 2024 07:29:34 -0500 Subject: [PATCH 16/29] feat(Pane.split_window): Add `direction`, deprecate `vertical`. --- src/libtmux/pane.py | 44 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/src/libtmux/pane.py b/src/libtmux/pane.py index 234c0f2aa..ecc99a838 100644 --- a/src/libtmux/pane.py +++ b/src/libtmux/pane.py @@ -14,7 +14,9 @@ from libtmux.common import has_gte_version, has_lt_version, tmux_cmd from libtmux.constants import ( + PANE_DIRECTION_FLAG_MAP, RESIZE_ADJUSTMENT_DIRECTION_FLAG_MAP, + PaneDirection, ResizeAdjustmentDirection, ) from libtmux.formats import FORMAT_SEPARATOR @@ -489,15 +491,14 @@ def split( self, start_directory: t.Optional[str] = None, attach: bool = False, - vertical: bool = True, + direction: t.Optional[PaneDirection] = None, shell: t.Optional[str] = None, size: t.Optional[t.Union[str, int]] = None, - percent: t.Optional[int] = None, # deprecated environment: t.Optional[t.Dict[str, str]] = None, + percent: t.Optional[int] = None, # deprecated + vertical: t.Optional[bool] = None, # deprecated ) -> "Pane": - """Split window and return the created :class:`Pane`. - - Used for splitting window and holding in a python object. + """Split window and return :class:`Pane`, by default beneath current pane. Parameters ---------- @@ -506,8 +507,8 @@ def split( True. start_directory : str, optional specifies the working directory in which the new window is created. - vertical : bool, optional - split vertically + direction : PaneDirection, optional + split in direction. If none is specified, assume down. shell : str, optional execute a command on splitting the window. The pane will close when the command exits. @@ -522,6 +523,8 @@ def split( window. environment: dict, optional Environmental variables for new pane. tmux 3.0+ only. Passthrough to ``-e``. + vertical : bool, optional + split vertically, deprecated by ``direction``. Notes ----- @@ -534,6 +537,11 @@ def split( active. To remain on the same window and split the pane in another target window, pass in ``attach=False``. + .. deprecated:: 0.33.0 + + ``vertical=True`` deprecated in favor of + ``direction=PaneDirection.Below``. + .. versionchanged:: 0.28.0 ``attach`` default changed from ``True`` to ``False``. @@ -546,10 +554,26 @@ def split( tmux_args: t.Tuple[str, ...] = () - if vertical: - tmux_args += ("-v",) + if direction: + tmux_args += tuple(PANE_DIRECTION_FLAG_MAP[direction]) + if vertical is not None: + warnings.warn( + "vertical is not required to pass with direction.", + category=DeprecationWarning, + stacklevel=2, + ) + elif vertical is not None: + warnings.warn( + "vertical is deprecated in favor of direction.", + category=DeprecationWarning, + stacklevel=2, + ) + if vertical: + tmux_args += ("-v",) + else: + tmux_args += ("-h",) else: - tmux_args += ("-h",) + tmux_args += tuple(PANE_DIRECTION_FLAG_MAP[PaneDirection.Below]) if size is not None: if has_lt_version("3.1"): From 2297232ca81352015dc07a2b0daf3805ee67dba8 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 10 Mar 2024 07:50:44 -0500 Subject: [PATCH 17/29] feat(Pane.split_window): Add `full_window_split` --- src/libtmux/pane.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/libtmux/pane.py b/src/libtmux/pane.py index ecc99a838..3b23bc040 100644 --- a/src/libtmux/pane.py +++ b/src/libtmux/pane.py @@ -492,6 +492,7 @@ def split( start_directory: t.Optional[str] = None, attach: bool = False, direction: t.Optional[PaneDirection] = None, + full_window_split: t.Optional[bool] = None, shell: t.Optional[str] = None, size: t.Optional[t.Union[str, int]] = None, environment: t.Optional[t.Dict[str, str]] = None, @@ -509,6 +510,8 @@ def split( specifies the working directory in which the new window is created. direction : PaneDirection, optional split in direction. If none is specified, assume down. + full_window_split: bool, optional + split across full window width or height, rather than active pane. shell : str, optional execute a command on splitting the window. The pane will close when the command exits. @@ -587,6 +590,9 @@ def split( else: tmux_args += (f"-l{size}",) + if full_window_split: + tmux_args += ("-f",) + if percent is not None: # Deprecated in 3.1 in favor of -l warnings.warn( From ffd9b74500dad13000877e73d9a29805a59993a7 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 10 Mar 2024 15:20:46 -0500 Subject: [PATCH 18/29] Pane(split): Clean up signature, use `direction` --- src/libtmux/pane.py | 62 +++------------------------------------------ 1 file changed, 3 insertions(+), 59 deletions(-) diff --git a/src/libtmux/pane.py b/src/libtmux/pane.py index 3b23bc040..3d9681e93 100644 --- a/src/libtmux/pane.py +++ b/src/libtmux/pane.py @@ -496,8 +496,6 @@ def split( shell: t.Optional[str] = None, size: t.Optional[t.Union[str, int]] = None, environment: t.Optional[t.Dict[str, str]] = None, - percent: t.Optional[int] = None, # deprecated - vertical: t.Optional[bool] = None, # deprecated ) -> "Pane": """Split window and return :class:`Pane`, by default beneath current pane. @@ -521,37 +519,8 @@ def split( window upon completion is desired. size: int, optional Cell/row or percentage to occupy with respect to current window. - percent: int, optional - Deprecated in favor of size. Percentage to occupy with respect to current - window. environment: dict, optional Environmental variables for new pane. tmux 3.0+ only. Passthrough to ``-e``. - vertical : bool, optional - split vertically, deprecated by ``direction``. - - Notes - ----- - :term:`tmux(1)` will move window to the new pane if the - ``split-window`` target is off screen. tmux handles the ``-d`` the - same way as ``new-window`` and ``attach`` in - :class:`Session.new_window`. - - By default, this will make the window the pane is created in - active. To remain on the same window and split the pane in another - target window, pass in ``attach=False``. - - .. deprecated:: 0.33.0 - - ``vertical=True`` deprecated in favor of - ``direction=PaneDirection.Below``. - - .. versionchanged:: 0.28.0 - - ``attach`` default changed from ``True`` to ``False``. - - .. deprecated:: 0.28.0 - - ``percent=25`` deprecated in favor of ``size="25%"``. """ tmux_formats = ["#{pane_id}" + FORMAT_SEPARATOR] @@ -559,22 +528,6 @@ def split( if direction: tmux_args += tuple(PANE_DIRECTION_FLAG_MAP[direction]) - if vertical is not None: - warnings.warn( - "vertical is not required to pass with direction.", - category=DeprecationWarning, - stacklevel=2, - ) - elif vertical is not None: - warnings.warn( - "vertical is deprecated in favor of direction.", - category=DeprecationWarning, - stacklevel=2, - ) - if vertical: - tmux_args += ("-v",) - else: - tmux_args += ("-h",) else: tmux_args += tuple(PANE_DIRECTION_FLAG_MAP[PaneDirection.Below]) @@ -593,16 +546,6 @@ def split( if full_window_split: tmux_args += ("-f",) - if percent is not None: - # Deprecated in 3.1 in favor of -l - warnings.warn( - f'Deprecated in favor of size="{str(percent).rstrip("%")}%" ' - + ' ("-l" flag) in tmux 3.1+.', - category=DeprecationWarning, - stacklevel=2, - ) - tmux_args += (f"-p{percent}",) - tmux_args += ("-P", "-F%s" % "".join(tmux_formats)) # output if start_directory is not None: @@ -789,13 +732,14 @@ def split_window( category=DeprecationWarning, stacklevel=2, ) + if size is None and percent is not None: + size = f'{str(percent).rstrip("%")}%' return self.split( attach=attach, start_directory=start_directory, - vertical=vertical, + direction=PaneDirection.Below if vertical else PaneDirection.Right, shell=shell, size=size, - percent=percent, environment=environment, ) From dbeb55428c134af99f6c8d58a4d0d946b33060ad Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 10 Mar 2024 15:21:02 -0500 Subject: [PATCH 19/29] Window(split): Clean up signature, use `direction` --- src/libtmux/window.py | 34 ++++++++++------------------------ 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/src/libtmux/window.py b/src/libtmux/window.py index 32064b092..1ccd910f0 100644 --- a/src/libtmux/window.py +++ b/src/libtmux/window.py @@ -15,6 +15,7 @@ from libtmux.common import has_gte_version, tmux_cmd from libtmux.constants import ( RESIZE_ADJUSTMENT_DIRECTION_FLAG_MAP, + PaneDirection, ResizeAdjustmentDirection, WindowDirection, ) @@ -201,7 +202,8 @@ def split( target: t.Optional[t.Union[int, str]] = None, start_directory: t.Optional[str] = None, attach: bool = False, - vertical: bool = True, + direction: t.Optional[PaneDirection] = None, + full_window_split: t.Optional[bool] = None, shell: t.Optional[str] = None, size: t.Optional[t.Union[str, int]] = None, environment: t.Optional[t.Dict[str, str]] = None, @@ -215,8 +217,10 @@ def split( True. start_directory : str, optional specifies the working directory in which the new window is created. - vertical : str - split vertically + direction : PaneDirection, optional + split in direction. If none is specified, assume down. + full_window_split: bool, optional + split across full window width or height, rather than active pane. shell : str, optional execute a command on splitting the window. The pane will close when the command exits. @@ -228,31 +232,13 @@ def split( Cell/row or percentage to occupy with respect to current window. environment: dict, optional Environmental variables for new pane. tmux 3.0+ only. Passthrough to ``-e``. - - Notes - ----- - :term:`tmux(1)` will move window to the new pane if the - ``split-window`` target is off screen. tmux handles the ``-d`` the - same way as ``new-window`` and ``attach`` in - :class:`Session.new_window`. - - By default, this will make the window the pane is created in - active. To remain on the same window and split the pane in another - target window, pass in ``attach=False``. - - .. versionchanged:: 0.28.0 - - ``attach`` default changed from ``True`` to ``False``. - - .. deprecated:: 0.28.0 - - ``percent=25`` deprecated in favor of ``size="25%"``. """ active_pane = self.active_pane or self.panes[0] return active_pane.split( start_directory=start_directory, attach=attach, - vertical=vertical, + direction=direction, + full_window_split=full_window_split, shell=shell, size=size, environment=environment, @@ -863,7 +849,7 @@ def split_window( target=target, start_directory=start_directory, attach=attach, - vertical=vertical, + direction=PaneDirection.Below if vertical else PaneDirection.Right, shell=shell, size=size, environment=environment, From 95bc838a6e06161795c1f4b3ae5a172ae953060b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 10 Mar 2024 15:21:52 -0500 Subject: [PATCH 20/29] tests(Pane,Window): Use `direction` in .split() --- tests/legacy_api/test_window.py | 32 ++++++++++++++++++++++++++++++++ tests/test_pane.py | 10 +++++----- tests/test_window.py | 14 +++++++++----- 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/tests/legacy_api/test_window.py b/tests/legacy_api/test_window.py index f103bab00..811065953 100644 --- a/tests/legacy_api/test_window.py +++ b/tests/legacy_api/test_window.py @@ -159,6 +159,7 @@ def test_split_window_horizontal(session: Session) -> None: @pytest.mark.filterwarnings("ignore:.*deprecated in favor of Window.split()") +@pytest.mark.filterwarnings("ignore:.*vertical is not required to pass with direction.") def test_split_percentage( session: Session, ) -> None: @@ -178,6 +179,37 @@ def test_split_percentage( assert pane.pane_height == str(int(window_height_before * 0.1)) +def test_split_window_size(session: Session) -> None: + """Window.split_window() respects size.""" + window = session.new_window(window_name="split_window window size") + window.resize(height=100, width=100) + + if has_gte_version("3.1"): + pane = window.split_window(size=10) + assert pane.pane_height == "10" + + pane = window.split_window(vertical=False, size=10) + assert pane.pane_width == "10" + + pane = window.split_window(size="10%") + assert pane.pane_height == "8" + + pane = window.split_window(vertical=False, size="10%") + assert pane.pane_width == "8" + else: + window_height_before = ( + int(window.window_height) if isinstance(window.window_height, str) else 0 + ) + window_width_before = ( + int(window.window_width) if isinstance(window.window_width, str) else 0 + ) + pane = window.split_window(size="10%") + assert pane.pane_height == str(int(window_height_before * 0.1)) + + pane = window.split_window(vertical=False, size="10%") + assert pane.pane_width == str(int(window_width_before * 0.1)) + + @pytest.mark.parametrize( "window_name_before,window_name_after", [("test", "ha ha ha fjewlkjflwef"), ("test", "hello \\ wazzup 0")], diff --git a/tests/test_pane.py b/tests/test_pane.py index 233cd66db..05fdf1be2 100644 --- a/tests/test_pane.py +++ b/tests/test_pane.py @@ -6,7 +6,7 @@ import pytest from libtmux.common import has_gte_version, has_lt_version -from libtmux.constants import ResizeAdjustmentDirection +from libtmux.constants import PaneDirection, ResizeAdjustmentDirection from libtmux.session import Session logger = logging.getLogger(__name__) @@ -145,7 +145,7 @@ def test_resize_pane( window = session.active_window pane = window.split(attach=False) - window.split(vertical=True, attach=False) + window.split(direction=PaneDirection.Above, attach=False) assert pane is not None @@ -236,13 +236,13 @@ def test_split_pane_size(session: Session) -> None: short_pane = pane.split(size=10) assert short_pane.pane_height == "10" - narrow_pane = pane.split(vertical=False, size=10) + narrow_pane = pane.split(direction=PaneDirection.Right, size=10) assert narrow_pane.pane_width == "10" new_pane = pane.split(size="10%") assert new_pane.pane_height == "8" - new_pane = short_pane.split(vertical=False, size="10%") + new_pane = short_pane.split(direction=PaneDirection.Right, size="10%") assert new_pane.pane_width == "10" else: window_height_before = ( @@ -254,5 +254,5 @@ def test_split_pane_size(session: Session) -> None: new_pane = pane.split(size="10%") assert new_pane.pane_height == str(int(window_height_before * 0.1)) - new_pane = new_pane.split(vertical=False, size="10%") + new_pane = new_pane.split(direction=PaneDirection.Right, size="10%") assert new_pane.pane_width == str(int(window_width_before * 0.1)) diff --git a/tests/test_window.py b/tests/test_window.py index 79f75295b..e16dd6410 100644 --- a/tests/test_window.py +++ b/tests/test_window.py @@ -10,7 +10,11 @@ from libtmux import exc from libtmux._internal.query_list import ObjectDoesNotExist from libtmux.common import has_gte_version, has_lt_version -from libtmux.constants import ResizeAdjustmentDirection, WindowDirection +from libtmux.constants import ( + PaneDirection, + ResizeAdjustmentDirection, + WindowDirection, +) from libtmux.pane import Pane from libtmux.server import Server from libtmux.session import Session @@ -160,7 +164,7 @@ def test_split_horizontal(session: Session) -> None: """Window.split() splits window, returns new Pane, horizontal.""" window_name = "test split window" window = session.new_window(window_name=window_name, attach=True) - pane = window.split(vertical=False) + pane = window.split(direction=PaneDirection.Right) assert len(window.panes) == 2 assert isinstance(pane, Pane) @@ -181,13 +185,13 @@ def test_split_size(session: Session) -> None: pane = window.split(size=10) assert pane.pane_height == "10" - pane = window.split(vertical=False, size=10) + pane = window.split(direction=PaneDirection.Right, size=10) assert pane.pane_width == "10" pane = window.split(size="10%") assert pane.pane_height == "8" - pane = window.split(vertical=False, size="10%") + pane = window.split(direction=PaneDirection.Right, size="10%") assert pane.pane_width == "8" else: window_height_before = ( @@ -199,7 +203,7 @@ def test_split_size(session: Session) -> None: pane = window.split(size="10%") assert pane.pane_height == str(int(window_height_before * 0.1)) - pane = window.split(vertical=False, size="10%") + pane = window.split(direction=PaneDirection.Right, size="10%") assert pane.pane_width == str(int(window_width_before * 0.1)) From 51bbd3e4afaa97933ea6a64b08938a55f7f8cde6 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 16 Mar 2024 07:27:49 -0500 Subject: [PATCH 21/29] tests(doctests): split_window() -> split() --- src/libtmux/pane.py | 12 ++++++------ src/libtmux/test.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libtmux/pane.py b/src/libtmux/pane.py index 3d9681e93..4d9e374f7 100644 --- a/src/libtmux/pane.py +++ b/src/libtmux/pane.py @@ -312,7 +312,7 @@ def send_keys( Examples -------- - >>> pane = window.split_window(shell='sh') + >>> pane = window.split(shell='sh') >>> pane.capture_pane() ['$'] @@ -381,7 +381,7 @@ def kill( -------- Kill a pane: - >>> pane_1 = pane.split_window() + >>> pane_1 = pane.split() >>> pane_1 in window.panes True @@ -396,10 +396,10 @@ def kill( >>> pane.window.resize(height=100, width=100) Window(@1 1...) - >>> one_pane_to_rule_them_all = pane.split_window() + >>> one_pane_to_rule_them_all = pane.split() - >>> other_panes = pane.split_window( - ... ), pane.split_window() + >>> other_panes = pane.split( + ... ), pane.split() >>> all([p in window.panes for p in other_panes]) True @@ -440,7 +440,7 @@ def select(self) -> "Pane": Examples -------- >>> pane = window.active_pane - >>> new_pane = window.split_window() + >>> new_pane = window.split() >>> pane.refresh() >>> active_panes = [p for p in window.panes if p.pane_active == '1'] diff --git a/src/libtmux/test.py b/src/libtmux/test.py index 6a1294c01..6413b7b0b 100644 --- a/src/libtmux/test.py +++ b/src/libtmux/test.py @@ -271,7 +271,7 @@ def temp_window( >>> with temp_window(session) as window: - ... window.split_window() + ... window.split() Pane(%4 Window(@3 2:libtmux_..., Session($1 libtmux_...))) """ if "window_name" not in kwargs: From 5918d9311ea33c1824d2bd9568014fc82dd6f093 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 16 Mar 2024 08:27:16 -0500 Subject: [PATCH 22/29] Obj: Add pane_at_{left,top,bottom,right} --- src/libtmux/neo.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libtmux/neo.py b/src/libtmux/neo.py index 6b34e555f..23372d36f 100644 --- a/src/libtmux/neo.py +++ b/src/libtmux/neo.py @@ -89,6 +89,10 @@ class Obj: next_session_id: t.Union[str, None] = None origin_flag: t.Union[str, None] = None pane_active: t.Union[str, None] = None # Not detected by script + pane_at_bottom: t.Union[str, None] = None + pane_at_left: t.Union[str, None] = None + pane_at_right: t.Union[str, None] = None + pane_at_top: t.Union[str, None] = None pane_bg: t.Union[str, None] = None pane_bottom: t.Union[str, None] = None pane_current_command: t.Union[str, None] = None From 24fcb33cc15538132a110821f9259996acac189b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 16 Mar 2024 08:42:31 -0500 Subject: [PATCH 23/29] Obj: Remove newline --- src/libtmux/neo.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libtmux/neo.py b/src/libtmux/neo.py index 23372d36f..e5d3ae32e 100644 --- a/src/libtmux/neo.py +++ b/src/libtmux/neo.py @@ -114,7 +114,6 @@ class Obj: pane_top: t.Union[str, None] = None pane_tty: t.Union[str, None] = None pane_width: t.Union[str, None] = None - pid: t.Union[str, None] = None scroll_position: t.Union[str, None] = None scroll_region_lower: t.Union[str, None] = None From f94523b77d6f0719b68184bc192edb4b8f81ea59 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 16 Mar 2024 08:47:49 -0500 Subject: [PATCH 24/29] feat(Pane): Add at_{top,bottom,left,right} --- src/libtmux/pane.py | 48 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/libtmux/pane.py b/src/libtmux/pane.py index 4d9e374f7..09b26057f 100644 --- a/src/libtmux/pane.py +++ b/src/libtmux/pane.py @@ -693,6 +693,54 @@ def width(self) -> t.Optional[str]: """ return self.pane_width + @property + def at_top(self) -> bool: + """Typed, converted wrapper around :attr:`Pane.pane_at_top`. + + >>> pane.pane_at_top + '1' + + >>> pane.at_top + True + """ + return self.pane_at_top == "1" + + @property + def at_bottom(self) -> bool: + """Typed, converted wrapper around :attr:`Pane.pane_at_bottom`. + + >>> pane.pane_at_bottom + '1' + + >>> pane.at_bottom + True + """ + return self.pane_at_bottom == "1" + + @property + def at_left(self) -> bool: + """Typed, converted wrapper around :attr:`Pane.pane_at_left`. + + >>> pane.pane_at_left + '1' + + >>> pane.at_left + True + """ + return self.pane_at_left == "1" + + @property + def at_right(self) -> bool: + """Typed, converted wrapper around :attr:`Pane.pane_at_right`. + + >>> pane.pane_at_right + '1' + + >>> pane.at_right + True + """ + return self.pane_at_right == "1" + # # Legacy: Redundant stuff we want to remove # From cb80530f47295a68b270a5f6b8df5b23763e039e Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 16 Mar 2024 08:27:33 -0500 Subject: [PATCH 25/29] tests(doctests): For Pane.split --- src/libtmux/pane.py | 53 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/libtmux/pane.py b/src/libtmux/pane.py index 09b26057f..f7bdfdc6c 100644 --- a/src/libtmux/pane.py +++ b/src/libtmux/pane.py @@ -521,6 +521,59 @@ def split( Cell/row or percentage to occupy with respect to current window. environment: dict, optional Environmental variables for new pane. tmux 3.0+ only. Passthrough to ``-e``. + + Examples + -------- + >>> (pane.at_left, pane.at_right, + ... pane.at_top, pane.at_bottom) + (True, True, + True, True) + + >>> new_pane = pane.split() + + >>> (new_pane.at_left, new_pane.at_right, + ... new_pane.at_top, new_pane.at_bottom) + (True, True, + False, True) + + >>> right_pane = pane.split(direction=PaneDirection.Right) + + >>> (right_pane.at_left, right_pane.at_right, + ... right_pane.at_top, right_pane.at_bottom) + (False, True, + True, False) + + >>> left_pane = pane.split(direction=PaneDirection.Left) + + >>> (left_pane.at_left, left_pane.at_right, + ... left_pane.at_top, left_pane.at_bottom) + (True, False, + True, False) + + >>> top_pane = pane.split(direction=PaneDirection.Above) + + >>> (top_pane.at_left, top_pane.at_right, + ... top_pane.at_top, top_pane.at_bottom) + (False, False, + True, False) + + >>> pane = session.new_window().active_pane + + >>> top_pane = pane.split(direction=PaneDirection.Above, full_window_split=True) + + >>> (top_pane.at_left, top_pane.at_right, + ... top_pane.at_top, top_pane.at_bottom) + (True, True, + True, False) + + >>> bottom_pane = pane.split( + ... direction=PaneDirection.Below, + ... full_window_split=True) + + >>> (bottom_pane.at_left, bottom_pane.at_right, + ... bottom_pane.at_top, bottom_pane.at_bottom) + (True, True, + False, True) """ tmux_formats = ["#{pane_id}" + FORMAT_SEPARATOR] From eeb396f4fee6af85f8817b892ad946d5aff8d533 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 16 Mar 2024 08:58:24 -0500 Subject: [PATCH 26/29] tests(test_pane): Check at_{left,right,bottom,top} --- tests/test_pane.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_pane.py b/tests/test_pane.py index 05fdf1be2..b17230f6e 100644 --- a/tests/test_pane.py +++ b/tests/test_pane.py @@ -236,14 +236,27 @@ def test_split_pane_size(session: Session) -> None: short_pane = pane.split(size=10) assert short_pane.pane_height == "10" + assert short_pane.at_left + assert short_pane.at_right + assert not short_pane.at_top + assert short_pane.at_bottom + narrow_pane = pane.split(direction=PaneDirection.Right, size=10) assert narrow_pane.pane_width == "10" + assert not narrow_pane.at_left + assert narrow_pane.at_right + assert narrow_pane.at_top + assert not narrow_pane.at_bottom + new_pane = pane.split(size="10%") assert new_pane.pane_height == "8" new_pane = short_pane.split(direction=PaneDirection.Right, size="10%") assert new_pane.pane_width == "10" + + assert not new_pane.at_left + assert new_pane.at_right else: window_height_before = ( int(window.window_height) if isinstance(window.window_height, str) else 0 From 111716e68f15f7c526935fae7d36206bf76922bf Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 16 Mar 2024 12:46:01 -0500 Subject: [PATCH 27/29] feat({Window,Pane}.split): Add zoom --- src/libtmux/pane.py | 6 ++++++ src/libtmux/window.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/src/libtmux/pane.py b/src/libtmux/pane.py index f7bdfdc6c..2ef250c5c 100644 --- a/src/libtmux/pane.py +++ b/src/libtmux/pane.py @@ -493,6 +493,7 @@ def split( attach: bool = False, direction: t.Optional[PaneDirection] = None, full_window_split: t.Optional[bool] = None, + zoom: t.Optional[bool] = None, shell: t.Optional[str] = None, size: t.Optional[t.Union[str, int]] = None, environment: t.Optional[t.Dict[str, str]] = None, @@ -510,6 +511,8 @@ def split( split in direction. If none is specified, assume down. full_window_split: bool, optional split across full window width or height, rather than active pane. + zoom: bool, optional + expand pane shell : str, optional execute a command on splitting the window. The pane will close when the command exits. @@ -599,6 +602,9 @@ def split( if full_window_split: tmux_args += ("-f",) + if zoom: + tmux_args += ("-Z",) + tmux_args += ("-P", "-F%s" % "".join(tmux_formats)) # output if start_directory is not None: diff --git a/src/libtmux/window.py b/src/libtmux/window.py index 1ccd910f0..5d6047660 100644 --- a/src/libtmux/window.py +++ b/src/libtmux/window.py @@ -204,6 +204,7 @@ def split( attach: bool = False, direction: t.Optional[PaneDirection] = None, full_window_split: t.Optional[bool] = None, + zoom: t.Optional[bool] = None, shell: t.Optional[str] = None, size: t.Optional[t.Union[str, int]] = None, environment: t.Optional[t.Dict[str, str]] = None, @@ -221,6 +222,8 @@ def split( split in direction. If none is specified, assume down. full_window_split: bool, optional split across full window width or height, rather than active pane. + zoom: bool, optional + expand pane shell : str, optional execute a command on splitting the window. The pane will close when the command exits. @@ -239,6 +242,7 @@ def split( attach=attach, direction=direction, full_window_split=full_window_split, + zoom=zoom, shell=shell, size=size, environment=environment, From 47254522eacdf6b668ce2e6bb9f00089b8ebe335 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 16 Mar 2024 13:01:38 -0500 Subject: [PATCH 28/29] tests({Pane,Window}.split_window): Add tests for zoom --- tests/test_pane.py | 32 +++++++++++++++++++++++++++++++- tests/test_window.py | 28 +++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/tests/test_pane.py b/tests/test_pane.py index b17230f6e..428fda2e3 100644 --- a/tests/test_pane.py +++ b/tests/test_pane.py @@ -5,7 +5,7 @@ import pytest -from libtmux.common import has_gte_version, has_lt_version +from libtmux.common import has_gte_version, has_lt_version, has_lte_version from libtmux.constants import PaneDirection, ResizeAdjustmentDirection from libtmux.session import Session @@ -133,6 +133,36 @@ def test_capture_pane_end(session: Session) -> None: assert pane_contents == '$ printf "%s"\n$' +@pytest.mark.skipif( + has_lte_version("3.1"), + reason="3.2 has the -Z flag on split-window", +) +def test_pane_split_window_zoom( + session: Session, +) -> None: + """Verify splitting window with zoom.""" + window_without_zoom = session.new_window(window_name="split_without_zoom") + initial_pane_without_zoom = window_without_zoom.active_pane + assert initial_pane_without_zoom is not None + window_with_zoom = session.new_window(window_name="split_with_zoom") + initial_pane_with_zoom = window_with_zoom.active_pane + assert initial_pane_with_zoom is not None + pane_without_zoom = initial_pane_without_zoom.split( + zoom=False, + ) + pane_with_zoom = initial_pane_with_zoom.split( + zoom=True, + ) + + assert pane_without_zoom.width == pane_without_zoom.window_width + assert pane_without_zoom.height is not None + assert pane_without_zoom.window_height is not None + assert pane_without_zoom.height < pane_without_zoom.window_height + + assert pane_with_zoom.width == pane_with_zoom.window_width + assert pane_with_zoom.height == pane_with_zoom.window_height + + @pytest.mark.skipif( has_lt_version("2.9"), reason="resize-window only exists in tmux 2.9+", diff --git a/tests/test_window.py b/tests/test_window.py index e16dd6410..80091a4b2 100644 --- a/tests/test_window.py +++ b/tests/test_window.py @@ -9,7 +9,7 @@ from libtmux import exc from libtmux._internal.query_list import ObjectDoesNotExist -from libtmux.common import has_gte_version, has_lt_version +from libtmux.common import has_gte_version, has_lt_version, has_lte_version from libtmux.constants import ( PaneDirection, ResizeAdjustmentDirection, @@ -413,6 +413,32 @@ def test_split_with_environment( assert pane.capture_pane()[-2] == v +@pytest.mark.skipif( + has_lte_version("3.1"), + reason="3.2 has the -Z flag on split-window", +) +def test_split_window_zoom( + session: Session, +) -> None: + """Verify splitting window with zoom.""" + window_without_zoom = session.new_window(window_name="split_without_zoom") + window_with_zoom = session.new_window(window_name="split_with_zoom") + pane_without_zoom = window_without_zoom.split( + zoom=False, + ) + pane_with_zoom = window_with_zoom.split( + zoom=True, + ) + + assert pane_without_zoom.width == pane_without_zoom.window_width + assert pane_without_zoom.height is not None + assert pane_without_zoom.window_height is not None + assert pane_without_zoom.height < pane_without_zoom.window_height + + assert pane_with_zoom.width == pane_with_zoom.window_width + assert pane_with_zoom.height == pane_with_zoom.window_height + + @pytest.mark.skipif( has_gte_version("3.0"), reason="3.0 has the -e flag on split-window", From 0503da87a293b54eb5070acedcad9dadf4c0ab55 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 3 Mar 2024 12:35:12 -0600 Subject: [PATCH 29/29] docs(CHANGES,MIGRATION): Note split window updates --- CHANGES | 33 +++++++++++++++++++++++++++++++++ MIGRATION | 7 +++++++ 2 files changed, 40 insertions(+) diff --git a/CHANGES b/CHANGES index 5bfbab5bf..50e7ce57a 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,39 @@ $ pip install --user --upgrade --pre libtmux +### Breaking changes + +#### Improved new sessions (#532) + +- `Session.new_window()` to {meth}`Session.new_window()` + + - Learned `direction`, via {class}`~libtmux.constants.WindowDirection`). + +#### Improved window splitting (#532) + +- `Window.split_window()` to {meth}`Window.split()` + + - Deprecate `Window.split_window()` + +- `Pane.split_window()` to {meth}`Pane.split()` + + - Deprecate `Pane.split_window()` + - Learned `direction`, via {class}`~libtmux.constants.PaneDirection`). + + - Deprecate `vertical` and `horizontal` in favor of `direction`. + + - Learned `zoom` + +#### Tweak: Pane position (#532) + +It's now possible to retrieve the position of a pane in a window via a +`bool` helper:: + +- {attr}`Pane.at_left` +- {attr}`Pane.at_right` +- {attr}`Pane.at_bottom` +- {attr}`Pane.at_right` + ### Development - poetry: 1.7.1 -> 1.8.1 diff --git a/MIGRATION b/MIGRATION index aa35fc214..9d0b021ba 100644 --- a/MIGRATION +++ b/MIGRATION @@ -25,6 +25,13 @@ _Detailed migration steps for the next version will be posted here._ +## 0.33.0: Deprecations for splitting (2024-03-03) + +### Deprecations (#532) + +- `Window.split_window()` to {meth}`Window.split()` +- `Pane.split_window()` to {meth}`Pane.split()` + ## 0.31.0: Renaming and command cleanup (2024-02-17) ### Cleanups (#527)