Skip to main content

iterm2_client/
app.rs

1//! High-level entry point for the iTerm2 API.
2
3use crate::connection::Connection;
4use crate::error::{Error, Result};
5use crate::proto;
6use crate::request;
7use crate::session::Session;
8use crate::tab::Tab;
9use crate::window::Window;
10use std::sync::Arc;
11use tokio::io::{AsyncRead, AsyncWrite};
12
13/// High-level handle to the iTerm2 application.
14///
15/// Provides ergonomic methods for listing sessions, creating tabs, managing
16/// transactions, and subscribing to notifications. Wraps an `Arc<Connection>`.
17pub struct App<S> {
18    conn: Arc<Connection<S>>,
19}
20
21impl<S: AsyncRead + AsyncWrite + Unpin + Send + 'static> App<S> {
22    /// Create an `App` from a [`Connection`].
23    pub fn new(conn: Connection<S>) -> Self {
24        Self {
25            conn: Arc::new(conn),
26        }
27    }
28
29    /// Create an `App` from a shared connection.
30    pub fn from_arc(conn: Arc<Connection<S>>) -> Self {
31        Self { conn }
32    }
33
34    /// List all windows, tabs, and sessions in iTerm2.
35    pub async fn list_sessions(&self) -> Result<ListSessionsResult<S>> {
36        let resp = self.conn.call(request::list_sessions()).await?;
37        match resp.submessage {
38            Some(proto::server_originated_message::Submessage::ListSessionsResponse(r)) => {
39                Ok(self.parse_list_sessions(r))
40            }
41            _ => Err(Error::UnexpectedResponse {
42                expected: "ListSessionsResponse",
43            }),
44        }
45    }
46
47    fn parse_list_sessions(&self, resp: proto::ListSessionsResponse) -> ListSessionsResult<S> {
48        let mut windows = Vec::new();
49        for w in resp.windows {
50            let window_id = w.window_id.unwrap_or_default();
51            let mut tabs = Vec::new();
52            for t in w.tabs {
53                let tab_id = t.tab_id.unwrap_or_default();
54                let sessions = collect_sessions_from_tree(t.root.as_ref(), &self.conn);
55                tabs.push(TabInfo {
56                    tab: Tab::new_unchecked(tab_id, Arc::clone(&self.conn)),
57                    sessions,
58                });
59            }
60            windows.push(WindowInfo {
61                window: Window::new_unchecked(window_id, Arc::clone(&self.conn)),
62                tabs,
63            });
64        }
65
66        let buried_sessions = resp
67            .buried_sessions
68            .into_iter()
69            .map(|s| {
70                Session::new_unchecked(
71                    s.unique_identifier.unwrap_or_default(),
72                    s.title,
73                    Arc::clone(&self.conn),
74                )
75            })
76            .collect();
77
78        ListSessionsResult {
79            windows,
80            buried_sessions,
81        }
82    }
83
84    /// Create a new tab, optionally in an existing window with a named profile.
85    pub async fn create_tab(
86        &self,
87        profile_name: Option<&str>,
88        window_id: Option<&str>,
89    ) -> Result<CreateTabResult<S>> {
90        let resp = self
91            .conn
92            .call(request::create_tab(profile_name, window_id))
93            .await?;
94        match resp.submessage {
95            Some(proto::server_originated_message::Submessage::CreateTabResponse(r)) => {
96                check_status_i32(r.status, "CreateTab")?;
97                let session_id = r.session_id.unwrap_or_default();
98                let tab_id = r.tab_id.map(|id| id.to_string()).unwrap_or_default();
99                let window_id = r.window_id.unwrap_or_default();
100                Ok(CreateTabResult {
101                    window: Window::new_unchecked(window_id, Arc::clone(&self.conn)),
102                    tab: Tab::new_unchecked(tab_id, Arc::clone(&self.conn)),
103                    session: Session::new_unchecked(session_id, None, Arc::clone(&self.conn)),
104                })
105            }
106            _ => Err(Error::UnexpectedResponse {
107                expected: "CreateTabResponse",
108            }),
109        }
110    }
111
112    /// Get the current focus state (active window, tab, session).
113    pub async fn focus(&self) -> Result<Vec<proto::FocusChangedNotification>> {
114        let resp = self.conn.call(request::focus()).await?;
115        match resp.submessage {
116            Some(proto::server_originated_message::Submessage::FocusResponse(r)) => {
117                Ok(r.notifications)
118            }
119            _ => Err(Error::UnexpectedResponse {
120                expected: "FocusResponse",
121            }),
122        }
123    }
124
125    /// Activate the iTerm2 application, optionally raising all windows.
126    pub async fn activate(&self, raise_all: bool, ignoring_other_apps: bool) -> Result<()> {
127        let resp = self
128            .conn
129            .call(request::activate_app(raise_all, ignoring_other_apps))
130            .await?;
131        match resp.submessage {
132            Some(proto::server_originated_message::Submessage::ActivateResponse(r)) => {
133                check_status_i32(r.status, "Activate")
134            }
135            _ => Err(Error::UnexpectedResponse {
136                expected: "ActivateResponse",
137            }),
138        }
139    }
140
141    /// List profiles, optionally filtering by properties and GUIDs.
142    pub async fn list_profiles(
143        &self,
144        properties: Vec<String>,
145        guids: Vec<String>,
146    ) -> Result<proto::ListProfilesResponse> {
147        let resp = self
148            .conn
149            .call(request::list_profiles(properties, guids))
150            .await?;
151        match resp.submessage {
152            Some(proto::server_originated_message::Submessage::ListProfilesResponse(r)) => Ok(r),
153            _ => Err(Error::UnexpectedResponse {
154                expected: "ListProfilesResponse",
155            }),
156        }
157    }
158
159    /// Begin a transaction. The app's main loop freezes until [`end_transaction`](Self::end_transaction) is called.
160    pub async fn begin_transaction(&self) -> Result<()> {
161        let resp = self.conn.call(request::begin_transaction()).await?;
162        match resp.submessage {
163            Some(proto::server_originated_message::Submessage::TransactionResponse(r)) => {
164                check_status_i32(r.status, "Transaction")
165            }
166            _ => Err(Error::UnexpectedResponse {
167                expected: "TransactionResponse",
168            }),
169        }
170    }
171
172    /// End a previously started transaction.
173    pub async fn end_transaction(&self) -> Result<()> {
174        let resp = self.conn.call(request::end_transaction()).await?;
175        match resp.submessage {
176            Some(proto::server_originated_message::Submessage::TransactionResponse(r)) => {
177                check_status_i32(r.status, "Transaction")
178            }
179            _ => Err(Error::UnexpectedResponse {
180                expected: "TransactionResponse",
181            }),
182        }
183    }
184
185    /// List available color preset names.
186    pub async fn list_color_presets(&self) -> Result<Vec<String>> {
187        let resp = self.conn.call(request::list_color_presets()).await?;
188        match resp.submessage {
189            Some(proto::server_originated_message::Submessage::ColorPresetResponse(r)) => {
190                check_status_i32(r.status, "ColorPreset")?;
191                match r.response {
192                    Some(proto::color_preset_response::Response::ListPresets(lp)) => Ok(lp.name),
193                    _ => Ok(vec![]),
194                }
195            }
196            _ => Err(Error::UnexpectedResponse {
197                expected: "ColorPresetResponse",
198            }),
199        }
200    }
201
202    /// List saved window arrangement names.
203    pub async fn list_arrangements(&self) -> Result<Vec<String>> {
204        let resp = self.conn.call(request::list_arrangements()).await?;
205        match resp.submessage {
206            Some(proto::server_originated_message::Submessage::SavedArrangementResponse(r)) => {
207                check_status_i32(r.status, "SavedArrangement")?;
208                Ok(r.names)
209            }
210            _ => Err(Error::UnexpectedResponse {
211                expected: "SavedArrangementResponse",
212            }),
213        }
214    }
215
216    /// Get broadcast domains (groups of sessions that receive the same input).
217    pub async fn get_broadcast_domains(&self) -> Result<Vec<proto::BroadcastDomain>> {
218        let resp = self.conn.call(request::get_broadcast_domains()).await?;
219        match resp.submessage {
220            Some(proto::server_originated_message::Submessage::GetBroadcastDomainsResponse(r)) => {
221                Ok(r.broadcast_domains)
222            }
223            _ => Err(Error::UnexpectedResponse {
224                expected: "GetBroadcastDomainsResponse",
225            }),
226        }
227    }
228
229    /// Subscribe to spontaneous notifications from iTerm2.
230    pub fn subscribe_notifications(&self) -> tokio::sync::broadcast::Receiver<proto::Notification> {
231        self.conn.subscribe_notifications()
232    }
233
234    /// Get a reference to the underlying connection.
235    pub fn connection(&self) -> &Connection<S> {
236        &self.conn
237    }
238
239    /// Get a shared reference to the underlying connection.
240    pub fn connection_arc(&self) -> Arc<Connection<S>> {
241        Arc::clone(&self.conn)
242    }
243}
244
245fn collect_sessions_from_tree<S: AsyncRead + AsyncWrite + Unpin + Send + 'static>(
246    node: Option<&proto::SplitTreeNode>,
247    conn: &Arc<Connection<S>>,
248) -> Vec<Session<S>> {
249    let mut sessions = Vec::new();
250    if let Some(node) = node {
251        for link in &node.links {
252            if let Some(child) = &link.child {
253                match child {
254                    proto::split_tree_node::split_tree_link::Child::Session(s) => {
255                        sessions.push(Session::new_unchecked(
256                            s.unique_identifier.clone().unwrap_or_default(),
257                            s.title.clone(),
258                            Arc::clone(conn),
259                        ));
260                    }
261                    proto::split_tree_node::split_tree_link::Child::Node(n) => {
262                        sessions.extend(collect_sessions_from_tree(Some(n), conn));
263                    }
264                }
265            }
266        }
267    }
268    sessions
269}
270
271/// Result of [`App::list_sessions`], containing all windows and buried sessions.
272pub struct ListSessionsResult<S> {
273    pub windows: Vec<WindowInfo<S>>,
274    pub buried_sessions: Vec<Session<S>>,
275}
276
277/// A window and its tabs, as returned by [`App::list_sessions`].
278pub struct WindowInfo<S> {
279    pub window: Window<S>,
280    pub tabs: Vec<TabInfo<S>>,
281}
282
283/// A tab and its sessions, as returned by [`App::list_sessions`].
284pub struct TabInfo<S> {
285    pub tab: Tab<S>,
286    pub sessions: Vec<Session<S>>,
287}
288
289/// Result of [`App::create_tab`], containing the new window, tab, and session.
290pub struct CreateTabResult<S> {
291    pub window: Window<S>,
292    pub tab: Tab<S>,
293    pub session: Session<S>,
294}
295
296fn check_status_i32(status: Option<i32>, op: &str) -> Result<()> {
297    match status {
298        Some(0) | None => Ok(()),
299        Some(code) => Err(Error::Status(format!("{op} returned status {code}"))),
300    }
301}