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        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    /// Swaps two panes within the same session without renumbering either pane.
30    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    /// Moves one pane next to another pane in the same session.
113    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}