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 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 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 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}