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