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