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            if options.full_size {
193                let _ = target_window.resize_pane_to(
194                    moved_pane_index,
195                    options.direction,
196                    requested_size,
197                );
198            } else {
199                let _ = target_window.resize_new_split_pane_to(
200                    moved_pane_index,
201                    options.direction,
202                    requested_size,
203                    options.before,
204                );
205            }
206        }
207        if !options.detached {
208            target_window.select_pane(moved_pane_index);
209            self.select_window(target.window_index)?;
210        }
211
212        if source_window.pane_count() == 0 {
213            source_session.remove_window_allowing_empty(source.window_index)?;
214        }
215
216        Ok(())
217    }
218
219    /// Breaks one pane out into another session as its own window.
220    pub fn break_pane_to_session(
221        &mut self,
222        source: SessionPaneTarget,
223        destination_session: &mut Session,
224        options: BreakPaneOptions,
225    ) -> Result<u32, RmuxError> {
226        let source_window = self
227            .window_at(source.window_index)
228            .ok_or_else(|| invalid_window_target(&self.name, source.window_index))?;
229        if source_window.pane(source.pane_index).is_none() {
230            return Err(invalid_pane_target(
231                &self.name,
232                source.window_index,
233                source.pane_index,
234                "pane index does not exist in session",
235            ));
236        }
237        let destination_index = prepare_break_destination(
238            destination_session,
239            options.target_window_index,
240            options.after,
241            options.before,
242        )?;
243
244        if source_window.pane_count() == 1 {
245            let mut moved_window = self.remove_window_allowing_empty(source.window_index)?;
246            moved_window.renumber_single_pane_to_zero();
247            destination_session.insert_existing_window(destination_index, moved_window)?;
248            if let Some(name) = options.name {
249                destination_session.rename_window(destination_index, name)?;
250            }
251            if !options.detached {
252                destination_session.select_window(destination_index)?;
253            }
254            return Ok(destination_index);
255        }
256
257        let source_size = source_window.size();
258        let source_window = self
259            .window_at_mut(source.window_index)
260            .expect("source window must exist");
261        source_window.auto_unzoom();
262        let moved_pane = source_window
263            .extract_pane(source.pane_index)
264            .expect("validated source pane must extract");
265        let mut new_window = Window::new_with_initial_pane(
266            source_size,
267            moved_pane.id(),
268            destination_session.allocate_window_id(),
269        );
270        if let Some(name) = options.name {
271            new_window.set_name(name);
272        }
273        destination_session.insert_existing_window(destination_index, new_window)?;
274        if !options.detached {
275            destination_session.select_window(destination_index)?;
276        }
277
278        Ok(destination_index)
279    }
280}
281
282fn prepare_break_destination(
283    destination_session: &mut Session,
284    target_window_index: Option<u32>,
285    after: bool,
286    before: bool,
287) -> Result<u32, RmuxError> {
288    if !(after || before) {
289        return resolve_break_destination_index(destination_session, target_window_index, None);
290    }
291
292    let anchor_index = target_window_index.unwrap_or(destination_session.active_window_index());
293    if target_window_index.is_some() && destination_session.window_at(anchor_index).is_none() {
294        return Err(invalid_window_target(
295            &destination_session.name,
296            anchor_index,
297        ));
298    }
299    let destination_index = if before {
300        anchor_index
301    } else {
302        anchor_index
303            .checked_add(1)
304            .ok_or_else(|| RmuxError::Server("window index space exhausted".to_owned()))?
305    };
306    shift_windows_up_from(destination_session, destination_index)?;
307    Ok(destination_index)
308}
309
310fn shift_windows_up_from(session: &mut Session, start_index: u32) -> Result<(), RmuxError> {
311    if session
312        .windows
313        .keys()
314        .next_back()
315        .is_some_and(|window_index| *window_index == u32::MAX)
316    {
317        return Err(RmuxError::Server("window index space exhausted".to_owned()));
318    }
319
320    let shifted_windows = session
321        .windows
322        .range(start_index..)
323        .map(|(window_index, _)| *window_index)
324        .collect::<Vec<_>>();
325    for window_index in shifted_windows.into_iter().rev() {
326        let new_index = window_index
327            .checked_add(1)
328            .ok_or_else(|| RmuxError::Server("window index space exhausted".to_owned()))?;
329        let window = session
330            .windows
331            .remove(&window_index)
332            .expect("shifted window must exist");
333        let flags = session
334            .winlink_alert_flags
335            .remove(&window_index)
336            .unwrap_or_else(crate::AlertFlags::empty);
337        let replaced_window = session.windows.insert(new_index, window);
338        debug_assert!(replaced_window.is_none());
339        let replaced_flags = session.winlink_alert_flags.insert(new_index, flags);
340        debug_assert!(replaced_flags.is_none());
341    }
342    if session.active_window >= start_index {
343        session.active_window = session.active_window.saturating_add(1);
344    }
345    if let Some(last_window) = session.last_window.filter(|index| *index >= start_index) {
346        session.last_window = Some(last_window.saturating_add(1));
347    }
348    Ok(())
349}
350
351fn join_requested_size(
352    target_window: &Window,
353    target_pane_index: u32,
354    direction: SplitDirection,
355    full_size: bool,
356    size: Option<PaneSplitSize>,
357) -> Result<Option<u32>, RmuxError> {
358    let Some(size) = size else {
359        return Ok(None);
360    };
361    let base = if full_size {
362        join_axis_for_size(target_window.size(), direction)
363    } else {
364        let pane = target_window.pane(target_pane_index).ok_or_else(|| {
365            RmuxError::Server(format!(
366                "cannot size missing target pane index {target_pane_index}"
367            ))
368        })?;
369        join_axis_for_size(
370            rmux_proto::TerminalSize {
371                cols: pane.geometry().cols(),
372                rows: pane.geometry().rows(),
373            },
374            direction,
375        )
376    };
377
378    Ok(Some(match size {
379        PaneSplitSize::Absolute(value) => value.max(1),
380        PaneSplitSize::Percentage(value) => {
381            let scaled = (base.saturating_mul(u32::from(value))) / 100;
382            scaled.max(1)
383        }
384    }))
385}
386
387fn join_axis_for_size(size: rmux_proto::TerminalSize, direction: SplitDirection) -> u32 {
388    match direction {
389        SplitDirection::Vertical => u32::from(size.cols),
390        SplitDirection::Horizontal => u32::from(size.rows),
391    }
392}