Skip to main content

rmux_core/session/
pane_transfer_cross.rs

1use super::pane_transfer_shared::{
2    apply_swap_between_windows, resolve_break_destination_index, validate_swap_destination,
3    SwapPaneEntry,
4};
5use super::target_error::{invalid_pane_target, invalid_window_target};
6use super::{BreakPaneOptions, PaneJoinOptions, PaneSwapOptions, Session, SessionPaneTarget};
7use crate::{Pane, Window};
8use rmux_proto::{PaneSplitSize, RmuxError, SplitDirection};
9
10impl Session {
11    /// Swaps one pane in this session with one pane in another session.
12    pub fn swap_panes_with_session(
13        &mut self,
14        source: SessionPaneTarget,
15        target_session: &mut Session,
16        target: SessionPaneTarget,
17        options: PaneSwapOptions,
18    ) -> Result<(), RmuxError> {
19        let source_pane = self
20            .window_at(source.window_index)
21            .ok_or_else(|| invalid_window_target(&self.name, source.window_index))?
22            .pane(source.pane_index)
23            .cloned()
24            .ok_or_else(|| {
25                invalid_pane_target(
26                    &self.name,
27                    source.window_index,
28                    source.pane_index,
29                    "pane index does not exist in session",
30                )
31            })?;
32        let target_pane = target_session
33            .window_at(target.window_index)
34            .ok_or_else(|| invalid_window_target(&target_session.name, target.window_index))?
35            .pane(target.pane_index)
36            .cloned()
37            .ok_or_else(|| {
38                invalid_pane_target(
39                    &target_session.name,
40                    target.window_index,
41                    target.pane_index,
42                    "pane index does not exist in session",
43                )
44            })?;
45
46        validate_swap_destination(
47            self.window_at(source.window_index)
48                .expect("source window must exist"),
49            &target_pane,
50            source.pane_index,
51        )?;
52        validate_swap_destination(
53            target_session
54                .window_at(target.window_index)
55                .expect("target window must exist"),
56            &source_pane,
57            target.pane_index,
58        )?;
59
60        let source_window = self
61            .window_at_mut(source.window_index)
62            .expect("source window must exist");
63        let target_window = target_session
64            .window_at_mut(target.window_index)
65            .expect("target window must exist");
66        apply_swap_between_windows(
67            source_window,
68            SwapPaneEntry {
69                index: source.pane_index,
70                pane: source_pane,
71            },
72            target_window,
73            SwapPaneEntry {
74                index: target.pane_index,
75                pane: target_pane,
76            },
77            options,
78        )
79    }
80
81    /// Moves one pane from another session into this session.
82    pub fn join_pane_from_session(
83        &mut self,
84        target: SessionPaneTarget,
85        source_session: &mut Session,
86        source: SessionPaneTarget,
87        options: PaneJoinOptions,
88    ) -> Result<(), RmuxError> {
89        let source_window = source_session
90            .window_at(source.window_index)
91            .ok_or_else(|| invalid_window_target(&source_session.name, source.window_index))?;
92        let source_pane = source_window
93            .pane(source.pane_index)
94            .cloned()
95            .ok_or_else(|| {
96                invalid_pane_target(
97                    &source_session.name,
98                    source.window_index,
99                    source.pane_index,
100                    "pane index does not exist in session",
101                )
102            })?;
103        let target_position = self
104            .window_at(target.window_index)
105            .ok_or_else(|| invalid_window_target(&self.name, target.window_index))?
106            .pane_position(target.pane_index)
107            .ok_or_else(|| {
108                invalid_pane_target(
109                    &self.name,
110                    target.window_index,
111                    target.pane_index,
112                    "pane index does not exist in session",
113                )
114            })?;
115        let requested_size = join_requested_size(
116            self.window_at(target.window_index)
117                .expect("target window must exist"),
118            target.pane_index,
119            options.direction,
120            options.full_size,
121            options.size,
122        )?;
123        let transient_index = self
124            .window_at(target.window_index)
125            .expect("target window must exist")
126            .panes()
127            .iter()
128            .map(Pane::index)
129            .max()
130            .unwrap_or(0)
131            .saturating_add(1);
132        let mut source_pane_for_validation = source_pane.clone();
133        source_pane_for_validation.set_index(transient_index);
134
135        self.window_at(target.window_index)
136            .expect("target window must exist")
137            .ensure_accepts_pane(&source_pane_for_validation, None)?;
138        let target_active_before_id = self
139            .window_at(target.window_index)
140            .and_then(Window::active_pane)
141            .map(Pane::id)
142            .expect("validated target window must have an active pane");
143        let target_last_before_id = self.window_at(target.window_index).and_then(|window| {
144            window
145                .last_pane_index()
146                .and_then(|pane_index| window.pane(pane_index).map(Pane::id))
147        });
148
149        let target_window = self
150            .window_at_mut(target.window_index)
151            .expect("target window must exist");
152        target_window.auto_unzoom();
153        let source_window = source_session
154            .window_at_mut(source.window_index)
155            .expect("source window must exist");
156        source_window.auto_unzoom();
157        let mut moved_pane = source_window
158            .extract_pane(source.pane_index)
159            .expect("validated source pane must extract");
160        let moved_pane_id = moved_pane.id();
161        moved_pane.set_index(transient_index);
162        if options.full_size {
163            target_window.insert_pane_full_size(moved_pane, options.direction, options.before)?;
164        } else {
165            let insert_position = if options.before {
166                target_position
167            } else {
168                target_position + 1
169            };
170            target_window.insert_pane_at_position(
171                insert_position,
172                moved_pane,
173                options.direction,
174            )?;
175        }
176        let (active_after_id, last_after_id) = if options.detached {
177            (target_active_before_id, target_last_before_id)
178        } else {
179            (
180                moved_pane_id,
181                (target_active_before_id != moved_pane_id).then_some(target_active_before_id),
182            )
183        };
184        target_window.renumber_panes_by_position(active_after_id, last_after_id);
185        let moved_pane_index = target_window
186            .panes()
187            .iter()
188            .find(|pane| pane.id() == moved_pane_id)
189            .map(Pane::index)
190            .expect("moved pane must survive cross-session join");
191        if let Some(requested_size) = requested_size {
192            let _ =
193                target_window.resize_pane_to(moved_pane_index, options.direction, requested_size);
194        }
195        if !options.detached {
196            target_window.select_pane(moved_pane_index);
197            self.select_window(target.window_index)?;
198        }
199
200        if source_window.pane_count() == 0 {
201            source_session.remove_window_allowing_empty(source.window_index)?;
202        }
203
204        Ok(())
205    }
206
207    /// Breaks one pane out into another session as its own window.
208    pub fn break_pane_to_session(
209        &mut self,
210        source: SessionPaneTarget,
211        destination_session: &mut Session,
212        options: BreakPaneOptions,
213    ) -> Result<u32, RmuxError> {
214        let source_window = self
215            .window_at(source.window_index)
216            .ok_or_else(|| invalid_window_target(&self.name, source.window_index))?;
217        if source_window.pane(source.pane_index).is_none() {
218            return Err(invalid_pane_target(
219                &self.name,
220                source.window_index,
221                source.pane_index,
222                "pane index does not exist in session",
223            ));
224        }
225        let destination_index = prepare_break_destination(
226            destination_session,
227            options.target_window_index,
228            options.after,
229            options.before,
230        )?;
231
232        if source_window.pane_count() == 1 {
233            let mut moved_window = self.remove_window_allowing_empty(source.window_index)?;
234            moved_window.renumber_single_pane_to_zero();
235            destination_session.insert_existing_window(destination_index, moved_window)?;
236            if let Some(name) = options.name {
237                destination_session.rename_window(destination_index, name)?;
238            }
239            if !options.detached {
240                destination_session.select_window(destination_index)?;
241            }
242            return Ok(destination_index);
243        }
244
245        let source_size = source_window.size();
246        let source_window = self
247            .window_at_mut(source.window_index)
248            .expect("source window must exist");
249        source_window.auto_unzoom();
250        let moved_pane = source_window
251            .extract_pane(source.pane_index)
252            .expect("validated source pane must extract");
253        let mut new_window = Window::new_with_initial_pane(
254            source_size,
255            moved_pane.id(),
256            destination_session.allocate_window_id(),
257        );
258        if let Some(name) = options.name {
259            new_window.set_name(name);
260        }
261        destination_session.insert_existing_window(destination_index, new_window)?;
262        if !options.detached {
263            destination_session.select_window(destination_index)?;
264        }
265
266        Ok(destination_index)
267    }
268}
269
270fn prepare_break_destination(
271    destination_session: &mut Session,
272    target_window_index: Option<u32>,
273    after: bool,
274    before: bool,
275) -> Result<u32, RmuxError> {
276    if !(after || before) {
277        return resolve_break_destination_index(destination_session, target_window_index, None);
278    }
279
280    let anchor_index = target_window_index.unwrap_or(destination_session.active_window_index());
281    if target_window_index.is_some() && destination_session.window_at(anchor_index).is_none() {
282        return Err(invalid_window_target(
283            &destination_session.name,
284            anchor_index,
285        ));
286    }
287    let destination_index = if before {
288        anchor_index
289    } else {
290        anchor_index
291            .checked_add(1)
292            .ok_or_else(|| RmuxError::Server("window index space exhausted".to_owned()))?
293    };
294    shift_windows_up_from(destination_session, destination_index)?;
295    Ok(destination_index)
296}
297
298fn shift_windows_up_from(session: &mut Session, start_index: u32) -> Result<(), RmuxError> {
299    if session
300        .windows
301        .keys()
302        .next_back()
303        .is_some_and(|window_index| *window_index == u32::MAX)
304    {
305        return Err(RmuxError::Server("window index space exhausted".to_owned()));
306    }
307
308    let shifted_windows = session
309        .windows
310        .range(start_index..)
311        .map(|(window_index, _)| *window_index)
312        .collect::<Vec<_>>();
313    for window_index in shifted_windows.into_iter().rev() {
314        let new_index = window_index
315            .checked_add(1)
316            .ok_or_else(|| RmuxError::Server("window index space exhausted".to_owned()))?;
317        let window = session
318            .windows
319            .remove(&window_index)
320            .expect("shifted window must exist");
321        let flags = session
322            .winlink_alert_flags
323            .remove(&window_index)
324            .unwrap_or_else(crate::AlertFlags::empty);
325        let replaced_window = session.windows.insert(new_index, window);
326        debug_assert!(replaced_window.is_none());
327        let replaced_flags = session.winlink_alert_flags.insert(new_index, flags);
328        debug_assert!(replaced_flags.is_none());
329    }
330    if session.active_window >= start_index {
331        session.active_window = session.active_window.saturating_add(1);
332    }
333    if let Some(last_window) = session.last_window.filter(|index| *index >= start_index) {
334        session.last_window = Some(last_window.saturating_add(1));
335    }
336    Ok(())
337}
338
339fn join_requested_size(
340    target_window: &Window,
341    target_pane_index: u32,
342    direction: SplitDirection,
343    full_size: bool,
344    size: Option<PaneSplitSize>,
345) -> Result<Option<u32>, RmuxError> {
346    let Some(size) = size else {
347        return Ok(None);
348    };
349    let base = if full_size {
350        join_axis_for_size(target_window.size(), direction)
351    } else {
352        let pane = target_window.pane(target_pane_index).ok_or_else(|| {
353            RmuxError::Server(format!(
354                "cannot size missing target pane index {target_pane_index}"
355            ))
356        })?;
357        join_axis_for_size(
358            rmux_proto::TerminalSize {
359                cols: pane.geometry().cols(),
360                rows: pane.geometry().rows(),
361            },
362            direction,
363        )
364    };
365
366    Ok(Some(match size {
367        PaneSplitSize::Absolute(value) => value.max(1),
368        PaneSplitSize::Percentage(value) => {
369            let scaled = (base.saturating_mul(u32::from(value))) / 100;
370            scaled.max(1)
371        }
372    }))
373}
374
375fn join_axis_for_size(size: rmux_proto::TerminalSize, direction: SplitDirection) -> u32 {
376    match direction {
377        SplitDirection::Vertical => u32::from(size.cols),
378        SplitDirection::Horizontal => u32::from(size.rows),
379    }
380}