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