1use super::pane_transfer_shared::{
2 adjusted_insert_position, adjusted_insert_position_before, apply_swap_between_windows,
3 validate_swap_destination, SwapPaneEntry,
4};
5use super::target_error::{invalid_pane_target, invalid_window_target};
6use super::{PaneJoinOptions, PaneSwapOptions, Session, SessionPaneTarget};
7use crate::{Pane, Window};
8use rmux_proto::{PaneSplitSize, RmuxError, SplitDirection};
9
10#[path = "pane_transfer/break_window.rs"]
11mod break_window;
12
13impl Session {
14 pub fn last_pane_in_window(&mut self, window_index: u32) -> Result<u32, RmuxError> {
16 self.last_pane_in_window_with_zoom(window_index, false)
17 }
18
19 pub fn last_pane_in_window_with_zoom(
21 &mut self,
22 window_index: u32,
23 preserve_zoom: bool,
24 ) -> Result<u32, RmuxError> {
25 let last_pane = self
26 .window_at(window_index)
27 .ok_or_else(|| invalid_window_target(&self.name, window_index))?
28 .last_pane_index()
29 .ok_or_else(|| RmuxError::Server("no last pane".to_owned()))?;
30
31 let window = self
32 .window_at_mut(window_index)
33 .expect("addressed session window must exist");
34 if preserve_zoom {
35 let _ = window.push_zoom(true);
36 }
37 let selected = window.select_pane(last_pane);
38 if preserve_zoom {
39 window.pop_zoom();
40 }
41 debug_assert!(selected, "validated last pane must be selectable");
42
43 Ok(last_pane)
44 }
45
46 pub fn swap_panes(
48 &mut self,
49 source: SessionPaneTarget,
50 target: SessionPaneTarget,
51 options: PaneSwapOptions,
52 ) -> Result<(), RmuxError> {
53 if source.window_index == target.window_index {
54 return self.swap_panes_within_window(
55 source.window_index,
56 source.pane_index,
57 target.pane_index,
58 options,
59 );
60 }
61
62 let source_pane = self
63 .window_at(source.window_index)
64 .ok_or_else(|| invalid_window_target(&self.name, source.window_index))?
65 .pane(source.pane_index)
66 .cloned()
67 .ok_or_else(|| {
68 invalid_pane_target(
69 &self.name,
70 source.window_index,
71 source.pane_index,
72 "pane index does not exist in session",
73 )
74 })?;
75 let target_pane = self
76 .window_at(target.window_index)
77 .ok_or_else(|| invalid_window_target(&self.name, target.window_index))?
78 .pane(target.pane_index)
79 .cloned()
80 .ok_or_else(|| {
81 invalid_pane_target(
82 &self.name,
83 target.window_index,
84 target.pane_index,
85 "pane index does not exist in session",
86 )
87 })?;
88
89 validate_swap_destination(
90 self.window_at(source.window_index)
91 .expect("source window must exist"),
92 &target_pane,
93 source.pane_index,
94 )?;
95 validate_swap_destination(
96 self.window_at(target.window_index)
97 .expect("target window must exist"),
98 &source_pane,
99 target.pane_index,
100 )?;
101
102 let mut source_window = self
103 .windows
104 .remove(&source.window_index)
105 .expect("source window must exist for swap");
106 let mut target_window = self
107 .windows
108 .remove(&target.window_index)
109 .expect("target window must exist for swap");
110 apply_swap_between_windows(
111 &mut source_window,
112 SwapPaneEntry {
113 index: source.pane_index,
114 pane: source_pane,
115 },
116 &mut target_window,
117 SwapPaneEntry {
118 index: target.pane_index,
119 pane: target_pane,
120 },
121 options,
122 )?;
123 self.windows.insert(source.window_index, source_window);
124 self.windows.insert(target.window_index, target_window);
125
126 Ok(())
127 }
128
129 pub fn join_pane(
131 &mut self,
132 source: SessionPaneTarget,
133 target: SessionPaneTarget,
134 options: PaneJoinOptions,
135 ) -> Result<(), RmuxError> {
136 if source.window_index == target.window_index {
137 return self.join_pane_within_window(source, target, options);
138 }
139
140 let source_pane = self
141 .window_at(source.window_index)
142 .ok_or_else(|| invalid_window_target(&self.name, source.window_index))?
143 .pane(source.pane_index)
144 .cloned()
145 .ok_or_else(|| {
146 invalid_pane_target(
147 &self.name,
148 source.window_index,
149 source.pane_index,
150 "pane index does not exist in session",
151 )
152 })?;
153 let target_position = self
154 .window_at(target.window_index)
155 .ok_or_else(|| invalid_window_target(&self.name, target.window_index))?
156 .pane_position(target.pane_index)
157 .ok_or_else(|| {
158 invalid_pane_target(
159 &self.name,
160 target.window_index,
161 target.pane_index,
162 "pane index does not exist in session",
163 )
164 })?;
165 let requested_size = join_requested_size(
166 self.window_at(target.window_index)
167 .expect("target window must exist"),
168 target.pane_index,
169 options.direction,
170 options.full_size,
171 options.size,
172 )?;
173 let transient_index = self
174 .window_at(target.window_index)
175 .expect("target window must exist")
176 .panes()
177 .iter()
178 .map(|pane| pane.index())
179 .max()
180 .unwrap_or(0)
181 .saturating_add(1);
182 let mut source_pane_for_validation = source_pane.clone();
183 source_pane_for_validation.set_index(transient_index);
184
185 self.window_at(target.window_index)
186 .expect("target window must exist")
187 .ensure_accepts_pane(&source_pane_for_validation, None)?;
188 let target_active_before_id = self
189 .window_at(target.window_index)
190 .and_then(Window::active_pane)
191 .map(Pane::id)
192 .expect("validated target window must have an active pane");
193 let target_last_before_id = self.window_at(target.window_index).and_then(|window| {
194 window
195 .last_pane_index()
196 .and_then(|pane_index| window.pane(pane_index).map(Pane::id))
197 });
198
199 let mut source_window = self
200 .windows
201 .remove(&source.window_index)
202 .expect("source window must exist for join");
203 let mut target_window = self
204 .windows
205 .remove(&target.window_index)
206 .expect("target window must exist for join");
207 source_window.auto_unzoom();
208 target_window.auto_unzoom();
209 let mut moved_pane = source_window
210 .extract_pane(source.pane_index)
211 .expect("validated source pane must extract");
212 let moved_pane_id = moved_pane.id();
213 moved_pane.set_index(transient_index);
214 if options.full_size {
215 target_window.insert_pane_full_size(moved_pane, options.direction, options.before)?;
216 } else {
217 let insert_position = if options.before {
218 target_position
219 } else {
220 target_position + 1
221 };
222 target_window.insert_pane_at_position(
223 insert_position,
224 moved_pane,
225 options.direction,
226 )?;
227 }
228 let (active_after_id, last_after_id) = if options.detached {
229 (target_active_before_id, target_last_before_id)
230 } else {
231 (
232 moved_pane_id,
233 (target_active_before_id != moved_pane_id).then_some(target_active_before_id),
234 )
235 };
236 target_window.renumber_panes_by_position(active_after_id, last_after_id);
237 let moved_pane_index = target_window
238 .panes()
239 .iter()
240 .find(|pane| pane.id() == moved_pane_id)
241 .map(|pane| pane.index())
242 .expect("moved pane must survive cross-window join");
243 if let Some(requested_size) = requested_size {
244 let _ =
245 target_window.resize_pane_to(moved_pane_index, options.direction, requested_size);
246 }
247
248 let source_was_empty = source_window.pane_count() == 0;
249 self.windows.insert(target.window_index, target_window);
250 if source_was_empty {
251 if self.windows.is_empty() {
252 self.windows.insert(source.window_index, source_window);
253 return Err(RmuxError::Server(format!(
254 "cannot kill the only window in session {}",
255 self.name
256 )));
257 }
258 if self.active_window == source.window_index {
259 self.active_window = self.next_active_window_after_removal(source.window_index);
260 }
261 if self.last_window == Some(source.window_index) {
262 self.last_window = None;
263 }
264 } else {
265 self.windows.insert(source.window_index, source_window);
266 }
267
268 if !options.detached {
269 self.select_window(target.window_index)?;
270 }
271
272 Ok(())
273 }
274
275 fn swap_panes_within_window(
276 &mut self,
277 window_index: u32,
278 source_pane_index: u32,
279 target_pane_index: u32,
280 options: PaneSwapOptions,
281 ) -> Result<(), RmuxError> {
282 if source_pane_index == target_pane_index {
283 let window = self
284 .window_at(window_index)
285 .ok_or_else(|| invalid_window_target(&self.name, window_index))?;
286 if window.pane(source_pane_index).is_none() {
287 return Err(invalid_pane_target(
288 &self.name,
289 window_index,
290 source_pane_index,
291 "pane index does not exist in session",
292 ));
293 }
294 return Ok(());
295 }
296
297 let window = self
298 .window_at(window_index)
299 .ok_or_else(|| invalid_window_target(&self.name, window_index))?;
300 if window.pane(source_pane_index).is_none() {
301 return Err(invalid_pane_target(
302 &self.name,
303 window_index,
304 source_pane_index,
305 "pane index does not exist in session",
306 ));
307 }
308 if window.pane(target_pane_index).is_none() {
309 return Err(invalid_pane_target(
310 &self.name,
311 window_index,
312 target_pane_index,
313 "pane index does not exist in session",
314 ));
315 }
316
317 let window = self
318 .window_at_mut(window_index)
319 .expect("window must exist for in-window swap");
320 window.push_zoom(options.preserve_zoom);
321 let target_pane_id = window
322 .pane(target_pane_index)
323 .expect("validated target pane must exist before swap")
324 .id();
325 let swapped = window.swap_panes(source_pane_index, target_pane_index);
326 debug_assert!(swapped, "validated in-window swap must succeed");
327
328 if !options.detached {
329 window.select_pane_by_id(target_pane_id);
330 }
331 window.pop_zoom();
332
333 Ok(())
334 }
335
336 fn join_pane_within_window(
337 &mut self,
338 source: SessionPaneTarget,
339 target: SessionPaneTarget,
340 options: PaneJoinOptions,
341 ) -> Result<(), RmuxError> {
342 if source.pane_index == target.pane_index {
343 return Err(RmuxError::Server(
344 "source and target panes must be different".to_owned(),
345 ));
346 }
347
348 let window = self
349 .window_at(source.window_index)
350 .ok_or_else(|| invalid_window_target(&self.name, source.window_index))?;
351 let source_position = window.pane_position(source.pane_index).ok_or_else(|| {
352 invalid_pane_target(
353 &self.name,
354 source.window_index,
355 source.pane_index,
356 "pane index does not exist in session",
357 )
358 })?;
359 let target_position = window.pane_position(target.pane_index).ok_or_else(|| {
360 invalid_pane_target(
361 &self.name,
362 target.window_index,
363 target.pane_index,
364 "pane index does not exist in session",
365 )
366 })?;
367 let source_pane = window
368 .pane(source.pane_index)
369 .cloned()
370 .expect("validated source pane must exist");
371 let source_pane_id = source_pane.id();
372 let target_pane_id = window
373 .pane(target.pane_index)
374 .expect("validated target pane must exist")
375 .id();
376 window.ensure_accepts_pane(&source_pane, Some(source_position))?;
377 let requested_size = join_requested_size(
378 window,
379 target.pane_index,
380 options.direction,
381 options.full_size,
382 options.size,
383 )?;
384 let active_before_id = window
385 .active_pane()
386 .expect("validated window must have an active pane")
387 .id();
388 let last_before_id = window
389 .last_pane_index()
390 .and_then(|pane_index| window.pane(pane_index).map(|pane| pane.id()));
391 let detached_active_after_removal_id = if active_before_id == source_pane_id {
392 last_before_id
393 .filter(|pane_id| *pane_id != source_pane_id)
394 .or_else(|| {
395 if source_position > 0 {
396 window.panes().get(source_position - 1).map(Pane::id)
397 } else {
398 window.panes().get(source_position + 1).map(Pane::id)
399 }
400 })
401 } else {
402 Some(active_before_id)
403 };
404 let detached_last_after_removal_id = if active_before_id == source_pane_id {
405 detached_active_after_removal_id
406 .is_some_and(|pane_id| pane_id != target_pane_id)
407 .then_some(target_pane_id)
408 } else {
409 last_before_id.filter(|pane_id| *pane_id != source_pane_id)
410 };
411
412 let insert_position = if options.full_size {
413 usize::from(!options.before)
414 } else if options.before {
415 adjusted_insert_position_before(source_position, target_position)
416 } else {
417 adjusted_insert_position(source_position, target_position)
418 };
419 let window = self
420 .window_at_mut(source.window_index)
421 .expect("window must exist for in-window join");
422 let moved_pane_id = if options.full_size {
423 window.auto_unzoom();
424 let mut moved_pane = window
425 .extract_pane(source.pane_index)
426 .expect("validated source pane must extract");
427 let moved_pane_id = moved_pane.id();
428 let transient_index = window
429 .panes()
430 .iter()
431 .map(|pane| pane.index())
432 .max()
433 .unwrap_or(0)
434 .saturating_add(1);
435 moved_pane.set_index(transient_index);
436 window.insert_pane_full_size(moved_pane, options.direction, options.before)?;
437 moved_pane_id
438 } else {
439 window.move_pane_by_splitting_target(
440 source_position,
441 target_position,
442 insert_position,
443 options.direction,
444 options.before,
445 )?
446 };
447 let (active_after_id, last_after_id) = if options.detached {
448 (
449 detached_active_after_removal_id
450 .expect("detached pane move should leave an active pane"),
451 detached_last_after_removal_id,
452 )
453 } else {
454 (
455 moved_pane_id,
456 (active_before_id != moved_pane_id).then_some(active_before_id),
457 )
458 };
459 window.renumber_panes_by_position(active_after_id, last_after_id);
460 let moved_pane_index = window
461 .panes()
462 .iter()
463 .find(|pane| pane.id() == moved_pane_id)
464 .map(|pane| pane.index())
465 .expect("moved pane must survive in-window join");
466 if let Some(requested_size) = requested_size {
467 let _ = window.resize_pane_to(moved_pane_index, options.direction, requested_size);
468 }
469
470 Ok(())
471 }
472}
473
474fn join_requested_size(
475 target_window: &Window,
476 target_pane_index: u32,
477 direction: SplitDirection,
478 full_size: bool,
479 size: Option<PaneSplitSize>,
480) -> Result<Option<u32>, RmuxError> {
481 let Some(size) = size else {
482 return Ok(None);
483 };
484 let base = if full_size {
485 join_axis_for_size(target_window.size(), direction)
486 } else {
487 let pane = target_window.pane(target_pane_index).ok_or_else(|| {
488 RmuxError::Server(format!(
489 "cannot size missing target pane index {target_pane_index}"
490 ))
491 })?;
492 join_axis_for_size(
493 rmux_proto::TerminalSize {
494 cols: pane.geometry().cols(),
495 rows: pane.geometry().rows(),
496 },
497 direction,
498 )
499 };
500
501 Ok(Some(match size {
502 PaneSplitSize::Absolute(value) => value.max(1),
503 PaneSplitSize::Percentage(value) => {
504 let scaled = (base.saturating_mul(u32::from(value))) / 100;
505 scaled.max(1)
506 }
507 }))
508}
509
510fn join_axis_for_size(size: rmux_proto::TerminalSize, direction: SplitDirection) -> u32 {
511 match direction {
512 SplitDirection::Vertical => u32::from(size.cols),
513 SplitDirection::Horizontal => u32::from(size.rows),
514 }
515}