← back to Iterm Project Grouper
iterm-regroup.py
112 lines
#!/usr/bin/env python3
"""
iterm-regroup.py — (Option C) iTerm2 Python API tool to read the AUTHORITATIVE
project grouping of all live sessions and, where the platform allows, assist
regrouping.
Run AFTER enabling the API: iTerm2 → Settings → General → Magic →
"Enable Python API" (it prompts to allow on first connect). Requires a
restart of iTerm2 to take effect (which ends current sessions — do it only
when they can be dropped).
python3 iterm-regroup.py --plan # print the authoritative grouping (stable window ids)
python3 iterm-regroup.py --apply # attempt to regroup (see the honest note below)
HONEST NOTE ON --apply
----------------------
iTerm2 does NOT expose moving an existing tab from one window to another —
neither in AppleScript nor (as of this writing) in the Python API. The API can
CREATE windows/tabs, split panes, activate, run commands, and read variables,
but there is no supported "reparent this live tab into that window."
Consequences:
* --plan is fully supported and reliable (reads each session's shell `path`).
* --apply cannot MOVE live tabs without recreating (killing) the sessions,
which this tool will NOT do. If a future iTerm2 build adds a tab-move API,
wire it into `move_tab_into()` below and flip SUPPORTS_MOVE = True.
The durable, non-destructive answer is the `claude-proj` launcher (Option B):
new project sessions open as tabs inside their project's window from the start.
"""
import sys, os, subprocess, collections
SUPPORTS_MOVE = False # flip to True only when a real tab-move API is wired in
def git_root(path):
try:
r = subprocess.run(["git", "-C", path, "rev-parse", "--show-toplevel"],
capture_output=True, text=True)
return r.stdout.strip() or path
except Exception:
return path
async def main(connection):
import iterm2
app = await iterm2.async_get_app(connection)
groups = collections.defaultdict(list) # project -> [(window_id, tab_id, path)]
for window in app.terminal_windows:
for tab in window.tabs:
# a tab's project = its (active) session's shell path
sess = tab.current_session or (tab.sessions[0] if tab.sessions else None)
path = ""
if sess:
try:
path = await sess.async_get_variable("path") or ""
except Exception:
path = ""
proj = os.path.basename(git_root(path)) if path else "(no path)"
groups[proj].append((window.window_id, tab.tab_id, path))
print("=== AUTHORITATIVE grouping (iTerm2 Python API) ===")
for proj, items in sorted(groups.items(), key=lambda kv: -len(kv[1])):
wins = sorted({w for (w, _, _) in items})
print(f"{len(items):2d} tab(s) / {len(wins)} window(s) {proj}")
for (w, t, p) in items:
print(f" window={w} tab={t} {p}")
if "--apply" in sys.argv:
multi = {p: it for p, it in groups.items()
if p != "(no path)" and len({w for (w, _, _) in it}) > 1}
print("\n--apply requested.")
if not SUPPORTS_MOVE:
print("iTerm2 exposes no supported tab-move-between-windows API, so a")
print("NON-DESTRUCTIVE regroup of existing sessions is not possible.")
print("Projects that are currently split across windows:")
for p, it in multi.items():
print(f" {p}: windows {sorted({w for (w,_,_) in it})}")
print("\nUse the `claude-proj` launcher for all NEW sessions (Option B),")
print("or drag these tabs together manually. This tool will not recreate")
print("(kill) live sessions to force a regroup.")
else:
for p, it in multi.items():
await regroup_project(app, p, it)
async def move_tab_into(app, tab, target_window):
"""Placeholder for a future iTerm2 tab-move API. Not currently supported."""
raise NotImplementedError("iTerm2 has no supported move-tab-between-windows API")
async def regroup_project(app, proj, items):
wins = sorted({w for (w, _, _) in items})
target = app.get_window_by_id(wins[0])
for (w, t, _) in items:
if w == wins[0]:
continue
tab = None
for win in app.terminal_windows:
for tb in win.tabs:
if tb.tab_id == t:
tab = tb
if tab:
await move_tab_into(app, tab, target)
if __name__ == "__main__":
if not any(a in sys.argv for a in ("--plan", "--apply")):
print(__doc__); sys.exit(0)
try:
import iterm2
except ModuleNotFoundError:
sys.exit("iterm2 module not found. Enable the API and run with iTerm2's "
"python3, e.g.: ~/Library/ApplicationSupport/iTerm2/iterm2env/versions/*/bin/python3 "
"iterm-regroup.py --plan")
iterm2.run_until_complete(main)