Skip to main content

rmux_core/session/
window_ops.rs

1use std::collections::BTreeMap;
2
3use rmux_proto::{RmuxError, RotateWindowDirection, TerminalSize};
4
5use super::target_error::{invalid_window_target, invalid_window_target_with_reason};
6use super::{cyclic_previous_window_index, Session};
7use crate::{Pane, PaneId, Window};
8
9#[path = "window_ops/navigation.rs"]
10mod navigation;
11
12impl Session {
13    /// Creates a new window at the lowest available index and returns its window index together with the initial pane ID.
14    pub fn create_window(&mut self, size: TerminalSize) -> Result<(u32, PaneId), RmuxError> {
15        self.create_window_at_or_above(size, 0)
16    }
17
18    /// Creates a new window at the lowest available index at or above `minimum_index`.
19    pub fn create_window_at_or_above(
20        &mut self,
21        size: TerminalSize,
22        minimum_index: u32,
23    ) -> Result<(u32, PaneId), RmuxError> {
24        let pane_id = self.allocate_pane_id();
25        self.create_window_at_or_above_with_pane_id(size, minimum_index, pane_id)
26    }
27
28    /// Creates a new window at the lowest available index at or above `minimum_index`
29    /// using the provided initial pane identity.
30    pub fn create_window_at_or_above_with_pane_id(
31        &mut self,
32        size: TerminalSize,
33        minimum_index: u32,
34        pane_id: PaneId,
35    ) -> Result<(u32, PaneId), RmuxError> {
36        let window_index = self.lowest_available_window_index_at_or_above(minimum_index)?;
37        let window_id = self.allocate_window_id();
38        self.windows.insert(
39            window_index,
40            Window::new_with_initial_pane(size, pane_id, window_id),
41        );
42        self.winlink_alert_flags
43            .insert(window_index, crate::AlertFlags::empty());
44        Ok((window_index, pane_id))
45    }
46
47    /// Inserts a window with its initial pane at the provided window index.
48    pub fn insert_window_with_initial_pane(
49        &mut self,
50        window_index: u32,
51        size: TerminalSize,
52    ) -> Result<(), RmuxError> {
53        let pane_id = self.allocate_pane_id();
54        self.insert_window_with_initial_pane_with_id(window_index, size, pane_id)
55    }
56
57    /// Inserts a window with its initial pane at the provided window index using the
58    /// supplied pane identity.
59    pub fn insert_window_with_initial_pane_with_id(
60        &mut self,
61        window_index: u32,
62        size: TerminalSize,
63        pane_id: PaneId,
64    ) -> Result<(), RmuxError> {
65        if self.windows.contains_key(&window_index) {
66            return Err(invalid_window_target_with_reason(
67                &self.name,
68                window_index,
69                "window index already exists in session",
70            ));
71        }
72
73        let window_id = self.allocate_window_id();
74        self.windows.insert(
75            window_index,
76            Window::new_with_initial_pane(size, pane_id, window_id),
77        );
78        self.winlink_alert_flags
79            .insert(window_index, crate::AlertFlags::empty());
80        Ok(())
81    }
82
83    /// Inserts an existing window at the provided window index without rewriting its identities.
84    pub fn insert_existing_window(
85        &mut self,
86        window_index: u32,
87        window: Window,
88    ) -> Result<(), RmuxError> {
89        if self.windows.contains_key(&window_index) {
90            return Err(invalid_window_target_with_reason(
91                &self.name,
92                window_index,
93                "window index already exists in session",
94            ));
95        }
96
97        self.bump_allocators_for_window(&window);
98        self.windows.insert(window_index, window);
99        self.winlink_alert_flags
100            .insert(window_index, crate::AlertFlags::empty());
101        Ok(())
102    }
103
104    /// Opens a destination slot for insertion and returns every shifted old-to-new winlink index.
105    pub fn make_room_for_window(
106        &mut self,
107        window_index: u32,
108    ) -> Result<BTreeMap<u32, u32>, RmuxError> {
109        let first_gap = self.lowest_available_window_index_at_or_above(window_index)?;
110        if first_gap == window_index {
111            return Ok(BTreeMap::new());
112        }
113
114        let mut index_map = BTreeMap::new();
115        for source_index in (window_index..first_gap).rev() {
116            let destination_index = source_index.checked_add(1).ok_or_else(|| {
117                RmuxError::Server(format!(
118                    "window index space exhausted for session {}",
119                    self.name
120                ))
121            })?;
122            let _ = self.move_window(source_index, destination_index, false, false)?;
123            index_map.insert(source_index, destination_index);
124        }
125
126        Ok(index_map)
127    }
128
129    /// Inserts a linked copy of an existing window at the destination slot.
130    pub fn link_window(
131        &mut self,
132        window_index: u32,
133        window: Window,
134        kill_destination: bool,
135        select_destination: bool,
136    ) -> Result<Option<Window>, RmuxError> {
137        if self.windows.contains_key(&window_index) && !kill_destination {
138            return Err(invalid_window_target_with_reason(
139                &self.name,
140                window_index,
141                "window index already exists in session",
142            ));
143        }
144
145        let removed = if kill_destination {
146            let removed = self.replace_window(window_index, window)?;
147            let _ = self.clear_all_winlink_alert_flags(window_index);
148            removed
149        } else {
150            self.insert_existing_window(window_index, window)?;
151            return if select_destination {
152                self.select_window(window_index)?;
153                Ok(None)
154            } else {
155                Ok(None)
156            };
157        };
158
159        if select_destination {
160            self.select_window(window_index)?;
161        }
162
163        Ok(Some(removed))
164    }
165
166    /// Replaces an existing window at the provided index and returns the removed window.
167    pub fn replace_window(
168        &mut self,
169        window_index: u32,
170        window: Window,
171    ) -> Result<Window, RmuxError> {
172        if !self.windows.contains_key(&window_index) {
173            return Err(invalid_window_target(&self.name, window_index));
174        }
175
176        self.bump_allocators_for_window(&window);
177        Ok(self
178            .windows
179            .insert(window_index, window)
180            .expect("replaced window must exist at the addressed index"))
181    }
182
183    /// Removes the addressed window and returns it, using tmux's last-window then cyclic-previous fallback when needed.
184    pub fn remove_window(&mut self, window_index: u32) -> Result<Window, RmuxError> {
185        if !self.windows.contains_key(&window_index) {
186            return Err(invalid_window_target(&self.name, window_index));
187        }
188
189        if self.windows.len() == 1 {
190            return Err(RmuxError::Server(format!(
191                "cannot kill the only window in session {}",
192                self.name
193            )));
194        }
195
196        self.remove_window_allowing_empty(window_index)
197    }
198
199    /// Removes a window even when it is the final window in the session.
200    ///
201    /// Callers must destroy the now-empty session in the same transaction.
202    pub fn remove_window_allowing_empty(&mut self, window_index: u32) -> Result<Window, RmuxError> {
203        if !self.windows.contains_key(&window_index) {
204            return Err(invalid_window_target(&self.name, window_index));
205        }
206
207        let next_active = if self.active_window == window_index {
208            (self.windows.len() > 1).then(|| self.next_active_window_after_removal(window_index))
209        } else {
210            None
211        };
212
213        let removed = self
214            .windows
215            .remove(&window_index)
216            .expect("window existence was checked before removal");
217        self.winlink_alert_flags.remove(&window_index);
218
219        if let Some(next_active) = next_active {
220            self.select_window(next_active)
221                .expect("replacement window must exist after removal");
222        }
223
224        if self.last_window == Some(window_index) {
225            self.last_window = None;
226        }
227
228        Ok(removed)
229    }
230
231    /// Renames the addressed window and disables automatic renaming for it.
232    pub fn rename_window(&mut self, window_index: u32, name: String) -> Result<(), RmuxError> {
233        self.resolve_window_target_mut(window_index)?.set_name(name);
234        Ok(())
235    }
236
237    /// Updates the addressed window name while preserving automatic renaming.
238    pub fn set_automatic_window_name(
239        &mut self,
240        window_index: u32,
241        name: String,
242    ) -> Result<(), RmuxError> {
243        let window = self.resolve_window_target_mut(window_index)?;
244        if window.automatic_rename() {
245            window.set_automatic_name(name);
246        }
247        Ok(())
248    }
249
250    /// Reindexes sparse window slots into a contiguous `0..n` range without rewriting the windows themselves.
251    pub fn reindex_windows(&mut self) -> Result<BTreeMap<u32, u32>, RmuxError> {
252        self.reindex_windows_from(0)
253    }
254
255    /// Reindexes sparse window slots into a contiguous range starting at `first_index`.
256    pub fn reindex_windows_from(
257        &mut self,
258        first_index: u32,
259    ) -> Result<BTreeMap<u32, u32>, RmuxError> {
260        if !self.windows.is_empty() {
261            let last_offset = self.windows.len().saturating_sub(1) as u32;
262            first_index.checked_add(last_offset).ok_or_else(|| {
263                RmuxError::Server(format!(
264                    "window index space exhausted for session {}",
265                    self.name
266                ))
267            })?;
268        }
269        let previous_windows = std::mem::take(&mut self.windows);
270        let mut reindexed = BTreeMap::new();
271        let mut index_map = BTreeMap::new();
272        let mut next_index = first_index;
273
274        let window_count = previous_windows.len();
275        for (position, (window_index, window)) in previous_windows.into_iter().enumerate() {
276            index_map.insert(window_index, next_index);
277            reindexed.insert(next_index, window);
278            if position + 1 < window_count {
279                next_index = next_index.checked_add(1).ok_or_else(|| {
280                    RmuxError::Server(format!(
281                        "window index space exhausted for session {}",
282                        self.name
283                    ))
284                })?;
285            }
286        }
287
288        self.windows = reindexed;
289        self.winlink_alert_flags = self
290            .winlink_alert_flags
291            .iter()
292            .filter_map(|(window_index, flags)| {
293                index_map
294                    .get(window_index)
295                    .copied()
296                    .map(|next_index| (next_index, *flags))
297            })
298            .collect();
299        self.active_window = *index_map
300            .get(&self.active_window)
301            .expect("active window must survive reindexing");
302        self.last_window = self
303            .last_window
304            .and_then(|window_index| index_map.get(&window_index).copied());
305
306        Ok(index_map)
307    }
308
309    /// Moves one window to another slot within the same session, optionally removing an occupied destination.
310    ///
311    /// When `select_destination` is `true`, the destination slot becomes active
312    /// using tmux's winlink-based selection semantics instead of following the
313    /// moved window by identity.
314    pub fn move_window(
315        &mut self,
316        source_index: u32,
317        destination_index: u32,
318        kill_destination: bool,
319        select_destination: bool,
320    ) -> Result<Option<Window>, RmuxError> {
321        if !self.windows.contains_key(&source_index) {
322            return Err(invalid_window_target(&self.name, source_index));
323        }
324        if source_index == destination_index {
325            return Ok(None);
326        }
327        if self.windows.contains_key(&destination_index) && !kill_destination {
328            return Err(invalid_window_target_with_reason(
329                &self.name,
330                destination_index,
331                "window index already exists in session",
332            ));
333        }
334
335        let previous_active = self.active_window;
336        let previous_last = self.last_window;
337        let moved_window = self.extract_window_for_move(source_index)?;
338        let moved_alert_flags = self
339            .winlink_alert_flags
340            .remove(&source_index)
341            .unwrap_or_else(crate::AlertFlags::empty);
342        let removed_window = if kill_destination {
343            let _ = self.winlink_alert_flags.remove(&destination_index);
344            self.windows.remove(&destination_index)
345        } else {
346            None
347        };
348
349        self.windows.insert(destination_index, moved_window);
350        self.winlink_alert_flags
351            .insert(destination_index, moved_alert_flags);
352        self.apply_move_tracking(
353            source_index,
354            destination_index,
355            previous_active,
356            previous_last,
357            select_destination,
358        );
359
360        Ok(removed_window)
361    }
362
363    /// Swaps two window slots within the same session while preserving the underlying windows.
364    pub fn swap_windows(
365        &mut self,
366        source_index: u32,
367        destination_index: u32,
368    ) -> Result<(), RmuxError> {
369        if !self.windows.contains_key(&source_index) {
370            return Err(invalid_window_target(&self.name, source_index));
371        }
372        if !self.windows.contains_key(&destination_index) {
373            return Err(invalid_window_target(&self.name, destination_index));
374        }
375        if source_index == destination_index {
376            return Ok(());
377        }
378
379        let source_window = self
380            .windows
381            .remove(&source_index)
382            .expect("source window must exist for swap");
383        let destination_window = self
384            .windows
385            .remove(&destination_index)
386            .expect("destination window must exist for swap");
387        let source_flags = self
388            .winlink_alert_flags
389            .remove(&source_index)
390            .unwrap_or_else(crate::AlertFlags::empty);
391        let destination_flags = self
392            .winlink_alert_flags
393            .remove(&destination_index)
394            .unwrap_or_else(crate::AlertFlags::empty);
395
396        self.windows.insert(source_index, destination_window);
397        self.windows.insert(destination_index, source_window);
398        self.winlink_alert_flags
399            .insert(source_index, destination_flags);
400        self.winlink_alert_flags
401            .insert(destination_index, source_flags);
402        Ok(())
403    }
404
405    /// Rotates pane positions in the addressed window without renumbering the panes themselves.
406    pub fn rotate_window(
407        &mut self,
408        window_index: u32,
409        direction: RotateWindowDirection,
410    ) -> Result<(), RmuxError> {
411        self.resolve_window_target_mut(window_index)?
412            .rotate_panes(direction);
413        Ok(())
414    }
415
416    /// Rotates pane positions with zoom save/restore via `-Z`.
417    pub fn rotate_window_with_zoom(
418        &mut self,
419        window_index: u32,
420        direction: RotateWindowDirection,
421        restore_zoom: bool,
422    ) -> Result<(), RmuxError> {
423        self.resolve_window_target_mut(window_index)?
424            .rotate_panes_with_zoom(direction, restore_zoom);
425        Ok(())
426    }
427
428    /// Sets the explicit size of the addressed window.
429    pub fn resize_window(
430        &mut self,
431        window_index: u32,
432        size: TerminalSize,
433    ) -> Result<(), RmuxError> {
434        self.resolve_window_target_mut(window_index)?.set_size(size);
435        Ok(())
436    }
437
438    /// Respawns the addressed window, replacing it with a single pane.
439    /// Returns the pane ID retained for the respawned pane.
440    pub fn respawn_window(&mut self, window_index: u32) -> Result<PaneId, RmuxError> {
441        let pane_id = self
442            .window_at(window_index)
443            .ok_or_else(|| invalid_window_target(&self.name, window_index))?
444            .panes()
445            .first()
446            .map(Pane::id)
447            .ok_or_else(|| RmuxError::Server("window has no panes".to_owned()))?;
448        self.respawn_window_with_pane_id(window_index, pane_id)
449    }
450
451    /// Respawns the addressed window with an explicitly provided pane identity.
452    pub fn respawn_window_with_pane_id(
453        &mut self,
454        window_index: u32,
455        pane_id: PaneId,
456    ) -> Result<PaneId, RmuxError> {
457        let window = self.resolve_window_target_mut(window_index)?;
458        window.respawn(pane_id);
459        Ok(pane_id)
460    }
461
462    fn extract_window_for_move(&mut self, window_index: u32) -> Result<Window, RmuxError> {
463        self.windows
464            .remove(&window_index)
465            .ok_or_else(|| invalid_window_target(&self.name, window_index))
466    }
467
468    fn apply_move_tracking(
469        &mut self,
470        source_index: u32,
471        destination_index: u32,
472        previous_active: u32,
473        previous_last: Option<u32>,
474        select_destination: bool,
475    ) {
476        let source_was_active = previous_active == source_index;
477        let destination_was_active = previous_active == destination_index;
478
479        self.active_window = if source_was_active {
480            if select_destination {
481                destination_index
482            } else {
483                self.next_active_window_after_detach(source_index, previous_last)
484            }
485        } else if select_destination {
486            destination_index
487        } else {
488            previous_active
489        };
490
491        self.last_window = if source_was_active {
492            if select_destination {
493                self.preserved_last_after_selecting_destination(
494                    source_index,
495                    destination_index,
496                    previous_last,
497                )
498            } else {
499                None
500            }
501        } else if select_destination {
502            if destination_was_active {
503                self.preserved_last_after_selecting_destination(
504                    source_index,
505                    destination_index,
506                    previous_last,
507                )
508            } else {
509                Some(previous_active)
510            }
511        } else if previous_last == Some(source_index) {
512            None
513        } else {
514            previous_last.filter(|window_index| self.windows.contains_key(window_index))
515        };
516
517        if self.last_window == Some(self.active_window) {
518            self.last_window = None;
519        }
520    }
521
522    fn next_active_window_after_detach(
523        &self,
524        removed_index: u32,
525        previous_last: Option<u32>,
526    ) -> u32 {
527        if let Some(last_window) = previous_last {
528            if last_window != removed_index && self.windows.contains_key(&last_window) {
529                return last_window;
530            }
531        }
532
533        cyclic_previous_window_index(&self.windows, removed_index)
534            .expect("a non-empty session must have a replacement window")
535    }
536
537    fn preserved_last_after_selecting_destination(
538        &self,
539        source_index: u32,
540        destination_index: u32,
541        previous_last: Option<u32>,
542    ) -> Option<u32> {
543        previous_last.filter(|window_index| {
544            *window_index != source_index
545                && *window_index != destination_index
546                && self.windows.contains_key(window_index)
547        })
548    }
549
550    fn bump_allocators_for_window(&mut self, window: &Window) {
551        self.next_window_id
552            .bump_to(window.id().as_u32().saturating_add(1));
553        self.next_pane_id = self.next_pane_id.max(
554            window
555                .panes()
556                .iter()
557                .map(|pane| pane.id().as_u32().saturating_add(1))
558                .max()
559                .unwrap_or(self.next_pane_id),
560        );
561    }
562}