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::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 self.window_at(target.window_index)
116 .expect("target window must exist")
117 .ensure_accepts_pane(&source_pane, None)?;
118 let requested_size = join_requested_size(
119 self.window_at(target.window_index)
120 .expect("target window must exist"),
121 target.pane_index,
122 options.direction,
123 options.full_size,
124 options.size,
125 )?;
126
127 if source_window.pane_count() == 1 && source_session.windows.len() == 1 {
128 return Err(RmuxError::Server(format!(
129 "cannot kill the only window in session {}",
130 source_session.name
131 )));
132 }
133
134 let target_window = self
135 .window_at_mut(target.window_index)
136 .expect("target window must exist");
137 target_window.auto_unzoom();
138 let source_window = source_session
139 .window_at_mut(source.window_index)
140 .expect("source window must exist");
141 source_window.auto_unzoom();
142 let moved_pane = source_window
143 .extract_pane(source.pane_index)
144 .expect("validated source pane must extract");
145 if options.full_size {
146 target_window.insert_pane_full_size(moved_pane, options.direction, options.before)?;
147 } else {
148 let insert_position = if options.before {
149 target_position
150 } else {
151 target_position + 1
152 };
153 target_window.insert_pane_at_position(
154 insert_position,
155 moved_pane,
156 options.direction,
157 )?;
158 }
159 if let Some(requested_size) = requested_size {
160 let _ = target_window.resize_pane_to(
161 source_pane.index(),
162 options.direction,
163 requested_size,
164 );
165 }
166 if !options.detached {
167 target_window.select_pane(source_pane.index());
168 self.select_window(target.window_index)?;
169 }
170
171 if source_window.pane_count() == 0 {
172 source_session.remove_window(source.window_index)?;
173 }
174
175 Ok(())
176 }
177
178 pub fn break_pane_to_session(
180 &mut self,
181 source: SessionPaneTarget,
182 destination_session: &mut Session,
183 options: BreakPaneOptions,
184 ) -> Result<u32, RmuxError> {
185 let source_window = self
186 .window_at(source.window_index)
187 .ok_or_else(|| invalid_window_target(&self.name, source.window_index))?;
188 if source_window.pane(source.pane_index).is_none() {
189 return Err(invalid_pane_target(
190 &self.name,
191 source.window_index,
192 source.pane_index,
193 "pane index does not exist in session",
194 ));
195 }
196 let destination_index = prepare_break_destination(
197 destination_session,
198 options.target_window_index,
199 options.after,
200 options.before,
201 )?;
202
203 if source_window.pane_count() == 1 {
204 if self.windows.len() == 1 {
205 return Err(RmuxError::Server(format!(
206 "cannot kill the only window in session {}",
207 self.name
208 )));
209 }
210
211 let mut moved_window = self.remove_window(source.window_index)?;
212 moved_window.renumber_single_pane_to_zero();
213 destination_session.insert_existing_window(destination_index, moved_window)?;
214 if let Some(name) = options.name {
215 destination_session.rename_window(destination_index, name)?;
216 }
217 if !options.detached {
218 destination_session.select_window(destination_index)?;
219 }
220 return Ok(destination_index);
221 }
222
223 let source_size = source_window.size();
224 let source_window = self
225 .window_at_mut(source.window_index)
226 .expect("source window must exist");
227 source_window.auto_unzoom();
228 let moved_pane = source_window
229 .extract_pane(source.pane_index)
230 .expect("validated source pane must extract");
231 let mut new_window = Window::new_with_initial_pane(
232 source_size,
233 moved_pane.id(),
234 destination_session.allocate_window_id(),
235 );
236 if let Some(name) = options.name {
237 new_window.set_name(name);
238 }
239 destination_session.insert_existing_window(destination_index, new_window)?;
240 if !options.detached {
241 destination_session.select_window(destination_index)?;
242 }
243
244 Ok(destination_index)
245 }
246}
247
248fn prepare_break_destination(
249 destination_session: &mut Session,
250 target_window_index: Option<u32>,
251 after: bool,
252 before: bool,
253) -> Result<u32, RmuxError> {
254 if !(after || before) {
255 return resolve_break_destination_index(destination_session, target_window_index, None);
256 }
257
258 let anchor_index = target_window_index.unwrap_or(destination_session.active_window_index());
259 if target_window_index.is_some() && destination_session.window_at(anchor_index).is_none() {
260 return Err(invalid_window_target(
261 &destination_session.name,
262 anchor_index,
263 ));
264 }
265 let destination_index = if before {
266 anchor_index
267 } else {
268 anchor_index
269 .checked_add(1)
270 .ok_or_else(|| RmuxError::Server("window index space exhausted".to_owned()))?
271 };
272 shift_windows_up_from(destination_session, destination_index)?;
273 Ok(destination_index)
274}
275
276fn shift_windows_up_from(session: &mut Session, start_index: u32) -> Result<(), RmuxError> {
277 if session
278 .windows
279 .keys()
280 .next_back()
281 .is_some_and(|window_index| *window_index == u32::MAX)
282 {
283 return Err(RmuxError::Server("window index space exhausted".to_owned()));
284 }
285
286 let shifted_windows = session
287 .windows
288 .range(start_index..)
289 .map(|(window_index, _)| *window_index)
290 .collect::<Vec<_>>();
291 for window_index in shifted_windows.into_iter().rev() {
292 let new_index = window_index
293 .checked_add(1)
294 .ok_or_else(|| RmuxError::Server("window index space exhausted".to_owned()))?;
295 let window = session
296 .windows
297 .remove(&window_index)
298 .expect("shifted window must exist");
299 let flags = session
300 .winlink_alert_flags
301 .remove(&window_index)
302 .unwrap_or_else(crate::AlertFlags::empty);
303 let replaced_window = session.windows.insert(new_index, window);
304 debug_assert!(replaced_window.is_none());
305 let replaced_flags = session.winlink_alert_flags.insert(new_index, flags);
306 debug_assert!(replaced_flags.is_none());
307 }
308 if session.active_window >= start_index {
309 session.active_window = session.active_window.saturating_add(1);
310 }
311 if let Some(last_window) = session.last_window.filter(|index| *index >= start_index) {
312 session.last_window = Some(last_window.saturating_add(1));
313 }
314 Ok(())
315}
316
317fn join_requested_size(
318 target_window: &Window,
319 target_pane_index: u32,
320 direction: SplitDirection,
321 full_size: bool,
322 size: Option<PaneSplitSize>,
323) -> Result<Option<u32>, RmuxError> {
324 let Some(size) = size else {
325 return Ok(None);
326 };
327 let base = if full_size {
328 join_axis_for_size(target_window.size(), direction)
329 } else {
330 let pane = target_window.pane(target_pane_index).ok_or_else(|| {
331 RmuxError::Server(format!(
332 "cannot size missing target pane index {target_pane_index}"
333 ))
334 })?;
335 join_axis_for_size(
336 rmux_proto::TerminalSize {
337 cols: pane.geometry().cols(),
338 rows: pane.geometry().rows(),
339 },
340 direction,
341 )
342 };
343
344 Ok(Some(match size {
345 PaneSplitSize::Absolute(value) => value.max(1),
346 PaneSplitSize::Percentage(value) => {
347 let scaled = (base.saturating_mul(u32::from(value))) / 100;
348 scaled.max(1)
349 }
350 }))
351}
352
353fn join_axis_for_size(size: rmux_proto::TerminalSize, direction: SplitDirection) -> u32 {
354 match direction {
355 SplitDirection::Vertical => u32::from(size.cols),
356 SplitDirection::Horizontal => u32::from(size.rows),
357 }
358}