Skip to main content

rmux_core/session/
pane_transfer.rs

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    /// Selects the previously active pane in the addressed window.
15    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    /// Selects the previously active pane in the addressed window, preserving zoom when requested.
20    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    /// Swaps two panes within the same session without renumbering either pane.
47    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    /// Moves one pane next to another pane in the same session.
130    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}