Skip to main content

rmux_core/session/
window_ops.rs

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    /// Creates a new window at the lowest available index and returns its window index together with the initial pane ID.
15    pub fn create_window(&mut self, size: TerminalSize) -> Result<(u32, PaneId), RmuxError> {
16        self.create_window_at_or_above(size, 0)
17    }
18
19    /// Creates a new window at the lowest available index at or above `minimum_index`.
20    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    /// Creates a new window at the lowest available index at or above `minimum_index`
30    /// using the provided initial pane identity.
31    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    /// Inserts a window with its initial pane at the provided window index.
49    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    /// Inserts a window with its initial pane at the provided window index using the
59    /// supplied pane identity.
60    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    /// Inserts an existing window at the provided window index without rewriting its identities.
85    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    /// Opens a destination slot for insertion and returns every shifted old-to-new winlink index.
106    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    /// Inserts a linked copy of an existing window at the destination slot.
131    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    /// Replaces an existing window at the provided index and returns the removed window.
168    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    /// Removes the addressed window and returns it, using tmux's last-then-previous-then-next active fallback when needed.
185    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    /// Removes a window even when it is the final window in the session.
201    ///
202    /// Callers must destroy the now-empty session in the same transaction.
203    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    /// Renames the addressed window and disables automatic renaming for it.
233    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    /// Updates the addressed window name while preserving automatic renaming.
239    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    /// Reindexes sparse window slots into a contiguous `0..n` range without rewriting the windows themselves.
252    pub fn reindex_windows(&mut self) -> Result<BTreeMap<u32, u32>, RmuxError> {
253        self.reindex_windows_from(0)
254    }
255
256    /// Reindexes sparse window slots into a contiguous range starting at `first_index`.
257    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    /// Moves one window to another slot within the same session, optionally removing an occupied destination.
311    ///
312    /// When `select_destination` is `true`, the destination slot becomes active
313    /// using tmux's winlink-based selection semantics instead of following the
314    /// moved window by identity.
315    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    /// Swaps two window slots within the same session while preserving the underlying windows.
365    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    /// Rotates pane positions in the addressed window without renumbering the panes themselves.
407    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    /// Rotates pane positions with zoom save/restore via `-Z`.
418    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    /// Sets the explicit size of the addressed window.
430    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    /// Respawns the addressed window, replacing it with a single pane.
440    /// Returns the pane ID retained for the respawned pane.
441    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    /// Respawns the addressed window with an explicitly provided pane identity.
453    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}