1use std::path::PathBuf;
8
9use crate::handles::session::unexpected_response;
10use crate::{
11 Pane, PaneSet, ProcessCommandSpec, Result, RmuxError, Session, SplitDirection, WindowRef,
12};
13use rmux_proto::{Request, Response, SelectLayoutTarget, SpreadLayoutRequest};
14
15#[derive(Debug)]
17pub struct SessionLayoutBuilder<'a> {
18 session: &'a Session,
19 window_index: u32,
20}
21
22impl<'a> SessionLayoutBuilder<'a> {
23 pub(crate) const fn new(session: &'a Session) -> Self {
24 Self {
25 session,
26 window_index: 0,
27 }
28 }
29
30 #[must_use]
36 pub const fn window(mut self, window_index: u32) -> Self {
37 self.window_index = window_index;
38 self
39 }
40
41 #[must_use]
47 pub const fn grid(self, columns: usize, rows: usize) -> GridLayoutBuilder<'a> {
48 GridLayoutBuilder {
49 session: self.session,
50 window_index: self.window_index,
51 columns,
52 rows,
53 replace_existing_root_process: true,
54 replace_existing_panes: false,
55 panes: Vec::new(),
56 }
57 }
58}
59
60#[derive(Debug)]
62pub struct GridLayoutBuilder<'a> {
63 session: &'a Session,
64 window_index: u32,
65 columns: usize,
66 rows: usize,
67 replace_existing_root_process: bool,
68 replace_existing_panes: bool,
69 panes: Vec<LayoutPaneSpec>,
70}
71
72impl<'a> GridLayoutBuilder<'a> {
73 #[must_use]
83 pub const fn replace_existing_root_process(mut self, replace: bool) -> Self {
84 self.replace_existing_root_process = replace;
85 self
86 }
87
88 #[must_use]
97 pub const fn replace_existing_panes(mut self, replace: bool) -> Self {
98 self.replace_existing_panes = replace;
99 self
100 }
101
102 #[must_use]
107 pub fn pane(self, title: impl Into<String>) -> LayoutPaneBuilder<'a> {
108 LayoutPaneBuilder {
109 builder: self,
110 spec: LayoutPaneSpec::new(title.into()),
111 }
112 }
113
114 pub async fn apply(self) -> Result<PaneSet> {
116 apply_grid(self).await
117 }
118}
119
120#[derive(Debug)]
123pub struct LayoutPaneBuilder<'a> {
124 builder: GridLayoutBuilder<'a>,
125 spec: LayoutPaneSpec,
126}
127
128impl<'a> LayoutPaneBuilder<'a> {
129 #[must_use]
131 pub fn spawn<I, S>(mut self, command: I) -> Self
132 where
133 I: IntoIterator<Item = S>,
134 S: Into<String>,
135 {
136 self.spec.command = Some(ProcessCommandSpec::Argv(
137 command.into_iter().map(Into::into).collect(),
138 ));
139 self
140 }
141
142 #[must_use]
144 pub fn shell(mut self, command: impl Into<String>) -> Self {
145 self.spec.command = Some(ProcessCommandSpec::Shell(command.into()));
146 self
147 }
148
149 #[must_use]
151 pub fn cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
152 self.spec.cwd = Some(cwd.into());
153 self
154 }
155
156 #[must_use]
158 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
159 self.spec.env.push((key.into(), value.into()));
160 self
161 }
162
163 #[must_use]
165 pub const fn keep_alive_on_exit(mut self, keep_alive: bool) -> Self {
166 self.spec.keep_alive_on_exit = Some(keep_alive);
167 self
168 }
169
170 #[must_use]
172 pub fn pane(self, title: impl Into<String>) -> Self {
173 self.finish().pane(title)
174 }
175
176 pub async fn apply(self) -> Result<PaneSet> {
178 self.finish().apply().await
179 }
180
181 fn finish(mut self) -> GridLayoutBuilder<'a> {
182 self.builder.panes.push(self.spec);
183 self.builder
184 }
185}
186
187#[derive(Debug, Clone)]
188struct LayoutPaneSpec {
189 title: String,
190 command: Option<ProcessCommandSpec>,
191 cwd: Option<PathBuf>,
192 env: Vec<(String, String)>,
193 keep_alive_on_exit: Option<bool>,
194}
195
196impl LayoutPaneSpec {
197 fn new(title: String) -> Self {
198 Self {
199 title,
200 command: None,
201 cwd: None,
202 env: Vec::new(),
203 keep_alive_on_exit: None,
204 }
205 }
206}
207
208async fn apply_grid(builder: GridLayoutBuilder<'_>) -> Result<PaneSet> {
209 let mut created_panes = Vec::new();
210 let result = apply_grid_inner(builder, &mut created_panes).await;
211 if result.is_err() {
212 rollback_created_panes(created_panes).await;
213 }
214 result
215}
216
217async fn apply_grid_inner(
218 builder: GridLayoutBuilder<'_>,
219 created_panes: &mut Vec<Pane>,
220) -> Result<PaneSet> {
221 let capacity = validate_grid(builder.columns, builder.rows)?;
222 validate_pane_count(builder.panes.len(), capacity)?;
223
224 let window = builder.session.window(builder.window_index);
225 let mut existing = window.panes().await?;
226 if existing.len() != 1 && builder.replace_existing_panes {
227 close_extra_panes(builder.session, &existing).await?;
228 existing = window.panes().await?;
229 }
230 if existing.len() != 1 {
231 return Err(layout_error(format!(
232 "layout builder expects exactly one existing pane in window {}; found {}. \
233 Use replace_existing_panes(true) to close extras first",
234 builder.window_index,
235 existing.len()
236 )));
237 }
238
239 let root_target = &existing[0].target;
240 let root = builder
241 .session
242 .pane(root_target.window_index, root_target.pane_index);
243 let mut panes = vec![None; builder.panes.len()];
244
245 let root_pane = configure_existing_root(
246 builder.session,
247 root,
248 &builder.panes[0],
249 builder.replace_existing_root_process,
250 )
251 .await?;
252 panes[0] = Some(root_pane.clone());
253
254 let row_count = row_count(builder.panes.len(), builder.columns);
255 let mut row_anchors = Vec::with_capacity(row_count);
256 row_anchors.push(root_pane);
257
258 for row in 1..row_count {
259 let spec_index = row * builder.columns;
260 let anchor = row_anchors[row - 1].clone();
261 let pane = split_new_pane(
262 builder.session,
263 &anchor,
264 SplitDirection::Down,
265 &builder.panes[spec_index],
266 )
267 .await?;
268 panes[spec_index] = Some(pane.clone());
269 created_panes.push(pane.clone());
270 row_anchors.push(pane);
271 }
272
273 for (row, row_anchor) in row_anchors.iter().enumerate() {
274 let row_start = row * builder.columns;
275 let row_end = usize::min(row_start + builder.columns, builder.panes.len());
276 let mut previous = row_anchor.clone();
277 for (spec_index, slot) in panes
278 .iter_mut()
279 .enumerate()
280 .take(row_end)
281 .skip(row_start + 1)
282 {
283 let pane = split_new_pane(
284 builder.session,
285 &previous,
286 SplitDirection::Right,
287 &builder.panes[spec_index],
288 )
289 .await?;
290 *slot = Some(pane.clone());
291 created_panes.push(pane.clone());
292 previous = pane;
293 }
294 }
295
296 spread_window(builder.session, builder.window_index).await?;
297 Ok(PaneSet::new(
298 panes
299 .into_iter()
300 .map(|pane| pane.expect("every declared pane is created"))
301 .collect::<Vec<_>>(),
302 ))
303}
304
305async fn rollback_created_panes(mut panes: Vec<Pane>) {
306 while let Some(pane) = panes.pop() {
307 let _ = pane.close().await;
308 }
309}
310
311async fn close_extra_panes(session: &Session, panes: &[crate::WindowPane]) -> Result<()> {
312 for pane in panes.iter().skip(1).rev() {
313 session.pane_by_id(pane.id).await?.close().await?;
314 }
315 Ok(())
316}
317
318async fn configure_existing_root(
319 session: &Session,
320 pane: Pane,
321 spec: &LayoutPaneSpec,
322 replace_existing: bool,
323) -> Result<Pane> {
324 match spec.command.clone() {
325 Some(ProcessCommandSpec::Argv(argv)) => {
326 let mut spawn = pane.spawn(argv).kill_existing(replace_existing);
327 spawn = apply_spawn_options(spawn, spec);
328 spawn.await?;
329 }
330 Some(ProcessCommandSpec::Shell(command)) => {
331 let mut spawn = pane.shell(command).kill_existing(replace_existing);
332 spawn = apply_spawn_options(spawn, spec);
333 spawn.await?;
334 }
335 None => {
336 validate_existing_root_options(spec)?;
337 pane.set_title(spec.title.clone()).await?;
338 }
339 }
340
341 stable_pane(session, &pane).await
342}
343
344fn apply_spawn_options<'a>(
345 mut spawn: crate::PaneSpawnBuilder<'a>,
346 spec: &LayoutPaneSpec,
347) -> crate::PaneSpawnBuilder<'a> {
348 if let Some(cwd) = spec.cwd.clone() {
349 spawn = spawn.cwd(cwd);
350 }
351 for (key, value) in &spec.env {
352 spawn = spawn.env(key.clone(), value.clone());
353 }
354 if let Some(keep_alive) = spec.keep_alive_on_exit {
355 spawn = spawn.keep_alive_on_exit(keep_alive);
356 }
357 spawn.title(spec.title.clone())
358}
359
360async fn split_new_pane(
361 session: &Session,
362 anchor: &Pane,
363 direction: SplitDirection,
364 spec: &LayoutPaneSpec,
365) -> Result<Pane> {
366 let mut split = anchor.split_with(direction);
367 if let Some(cwd) = spec.cwd.clone() {
368 split = split.cwd(cwd);
369 }
370 for (key, value) in &spec.env {
371 split = split.env(key.clone(), value.clone());
372 }
373 if let Some(keep_alive) = spec.keep_alive_on_exit {
374 split = split.keep_alive_on_exit(keep_alive);
375 }
376 split = match spec.command.clone() {
377 Some(ProcessCommandSpec::Argv(argv)) => split.spawn(argv),
378 Some(ProcessCommandSpec::Shell(command)) => split.shell(command),
379 None => split,
380 };
381 split = split.title(spec.title.clone());
382
383 let pane = split.await?;
384 stable_pane(session, &pane).await
385}
386
387async fn stable_pane(session: &Session, pane: &Pane) -> Result<Pane> {
388 let pane_id = pane
389 .id()
390 .await?
391 .ok_or_else(|| layout_error("created pane vanished before its id could be read"))?;
392 session.pane_by_id(pane_id).await
393}
394
395async fn spread_window(session: &Session, window_index: u32) -> Result<()> {
396 let target = WindowRef::new(session.name().clone(), window_index);
397 match session
398 .transport()
399 .request(Request::SpreadLayout(SpreadLayoutRequest {
400 target: SelectLayoutTarget::Window(target.to_proto()),
401 }))
402 .await?
403 {
404 Response::SelectLayout(_) => Ok(()),
405 response => Err(unexpected_response("select-layout -E", response)),
406 }
407}
408
409fn validate_existing_root_options(spec: &LayoutPaneSpec) -> Result<()> {
410 if spec.cwd.is_some() || !spec.env.is_empty() || spec.keep_alive_on_exit.is_some() {
411 return Err(layout_error(
412 "cwd, env, and keep_alive_on_exit on the existing root pane require spawn() or shell()",
413 ));
414 }
415 Ok(())
416}
417
418fn validate_grid(columns: usize, rows: usize) -> Result<usize> {
419 if columns == 0 || rows == 0 {
420 return Err(layout_error(
421 "grid columns and rows must be greater than zero",
422 ));
423 }
424 columns
425 .checked_mul(rows)
426 .ok_or_else(|| layout_error("grid dimensions overflow usize"))
427}
428
429fn validate_pane_count(count: usize, capacity: usize) -> Result<()> {
430 if count == 0 {
431 return Err(layout_error("layout must declare at least one pane"));
432 }
433 if count > capacity {
434 return Err(layout_error(format!(
435 "layout declares {count} panes but grid capacity is {capacity}"
436 )));
437 }
438 Ok(())
439}
440
441fn row_count(pane_count: usize, columns: usize) -> usize {
442 ((pane_count - 1) / columns) + 1
443}
444
445fn layout_error(message: impl Into<String>) -> RmuxError {
446 RmuxError::protocol(rmux_proto::RmuxError::Server(format!(
447 "invalid layout builder request: {}",
448 message.into()
449 )))
450}