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