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