Move Sessions Between Tabs, Windows, and Split Panes¶
This script registers three functions that demonstrate
iterm2.App.async_apply_layout() — a single API call that reshapes one
or more tabs atomically. You can bind each function to a keystroke in
Prefs > Keys by selecting Invoke Script Function and using one of:
swap_first_two_panes()move_active_pane_to_next_tab()move_active_pane_to_other_window()
The shared idea: build a spec (a plain Python dict) describing the
tabs you want to reshape and any sessions/tabs/windows you want to
close, then hand it to app.async_apply_layout(spec). The whole
spec is validated up front and applied as a single transaction.
Spec basics¶
A spec has up to four optional fields:
{
"tabs": [{"tab_id": "...", "root": <node>}, ...],
"close_sessions": ["<session-guid>", ...],
"close_tabs": ["<tab-id>", ...],
"close_windows": ["<window-guid>", ...],
}
A <node> is either a leaf referring to a live session:
{"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).
Example script¶
#!/usr/bin/env python3.7
"""
Demo: move sessions between tabs, windows, and split panes via
App.async_apply_layout.
Registers three RPCs:
swap_first_two_panes()
move_active_pane_to_next_tab()
move_active_pane_to_other_window()
Bind any of these to a keystroke in Prefs > Keys via "Invoke Script
Function".
"""
import iterm2
def leaf(session):
"""Build a leaf node for a session."""
return {"session_id": session.session_id}
def vrow(sessions):
"""Build a vertical-divider row of one or more sessions.
A single session is returned as a leaf; two or more become a
splitter. apply_layout requires splitters to have at least two
children.
"""
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():
"""Swap the first two panes of the current tab."""
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():
"""Move the active pane from the current tab into the next
tab in the same window."""
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:
# Source tab keeps the other panes.
spec["tabs"].append(
{"tab_id": src.tab_id, "root": vrow(remaining)})
# Destination tab gains the moved pane on the right.
spec["tabs"].append({
"tab_id": dst.tab_id,
"root": vrow(list(dst.sessions) + [active]),
})
# If the source tab loses every pane, apply_layout will
# close it for us automatically — no need to list it in
# close_tabs.
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():
"""Move the active pane to the current tab of another
window. Picks the next window in app.terminal_windows."""
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)
Notes¶
Atomicity. The whole spec is validated before any mutation begins, so a malformed spec leaves the workspace untouched. If a per-tab mutation fails partway through (e.g. 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. You don’t need to list it in
close_tabs. Same idea for windows whose last tab goes away.Cross-tab and cross-window moves work the same way — both are expressed as a session GUID appearing in a different tab’s
rootthan where it currently lives.apply_layoutfigures out the detach/reattach automatically.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).
apply_layoutrejects either at validation time.
See iterm2.App.async_apply_layout() for the full reference.
