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