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