Skip to main content

rmux_sdk/handles/
session.rs

1//! Daemon-backed session handle.
2
3use std::fmt;
4use std::time::Duration;
5
6use crate::transport::TransportClient;
7use crate::{PaneId, PaneRef, Result, RmuxEndpoint, RmuxError, SessionName, WindowRef};
8use rmux_proto::{HasSessionRequest, KillSessionRequest, ListSessionsRequest, Request, Response};
9
10use super::{Pane, Window};
11
12/// Opaque handle for one live daemon session.
13///
14/// The handle stores the daemon transport and exact protocol-owned session
15/// name. It never stores process environment supplied while creating the
16/// session.
17pub struct Session {
18    name: SessionName,
19    endpoint: RmuxEndpoint,
20    default_timeout: Option<Duration>,
21    transport: TransportClient,
22    created: bool,
23    creation_tags: Option<Vec<String>>,
24}
25
26impl Session {
27    pub(crate) fn new(
28        name: SessionName,
29        endpoint: RmuxEndpoint,
30        default_timeout: Option<Duration>,
31        transport: TransportClient,
32        created: bool,
33        creation_tags: Option<Vec<String>>,
34    ) -> Self {
35        Self {
36            name,
37            endpoint,
38            default_timeout,
39            transport,
40            created,
41            creation_tags,
42        }
43    }
44
45    /// Returns the exact protocol-owned session name addressed by this handle.
46    #[must_use]
47    pub fn name(&self) -> &SessionName {
48        &self.name
49    }
50
51    /// Returns the endpoint that was resolved when this handle was created.
52    #[must_use]
53    pub fn endpoint(&self) -> &RmuxEndpoint {
54        &self.endpoint
55    }
56
57    /// Returns the default timeout configured on the parent facade.
58    #[must_use]
59    pub const fn configured_default_timeout(&self) -> Option<Duration> {
60        self.default_timeout
61    }
62
63    pub(crate) const fn transport(&self) -> &TransportClient {
64        &self.transport
65    }
66
67    /// Returns whether the ensure operation created the session.
68    ///
69    /// `false` means the handle was bound to a session that already existed
70    /// before the ensure request completed.
71    #[must_use]
72    pub const fn was_created(&self) -> bool {
73        self.created
74    }
75
76    /// Returns caller-supplied creation tag intent, preserving explicit empty
77    /// tag sets.
78    #[must_use]
79    pub fn creation_tags(&self) -> Option<&[String]> {
80        self.creation_tags.as_deref()
81    }
82
83    /// Checks the live daemon for this session.
84    pub async fn exists(&self) -> Result<bool> {
85        has_session(&self.transport, self.name.clone()).await
86    }
87
88    /// Checks whether this session appears in the daemon's `list-sessions`
89    /// projection.
90    pub async fn is_listed(&self) -> Result<bool> {
91        Ok(list_session_names(&self.transport)
92            .await?
93            .iter()
94            .any(|candidate| candidate == &self.name))
95    }
96
97    /// Lists exact session names currently reported by the daemon.
98    pub async fn list_session_names(&self) -> Result<Vec<SessionName>> {
99        list_session_names(&self.transport).await
100    }
101
102    /// Returns a handle for a window slot in this session.
103    ///
104    /// The handle is intentionally lazy: it records the exact target and
105    /// verifies liveness only when an operation such as `split`, `panes`,
106    /// `info`, or `close` is invoked. Linked windows and grouped sessions are
107    /// still resolved by the daemon on each operation rather than cached by the
108    /// handle.
109    #[must_use]
110    pub fn window(&self, window_index: u32) -> Window {
111        Window::new(
112            WindowRef::new(self.name.clone(), window_index),
113            self.endpoint.clone(),
114            self.default_timeout,
115            self.transport.clone(),
116        )
117    }
118
119    /// Returns a handle for one pane slot inside a window of this session.
120    ///
121    /// The handle records the exact `(session, window, pane)` triple and
122    /// resolves it through the daemon on every operation, so linked windows
123    /// and grouped sessions keep returning the same stable pane identity
124    /// across sibling views.
125    #[must_use]
126    pub fn pane(&self, window_index: u32, pane_index: u32) -> Pane {
127        Pane::new(
128            PaneRef::new(self.name.clone(), window_index, pane_index),
129            self.endpoint.clone(),
130            self.default_timeout,
131            self.transport.clone(),
132        )
133    }
134
135    /// Returns a pane handle addressed by stable pane id.
136    ///
137    /// The returned [`Pane`] has the same public type as a slot-based pane,
138    /// but input, resize, lifecycle, title, and snapshot operations use the
139    /// daemon's stable pane-id targeting path. `PaneId` is stable only for
140    /// one daemon lifetime; callers that persist ids across reconnects must
141    /// re-validate them.
142    pub async fn pane_by_id(&self, pane_id: PaneId) -> Result<Pane> {
143        let target = super::pane::resolve_pane_ref_for_id(&self.transport, &self.name, pane_id)
144            .await?
145            .ok_or_else(|| pane_not_found(&self.name, pane_id))?;
146        Ok(Pane::new_by_id(
147            target,
148            pane_id,
149            self.endpoint.clone(),
150            self.default_timeout,
151            self.transport.clone(),
152        ))
153    }
154
155    /// Creates a new window in this session and returns a live window handle.
156    ///
157    /// This is an eager daemon mutation. Use [`Self::window`] when you only
158    /// need a lazy handle for an existing or future window slot.
159    pub async fn new_window(&self) -> Result<Window> {
160        self.new_window_with().await
161    }
162
163    /// Starts building a configurable `new-window` request for this session.
164    ///
165    /// The default builder behavior matches tmux `new-window`: the created
166    /// window becomes active unless [`NewWindowBuilder::detached`] is set.
167    ///
168    /// [`NewWindowBuilder::detached`]: crate::NewWindowBuilder::detached
169    #[must_use]
170    pub fn new_window_with(&self) -> crate::NewWindowBuilder<'_> {
171        crate::NewWindowBuilder::new(self)
172    }
173
174    /// Starts a declarative SDK layout builder for this session.
175    ///
176    /// Layouts are SDK-side composition over the existing pane split,
177    /// spawn, title, and daemon spread-layout primitives. They do not add a
178    /// daemon-native transaction; if an intermediate split or spawn fails,
179    /// already-created panes remain visible for inspection and cleanup by the
180    /// caller.
181    #[must_use]
182    pub fn layout(&self) -> crate::SessionLayoutBuilder<'_> {
183        crate::SessionLayoutBuilder::new(self)
184    }
185
186    /// Destroys this session through the daemon.
187    ///
188    /// The returned boolean mirrors the daemon response: `true` means a
189    /// session existed and was removed.
190    pub async fn kill(&self) -> Result<bool> {
191        kill_session(&self.transport, self.name.clone()).await
192    }
193}
194
195fn pane_not_found(session_name: &SessionName, pane_id: PaneId) -> RmuxError {
196    RmuxError::protocol(rmux_proto::RmuxError::pane_not_found(
197        session_name.clone(),
198        pane_id,
199    ))
200}
201
202impl fmt::Debug for Session {
203    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
204        formatter
205            .debug_struct("Session")
206            .field("name", &self.name)
207            .field("created", &self.created)
208            .field("creation_tags", &self.creation_tags)
209            .finish_non_exhaustive()
210    }
211}
212
213pub(crate) async fn has_session(client: &TransportClient, name: SessionName) -> Result<bool> {
214    match client
215        .request(Request::HasSession(HasSessionRequest { target: name }))
216        .await?
217    {
218        Response::HasSession(response) => Ok(response.exists),
219        response => Err(unexpected_response("has-session", response)),
220    }
221}
222
223pub(crate) async fn kill_session(client: &TransportClient, name: SessionName) -> Result<bool> {
224    match client
225        .request(Request::KillSession(KillSessionRequest {
226            target: name,
227            kill_all_except_target: false,
228            clear_alerts: false,
229        }))
230        .await?
231    {
232        Response::KillSession(response) => Ok(response.existed),
233        response => Err(unexpected_response("kill-session", response)),
234    }
235}
236
237pub(crate) async fn list_session_names(client: &TransportClient) -> Result<Vec<SessionName>> {
238    let response = client
239        .request(Request::ListSessions(ListSessionsRequest {
240            format: Some("#{session_name}".to_owned()),
241            filter: None,
242            sort_order: Some("name".to_owned()),
243            reversed: false,
244        }))
245        .await?;
246
247    let output = match response {
248        Response::ListSessions(response) => response.output.stdout,
249        response => return Err(unexpected_response("list-sessions", response)),
250    };
251
252    String::from_utf8_lossy(&output)
253        .lines()
254        .map(SessionName::new)
255        .collect::<core::result::Result<Vec<_>, _>>()
256        .map_err(RmuxError::protocol)
257}
258
259pub(crate) fn unexpected_response(expected: &'static str, response: Response) -> RmuxError {
260    RmuxError::protocol(rmux_proto::RmuxError::Server(format!(
261        "rmux daemon sent `{}` response for `{expected}` request",
262        response.command_name()
263    )))
264}