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(
107 &mut self,
108 window_index: u32,
109 ) -> Result<BTreeMap<u32, u32>, RmuxError> {
110 let first_gap = self.lowest_available_window_index_at_or_above(window_index)?;
111 if first_gap == window_index {
112 return Ok(BTreeMap::new());
113 }
114
115 let mut index_map = BTreeMap::new();
116 for source_index in (window_index..first_gap).rev() {
117 let destination_index = source_index.checked_add(1).ok_or_else(|| {
118 RmuxError::Server(format!(
119 "window index space exhausted for session {}",
120 self.name
121 ))
122 })?;
123 let _ = self.move_window(source_index, destination_index, false, false)?;
124 index_map.insert(source_index, destination_index);
125 }
126
127 Ok(index_map)
128 }
129
130 pub fn link_window(
132 &mut self,
133 window_index: u32,
134 window: Window,
135 kill_destination: bool,
136 select_destination: bool,
137 ) -> Result<Option<Window>, RmuxError> {
138 if self.windows.contains_key(&window_index) && !kill_destination {
139 return Err(invalid_window_target_with_reason(
140 &self.name,
141 window_index,
142 "window index already exists in session",
143 ));
144 }
145
146 let removed = if kill_destination {
147 let removed = self.replace_window(window_index, window)?;
148 let _ = self.clear_all_winlink_alert_flags(window_index);
149 removed
150 } else {
151 self.insert_existing_window(window_index, window)?;
152 return if select_destination {
153 self.select_window(window_index)?;
154 Ok(None)
155 } else {
156 Ok(None)
157 };
158 };
159
160 if select_destination {
161 self.select_window(window_index)?;
162 }
163
164 Ok(Some(removed))
165 }
166
167 pub fn replace_window(
169 &mut self,
170 window_index: u32,
171 window: Window,
172 ) -> Result<Window, RmuxError> {
173 if !self.windows.contains_key(&window_index) {
174 return Err(invalid_window_target(&self.name, window_index));
175 }
176
177 self.bump_allocators_for_window(&window);
178 Ok(self
179 .windows
180 .insert(window_index, window)
181 .expect("replaced window must exist at the addressed index"))
182 }
183
184 pub fn remove_window(&mut self, window_index: u32) -> Result<Window, RmuxError> {
186 if !self.windows.contains_key(&window_index) {
187 return Err(invalid_window_target(&self.name, window_index));
188 }
189
190 if self.windows.len() == 1 {
191 return Err(RmuxError::Server(format!(
192 "cannot kill the only window in session {}",
193 self.name
194 )));
195 }
196
197 self.remove_window_allowing_empty(window_index)
198 }
199
200 pub fn remove_window_allowing_empty(&mut self, window_index: u32) -> Result<Window, RmuxError> {
204 if !self.windows.contains_key(&window_index) {
205 return Err(invalid_window_target(&self.name, window_index));
206 }
207
208 let next_active = if self.active_window == window_index {
209 (self.windows.len() > 1).then(|| self.next_active_window_after_removal(window_index))
210 } else {
211 None
212 };
213
214 let removed = self
215 .windows
216 .remove(&window_index)
217 .expect("window existence was checked before removal");
218 self.winlink_alert_flags.remove(&window_index);
219
220 if let Some(next_active) = next_active {
221 self.select_window(next_active)
222 .expect("replacement window must exist after removal");
223 }
224
225 if self.last_window == Some(window_index) {
226 self.last_window = None;
227 }
228
229 Ok(removed)
230 }
231
232 pub fn rename_window(&mut self, window_index: u32, name: String) -> Result<(), RmuxError> {
234 self.resolve_window_target_mut(window_index)?.set_name(name);
235 Ok(())
236 }
237
238 pub fn set_automatic_window_name(
240 &mut self,
241 window_index: u32,
242 name: String,
243 ) -> Result<(), RmuxError> {
244 let window = self.resolve_window_target_mut(window_index)?;
245 if window.automatic_rename() {
246 window.set_automatic_name(name);
247 }
248 Ok(())
249 }
250
251 pub fn reindex_windows(&mut self) -> Result<BTreeMap<u32, u32>, RmuxError> {
253 self.reindex_windows_from(0)
254 }
255
256 pub fn reindex_windows_from(
258 &mut self,
259 first_index: u32,
260 ) -> Result<BTreeMap<u32, u32>, RmuxError> {
261 if !self.windows.is_empty() {
262 let last_offset = self.windows.len().saturating_sub(1) as u32;
263 first_index.checked_add(last_offset).ok_or_else(|| {
264 RmuxError::Server(format!(
265 "window index space exhausted for session {}",
266 self.name
267 ))
268 })?;
269 }
270 let previous_windows = std::mem::take(&mut self.windows);
271 let mut reindexed = BTreeMap::new();
272 let mut index_map = BTreeMap::new();
273 let mut next_index = first_index;
274
275 let window_count = previous_windows.len();
276 for (position, (window_index, window)) in previous_windows.into_iter().enumerate() {
277 index_map.insert(window_index, next_index);
278 reindexed.insert(next_index, window);
279 if position + 1 < window_count {
280 next_index = next_index.checked_add(1).ok_or_else(|| {
281 RmuxError::Server(format!(
282 "window index space exhausted for session {}",
283 self.name
284 ))
285 })?;
286 }
287 }
288
289 self.windows = reindexed;
290 self.winlink_alert_flags = self
291 .winlink_alert_flags
292 .iter()
293 .filter_map(|(window_index, flags)| {
294 index_map
295 .get(window_index)
296 .copied()
297 .map(|next_index| (next_index, *flags))
298 })
299 .collect();
300 self.active_window = *index_map
301 .get(&self.active_window)
302 .expect("active window must survive reindexing");
303 self.last_window = self
304 .last_window
305 .and_then(|window_index| index_map.get(&window_index).copied());
306
307 Ok(index_map)
308 }
309
310 pub fn move_window(
316 &mut self,
317 source_index: u32,
318 destination_index: u32,
319 kill_destination: bool,
320 select_destination: bool,
321 ) -> Result<Option<Window>, RmuxError> {
322 if !self.windows.contains_key(&source_index) {
323 return Err(invalid_window_target(&self.name, source_index));
324 }
325 if source_index == destination_index {
326 return Ok(None);
327 }
328 if self.windows.contains_key(&destination_index) && !kill_destination {
329 return Err(invalid_window_target_with_reason(
330 &self.name,
331 destination_index,
332 "window index already exists in session",
333 ));
334 }
335
336 let previous_active = self.active_window;
337 let previous_last = self.last_window;
338 let moved_window = self.extract_window_for_move(source_index)?;
339 let moved_alert_flags = self
340 .winlink_alert_flags
341 .remove(&source_index)
342 .unwrap_or_else(crate::AlertFlags::empty);
343 let removed_window = if kill_destination {
344 let _ = self.winlink_alert_flags.remove(&destination_index);
345 self.windows.remove(&destination_index)
346 } else {
347 None
348 };
349
350 self.windows.insert(destination_index, moved_window);
351 self.winlink_alert_flags
352 .insert(destination_index, moved_alert_flags);
353 self.apply_move_tracking(
354 source_index,
355 destination_index,
356 previous_active,
357 previous_last,
358 select_destination,
359 );
360
361 Ok(removed_window)
362 }
363
364 pub fn swap_windows(
366 &mut self,
367 source_index: u32,
368 destination_index: u32,
369 ) -> Result<(), RmuxError> {
370 if !self.windows.contains_key(&source_index) {
371 return Err(invalid_window_target(&self.name, source_index));
372 }
373 if !self.windows.contains_key(&destination_index) {
374 return Err(invalid_window_target(&self.name, destination_index));
375 }
376 if source_index == destination_index {
377 return Ok(());
378 }
379
380 let source_window = self
381 .windows
382 .remove(&source_index)
383 .expect("source window must exist for swap");
384 let destination_window = self
385 .windows
386 .remove(&destination_index)
387 .expect("destination window must exist for swap");
388 let source_flags = self
389 .winlink_alert_flags
390 .remove(&source_index)
391 .unwrap_or_else(crate::AlertFlags::empty);
392 let destination_flags = self
393 .winlink_alert_flags
394 .remove(&destination_index)
395 .unwrap_or_else(crate::AlertFlags::empty);
396
397 self.windows.insert(source_index, destination_window);
398 self.windows.insert(destination_index, source_window);
399 self.winlink_alert_flags
400 .insert(source_index, destination_flags);
401 self.winlink_alert_flags
402 .insert(destination_index, source_flags);
403 Ok(())
404 }
405
406 pub fn rotate_window(
408 &mut self,
409 window_index: u32,
410 direction: RotateWindowDirection,
411 ) -> Result<(), RmuxError> {
412 self.resolve_window_target_mut(window_index)?
413 .rotate_panes(direction);
414 Ok(())
415 }
416
417 pub fn rotate_window_with_zoom(
419 &mut self,
420 window_index: u32,
421 direction: RotateWindowDirection,
422 restore_zoom: bool,
423 ) -> Result<(), RmuxError> {
424 self.resolve_window_target_mut(window_index)?
425 .rotate_panes_with_zoom(direction, restore_zoom);
426 Ok(())
427 }
428
429 pub fn resize_window(
431 &mut self,
432 window_index: u32,
433 size: TerminalSize,
434 ) -> Result<(), RmuxError> {
435 self.resolve_window_target_mut(window_index)?.set_size(size);
436 Ok(())
437 }
438
439 pub fn respawn_window(&mut self, window_index: u32) -> Result<PaneId, RmuxError> {
442 let pane_id = self
443 .window_at(window_index)
444 .ok_or_else(|| invalid_window_target(&self.name, window_index))?
445 .panes()
446 .first()
447 .map(Pane::id)
448 .ok_or_else(|| RmuxError::Server("window has no panes".to_owned()))?;
449 self.respawn_window_with_pane_id(window_index, pane_id)
450 }
451
452 pub fn respawn_window_with_pane_id(
454 &mut self,
455 window_index: u32,
456 pane_id: PaneId,
457 ) -> Result<PaneId, RmuxError> {
458 let window = self.resolve_window_target_mut(window_index)?;
459 window.respawn(pane_id);
460 Ok(pane_id)
461 }
462
463 fn extract_window_for_move(&mut self, window_index: u32) -> Result<Window, RmuxError> {
464 self.windows
465 .remove(&window_index)
466 .ok_or_else(|| invalid_window_target(&self.name, window_index))
467 }
468
469 fn apply_move_tracking(
470 &mut self,
471 source_index: u32,
472 destination_index: u32,
473 previous_active: u32,
474 previous_last: Option<u32>,
475 select_destination: bool,
476 ) {
477 let source_was_active = previous_active == source_index;
478 let destination_was_active = previous_active == destination_index;
479
480 self.active_window = if source_was_active {
481 if select_destination {
482 destination_index
483 } else {
484 self.next_active_window_after_detach(source_index, previous_last)
485 }
486 } else if select_destination {
487 destination_index
488 } else {
489 previous_active
490 };
491
492 self.last_window = if source_was_active {
493 if select_destination {
494 self.preserved_last_after_selecting_destination(
495 source_index,
496 destination_index,
497 previous_last,
498 )
499 } else {
500 None
501 }
502 } else if select_destination {
503 if destination_was_active {
504 self.preserved_last_after_selecting_destination(
505 source_index,
506 destination_index,
507 previous_last,
508 )
509 } else {
510 Some(previous_active)
511 }
512 } else if previous_last == Some(source_index) {
513 None
514 } else {
515 previous_last.filter(|window_index| self.windows.contains_key(window_index))
516 };
517
518 if self.last_window == Some(self.active_window) {
519 self.last_window = None;
520 }
521 }
522
523 fn next_active_window_after_detach(
524 &self,
525 removed_index: u32,
526 previous_last: Option<u32>,
527 ) -> u32 {
528 if let Some(last_window) = previous_last {
529 if last_window != removed_index && self.windows.contains_key(&last_window) {
530 return last_window;
531 }
532 }
533
534 if let Some((window_index, _)) = self.windows.range(..removed_index).next_back() {
535 return *window_index;
536 }
537
538 self.windows
539 .range((Excluded(removed_index), Unbounded))
540 .next()
541 .map(|(window_index, _)| *window_index)
542 .expect("a non-empty session must have a replacement window")
543 }
544
545 fn preserved_last_after_selecting_destination(
546 &self,
547 source_index: u32,
548 destination_index: u32,
549 previous_last: Option<u32>,
550 ) -> Option<u32> {
551 previous_last.filter(|window_index| {
552 *window_index != source_index
553 && *window_index != destination_index
554 && self.windows.contains_key(window_index)
555 })
556 }
557
558 fn bump_allocators_for_window(&mut self, window: &Window) {
559 self.next_window_id
560 .bump_to(window.id().as_u32().saturating_add(1));
561 self.next_pane_id = self.next_pane_id.max(
562 window
563 .panes()
564 .iter()
565 .map(|pane| pane.id().as_u32().saturating_add(1))
566 .max()
567 .unwrap_or(self.next_pane_id),
568 );
569 }
570}