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 self.remove_window_allowing_empty(window_index)
191 }
192
193 pub(crate) fn remove_window_allowing_empty(
194 &mut self,
195 window_index: u32,
196 ) -> Result<Window, RmuxError> {
197 if !self.windows.contains_key(&window_index) {
198 return Err(invalid_window_target(&self.name, window_index));
199 }
200
201 let next_active = if self.active_window == window_index {
202 (self.windows.len() > 1).then(|| self.next_active_window_after_removal(window_index))
203 } else {
204 None
205 };
206
207 let removed = self
208 .windows
209 .remove(&window_index)
210 .expect("window existence was checked before removal");
211 self.winlink_alert_flags.remove(&window_index);
212
213 if let Some(next_active) = next_active {
214 self.select_window(next_active)
215 .expect("replacement window must exist after removal");
216 }
217
218 if self.last_window == Some(window_index) {
219 self.last_window = None;
220 }
221
222 Ok(removed)
223 }
224
225 pub fn rename_window(&mut self, window_index: u32, name: String) -> Result<(), RmuxError> {
227 self.resolve_window_target_mut(window_index)?.set_name(name);
228 Ok(())
229 }
230
231 pub fn set_automatic_window_name(
233 &mut self,
234 window_index: u32,
235 name: String,
236 ) -> Result<(), RmuxError> {
237 let window = self.resolve_window_target_mut(window_index)?;
238 if window.automatic_rename() {
239 window.set_automatic_name(name);
240 }
241 Ok(())
242 }
243
244 pub fn reindex_windows(&mut self) -> Result<BTreeMap<u32, u32>, RmuxError> {
246 self.reindex_windows_from(0)
247 }
248
249 pub fn reindex_windows_from(
251 &mut self,
252 first_index: u32,
253 ) -> Result<BTreeMap<u32, u32>, RmuxError> {
254 if !self.windows.is_empty() {
255 let last_offset = self.windows.len().saturating_sub(1) as u32;
256 first_index.checked_add(last_offset).ok_or_else(|| {
257 RmuxError::Server(format!(
258 "window index space exhausted for session {}",
259 self.name
260 ))
261 })?;
262 }
263 let previous_windows = std::mem::take(&mut self.windows);
264 let mut reindexed = BTreeMap::new();
265 let mut index_map = BTreeMap::new();
266 let mut next_index = first_index;
267
268 let window_count = previous_windows.len();
269 for (position, (window_index, window)) in previous_windows.into_iter().enumerate() {
270 index_map.insert(window_index, next_index);
271 reindexed.insert(next_index, window);
272 if position + 1 < window_count {
273 next_index = next_index.checked_add(1).ok_or_else(|| {
274 RmuxError::Server(format!(
275 "window index space exhausted for session {}",
276 self.name
277 ))
278 })?;
279 }
280 }
281
282 self.windows = reindexed;
283 self.winlink_alert_flags = self
284 .winlink_alert_flags
285 .iter()
286 .filter_map(|(window_index, flags)| {
287 index_map
288 .get(window_index)
289 .copied()
290 .map(|next_index| (next_index, *flags))
291 })
292 .collect();
293 self.active_window = *index_map
294 .get(&self.active_window)
295 .expect("active window must survive reindexing");
296 self.last_window = self
297 .last_window
298 .and_then(|window_index| index_map.get(&window_index).copied());
299
300 Ok(index_map)
301 }
302
303 pub fn move_window(
309 &mut self,
310 source_index: u32,
311 destination_index: u32,
312 kill_destination: bool,
313 select_destination: bool,
314 ) -> Result<Option<Window>, RmuxError> {
315 if !self.windows.contains_key(&source_index) {
316 return Err(invalid_window_target(&self.name, source_index));
317 }
318 if source_index == destination_index {
319 return Ok(None);
320 }
321 if self.windows.contains_key(&destination_index) && !kill_destination {
322 return Err(invalid_window_target_with_reason(
323 &self.name,
324 destination_index,
325 "window index already exists in session",
326 ));
327 }
328
329 let previous_active = self.active_window;
330 let previous_last = self.last_window;
331 let moved_window = self.extract_window_for_move(source_index)?;
332 let moved_alert_flags = self
333 .winlink_alert_flags
334 .remove(&source_index)
335 .unwrap_or_else(crate::AlertFlags::empty);
336 let removed_window = if kill_destination {
337 let _ = self.winlink_alert_flags.remove(&destination_index);
338 self.windows.remove(&destination_index)
339 } else {
340 None
341 };
342
343 self.windows.insert(destination_index, moved_window);
344 self.winlink_alert_flags
345 .insert(destination_index, moved_alert_flags);
346 self.apply_move_tracking(
347 source_index,
348 destination_index,
349 previous_active,
350 previous_last,
351 select_destination,
352 );
353
354 Ok(removed_window)
355 }
356
357 pub fn swap_windows(
359 &mut self,
360 source_index: u32,
361 destination_index: u32,
362 ) -> Result<(), RmuxError> {
363 if !self.windows.contains_key(&source_index) {
364 return Err(invalid_window_target(&self.name, source_index));
365 }
366 if !self.windows.contains_key(&destination_index) {
367 return Err(invalid_window_target(&self.name, destination_index));
368 }
369 if source_index == destination_index {
370 return Ok(());
371 }
372
373 let source_window = self
374 .windows
375 .remove(&source_index)
376 .expect("source window must exist for swap");
377 let destination_window = self
378 .windows
379 .remove(&destination_index)
380 .expect("destination window must exist for swap");
381 let source_flags = self
382 .winlink_alert_flags
383 .remove(&source_index)
384 .unwrap_or_else(crate::AlertFlags::empty);
385 let destination_flags = self
386 .winlink_alert_flags
387 .remove(&destination_index)
388 .unwrap_or_else(crate::AlertFlags::empty);
389
390 self.windows.insert(source_index, destination_window);
391 self.windows.insert(destination_index, source_window);
392 self.winlink_alert_flags
393 .insert(source_index, destination_flags);
394 self.winlink_alert_flags
395 .insert(destination_index, source_flags);
396 Ok(())
397 }
398
399 pub fn rotate_window(
401 &mut self,
402 window_index: u32,
403 direction: RotateWindowDirection,
404 ) -> Result<(), RmuxError> {
405 self.resolve_window_target_mut(window_index)?
406 .rotate_panes(direction);
407 Ok(())
408 }
409
410 pub fn rotate_window_with_zoom(
412 &mut self,
413 window_index: u32,
414 direction: RotateWindowDirection,
415 restore_zoom: bool,
416 ) -> Result<(), RmuxError> {
417 self.resolve_window_target_mut(window_index)?
418 .rotate_panes_with_zoom(direction, restore_zoom);
419 Ok(())
420 }
421
422 pub fn resize_window(
424 &mut self,
425 window_index: u32,
426 size: TerminalSize,
427 ) -> Result<(), RmuxError> {
428 self.resolve_window_target_mut(window_index)?.set_size(size);
429 Ok(())
430 }
431
432 pub fn respawn_window(&mut self, window_index: u32) -> Result<PaneId, RmuxError> {
435 let pane_id = self
436 .window_at(window_index)
437 .ok_or_else(|| invalid_window_target(&self.name, window_index))?
438 .panes()
439 .first()
440 .map(Pane::id)
441 .ok_or_else(|| RmuxError::Server("window has no panes".to_owned()))?;
442 self.respawn_window_with_pane_id(window_index, pane_id)
443 }
444
445 pub fn respawn_window_with_pane_id(
447 &mut self,
448 window_index: u32,
449 pane_id: PaneId,
450 ) -> Result<PaneId, RmuxError> {
451 let window = self.resolve_window_target_mut(window_index)?;
452 window.respawn(pane_id);
453 Ok(pane_id)
454 }
455
456 fn extract_window_for_move(&mut self, window_index: u32) -> Result<Window, RmuxError> {
457 self.windows
458 .remove(&window_index)
459 .ok_or_else(|| invalid_window_target(&self.name, window_index))
460 }
461
462 fn apply_move_tracking(
463 &mut self,
464 source_index: u32,
465 destination_index: u32,
466 previous_active: u32,
467 previous_last: Option<u32>,
468 select_destination: bool,
469 ) {
470 let source_was_active = previous_active == source_index;
471 let destination_was_active = previous_active == destination_index;
472
473 self.active_window = if source_was_active {
474 if select_destination {
475 destination_index
476 } else {
477 self.next_active_window_after_detach(source_index, previous_last)
478 }
479 } else if select_destination {
480 destination_index
481 } else {
482 previous_active
483 };
484
485 self.last_window = if source_was_active {
486 if select_destination {
487 self.preserved_last_after_selecting_destination(
488 source_index,
489 destination_index,
490 previous_last,
491 )
492 } else {
493 None
494 }
495 } else if select_destination {
496 if destination_was_active {
497 self.preserved_last_after_selecting_destination(
498 source_index,
499 destination_index,
500 previous_last,
501 )
502 } else {
503 Some(previous_active)
504 }
505 } else if previous_last == Some(source_index) {
506 None
507 } else {
508 previous_last.filter(|window_index| self.windows.contains_key(window_index))
509 };
510
511 if self.last_window == Some(self.active_window) {
512 self.last_window = None;
513 }
514 }
515
516 fn next_active_window_after_detach(
517 &self,
518 removed_index: u32,
519 previous_last: Option<u32>,
520 ) -> u32 {
521 if let Some(last_window) = previous_last {
522 if last_window != removed_index && self.windows.contains_key(&last_window) {
523 return last_window;
524 }
525 }
526
527 if let Some((window_index, _)) = self.windows.range(..removed_index).next_back() {
528 return *window_index;
529 }
530
531 self.windows
532 .range((Excluded(removed_index), Unbounded))
533 .next()
534 .map(|(window_index, _)| *window_index)
535 .expect("a non-empty session must have a replacement window")
536 }
537
538 fn preserved_last_after_selecting_destination(
539 &self,
540 source_index: u32,
541 destination_index: u32,
542 previous_last: Option<u32>,
543 ) -> Option<u32> {
544 previous_last.filter(|window_index| {
545 *window_index != source_index
546 && *window_index != destination_index
547 && self.windows.contains_key(window_index)
548 })
549 }
550
551 fn bump_allocators_for_window(&mut self, window: &Window) {
552 self.next_window_id
553 .bump_to(window.id().as_u32().saturating_add(1));
554 self.next_pane_id = self.next_pane_id.max(
555 window
556 .panes()
557 .iter()
558 .map(|pane| pane.id().as_u32().saturating_add(1))
559 .max()
560 .unwrap_or(self.next_pane_id),
561 );
562 }
563}