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 if options.full_size {
245 let _ = target_window.resize_pane_to(
246 moved_pane_index,
247 options.direction,
248 requested_size,
249 );
250 } else {
251 let _ = target_window.resize_new_split_pane_to(
252 moved_pane_index,
253 options.direction,
254 requested_size,
255 options.before,
256 );
257 }
258 }
259
260 let source_was_empty = source_window.pane_count() == 0;
261 self.windows.insert(target.window_index, target_window);
262 if source_was_empty {
263 if self.windows.is_empty() {
264 self.windows.insert(source.window_index, source_window);
265 return Err(RmuxError::Server(format!(
266 "cannot kill the only window in session {}",
267 self.name
268 )));
269 }
270 if self.active_window == source.window_index {
271 self.active_window = self.next_active_window_after_removal(source.window_index);
272 }
273 if self.last_window == Some(source.window_index) {
274 self.last_window = None;
275 }
276 } else {
277 self.windows.insert(source.window_index, source_window);
278 }
279
280 if !options.detached {
281 self.select_window(target.window_index)?;
282 }
283
284 Ok(())
285 }
286
287 fn swap_panes_within_window(
288 &mut self,
289 window_index: u32,
290 source_pane_index: u32,
291 target_pane_index: u32,
292 options: PaneSwapOptions,
293 ) -> Result<(), RmuxError> {
294 if source_pane_index == target_pane_index {
295 let window = self
296 .window_at(window_index)
297 .ok_or_else(|| invalid_window_target(&self.name, window_index))?;
298 if window.pane(source_pane_index).is_none() {
299 return Err(invalid_pane_target(
300 &self.name,
301 window_index,
302 source_pane_index,
303 "pane index does not exist in session",
304 ));
305 }
306 return Ok(());
307 }
308
309 let window = self
310 .window_at(window_index)
311 .ok_or_else(|| invalid_window_target(&self.name, window_index))?;
312 if window.pane(source_pane_index).is_none() {
313 return Err(invalid_pane_target(
314 &self.name,
315 window_index,
316 source_pane_index,
317 "pane index does not exist in session",
318 ));
319 }
320 if window.pane(target_pane_index).is_none() {
321 return Err(invalid_pane_target(
322 &self.name,
323 window_index,
324 target_pane_index,
325 "pane index does not exist in session",
326 ));
327 }
328
329 let window = self
330 .window_at_mut(window_index)
331 .expect("window must exist for in-window swap");
332 window.push_zoom(options.preserve_zoom);
333 let active_before_id = window
334 .active_pane()
335 .expect("validated window must have an active pane before swap")
336 .id();
337 let source_pane_id = window
338 .pane(source_pane_index)
339 .expect("validated source pane must exist before swap")
340 .id();
341 let target_pane_id = window
342 .pane(target_pane_index)
343 .expect("validated target pane must exist before swap")
344 .id();
345 let swapped = window.swap_panes(source_pane_index, target_pane_index);
346 debug_assert!(swapped, "validated in-window swap must succeed");
347
348 if options.detached {
349 let active_after_id =
350 if active_before_id == source_pane_id || active_before_id == target_pane_id {
351 source_pane_id
352 } else {
353 active_before_id
354 };
355 window.select_pane_by_id(active_after_id);
356 } else {
357 window.select_pane_by_id(target_pane_id);
358 }
359 window.pop_zoom();
360
361 Ok(())
362 }
363
364 fn join_pane_within_window(
365 &mut self,
366 source: SessionPaneTarget,
367 target: SessionPaneTarget,
368 options: PaneJoinOptions,
369 ) -> Result<(), RmuxError> {
370 if source.pane_index == target.pane_index {
371 return Err(RmuxError::Server(
372 "source and target panes must be different".to_owned(),
373 ));
374 }
375
376 let window = self
377 .window_at(source.window_index)
378 .ok_or_else(|| invalid_window_target(&self.name, source.window_index))?;
379 let source_position = window.pane_position(source.pane_index).ok_or_else(|| {
380 invalid_pane_target(
381 &self.name,
382 source.window_index,
383 source.pane_index,
384 "pane index does not exist in session",
385 )
386 })?;
387 let target_position = window.pane_position(target.pane_index).ok_or_else(|| {
388 invalid_pane_target(
389 &self.name,
390 target.window_index,
391 target.pane_index,
392 "pane index does not exist in session",
393 )
394 })?;
395 let source_pane = window
396 .pane(source.pane_index)
397 .cloned()
398 .expect("validated source pane must exist");
399 let source_geometry = source_pane.geometry();
400 let source_pane_id = source_pane.id();
401 let target_pane = window
402 .pane(target.pane_index)
403 .expect("validated target pane must exist");
404 let target_geometry = target_pane.geometry();
405 let target_pane_id = target_pane.id();
406 window.ensure_accepts_pane(&source_pane, Some(source_position))?;
407 let source_axis_size = join_axis_for_size(
408 rmux_proto::TerminalSize {
409 cols: source_geometry.cols(),
410 rows: source_geometry.rows(),
411 },
412 options.direction,
413 );
414 let requested_size = join_requested_size(
415 window,
416 target.pane_index,
417 options.direction,
418 options.full_size,
419 options.size,
420 )?;
421 let active_before_id = window
422 .active_pane()
423 .expect("validated window must have an active pane")
424 .id();
425 let last_before_id = window
426 .last_pane_index()
427 .and_then(|pane_index| window.pane(pane_index).map(|pane| pane.id()));
428 let detached_active_after_removal_id = if active_before_id == source_pane_id {
429 last_before_id
430 .filter(|pane_id| *pane_id != source_pane_id)
431 .or_else(|| {
432 if source_position > 0 {
433 window.panes().get(source_position - 1).map(Pane::id)
434 } else {
435 window.panes().get(source_position + 1).map(Pane::id)
436 }
437 })
438 } else {
439 Some(active_before_id)
440 };
441 let detached_last_after_removal_id = if active_before_id == source_pane_id {
442 detached_active_after_removal_id
443 .is_some_and(|pane_id| pane_id != target_pane_id)
444 .then_some(target_pane_id)
445 } else {
446 last_before_id.filter(|pane_id| *pane_id != source_pane_id)
447 };
448
449 let insert_position = if options.full_size {
450 usize::from(!options.before)
451 } else if options.before {
452 adjusted_insert_position_before(source_position, target_position)
453 } else {
454 adjusted_insert_position(source_position, target_position)
455 };
456 let window = self
457 .window_at_mut(source.window_index)
458 .expect("window must exist for in-window join");
459 let moved_pane_id = if options.full_size {
460 window.auto_unzoom();
461 let mut moved_pane = window
462 .extract_pane(source.pane_index)
463 .expect("validated source pane must extract");
464 let moved_pane_id = moved_pane.id();
465 let transient_index = window
466 .panes()
467 .iter()
468 .map(|pane| pane.index())
469 .max()
470 .unwrap_or(0)
471 .saturating_add(1);
472 moved_pane.set_index(transient_index);
473 window.insert_pane_full_size(moved_pane, options.direction, options.before)?;
474 moved_pane_id
475 } else {
476 window.move_pane_by_splitting_target(
477 source_position,
478 target_position,
479 insert_position,
480 options.direction,
481 options.before,
482 )?
483 };
484 let (active_after_id, last_after_id) = if options.detached {
485 (
486 detached_active_after_removal_id
487 .expect("detached pane move should leave an active pane"),
488 detached_last_after_removal_id,
489 )
490 } else {
491 (
492 moved_pane_id,
493 (active_before_id != moved_pane_id).then_some(active_before_id),
494 )
495 };
496 window.renumber_panes_by_position(active_after_id, last_after_id);
497 let moved_pane_index = window
498 .panes()
499 .iter()
500 .find(|pane| pane.id() == moved_pane_id)
501 .map(|pane| pane.index())
502 .expect("moved pane must survive in-window join");
503 if let Some(requested_size) = requested_size {
504 let size_existing_target_side = !options.full_size
505 && !options.before
506 && same_axis_source_after_target(
507 source_geometry,
508 target_geometry,
509 options.direction,
510 );
511 let (sized_pane_index, size_to_apply) = if size_existing_target_side {
512 window
513 .panes()
514 .iter()
515 .find(|pane| pane.id() == target_pane_id)
516 .map(|pane| pane.index())
517 .map(|pane_index| {
518 (
519 pane_index,
520 source_axis_size.saturating_sub(requested_size).max(1),
521 )
522 })
523 .expect("target pane must survive in-window join")
524 } else {
525 (moved_pane_index, requested_size)
526 };
527 let _ = window.resize_pane_to(sized_pane_index, options.direction, size_to_apply);
528 }
529
530 Ok(())
531 }
532}
533
534fn join_requested_size(
535 target_window: &Window,
536 target_pane_index: u32,
537 direction: SplitDirection,
538 full_size: bool,
539 size: Option<PaneSplitSize>,
540) -> Result<Option<u32>, RmuxError> {
541 let Some(size) = size else {
542 return Ok(None);
543 };
544 let base = if full_size {
545 join_axis_for_size(target_window.size(), direction)
546 } else {
547 let pane = target_window.pane(target_pane_index).ok_or_else(|| {
548 RmuxError::Server(format!(
549 "cannot size missing target pane index {target_pane_index}"
550 ))
551 })?;
552 join_axis_for_size(
553 rmux_proto::TerminalSize {
554 cols: pane.geometry().cols(),
555 rows: pane.geometry().rows(),
556 },
557 direction,
558 )
559 };
560
561 Ok(Some(match size {
562 PaneSplitSize::Absolute(value) => value.max(1),
563 PaneSplitSize::Percentage(value) => {
564 let scaled = (base.saturating_mul(u32::from(value))) / 100;
565 scaled.max(1)
566 }
567 }))
568}
569
570fn join_axis_for_size(size: rmux_proto::TerminalSize, direction: SplitDirection) -> u32 {
571 match direction {
572 SplitDirection::Vertical => u32::from(size.cols),
573 SplitDirection::Horizontal => u32::from(size.rows),
574 }
575}
576
577fn same_axis_source_after_target(
578 source: crate::PaneGeometry,
579 target: crate::PaneGeometry,
580 direction: SplitDirection,
581) -> bool {
582 match direction {
583 SplitDirection::Vertical => {
584 source.y() == target.y() && source.rows() == target.rows() && source.x() > target.x()
585 }
586 SplitDirection::Horizontal => {
587 source.x() == target.x() && source.cols() == target.cols() && source.y() > target.y()
588 }
589 }
590}