window arrangement inside of a new tab

43 views
Skip to first unread message

Arkadiusz Miśkiewicz

unread,
Apr 7, 2026, 3:49:55 PMApr 7
to iterm2-...@googlegroups.com

Hello.

On startup I have my iterm2 setup so it opens with window arrangement
(1/2 split and then 2x1/4 split of one half for example). That works nicely.

Two questions:
1) how not to get such split automatically done on every new tab?

2) and could there be a different window arrangement to choose via right
mouse -> new tab -> profile somehow (I guess there would have to be per
profile default arrangement setting)

How to approach this?

--
Arkadiusz Miśkiewicz, arekm / ( maven.pl | pld-linux.org )

Arkadiusz Miśkiewicz

unread,
Apr 7, 2026, 6:07:03 PMApr 7
to iterm2-...@googlegroups.com
On 07/04/2026 21:49, Arkadiusz Miśkiewicz wrote:
>
> Hello.
>
> On startup I have my iterm2 setup so it opens with window arrangement
> (1/2 split and then 2x1/4 split of one half for example). That works
> nicely.
>
> Two questions:
> 1) how not to get such split automatically done on every new tab?
>
> 2) and could there be a different window arrangement to choose via right
> mouse -> new tab -> profile somehow (I guess there would have to be per
> profile default arrangement setting)
>
> How to approach this?
>

Python bindings are so capable. And AIs, too... Problem solved (a bit
differently from initial question.
tabsplit.py

Arkadiusz Miśkiewicz

unread,
Apr 8, 2026, 3:09:59 AMApr 8
to iterm2-...@googlegroups.com

>> How to approach this?
>>
>
> Python bindings are so capable. And AIs, too... Problem solved (a bit
> differently from initial question.

I'm playing with this more and there seem to be no way to move session
to another tab from python api.

The only workaround seems to be:
await session.async_activate()
await iterm2.MainMenu.async_select_menu_item(
connection, "Move Session to Tab")

but it's very limited (current focused session, creates new tab instead
of me point on desired tab, no direct info on where it went etc).


Some

Session.async_move_to_tab(tab=None) (None = create a new tab)

is what I'm looking for. Is that doable in stable or in nightly somehow?

Thanks,

George Nachman

unread,
Apr 11, 2026, 1:06:24 PMApr 11
to iterm2-...@googlegroups.com
Hi,

There is actually an API for this already, though I realize it's missing from the documentation (I'll fix that). The App class has an async_move_session method that can move a session between tabs and even between windows. It's been available since 3.5.0beta11.

It works by specifying a destination session (one that's already in the target tab), and the moved session gets split next to it:

await app.async_move_session(
    session=session_to_move,
    destination=session_in_target_tab,
    split_vertically=True,
    before=False)

If you want to move a session to a brand new tab by itself, there's a workaround since async_move_session always creates a split:

# Create a new tab (comes with a default session)
new_tab = await window.async_create_tab()
throwaway = new_tab.current_session

# Move the session into the new tab
await app.async_move_session(
    session=session_to_move,
    destination=throwaway,
    split_vertically=True,
    before=False)

# Close the throwaway session
await throwaway.async_close(force=True)

I know the create-move-close dance isn't ideal. I have a better solution in progress but it won't be ready for a while. In the meantime, this should get you unblocked.


--
You received this message because you are subscribed to the Google Groups "iterm2-discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to iterm2-discus...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/iterm2-discuss/ea7b0556-f0ce-456c-8b2f-83908f672907%40maven.pl.

George Nachman

unread,
Apr 12, 2026, 5:05:14 PMApr 12
to iterm2-...@googlegroups.com
I added two new methods on Session in commit 0e3f995483a5c7488f10e51659b5981b298bed5e, which will be available in the next nightly build and require iterm2 module version 2.15:

async_move_to_new_tab(window=None, tab_index=None)

Moves a session out of a split pane into its own tab.

- window: Which window to create the tab in. None means the session's current window.
- tab_index: Position in the tab bar (0 = first). None places it after the session's current tab (same window) or at the end
(different window).
- Returns the new tab's ID, or None for tmux clients (which are moved asynchronously by the server).

async_move_to_new_window()

Moves a session out of a split pane into a new window.

- Returns the new window's ID, or None for tmux clients.

The existing App.async_move_session(session, destination, split_vertically, before) continues to handle the case of moving a
session into a split pane alongside another session.

Example usage:

app = await iterm2.async_get_app(connection)
window = app.windows[0]
tab = window.tabs[0]
session = tab.sessions[0]

# Move to a new tab in the same window
tab_id = await session.async_move_to_new_tab()

# Move to a new tab in a different window, at position 0
tab_id = await session.async_move_to_new_tab(
 window=app.windows[1], tab_index=0)

# Move to a new window
window_id = await session.async_move_to_new_window()

These require the session to be one of at least two sessions in its tab (otherwise there's nothing to extract it from).

Arkadiusz Miśkiewicz

unread,
Apr 28, 2026, 8:51:21 AMApr 28
to iterm2-...@googlegroups.com
On 12/04/2026 23:05, George Nachman wrote:
> I added two new methods on Session in commit
> 0e3f995483a5c7488f10e51659b5981b298bed5e, which will be available in the
> next nightly build and require iterm2 module version 2.15:
>
> async_move_to_new_tab(window=None, tab_index=None)
>
> Moves a session out of a split pane into its own tab.
Thank you.

The biggest limitation now I'm hitting is no way to freely (re)arrange
panes.

There is only splitting and closing involved.

Would be it possible to get some async_apply_layout() which would apply
any layout user wants?

User could specify existing sessions or request NewSession in that
desired layout.


Example of what's currently impossible to do:

1) new in middle

Before — tree V[ H[A, B], H[C, D] ]:
+----+----+
| A | C |
+----+----+
| B | D |
+----+----+

Desired — tree V[ H[A, B], M, H[C, D] ]:
+----+----+----+
| A | | C |
+----+ M +----+
| B | | D |
+----+----+----+

2) swap

Before — V[A, B]:
+----+----+
| A | B |
+----+----+

Desired — V[B, A]:
+----+----+
| B | A |
+----+----+

3) rearrange

Before — V[A, H[B, V[C, D]]]:
+----+----+----+
| | B |
| A +----+----+
| | C | D |
+----+----+----+

Desired — V[A, C, H[B, D]]
+----+----+----+
| | | B |
| A | C +----+
| | | D |
+----+----+----+

4) add new pane

Before — any current layout, e.g. V[A, B]:
+----+----+
| A | B |
+----+----+

Desired
+---------+
| NEW |
+----+----+
| A | B |
+----+----+

5) new pane

+----+----+----+
| A | C | E |
+----+----+----+
| B | D | F |
+----+----+----+

Desired:
+----+----+----+----+
| A | | C | E |
+----+ M +----+----+
| B | | D | F |
+----+----+----+----+

George Nachman

unread,
May 1, 2026, 11:07:28 PM (11 days ago) May 1
to iterm2-...@googlegroups.com
The App.async_apply_layout API you asked about is now in. It reshapes one or more tabs in a single call: swap panes, change splitter orientation, move sessions across tabs and windows, close sessions/tabs/windows. Validation happens up front so a malformed spec leaves your workspace untouched.

Getting set up

You'll need:

1. A nightly build dated tomorrow or later. The feature lives in iTerm2 itself; nightlies are at https://iterm2.com/nightly/latest
2. iterm2 Python module 2.17 or later. Once tomorrow's nightly is out, pip3 install --upgrade iterm2 will pick it up.

How it works

You build a spec (a plain Python dict) describing the target state, then call await app.async_apply_layout(spec).

spec = {
    "tabs":           [{"tab_id": "...", "root": <node>}, ...],
    "close_sessions": ["<session-guid>", ...],
    "close_tabs":     ["<tab-id>", ...],
    "close_windows":  ["<window-guid>", ...],
}

A <node> is either a leaf:

{"session_id": "<session-guid>"}

…or a splitter with at least two children:

{"vertical": True, "children": [<node>, <node>, ...]}

Sessions, tabs, and windows are referred to by the same identifiers the rest of the Python API uses (session.session_id, tab.tab_id, window.window_id).

Working example

This script registers three RPCs you can bind to keystrokes via Prefs > Keys > Invoke Script Function:

#!/usr/bin/env python3.7
import iterm2

def leaf(session):
    return {"session_id": session.session_id}

def vrow(sessions):
    """One leaf if a single session, otherwise a vertical-divider row."""
    if len(sessions) == 1:
        return leaf(sessions[0])
    return {"vertical": True,
            "children": [leaf(s) for s in sessions]}

async def main(connection):
    app = await iterm2.async_get_app(connection)

    @iterm2.RPC
    async def swap_first_two_panes():
        tab = app.current_terminal_window.current_tab
        if len(tab.sessions) < 2:
            return
        a, b = tab.sessions[0], tab.sessions[1]
        spec = {"tabs": [{
            "tab_id": tab.tab_id,
            "root": {"vertical": True,
                     "children": [leaf(b), leaf(a)] +
                                 [leaf(s) for s in tab.sessions[2:]]},
        }]}
        await app.async_apply_layout(spec)
    await swap_first_two_panes.async_register(connection)

    @iterm2.RPC
    async def move_active_pane_to_next_tab():
        window = app.current_terminal_window
        tabs = window.tabs
        if len(tabs) < 2:
            return
        src = window.current_tab
        i = next(idx for idx, t in enumerate(tabs)
                 if t.tab_id == src.tab_id)
        dst = tabs[(i + 1) % len(tabs)]
        active = src.current_session
        if active is None:
            return

        remaining = [s for s in src.sessions
                     if s.session_id != active.session_id]
        spec = {"tabs": []}
        if remaining:
            spec["tabs"].append({"tab_id": src.tab_id, "root": vrow(remaining)})
        spec["tabs"].append({
            "tab_id": dst.tab_id,
            "root": vrow(list(dst.sessions) + [active]),
        })
        # If src loses its last pane, apply_layout auto-closes it.
        await app.async_apply_layout(spec)
    await move_active_pane_to_next_tab.async_register(connection)

    @iterm2.RPC
    async def move_active_pane_to_other_window():
        windows = app.terminal_windows
        if len(windows) < 2:
            return
        src_window = app.current_terminal_window
        i = next(idx for idx, w in enumerate(windows)
                 if w.window_id == src_window.window_id)
        dst_window = windows[(i + 1) % len(windows)]
        src_tab = src_window.current_tab
        dst_tab = dst_window.current_tab
        active = src_tab.current_session
        if active is None:
            return

        remaining = [s for s in src_tab.sessions
                     if s.session_id != active.session_id]
        spec = {"tabs": []}
        if remaining:
            spec["tabs"].append({"tab_id": src_tab.tab_id,
                                 "root": vrow(remaining)})
        spec["tabs"].append({
            "tab_id": dst_tab.tab_id,
            "root": vrow(list(dst_tab.sessions) + [active]),
        })
        await app.async_apply_layout(spec)
    await move_active_pane_to_other_window.async_register(connection)

iterm2.run_forever(main)

Save it under ~/Library/Application Support/iTerm2/Scripts/ and launch from the Scripts menu.

Things to know

- Atomicity. The spec is fully validated before any mutation begins; malformed specs are rejected with no side effects. If a per-tab mutation throws partway through (rare
— usually a race where a tab disappeared between validation and execution), already-applied changes stay applied. There is no rollback.
- Auto-close. When a tab loses its last session as a side effect of a session moving away, the tab closes automatically — no need to list it in close_tabs. Same for
windows whose last tab goes away.
- Cross-tab and cross-window moves are the same operation. Both are expressed as a session GUID appearing in a different tab's root than where it currently lives. apply_layout figures out the detach/reattach.
- Apply-then-close ordering. The new layout is applied first, then close_sessions runs against the result. So a session may legally appear in both a tab's root and close_sessions — it'll be placed in the tree by the reshape, then terminated immediately after.
- Splitter rules. Splitters need at least two children, and you can't nest a splitter of the same orientation as its parent (flatten it instead). Both are validation errors.
- Out of scope (for now). Inline session creation (new_session leaves), new_tabs, and new_windows are not supported — apply_layout only reshapes around live sessions. Use
the existing Window.async_create / Window.async_create_tab / Session.async_split_pane to make new things, then call apply_layout to arrange them.
- tmux integration tabs are not supported — tmux owns its own layout server-side.

Reach out if you hit anything unexpected.

--
You received this message because you are subscribed to the Google Groups "iterm2-discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to iterm2-discus...@googlegroups.com.

Arkadiusz Miśkiewicz

unread,
May 8, 2026, 1:21:14 PM (4 days ago) May 8
to iterm2-...@googlegroups.com
On 02/05/2026 05:07, George Nachman wrote:
> The App.async_apply_layout API you asked about is now in. It reshapes
> one or more tabs in a single call: swap panes, change splitter
> orientation, move sessions across tabs and windows, close sessions/tabs/
> windows. Validation happens up front so a malformed spec leaves your
> workspace untouched.

Thanks, works! All my previous splitting replaced with this.


Slow visually (I mean I see new sessions created and then rearrangement
of that happening in front of my eyes - I'm arranging to my desired 3
pane layout on each new tab creation) but I guess there is no way to
workaround that.



ps. one small unrelated thing to consider - ignoring __pycache__ dirs in
AutoLaunch etc (instead of showing a warning)

George Nachman

unread,
May 8, 2026, 2:18:51 PM (4 days ago) May 8
to iterm2-...@googlegroups.com
I don’t know if it will help, but you could try doing it inside a transaction. See: https://iterm2.com/python-api/transaction.html

--
You received this message because you are subscribed to the Google Groups "iterm2-discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to iterm2-discus...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages