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