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 link-window style insertion by shifting existing winlinks upward.
106    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    /// Inserts a linked copy of an existing window at the destination slot.
126    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    /// Replaces an existing window at the provided index and returns the removed window.
161    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    /// Removes the addressed window and returns it, using tmux's last-then-previous-then-next active fallback when needed.
178    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    /// Renames the addressed window and disables automatic renaming for it.
215    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    /// Updates the addressed window name while preserving automatic renaming.
221    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    /// Reindexes sparse window slots into a contiguous `0..n` range without rewriting the windows themselves.
234    pub fn reindex_windows(&mut self) -> Result<BTreeMap<u32, u32>, RmuxError> {
235        self.reindex_windows_from(0)
236    }
237
238    /// Reindexes sparse window slots into a contiguous range starting at `first_index`.
239    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    /// Moves one window to another slot within the same session, optionally removing an occupied destination.
293    ///
294    /// When `select_destination` is `true`, the destination slot becomes active
295    /// using tmux's winlink-based selection semantics instead of following the
296    /// moved window by identity.
297    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    /// Swaps two window slots within the same session while preserving the underlying windows.
347    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    /// Rotates pane positions in the addressed window without renumbering the panes themselves.
389    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    /// Rotates pane positions with zoom save/restore via `-Z`.
400    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    /// Sets the explicit size of the addressed window.
412    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    /// Respawns the addressed window, replacing it with a single pane.
422    /// Returns the pane ID retained for the respawned pane.
423    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    /// Respawns the addressed window with an explicitly provided pane identity.
435    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}