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