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