Skip to main content

iterm2_client/
session.rs

1//! High-level handle to an iTerm2 session (terminal pane).
2
3use crate::connection::Connection;
4use crate::error::{Error, Result};
5use crate::proto;
6use crate::request;
7use crate::validate;
8use std::sync::Arc;
9use tokio::io::{AsyncRead, AsyncWrite};
10
11/// A handle to an iTerm2 session (a single terminal pane).
12///
13/// Provides methods to send text, read the terminal buffer, split panes,
14/// manage variables/properties, and more.
15pub struct Session<S> {
16    /// The unique session identifier.
17    pub id: String,
18    /// The session's title, if available.
19    pub title: Option<String>,
20    conn: Arc<Connection<S>>,
21}
22
23impl<S: AsyncRead + AsyncWrite + Unpin + Send + 'static> Session<S> {
24    /// Create a session handle. Validates the session ID.
25    pub fn new(id: String, title: Option<String>, conn: Arc<Connection<S>>) -> Result<Self> {
26        validate::identifier(&id, "session")?;
27        Ok(Self { id, title, conn })
28    }
29
30    /// Create without validation — used internally when IDs come from the server.
31    pub(crate) fn new_unchecked(id: String, title: Option<String>, conn: Arc<Connection<S>>) -> Self {
32        Self { id, title, conn }
33    }
34
35    /// Send text to the session as if typed on the keyboard.
36    pub async fn send_text(&self, text: &str) -> Result<()> {
37        validate::text_len(text)?;
38        let resp = self.conn.call(request::send_text(&self.id, text)).await?;
39        match resp.submessage {
40            Some(proto::server_originated_message::Submessage::SendTextResponse(r)) => {
41                check_status_i32(r.status, "SendText")
42            }
43            _ => Err(Error::UnexpectedResponse {
44                expected: "SendTextResponse",
45            }),
46        }
47    }
48
49    /// Get the current visible screen contents as lines of text.
50    pub async fn get_screen_contents(&self) -> Result<Vec<String>> {
51        let resp = self
52            .conn
53            .call(request::get_buffer_screen(&self.id))
54            .await?;
55        match resp.submessage {
56            Some(proto::server_originated_message::Submessage::GetBufferResponse(r)) => {
57                check_buffer_status(r.status)?;
58                Ok(r.contents
59                    .into_iter()
60                    .map(|line| line.text.unwrap_or_default())
61                    .collect())
62            }
63            _ => Err(Error::UnexpectedResponse {
64                expected: "GetBufferResponse",
65            }),
66        }
67    }
68
69    /// Get the last N lines from the scrollback buffer.
70    pub async fn get_buffer_lines(&self, trailing_lines: i32) -> Result<Vec<String>> {
71        let resp = self
72            .conn
73            .call(request::get_buffer_trailing(&self.id, trailing_lines))
74            .await?;
75        match resp.submessage {
76            Some(proto::server_originated_message::Submessage::GetBufferResponse(r)) => {
77                check_buffer_status(r.status)?;
78                Ok(r.contents
79                    .into_iter()
80                    .map(|line| line.text.unwrap_or_default())
81                    .collect())
82            }
83            _ => Err(Error::UnexpectedResponse {
84                expected: "GetBufferResponse",
85            }),
86        }
87    }
88
89    /// Split this session's pane. Returns the new session ID(s).
90    pub async fn split(
91        &self,
92        direction: proto::split_pane_request::SplitDirection,
93        before: bool,
94        profile_name: Option<&str>,
95    ) -> Result<Vec<String>> {
96        let resp = self
97            .conn
98            .call(request::split_pane(&self.id, direction, before, profile_name))
99            .await?;
100        match resp.submessage {
101            Some(proto::server_originated_message::Submessage::SplitPaneResponse(r)) => {
102                check_split_status(r.status)?;
103                Ok(r.session_id)
104            }
105            _ => Err(Error::UnexpectedResponse {
106                expected: "SplitPaneResponse",
107            }),
108        }
109    }
110
111    /// Get a session variable by name. Returns JSON-encoded value.
112    pub async fn get_variable(&self, name: &str) -> Result<Option<String>> {
113        let resp = self
114            .conn
115            .call(request::get_variable_session(
116                &self.id,
117                vec![name.to_string()],
118            ))
119            .await?;
120        match resp.submessage {
121            Some(proto::server_originated_message::Submessage::VariableResponse(r)) => {
122                check_variable_status(r.status)?;
123                Ok(r.values.into_iter().next())
124            }
125            _ => Err(Error::UnexpectedResponse {
126                expected: "VariableResponse",
127            }),
128        }
129    }
130
131    /// Set a session variable. Name must start with `user.`. Value must be valid JSON.
132    pub async fn set_variable(&self, name: &str, json_value: &str) -> Result<()> {
133        validate::json_value(json_value)?;
134        let resp = self
135            .conn
136            .call(request::set_variable_session(
137                &self.id,
138                vec![(name.to_string(), json_value.to_string())],
139            ))
140            .await?;
141        match resp.submessage {
142            Some(proto::server_originated_message::Submessage::VariableResponse(r)) => {
143                check_variable_status(r.status)
144            }
145            _ => Err(Error::UnexpectedResponse {
146                expected: "VariableResponse",
147            }),
148        }
149    }
150
151    /// Get profile properties for this session.
152    pub async fn get_profile_property(&self, keys: Vec<String>) -> Result<Vec<proto::ProfileProperty>> {
153        let resp = self
154            .conn
155            .call(request::get_profile_property(&self.id, keys))
156            .await?;
157        match resp.submessage {
158            Some(proto::server_originated_message::Submessage::GetProfilePropertyResponse(r)) => {
159                check_status_i32(r.status, "GetProfileProperty")?;
160                Ok(r.properties)
161            }
162            _ => Err(Error::UnexpectedResponse {
163                expected: "GetProfilePropertyResponse",
164            }),
165        }
166    }
167
168    /// Set a profile property on this session's copy of the profile. Value must be valid JSON.
169    pub async fn set_profile_property(&self, key: &str, json_value: &str) -> Result<()> {
170        validate::json_value(json_value)?;
171        let resp = self
172            .conn
173            .call(request::set_profile_property_session(
174                &self.id, key, json_value,
175            ))
176            .await?;
177        match resp.submessage {
178            Some(proto::server_originated_message::Submessage::SetProfilePropertyResponse(r)) => {
179                check_status_i32(r.status, "SetProfileProperty")
180            }
181            _ => Err(Error::UnexpectedResponse {
182                expected: "SetProfilePropertyResponse",
183            }),
184        }
185    }
186
187    /// Inject bytes into the terminal as if produced by the running program.
188    pub async fn inject(&self, data: Vec<u8>) -> Result<()> {
189        let resp = self
190            .conn
191            .call(request::inject(vec![self.id.clone()], data))
192            .await?;
193        match resp.submessage {
194            Some(proto::server_originated_message::Submessage::InjectResponse(r)) => {
195                for status in &r.status {
196                    if *status != proto::inject_response::Status::Ok as i32 {
197                        return Err(Error::Status(format!(
198                            "Inject failed with status: {status}"
199                        )));
200                    }
201                }
202                Ok(())
203            }
204            _ => Err(Error::UnexpectedResponse {
205                expected: "InjectResponse",
206            }),
207        }
208    }
209
210    /// Restart the session's shell process.
211    pub async fn restart(&self, only_if_exited: bool) -> Result<()> {
212        let resp = self
213            .conn
214            .call(request::restart_session(&self.id, only_if_exited))
215            .await?;
216        match resp.submessage {
217            Some(proto::server_originated_message::Submessage::RestartSessionResponse(r)) => {
218                check_status_i32(r.status, "RestartSession")
219            }
220            _ => Err(Error::UnexpectedResponse {
221                expected: "RestartSessionResponse",
222            }),
223        }
224    }
225
226    /// Close this session. If `force` is true, skip the confirmation prompt.
227    pub async fn close(&self, force: bool) -> Result<()> {
228        let resp = self
229            .conn
230            .call(request::close_sessions(vec![self.id.clone()], force))
231            .await?;
232        match resp.submessage {
233            Some(proto::server_originated_message::Submessage::CloseResponse(_r)) => Ok(()),
234            _ => Err(Error::UnexpectedResponse {
235                expected: "CloseResponse",
236            }),
237        }
238    }
239
240    /// Activate this session (bring its window to front and select it).
241    pub async fn activate(&self) -> Result<()> {
242        let resp = self
243            .conn
244            .call(request::activate_session(&self.id))
245            .await?;
246        match resp.submessage {
247            Some(proto::server_originated_message::Submessage::ActivateResponse(r)) => {
248                check_status_i32(r.status, "Activate")
249            }
250            _ => Err(Error::UnexpectedResponse {
251                expected: "ActivateResponse",
252            }),
253        }
254    }
255
256    /// Get metadata about the current shell prompt (command, working directory, state).
257    pub async fn get_prompt(&self) -> Result<proto::GetPromptResponse> {
258        let resp = self.conn.call(request::get_prompt(&self.id)).await?;
259        match resp.submessage {
260            Some(proto::server_originated_message::Submessage::GetPromptResponse(r)) => Ok(r),
261            _ => Err(Error::UnexpectedResponse {
262                expected: "GetPromptResponse",
263            }),
264        }
265    }
266
267    /// Get a reference to the underlying connection.
268    pub fn connection(&self) -> &Connection<S> {
269        &self.conn
270    }
271}
272
273fn check_status_i32(status: Option<i32>, op: &str) -> Result<()> {
274    match status {
275        Some(0) | None => Ok(()),
276        Some(code) => Err(Error::Status(format!("{op} returned status {code}"))),
277    }
278}
279
280fn check_buffer_status(status: Option<i32>) -> Result<()> {
281    check_status_i32(status, "GetBuffer")
282}
283
284fn check_split_status(status: Option<i32>) -> Result<()> {
285    check_status_i32(status, "SplitPane")
286}
287
288fn check_variable_status(status: Option<i32>) -> Result<()> {
289    check_status_i32(status, "Variable")
290}