戰(zhàn):用 copycolor 腳本在分屏?xí)r保持標(biāo)簽頁顏色同步)
iTerm2 Python API 實(shí)戰(zhàn)用 copycolor 腳本在分屏?xí)r保持標(biāo)簽頁顏色同步【免費(fèi)下載鏈接】iTerm2iTerm2 is a terminal emulator for Mac OS X that does amazing things.項(xiàng)目地址: https://gitcode.com/gh_mirrors/it/iTerm2導(dǎo)讀在 iTerm2 中同一標(biāo)簽頁Tab內(nèi)的多個(gè)窗格Pane可以來自不同的 Profile如果這些 Profile 定義了不同的標(biāo)簽頁顏色分屏后就會出現(xiàn)同一標(biāo)簽頁內(nèi)兩個(gè)窗格顏色不一致的情況。iTerm2 官方 Python API 示例 copycolor.rst 提供了一段名為 Preserve Tab Color保持標(biāo)簽頁顏色的守護(hù)腳本它監(jiān)聽新會話的創(chuàng)建自動(dòng)把標(biāo)簽頁內(nèi)已有會話的標(biāo)簽顏色復(fù)制給新窗格并在不改動(dòng)底層 Profile 的前提下完成這一修改。讀完本文你將掌握LocalWriteOnlyProfile、NewSessionMonitor、async_set_profile_properties等核心 API 的用法并能直接部署這段腳本到自己的 iTerm2 環(huán)境中。問題背景分屏后標(biāo)簽頁顏色為什么會對不上iTerm2 允許每個(gè) Profile 單獨(dú)配置標(biāo)簽頁顏色Tab Color。當(dāng)你執(zhí)行用其他 Profile 在此處分割Split with a different profile操作時(shí)新窗格會繼承該 Profile 的標(biāo)簽頁顏色而舊窗格保留原 Profile 的顏色。如果兩個(gè) Profile 的顏色不同同一個(gè)標(biāo)簽頁就會呈現(xiàn)兩種顏色視覺上容易被誤認(rèn)為兩個(gè)獨(dú)立的標(biāo)簽頁。官方示例開篇即點(diǎn)明了這一場景If you split a session using a different profile, you can end up with two panes that have different tab colors. This script copies the old tab color to the new split pane so they stay in sync.解決思路很直接監(jiān)聽新會話創(chuàng)建事件找到它所在的標(biāo)簽頁讀取該標(biāo)簽頁中已經(jīng)存在的會話的標(biāo)簽顏色再用會話級而非 Profile 級的寫操作把顏色應(yīng)用到新會話上。這段腳本的核心價(jià)值在于第二個(gè)技巧——如何修改會話的 Profile 屬性而不修改底層 Profile因?yàn)橹苯痈牡讓?Profile 會影響所有使用該 Profile 的會話。完整示例代碼以下是 copycolor.rst 中的完整腳本約 44 行你可以從倉庫的 copycolor.its 下載該腳本的原始版本#!/usr/bin/env python3.7 import iterm2 async def main(connection): async def async_color_in_tab(tab, exclude): Return the tab color of any session that already existed. colors [] for session in tab.sessions: if session exclude: continue profile await session.async_get_profile() if not profile: continue color profile.tab_color if color: return color return None app await iterm2.async_get_app(connection) async with iterm2.NewSessionMonitor(connection) as mon: while True: # Wait for a new session to be created session_id await mon.async_get() session app.get_session_by_id(session_id) if not session: continue window, tab app.get_tab_and_window_for_session(session) if not tab: continue color await async_color_in_tab(tab, session) if not color: continue # Another session had a color in this tab. Change the tab # color property of the new session. Use LocalWriteOnlyProfile # and session.async_set_profile_properties to avoid changing the # underlying profile. change iterm2.LocalWriteOnlyProfile() change.set_tab_color(color) await session.async_set_profile_properties(change) iterm2.run_forever(main)腳本結(jié)構(gòu)清晰自頂向下可以分為四個(gè)部分讀取已有顏色、監(jiān)聽新會話、查找所屬標(biāo)簽頁、寫入顏色。下面逐段剖析。逐段剖析四個(gè)核心步驟1. 讀取標(biāo)簽頁中已有會話的顏色async def async_color_in_tab(tab, exclude): Return the tab color of any session that already existed. colors [] for session in tab.sessions: if session exclude: continue profile await session.async_get_profile() if not profile: continue color profile.tab_color if color: return color return Noneasync_color_in_tab遍歷標(biāo)簽頁內(nèi)的所有會話tab.sessions跳過新創(chuàng)建的那個(gè)會話exclude對每個(gè)已有會話調(diào)用session.async_get_profile()獲取其生效的 Profile讀取profile.tab_color。一旦找到非空顏色就立即返回全部沒有顏色則返回None。兩個(gè)細(xì)節(jié)值得注意async_get_profile()返回的 Profile包含會話本地修改。從 session.py 的實(shí)現(xiàn)看它通過iterm2.rpc.async_get_profile拉取數(shù)據(jù)docstring 明確寫著including any session-local changes not in the underlying profile包含不在底層 Profile 中的會話本地修改。這意味著即使舊窗格此前已被腳本改過顏色新窗格也能讀到最終生效的顏色而不是回退到 Profile 原始值。函數(shù)內(nèi)部聲明了colors []但從未使用屬于示例代碼遺留的冗余變量不影響邏輯可自行刪除。2. 監(jiān)聽新會話的創(chuàng)建app await iterm2.async_get_app(connection) async with iterm2.NewSessionMonitor(connection) as mon: while True: # Wait for a new session to be created session_id await mon.async_get() session app.get_session_by_id(session_id) if not session: continueiterm2.async_get_app(connection)獲取當(dāng)前 App 的對象模型包含窗口、標(biāo)簽頁、會話的樹狀結(jié)構(gòu)。NewSessionMonitor是一個(gè)異步上下文管理器進(jìn)入時(shí)訂閱新會話創(chuàng)建通知退出時(shí)自動(dòng)取消訂閱見 lifecycle.py 的實(shí)現(xiàn)__aenter__中調(diào)用async_subscribe_to_new_session_notification__aexit__中調(diào)用async_unsubscribe。mon.async_get()會阻塞等待下一個(gè)新會話事件返回新會話的 ID字符串。隨后用app.get_session_by_id(session_id)把 ID 解析為Session對象。如果解析失敗例如會話已被關(guān)閉直接continue等待下一個(gè)事件。整個(gè)while True循環(huán)讓腳本變成一個(gè)常駐守護(hù)進(jìn)程這正是run_forever入口的意義。3. 定位新會話所屬的標(biāo)簽頁window, tab app.get_tab_and_window_for_session(session) if not tab: continueget_tab_and_window_for_session返回(Window, Tab)二元組用于找出會話所屬的窗口和標(biāo)簽頁。值得注意的是從 app.py 的源碼注釋看這個(gè)方法已標(biāo)記為 Deprecated理由是名字與返回參數(shù)順序不符——它實(shí)際返回的是(Window, Tab)而非名字暗示的(Tab, Window)官方建議使用新方法get_window_and_tab_for_session。內(nèi)部實(shí)現(xiàn)是遍歷terminal_windows與window.tabs檢查session in tab.all_sessions來定位。新代碼中可以改用get_window_and_tab_for_session語義更清晰。如果找不到所屬標(biāo)簽頁例如會話已被關(guān)閉跳過本次事件。4. 通過 LocalWriteOnlyProfile 寫入顏色color await async_color_in_tab(tab, session) if not color: continue # Another session had a color in this tab. Change the tab # color property of the new session. Use LocalWriteOnlyProfile # and session.async_set_profile_properties to avoid changing the # underlying profile. change iterm2.LocalWriteOnlyProfile() change.set_tab_color(color) await session.async_set_profile_properties(change)這是全腳本最關(guān)鍵的部分不修改底層 Profile只修改這一個(gè)會話的生效配置。LocalWriteOnlyProfile是一個(gè)可寫不可讀的臨時(shí) Profile 對象從 profile.py 的實(shí)現(xiàn)看它的values屬性保存著一組(鍵, JSON 值)對本身并不與任何真實(shí) Profile 綁定。change.set_tab_color(color)會調(diào)用_color_set(Tab Color, value)把顏色對象序列化為 JSON 存進(jìn)values字典。session.async_set_profile_properties(change)負(fù)責(zé)把這份變更應(yīng)用到會話。從 session.py 的實(shí)現(xiàn)看當(dāng)連接支持多屬性批量設(shè)置supports_multiple_set_profile_properties時(shí)一次性把所有(key, json_value)打包為 assignments通過async_set_profile_properties_json發(fā)送對不支持的老版本3.3.0beta9 及更早則退化為逐條調(diào)用async_set_profile_property_json任一屬性寫入失敗都會拋出RPCException。其 docstring 明確說明When you use this function the underlying profile is not modified. The session will keep a copy of its profile with these modifications.使用此函數(shù)時(shí)底層 Profile 不會被修改會話會保留一份帶這些修改的 Profile 副本。這正是腳本能只影響新窗格、不影響其他會話的根本原因。關(guān)鍵 API 源碼級剖析LocalWriteOnlyProfile會話級臨時(shí)配置LocalWriteOnlyProfile是 iTerm2 Python API 中實(shí)現(xiàn)局部覆蓋的標(biāo)準(zhǔn)工具官方文檔在 profile.py 中直接引用本示例作為用法范例。它配合Session.async_set_profile_properties使用相關(guān)方法包括方法作用底層鍵名set_tab_color(color)設(shè)置標(biāo)簽頁顏色未啟用明暗分離顏色時(shí)生效Tab Colorset_tab_color_light(color)設(shè)置明色模式變體啟用明暗分離顏色時(shí)生效Tab Color (Light)set_tab_color_dark(color)設(shè)置暗色模式變體啟用明暗分離顏色時(shí)生效Tab Color (Dark)set_use_tab_color(value)是否啟用標(biāo)簽頁顏色Use Tab Color顏色對象通過_color_set內(nèi)部的value.get_dict()序列化為 JSON 存儲。值得注意的是set_tab_color的 docstring 特別說明它僅在未啟用獨(dú)立明暗模式顏色時(shí)使用used only when separate light/dark mode colors are not enabled。如果目標(biāo) Profile 啟用了Use separate colors for light and dark mode選項(xiàng)標(biāo)簽頁顏色實(shí)際由Tab Color (Light)/Tab Color (Dark)決定此時(shí)應(yīng)改用set_tab_color_light/set_tab_color_dark才能生效——這是本示例在特定配置下的一個(gè)擴(kuò)展點(diǎn)。Color顏色值的表示iterm2.Color見 color.py以 RGBA 描述顏色r、g、b、a取值均為 0-255a默認(rèn) 255color_space目前僅支持 sRGB。它還提供from_hex解析#aabbcc或 13 位十六進(jìn)制與from_cocoa解析 NSKeyedArchiver 編碼等工廠方法。本示例中顏色直接取自已有 Profile 的tab_color屬性屬于Color實(shí)例無需自行構(gòu)造。NewSessionMonitor事件驅(qū)動(dòng)的守護(hù)模式NewSessionMonitorlifecycle.py內(nèi)部使用asyncio.Queue緩存通知async_get()從隊(duì)列取出新會話 ID 并返回。它與其他 Monitor如SessionTerminationMonitor、LayoutChangeMonitor一樣遵循async with上下文管理器協(xié)議退出時(shí)自動(dòng)退訂。配合iterm2.run_forever(main)入口見 connection.py腳本可以無限期運(yùn)行這正是官方守護(hù)腳本的典型形態(tài)。對照實(shí)驗(yàn)settabcolor 示例帶來的啟發(fā)同目錄下的另一個(gè)官方示例 settabcolor.rst 與本腳本形成了很好的對照app await iterm2.async_get_app(connection) session app.current_terminal_window.current_tab.current_session change iterm2.LocalWriteOnlyProfile() color iterm2.Color(255, 128, 128) change.set_tab_color(color) change.set_use_tab_color(True) await session.async_set_profile_properties(change)settabcolor 演示的是主動(dòng)設(shè)置對當(dāng)前會話寫入硬編碼顏色并額外調(diào)用change.set_use_tab_color(True)開啟標(biāo)簽頁顏色開關(guān)最后用run_until_complete一次性執(zhí)行。而 copycolor 是被動(dòng)同步它只復(fù)制顏色值本身。對比兩者可以得出一個(gè)重要結(jié)論——copycolor 假設(shè)已有會話的標(biāo)簽頁顏色已經(jīng)處于生效狀態(tài)即舊 Profile 已開啟Use Tab Color新窗格沿用相同的顏色值時(shí)自然沿用開關(guān)狀態(tài)若舊窗格顏色來自關(guān)閉開關(guān)的狀態(tài)即未啟用標(biāo)簽顏色則profile.tab_color與渲染結(jié)果不一定一致這是使用本腳本時(shí)需要了解的邊界條件。若需在新窗格上強(qiáng)制開啟標(biāo)簽顏色可參考 settabcolor 追加change.set_use_tab_color(True)。安裝與運(yùn)行腳本以#!/usr/bin/env python3.7開頭要求 Python 3.7 及以上版本當(dāng)前 iTerm2 的 Python API 運(yùn)行時(shí)通常內(nèi)置 Python 3.10兼容無虞。運(yùn)行方式有兩種從倉庫下載腳本文件copycolor.its 是該示例的官方可下載版本可以通過 iTerm2 菜單Scripts Manage Import...導(dǎo)入.its 即 iTerm2 Script 文件的擴(kuò)展名。手動(dòng)創(chuàng)建腳本新建一個(gè)以.py結(jié)尾的文件粘貼上文完整代碼放入~/Library/Application Support/iTerm2/Scripts/AutoLaunch/目錄即可隨 iTerm2 啟動(dòng)自動(dòng)運(yùn)行或放入Scripts/目錄后從Scripts菜單手動(dòng)啟動(dòng)。由于腳本主體是while True循環(huán) run_forever(main)它屬于常駐守護(hù)腳本啟動(dòng)后持續(xù)監(jiān)聽直到 iTerm2 退出或腳本被手動(dòng)終止。分屏操作完成后新窗格的標(biāo)簽顏色會自動(dòng)與同標(biāo)簽頁內(nèi)已有會話對齊之后無需再手動(dòng)干預(yù)。關(guān)于 Python API 腳本的更多運(yùn)行機(jī)制普通腳本與守護(hù)腳本的區(qū)別、調(diào)試方法可以繼續(xù)閱讀倉庫中的 tutorial/index.rst、running.rst 與 daemons.rst。擴(kuò)展思路與注意事項(xiàng)顏色繼承方向腳本總是優(yōu)先取tab.sessions中第一個(gè)按會話順序有顏色的會話作為顏色來源。若同一標(biāo)簽頁內(nèi)多個(gè)窗格顏色本就不同新窗格會繼承最先遇到的那個(gè)方向不可配置。明暗分離模式若舊窗格 Profile 啟用了獨(dú)立明暗顏色腳本讀取到的tab_color可能為空此時(shí)需要改用tab_color_light/tab_color_dark屬性并對應(yīng)調(diào)用set_tab_color_light/set_tab_color_dark兩者均已在LocalWriteOnlyProfile中提供見 profile.py。臨時(shí)會話與 tmux 會話腳本通過app.get_session_by_id解析會話對任何新創(chuàng)建會話一視同仁如果session_id無法解析為 Session 對象如會話已關(guān)閉腳本會安全跳過不會崩潰。API 版本兼容性async_set_profile_properties內(nèi)部會自動(dòng)檢測連接能力對不支持批量寫入的老版本 iTerm23.3.0beta9 及更早退化為逐條寫入因此腳本具備較好的向后兼容性。與新方法的關(guān)系get_tab_and_window_for_session已廢棄新代碼建議改用get_window_and_tab_for_session返回順序同為(Window, Tab)??偠灾甤opycolor 腳本是一份麻雀雖小、五臟俱全的官方范例它完整演示了事件監(jiān)聽NewSessionMonitor 對象模型查詢async_get_app/get_session_by_id/get_tab_and_window_for_session 局部寫入LocalWriteOnlyProfileasync_set_profile_properties這一 iTerm2 Python API 的典型編程范式尤其適合作為學(xué)習(xí)在不污染底層 Profile 的前提下做會話級定制的入門教材?!久赓M(fèi)下載鏈接】iTerm2iTerm2 is a terminal emulator for Mac OS X that does amazing things.項(xiàng)目地址: https://gitcode.com/gh_mirrors/it/iTerm2創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考