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