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