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::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        self.window_at(target.window_index)
116            .expect("target window must exist")
117            .ensure_accepts_pane(&source_pane, None)?;
118        let requested_size = join_requested_size(
119            self.window_at(target.window_index)
120                .expect("target window must exist"),
121            target.pane_index,
122            options.direction,
123            options.full_size,
124            options.size,
125        )?;
126
127        if source_window.pane_count() == 1 && source_session.windows.len() == 1 {
128            return Err(RmuxError::Server(format!(
129                "cannot kill the only window in session {}",
130                source_session.name
131            )));
132        }
133
134        let target_window = self
135            .window_at_mut(target.window_index)
136            .expect("target window must exist");
137        target_window.auto_unzoom();
138        let source_window = source_session
139            .window_at_mut(source.window_index)
140            .expect("source window must exist");
141        source_window.auto_unzoom();
142        let moved_pane = source_window
143            .extract_pane(source.pane_index)
144            .expect("validated source pane must extract");
145        if options.full_size {
146            target_window.insert_pane_full_size(moved_pane, options.direction, options.before)?;
147        } else {
148            let insert_position = if options.before {
149                target_position
150            } else {
151                target_position + 1
152            };
153            target_window.insert_pane_at_position(
154                insert_position,
155                moved_pane,
156                options.direction,
157            )?;
158        }
159        if let Some(requested_size) = requested_size {
160            let _ = target_window.resize_pane_to(
161                source_pane.index(),
162                options.direction,
163                requested_size,
164            );
165        }
166        if !options.detached {
167            target_window.select_pane(source_pane.index());
168            self.select_window(target.window_index)?;
169        }
170
171        if source_window.pane_count() == 0 {
172            source_session.remove_window(source.window_index)?;
173        }
174
175        Ok(())
176    }
177
178    /// Breaks one pane out into another session as its own window.
179    pub fn break_pane_to_session(
180        &mut self,
181        source: SessionPaneTarget,
182        destination_session: &mut Session,
183        options: BreakPaneOptions,
184    ) -> Result<u32, RmuxError> {
185        let source_window = self
186            .window_at(source.window_index)
187            .ok_or_else(|| invalid_window_target(&self.name, source.window_index))?;
188        if source_window.pane(source.pane_index).is_none() {
189            return Err(invalid_pane_target(
190                &self.name,
191                source.window_index,
192                source.pane_index,
193                "pane index does not exist in session",
194            ));
195        }
196        let destination_index = prepare_break_destination(
197            destination_session,
198            options.target_window_index,
199            options.after,
200            options.before,
201        )?;
202
203        if source_window.pane_count() == 1 {
204            if self.windows.len() == 1 {
205                return Err(RmuxError::Server(format!(
206                    "cannot kill the only window in session {}",
207                    self.name
208                )));
209            }
210
211            let mut moved_window = self.remove_window(source.window_index)?;
212            moved_window.renumber_single_pane_to_zero();
213            destination_session.insert_existing_window(destination_index, moved_window)?;
214            if let Some(name) = options.name {
215                destination_session.rename_window(destination_index, name)?;
216            }
217            if !options.detached {
218                destination_session.select_window(destination_index)?;
219            }
220            return Ok(destination_index);
221        }
222
223        let source_size = source_window.size();
224        let source_window = self
225            .window_at_mut(source.window_index)
226            .expect("source window must exist");
227        source_window.auto_unzoom();
228        let moved_pane = source_window
229            .extract_pane(source.pane_index)
230            .expect("validated source pane must extract");
231        let mut new_window = Window::new_with_initial_pane(
232            source_size,
233            moved_pane.id(),
234            destination_session.allocate_window_id(),
235        );
236        if let Some(name) = options.name {
237            new_window.set_name(name);
238        }
239        destination_session.insert_existing_window(destination_index, new_window)?;
240        if !options.detached {
241            destination_session.select_window(destination_index)?;
242        }
243
244        Ok(destination_index)
245    }
246}
247
248fn prepare_break_destination(
249    destination_session: &mut Session,
250    target_window_index: Option<u32>,
251    after: bool,
252    before: bool,
253) -> Result<u32, RmuxError> {
254    if !(after || before) {
255        return resolve_break_destination_index(destination_session, target_window_index, None);
256    }
257
258    let anchor_index = target_window_index.unwrap_or(destination_session.active_window_index());
259    if target_window_index.is_some() && destination_session.window_at(anchor_index).is_none() {
260        return Err(invalid_window_target(
261            &destination_session.name,
262            anchor_index,
263        ));
264    }
265    let destination_index = if before {
266        anchor_index
267    } else {
268        anchor_index
269            .checked_add(1)
270            .ok_or_else(|| RmuxError::Server("window index space exhausted".to_owned()))?
271    };
272    shift_windows_up_from(destination_session, destination_index)?;
273    Ok(destination_index)
274}
275
276fn shift_windows_up_from(session: &mut Session, start_index: u32) -> Result<(), RmuxError> {
277    if session
278        .windows
279        .keys()
280        .next_back()
281        .is_some_and(|window_index| *window_index == u32::MAX)
282    {
283        return Err(RmuxError::Server("window index space exhausted".to_owned()));
284    }
285
286    let shifted_windows = session
287        .windows
288        .range(start_index..)
289        .map(|(window_index, _)| *window_index)
290        .collect::<Vec<_>>();
291    for window_index in shifted_windows.into_iter().rev() {
292        let new_index = window_index
293            .checked_add(1)
294            .ok_or_else(|| RmuxError::Server("window index space exhausted".to_owned()))?;
295        let window = session
296            .windows
297            .remove(&window_index)
298            .expect("shifted window must exist");
299        let flags = session
300            .winlink_alert_flags
301            .remove(&window_index)
302            .unwrap_or_else(crate::AlertFlags::empty);
303        let replaced_window = session.windows.insert(new_index, window);
304        debug_assert!(replaced_window.is_none());
305        let replaced_flags = session.winlink_alert_flags.insert(new_index, flags);
306        debug_assert!(replaced_flags.is_none());
307    }
308    if session.active_window >= start_index {
309        session.active_window = session.active_window.saturating_add(1);
310    }
311    if let Some(last_window) = session.last_window.filter(|index| *index >= start_index) {
312        session.last_window = Some(last_window.saturating_add(1));
313    }
314    Ok(())
315}
316
317fn join_requested_size(
318    target_window: &Window,
319    target_pane_index: u32,
320    direction: SplitDirection,
321    full_size: bool,
322    size: Option<PaneSplitSize>,
323) -> Result<Option<u32>, RmuxError> {
324    let Some(size) = size else {
325        return Ok(None);
326    };
327    let base = if full_size {
328        join_axis_for_size(target_window.size(), direction)
329    } else {
330        let pane = target_window.pane(target_pane_index).ok_or_else(|| {
331            RmuxError::Server(format!(
332                "cannot size missing target pane index {target_pane_index}"
333            ))
334        })?;
335        join_axis_for_size(
336            rmux_proto::TerminalSize {
337                cols: pane.geometry().cols(),
338                rows: pane.geometry().rows(),
339            },
340            direction,
341        )
342    };
343
344    Ok(Some(match size {
345        PaneSplitSize::Absolute(value) => value.max(1),
346        PaneSplitSize::Percentage(value) => {
347            let scaled = (base.saturating_mul(u32::from(value))) / 100;
348            scaled.max(1)
349        }
350    }))
351}
352
353fn join_axis_for_size(size: rmux_proto::TerminalSize, direction: SplitDirection) -> u32 {
354    match direction {
355        SplitDirection::Vertical => u32::from(size.cols),
356        SplitDirection::Horizontal => u32::from(size.rows),
357    }
358}