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