资讯动态

iTerm2 Python API 实战:用 App.async_apply_layout 原子化重塑标签页、窗口与分屏布局

发布时间:2026/9/21 1:28:10 来源:尧图企业网站定制
iTerm2 Python API 实战用 App.async_apply_layout 原子化重塑标签页、窗口与分屏布局【免费下载链接】iTerm2iTerm2 is a terminal emulator for Mac OS X that does amazing things.项目地址: https://gitcode.com/gh_mirrors/it/iTerm2导读iterm2.App.async_apply_layout是 iTerm2 Python API 中一条用于原子化重塑工作区布局的核心调用你只需构造一个描述目标状态的普通 Python dictspec即可在一次调用中完成分屏对调、会话跨标签页移动、跨窗口搬迁乃至自动关闭空标签页等操作。本文以官方示例 apply_layout.rst 为主体结合 Python API 的 app.py 实现与 Swift 侧 ApplyLayoutBuiltInFunction.swift、iTermLayoutMutator.swift 源码讲透 spec 语法、校验规则与底层执行链路并给出三个可直接绑定快捷键的完整 RPC 脚本。一、核心思路把布局当作一次声明式事务示例脚本注册了三个函数分别演示App.async_apply_layout的三种典型用法swap_first_two_panes()—— 对调当前标签页前两个分屏move_active_pane_to_next_tab()—— 把当前活动分屏移到同窗口的下一个标签页move_active_pane_to_other_window()—— 把当前活动分屏移到另一个窗口的当前标签页三者共享同一条思路先构建一个 spec普通 Python dict描述你想要的标签页形态以及要关闭的会话/标签页/窗口然后把整个 spec 交给app.async_apply_layout(spec)。整个 spec 会先被完整校验再作为单个事务一次性应用。你可以通过Prefs Keys里的Invoke Script Function将任意一个函数绑定到快捷键上运行。脚本本体以iterm2.run_forever(main)常驻运行绑定快捷键后即可随时触发。从源码结构看这条链路的完整调用路径是Python 侧App.async_apply_layoutapp.py→ 通过 expression 接口调用 Swift 内置函数iterm2.apply_layoutApplyLayoutBuiltInFunction.swift→LayoutSpec.parse解析 →LayoutResolver.resolve解析执行计划 →LayoutTransaction.execute按计划执行iTermLayoutMutator.swift。二、Spec 基础四个可选字段与两种节点一个 spec 最多包含四个可选字段{ tabs: [{tab_id: ..., root: node}, ...], close_sessions: [session-guid, ...], close_tabs: [tab-id, ...], close_windows: [window-guid, ...], }字段含义字段类型作用tabslist列出需要重塑的标签页每个元素包含tab_id要改写的标签页和root目标布局树close_sessionslist[str]显式关闭的会话 GUID 列表close_tabslist[str]显式关闭的标签页 ID 列表close_windowslist[str]显式关闭的窗口 GUID 列表node布局树节点有两种形式叶子节点——引用一个现存会话{session_id: session-guid}分割器节点——至少包含两个子节点vertical: True表示垂直分割左右分屏False表示水平分割上下分屏{vertical: True, children: [node, node, ...]}会话、标签页、窗口均使用 Python API 全项目通用的标识符session.session_id、tab.tab_id、window.window_id。补充第三种叶子new_session从 app.py 的 docstring 可以看到除了session_id叶子spec 还支持在已有标签页内就地创建全新会话的叶子节点{new_session: {profile: profile-guid, command: 可选}}profile必须指向真实存在的 profile GUIDcommand可覆盖默认启动命令若给出command它会在登录 shell 中运行PATH、别名、dotfiles 都会被加载效果等同于你亲手输入该命令。新分屏尺寸取其容器均分份额无法手动指定尺寸。工作目录遵循 profile 的 working-directory 设置Home 与 Custom Directory 按配置生效Reuse previous sessions directory 会继承目标标签页中现存分屏的目录。该能力需要较新版本的 iTerm2 支持调用前可通过 capabilities.py 中的supports_apply_layout_new_session()探测。new_session叶子只允许出现在已存在的标签页中spec 中的new_tabs、new_windows字段不受支持——需要新建标签页/窗口时应使用Window.async_create_tab/Window.async_create然后再用 apply_layout 重塑。三、完整示例脚本以下为官方示例 apply_layout.its同步维护在 RST 中的完整内容#!/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)三个函数逐个拆解swap_first_two_panes()取当前窗口当前标签页的前两个会话构造一棵新的分割树——b在前、a在后其余分屏原样追加然后整体替换该标签页的root。这是标签页内重塑的最小范例。move_active_pane_to_next_tab()在同窗口内找到当前标签页的下一个标签页取模回绕把当前活动会话从源标签页的root中移除追加到目标标签页root的最右侧。注意vrow()的巧思若源标签页只剩一个会话vrow返回叶子而非 splitter从而满足splitter 至少两个子节点的约束。move_active_pane_to_other_window()逻辑与上一函数几乎一致只是把目标换成app.terminal_windows中的下一个窗口。这证明跨标签页与跨窗口的移动在 spec 层面没有本质区别——都是会话 GUID 出现在与当前所处位置不同的 tab 的 root 中apply_layout会自动推导出 detach/reattach 过程。四、Notes行为语义与约束官方文档明确列出的行为语义配合源码可以理解得更加透彻1. 原子性Atomicity整个 spec 在任何变更开始前就会完成结构预校验因此畸形 spec 不会对工作区造成任何副作用。但若某个标签页的变更在中途失败例如校验与执行之间某个标签页恰好消失已应用的部分不会回滚——事务会中止并把错误抛给调用方。从 ApplyLayoutBuiltInFunction.swift 可以看到这一两阶段设计LayoutSpec.parse(dict)负责结构解析LayoutResolver.resolve(spec, environment:)负责解析执行计划含孤儿检查等跨树校验最后由LayoutTransaction.execute(plan:mutator:)按计划逐项执行。校验阶段通过后执行阶段的失败即按上述语义处理。2. 自动关闭Auto-close当一个标签页因会话被移走而失去最后一个会话时标签页会自动关闭无需在close_tabs中列出窗口同理——最后一个标签页消失时窗口自动关闭。对应实现见 iTermLayoutMutator.swiftdetachSession会把被移空的标签页记入emptyTabsToClose事务结束endTransaction时统一关闭。3. 跨标签页与跨窗口移动机制一致两者都表达为会话 GUID 出现在别的 tab 的 root 中。apply_layout自动完成 detach 与 reattachiTermLayoutMutator.detachSession先把会话从原标签页摘下并暂存到detachedSessions字典attachTree再按新布局树把会话收养进目标标签页iTermLayoutMutator.swift。4. 分割器规则分割器至少需要两个子节点不能把同向分割器嵌套进同向父分割器垂直套垂直 / 水平套水平需要自行拍平违反任一规则都会在校验阶段被拒绝。完整的服务端校验规则源自 app.py 与 ApplyLayoutBuiltInFunction.swift还包括校验规则说明至少 2 个 children分割器子节点数下限禁止同向嵌套V-in-V / H-in-H 拒绝需拍平会话 GUID 唯一spec 中每个 session GUID 至多出现一次孤儿检查被移动会话涉及的每个标签页都必须出现在tabs中或通过close_sessions/close_tabs交代去向profile 真实存在每个new_session的 profile GUID 必须有效树深度限制布局树嵌套过深会被拒绝tmux 标签页不支持tmux integration 标签页无法应用布局错误会以RPCException抛出错误信息包含指示出错节点的树路径tree-path便于定位 spec 中的问题位置。五、底层实现从 Python dict 到界面变更5.1 Python 侧封装app.pyApp.async_apply_layoutapp.py的实现要点先调用iterm2.capabilities.check_supports_apply_layout检查能力若 spec 含new_session叶子再额外检查supports_apply_layout_new_session。spec 以 base64 传输——源码注释解释了原因iTerm2 的表达式解析器不解码字符串字面量中的\而 JSON spec 几乎必然包含带引号的字符串因此把json.dumps(spec)编码为 base64 后传给iterm2.apply_layout(spec_json_b64)。最终通过async_invoke_function调用 Swift 内置函数。5.2 Swift 侧执行ApplyLayoutBuiltInFunctionApplyLayoutBuiltInFunction.swift 注册了命名空间iterm2下的apply_layout函数执行流水线为base64 解码 → JSONSerialization 解析 → LayoutSpec.parse(dict) → LayoutResolver.resolve(spec, environment:) → LayoutTransaction.execute(plan:mutator:)异常统一转为NSErrordomain 为com.iterm2.apply-layout返回给 Python 侧Python 侧再包装为RPCException。5.3 变更执行器iTermLayoutMutatoriTermLayoutMutator.swift 是生产环境执行器核心职责detachSession把会话从原标签页摘下暂存到detachedSessions若标签页因此变空则记入emptyTabsToCloseattachTree调用iTermSplitTreeRebuilder.replaceViewHierarchy重建视图层级并把暂存的跨标签会话收养进来即使本次操作没有任何会话进出例如仅做标签页内 reshape也会主动发出iTermSessionDidChangeTab通知保证 Python 侧App的缓存状态不会过期——否则后续 apply_layout 会因为读到旧布局而变成 no-opcreateNewSession就地创建新会话命令按ITAddressBookMgr.commandByWrapping(inLoginShell:)包装进登录 shell 执行并强制应用 profile 的工作目录设置endTransaction清理事务期间创建但未被收养的会话避免泄漏无头僵尸shell并关闭所有变空的标签页。5.4 能力探测Python 侧通过 capabilities.py 暴露两个探测函数supports_apply_layout(connection)—— 是否支持App.async_apply_layout()supports_apply_layout_new_session(connection)—— 是否支持new_session叶子就地建会话。六、上手步骤与适用边界在 iTerm2 中通过Scripts Manage New Python Script创建脚本或直接使用本示例的 apply_layout.its在Prefs Keys添加快捷键动作选择Invoke Script Function填入swap_first_two_panes()等已注册的函数名脚本需以 daemon 形式常驻运行iterm2.run_forever(main)注册的 RPC 才能被快捷键随时触发。适用边界当前仓库实际实现确认不支持在 spec 中创建新标签页/新窗口new_tabs/new_windows字段请先用Window.async_create_tab/Window.async_create创建再 reshape不支持tmux integration 标签页事务预校验 分步执行预校验通过后若执行中途出错已变更部分不回滚。参考官方示例文档apply_layout.rst可下载脚本本体apply_layout.itsPython API 实现app.pyApp.async_apply_layout完整 docstring 与实现能力探测capabilities.pySwift 内置函数ApplyLayoutBuiltInFunction.swift变更执行器iTermLayoutMutator.swift更多 Python API 示例docs/examples 目录/DSMLparameter /DSMLinvoke /DSMLtool_calls【免费下载链接】iTerm2iTerm2 is a terminal emulator for Mac OS X that does amazing things.项目地址: https://gitcode.com/gh_mirrors/it/iTerm2创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价