1use std::collections::BTreeMap;
2use std::ops::Bound::{Excluded, Unbounded};
3
4use rmux_proto::{RmuxError, RotateWindowDirection, TerminalSize};
5
6use super::target_error::{invalid_window_target, invalid_window_target_with_reason};
7use super::Session;
8use crate::{Pane, PaneId, Window};
9
10#[path = "window_ops/navigation.rs"]
11mod navigation;
12
13impl Session {
14 pub fn create_window(&mut self, size: TerminalSize) -> Result<(u32, PaneId), RmuxError> {
16 self.create_window_at_or_above(size, 0)
17 }
18
19 pub fn create_window_at_or_above(
21 &mut self,
22 size: TerminalSize,
23 minimum_index: u32,
24 ) -> Result<(u32, PaneId), RmuxError> {
25 let pane_id = self.allocate_pane_id();
26 self.create_window_at_or_above_with_pane_id(size, minimum_index, pane_id)
27 }
28
29 pub fn create_window_at_or_above_with_pane_id(
32 &mut self,
33 size: TerminalSize,
34 minimum_index: u32,
35 pane_id: PaneId,
36 ) -> Result<(u32, PaneId), RmuxError> {
37 let window_index = self.lowest_available_window_index_at_or_above(minimum_index)?;
38 let window_id = self.allocate_window_id();
39 self.windows.insert(
40 window_index,
41 Window::new_with_initial_pane(size, pane_id, window_id),
42 );
43 self.winlink_alert_flags
44 .insert(window_index, crate::AlertFlags::empty());
45 Ok((window_index, pane_id))
46 }
47
48 pub fn insert_window_with_initial_pane(
50 &mut self,
51 window_index: u32,
52 size: TerminalSize,
53 ) -> Result<(), RmuxError> {
54 let pane_id = self.allocate_pane_id();
55 self.insert_window_with_initial_pane_with_id(window_index, size, pane_id)
56 }
57
58 pub fn insert_window_with_initial_pane_with_id(
61 &mut self,
62 window_index: u32,
63 size: TerminalSize,
64 pane_id: PaneId,
65 ) -> Result<(), RmuxError> {
66 if self.windows.contains_key(&window_index) {
67 return Err(invalid_window_target_with_reason(
68 &self.name,
69 window_index,
70 "window index already exists in session",
71 ));
72 }
73
74 let window_id = self.allocate_window_id();
75 self.windows.insert(
76 window_index,
77 Window::new_with_initial_pane(size, pane_id, window_id),
78 );
79 self.winlink_alert_flags
80 .insert(window_index, crate::AlertFlags::empty());
81 Ok(())
82 }
83
84 pub fn insert_existing_window(
86 &mut self,
87 window_index: u32,
88 window: Window,
89 ) -> Result<(), RmuxError> {
90 if self.windows.contains_key(&window_index) {
91 return Err(invalid_window_target_with_reason(
92 &self.name,
93 window_index,
94 "window index already exists in session",
95 ));
96 }
97
98 self.bump_allocators_for_window(&window);
99 self.windows.insert(window_index, window);
100 self.winlink_alert_flags
101 .insert(window_index, crate::AlertFlags::empty());
102 Ok(())
103 }
104
105 pub fn make_room_for_window(&mut self, window_index: u32) -> Result<(), RmuxError> {
107 let first_gap = self.lowest_available_window_index_at_or_above(window_index)?;
108 if first_gap == window_index {
109 return Ok(());
110 }
111
112 for source_index in (window_index..first_gap).rev() {
113 let destination_index = source_index.checked_add(1).ok_or_else(|| {
114 RmuxError::Server(format!(
115 "window index space exhausted for session {}",
116 self.name
117 ))
118 })?;
119 let _ = self.move_window(source_index, destination_index, false, false)?;
120 }
121
122 Ok(())
123 }
124
125 pub fn link_window(
127 &mut self,
128 window_index: u32,
129 window: Window,
130 kill_destination: bool,
131 select_destination: bool,
132 ) -> Result<Option<Window>, RmuxError> {
133 if self.windows.contains_key(&window_index) && !kill_destination {
134 return Err(invalid_window_target_with_reason(
135 &self.name,
136 window_index,
137 "window index already exists in session",
138 ));
139 }
140
141 let removed = if kill_destination {
142 self.replace_window(window_index, window)?
143 } else {
144 self.insert_existing_window(window_index, window)?;
145 return if select_destination {
146 self.select_window(window_index)?;
147 Ok(None)
148 } else {
149 Ok(None)
150 };
151 };
152
153 if select_destination {
154 self.select_window(window_index)?;
155 }
156
157 Ok(Some(removed))
158 }
159
160 pub fn replace_window(
162 &mut self,
163 window_index: u32,
164 window: Window,
165 ) -> Result<Window, RmuxError> {
166 if !self.windows.contains_key(&window_index) {
167 return Err(invalid_window_target(&self.name, window_index));
168 }
169
170 self.bump_allocators_for_window(&window);
171 Ok(self
172 .windows
173 .insert(window_index, window)
174 .expect("replaced window must exist at the addressed index"))
175 }
176
177 pub fn remove_window(&mut self, window_index: u32) -> Result<Window, RmuxError> {
179 if !self.windows.contains_key(&window_index) {
180 return Err(invalid_window_target(&self.name, window_index));
181 }
182
183 if self.windows.len() == 1 {
184 return Err(RmuxError::Server(format!(
185 "cannot kill the only window in session {}",
186 self.name
187 )));
188 }
189
190 let next_active = if self.active_window == window_index {
191 Some(self.next_active_window_after_removal(window_index))
192 } else {
193 None
194 };
195
196 let removed = self
197 .windows
198 .remove(&window_index)
199 .expect("window existence was checked before removal");
200 self.winlink_alert_flags.remove(&window_index);
201
202 if let Some(next_active) = next_active {
203 self.select_window(next_active)
204 .expect("replacement window must exist after removal");
205 }
206
207 if self.last_window == Some(window_index) {
208 self.last_window = None;
209 }
210
211 Ok(removed)
212 }
213
214 pub fn rename_window(&mut self, window_index: u32, name: String) -> Result<(), RmuxError> {
216 self.resolve_window_target_mut(window_index)?.set_name(name);
217 Ok(())
218 }
219
220 pub fn set_automatic_window_name(
222 &mut self,
223 window_index: u32,
224 name: String,
225 ) -> Result<(), RmuxError> {
226 let window = self.resolve_window_target_mut(window_index)?;
227 if window.automatic_rename() {
228 window.set_automatic_name(name);
229 }
230 Ok(())
231 }
232
233 pub fn reindex_windows(&mut self) -> Result<BTreeMap<u32, u32>, RmuxError> {
235 let previous_windows = std::mem::take(&mut self.windows);
236 let mut reindexed = BTreeMap::new();
237 let mut index_map = BTreeMap::new();
238 let mut next_index = 0_u32;
239
240 for (window_index, window) in previous_windows {
241 index_map.insert(window_index, next_index);
242 reindexed.insert(next_index, window);
243 next_index = next_index.checked_add(1).ok_or_else(|| {
244 RmuxError::Server(format!(
245 "window index space exhausted for session {}",
246 self.name
247 ))
248 })?;
249 }
250
251 self.windows = reindexed;
252 self.winlink_alert_flags = self
253 .winlink_alert_flags
254 .iter()
255 .filter_map(|(window_index, flags)| {
256 index_map
257 .get(window_index)
258 .copied()
259 .map(|next_index| (next_index, *flags))
260 })
261 .collect();
262 self.active_window = *index_map
263 .get(&self.active_window)
264 .expect("active window must survive reindexing");
265 self.last_window = self
266 .last_window
267 .and_then(|window_index| index_map.get(&window_index).copied());
268
269 Ok(index_map)
270 }
271
272 pub fn move_window(
278 &mut self,
279 source_index: u32,
280 destination_index: u32,
281 kill_destination: bool,
282 select_destination: bool,
283 ) -> Result<Option<Window>, RmuxError> {
284 if !self.windows.contains_key(&source_index) {
285 return Err(invalid_window_target(&self.name, source_index));
286 }
287 if source_index == destination_index {
288 return Ok(None);
289 }
290 if self.windows.contains_key(&destination_index) && !kill_destination {
291 return Err(invalid_window_target_with_reason(
292 &self.name,
293 destination_index,
294 "window index already exists in session",
295 ));
296 }
297
298 let previous_active = self.active_window;
299 let previous_last = self.last_window;
300 let moved_window = self.extract_window_for_move(source_index)?;
301 let moved_alert_flags = self
302 .winlink_alert_flags
303 .remove(&source_index)
304 .unwrap_or_else(crate::AlertFlags::empty);
305 let removed_window = if kill_destination {
306 let _ = self.winlink_alert_flags.remove(&destination_index);
307 self.windows.remove(&destination_index)
308 } else {
309 None
310 };
311
312 self.windows.insert(destination_index, moved_window);
313 self.winlink_alert_flags
314 .insert(destination_index, moved_alert_flags);
315 self.apply_move_tracking(
316 source_index,
317 destination_index,
318 previous_active,
319 previous_last,
320 select_destination,
321 );
322
323 Ok(removed_window)
324 }
325
326 pub fn swap_windows(
328 &mut self,
329 source_index: u32,
330 destination_index: u32,
331 ) -> Result<(), RmuxError> {
332 if !self.windows.contains_key(&source_index) {
333 return Err(invalid_window_target(&self.name, source_index));
334 }
335 if !self.windows.contains_key(&destination_index) {
336 return Err(invalid_window_target(&self.name, destination_index));
337 }
338 if source_index == destination_index {
339 return Ok(());
340 }
341
342 let source_window = self
343 .windows
344 .remove(&source_index)
345 .expect("source window must exist for swap");
346 let destination_window = self
347 .windows
348 .remove(&destination_index)
349 .expect("destination window must exist for swap");
350 let source_flags = self
351 .winlink_alert_flags
352 .remove(&source_index)
353 .unwrap_or_else(crate::AlertFlags::empty);
354 let destination_flags = self
355 .winlink_alert_flags
356 .remove(&destination_index)
357 .unwrap_or_else(crate::AlertFlags::empty);
358
359 self.windows.insert(source_index, destination_window);
360 self.windows.insert(destination_index, source_window);
361 self.winlink_alert_flags
362 .insert(source_index, destination_flags);
363 self.winlink_alert_flags
364 .insert(destination_index, source_flags);
365 Ok(())
366 }
367
368 pub fn rotate_window(
370 &mut self,
371 window_index: u32,
372 direction: RotateWindowDirection,
373 ) -> Result<(), RmuxError> {
374 self.resolve_window_target_mut(window_index)?
375 .rotate_panes(direction);
376 Ok(())
377 }
378
379 pub fn rotate_window_with_zoom(
381 &mut self,
382 window_index: u32,
383 direction: RotateWindowDirection,
384 restore_zoom: bool,
385 ) -> Result<(), RmuxError> {
386 self.resolve_window_target_mut(window_index)?
387 .rotate_panes_with_zoom(direction, restore_zoom);
388 Ok(())
389 }
390
391 pub fn resize_window(
393 &mut self,
394 window_index: u32,
395 size: TerminalSize,
396 ) -> Result<(), RmuxError> {
397 self.resolve_window_target_mut(window_index)?.set_size(size);
398 Ok(())
399 }
400
401 pub fn respawn_window(&mut self, window_index: u32) -> Result<PaneId, RmuxError> {
404 let pane_id = self
405 .window_at(window_index)
406 .ok_or_else(|| invalid_window_target(&self.name, window_index))?
407 .panes()
408 .first()
409 .map(Pane::id)
410 .ok_or_else(|| RmuxError::Server("window has no panes".to_owned()))?;
411 self.respawn_window_with_pane_id(window_index, pane_id)
412 }
413
414 pub fn respawn_window_with_pane_id(
416 &mut self,
417 window_index: u32,
418 pane_id: PaneId,
419 ) -> Result<PaneId, RmuxError> {
420 let window = self.resolve_window_target_mut(window_index)?;
421 window.respawn(pane_id);
422 Ok(pane_id)
423 }
424
425 fn extract_window_for_move(&mut self, window_index: u32) -> Result<Window, RmuxError> {
426 self.windows
427 .remove(&window_index)
428 .ok_or_else(|| invalid_window_target(&self.name, window_index))
429 }
430
431 fn apply_move_tracking(
432 &mut self,
433 source_index: u32,
434 destination_index: u32,
435 previous_active: u32,
436 previous_last: Option<u32>,
437 select_destination: bool,
438 ) {
439 let source_was_active = previous_active == source_index;
440 let destination_was_active = previous_active == destination_index;
441
442 self.active_window = if source_was_active {
443 if select_destination {
444 destination_index
445 } else {
446 self.next_active_window_after_detach(source_index, previous_last)
447 }
448 } else if select_destination {
449 destination_index
450 } else {
451 previous_active
452 };
453
454 self.last_window = if source_was_active {
455 if select_destination {
456 self.preserved_last_after_selecting_destination(
457 source_index,
458 destination_index,
459 previous_last,
460 )
461 } else {
462 None
463 }
464 } else if select_destination {
465 if destination_was_active {
466 self.preserved_last_after_selecting_destination(
467 source_index,
468 destination_index,
469 previous_last,
470 )
471 } else {
472 Some(previous_active)
473 }
474 } else if previous_last == Some(source_index) {
475 None
476 } else {
477 previous_last.filter(|window_index| self.windows.contains_key(window_index))
478 };
479
480 if self.last_window == Some(self.active_window) {
481 self.last_window = None;
482 }
483 }
484
485 fn next_active_window_after_detach(
486 &self,
487 removed_index: u32,
488 previous_last: Option<u32>,
489 ) -> u32 {
490 if let Some(last_window) = previous_last {
491 if last_window != removed_index && self.windows.contains_key(&last_window) {
492 return last_window;
493 }
494 }
495
496 if let Some((window_index, _)) = self.windows.range(..removed_index).next_back() {
497 return *window_index;
498 }
499
500 self.windows
501 .range((Excluded(removed_index), Unbounded))
502 .next()
503 .map(|(window_index, _)| *window_index)
504 .expect("a non-empty session must have a replacement window")
505 }
506
507 fn preserved_last_after_selecting_destination(
508 &self,
509 source_index: u32,
510 destination_index: u32,
511 previous_last: Option<u32>,
512 ) -> Option<u32> {
513 previous_last.filter(|window_index| {
514 *window_index != source_index
515 && *window_index != destination_index
516 && self.windows.contains_key(window_index)
517 })
518 }
519
520 fn bump_allocators_for_window(&mut self, window: &Window) {
521 self.next_window_id
522 .bump_to(window.id().as_u32().saturating_add(1));
523 self.next_pane_id = self.next_pane_id.max(
524 window
525 .panes()
526 .iter()
527 .map(|pane| pane.id().as_u32().saturating_add(1))
528 .max()
529 .unwrap_or(self.next_pane_id),
530 );
531 }
532}