Skip to main content

rmux_core/
window.rs

1use rmux_proto::{LayoutName, RmuxError, RotateWindowDirection, SplitDirection, TerminalSize};
2
3use crate::layout::{LayoutDirection, LayoutTree};
4use crate::{Pane, PaneGeometry, PaneId, WindowId};
5
6/// Runtime alert flag bitset shared by window queue state and winlink-visible state.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8pub struct AlertFlags(u8);
9
10impl AlertFlags {
11    /// Returns an empty alert bitset.
12    #[must_use]
13    pub const fn empty() -> Self {
14        Self(0)
15    }
16
17    /// Returns whether all bits in `other` are present.
18    #[must_use]
19    pub const fn contains(self, other: Self) -> bool {
20        (self.0 & other.0) == other.0
21    }
22
23    /// Returns whether any bit in `other` is present.
24    #[must_use]
25    pub const fn intersects(self, other: Self) -> bool {
26        (self.0 & other.0) != 0
27    }
28
29    /// Returns whether no alert bits are set.
30    #[must_use]
31    pub const fn is_empty(self) -> bool {
32        self.0 == 0
33    }
34
35    /// Returns the union of `self` and `other`.
36    #[must_use]
37    pub const fn union(self, other: Self) -> Self {
38        Self(self.0 | other.0)
39    }
40
41    /// Adds the bits from `other`.
42    pub fn insert(&mut self, other: Self) {
43        self.0 |= other.0;
44    }
45
46    /// Clears the bits from `other`.
47    pub fn remove(&mut self, other: Self) {
48        self.0 &= !other.0;
49    }
50}
51
52/// Window alert queue bit for bells.
53pub const WINDOW_BELL: AlertFlags = AlertFlags(0x1);
54/// Window alert queue bit for activity.
55pub const WINDOW_ACTIVITY: AlertFlags = AlertFlags(0x2);
56/// Window alert queue bit for silence.
57pub const WINDOW_SILENCE: AlertFlags = AlertFlags(0x4);
58/// Combined window alert queue bits.
59pub const WINDOW_ALERTFLAGS: AlertFlags =
60    AlertFlags(WINDOW_BELL.0 | WINDOW_ACTIVITY.0 | WINDOW_SILENCE.0);
61
62/// Persistent winlink alert bit for bells.
63pub const WINLINK_BELL: AlertFlags = AlertFlags(0x1);
64/// Persistent winlink alert bit for activity.
65pub const WINLINK_ACTIVITY: AlertFlags = AlertFlags(0x2);
66/// Persistent winlink alert bit for silence.
67pub const WINLINK_SILENCE: AlertFlags = AlertFlags(0x4);
68/// Combined persistent winlink alert bits.
69pub const WINLINK_ALERTFLAGS: AlertFlags =
70    AlertFlags(WINLINK_BELL.0 | WINLINK_ACTIVITY.0 | WINLINK_SILENCE.0);
71
72/// A session-owned window whose pane order is independent from pane indices.
73///
74/// Pane order is preserved separately from pane indices so a split can insert a
75/// new pane immediately after its split target while still assigning the next
76/// sequential pane index. Stable pane IDs remain independent from those
77/// window-local display indices, which may contain gaps after deletion.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct Window {
80    id: WindowId,
81    panes: Vec<Pane>,
82    next_pane_index: u32,
83    active_pane: u32,
84    last_pane: Option<u32>,
85    layout: LayoutName,
86    last_layout: Option<LayoutName>,
87    layout_tree: Option<LayoutTree>,
88    custom_layout: bool,
89    old_layout: Option<String>,
90    size: TerminalSize,
91    name: Option<String>,
92    automatic_rename: bool,
93    zoomed: bool,
94    zoom_restore_pending: bool,
95    alert_flags: AlertFlags,
96    alerts_queued: bool,
97    // `resize-pane -x` sets an explicit main-pane width; otherwise layout derives it.
98    requested_main_width: Option<u16>,
99    // `resize-pane -y` sets an explicit main-pane height; otherwise layout derives it.
100    requested_main_height: Option<u16>,
101}
102
103#[path = "window/layout_cycle.rs"]
104mod layout_cycle;
105#[path = "window/layout_ops.rs"]
106mod layout_ops;
107#[path = "window/panes.rs"]
108mod panes;
109#[path = "window/zoom.rs"]
110mod zoom;
111
112use panes::layout_for_split;
113
114impl Window {
115    /// Creates the single V1 window with its initial pane.
116    #[must_use]
117    pub fn new(size: TerminalSize) -> Self {
118        Self::new_with_initial_pane(size, PaneId::new(0), WindowId::new(0))
119    }
120
121    pub(crate) fn new_with_initial_pane(size: TerminalSize, pane_id: PaneId, id: WindowId) -> Self {
122        let mut window = Self {
123            id,
124            panes: vec![Pane::new_with_id(
125                pane_id,
126                0,
127                PaneGeometry::new(0, 0, size.cols, size.rows),
128            )],
129            next_pane_index: 1,
130            active_pane: 0,
131            last_pane: None,
132            layout: LayoutName::MainVertical,
133            last_layout: None,
134            layout_tree: Some(LayoutTree::single(size)),
135            custom_layout: false,
136            old_layout: None,
137            size,
138            name: None,
139            automatic_rename: true,
140            zoomed: false,
141            zoom_restore_pending: false,
142            alert_flags: AlertFlags::empty(),
143            alerts_queued: false,
144            requested_main_width: None,
145            requested_main_height: None,
146        };
147        window.recalculate_geometry();
148        window
149    }
150
151    /// Returns the stable internal window identity.
152    #[must_use]
153    pub const fn id(&self) -> WindowId {
154        self.id
155    }
156
157    /// Returns the panes in window order.
158    #[must_use]
159    pub fn panes(&self) -> &[Pane] {
160        &self.panes
161    }
162
163    /// Returns the pane with the given stable pane index.
164    #[must_use]
165    pub fn pane(&self, pane_index: u32) -> Option<&Pane> {
166        self.panes.iter().find(|pane| pane.index() == pane_index)
167    }
168
169    /// Returns a mutable pane reference for the given stable pane index.
170    #[must_use]
171    pub fn pane_mut(&mut self, pane_index: u32) -> Option<&mut Pane> {
172        self.panes
173            .iter_mut()
174            .find(|pane| pane.index() == pane_index)
175    }
176
177    /// Returns the active pane index owned by the window.
178    #[must_use]
179    pub const fn active_pane_index(&self) -> u32 {
180        self.active_pane
181    }
182
183    /// Returns the previously active pane index when one exists.
184    #[must_use]
185    pub const fn last_pane_index(&self) -> Option<u32> {
186        self.last_pane
187    }
188
189    /// Returns the active pane when the window invariant is satisfied.
190    #[must_use]
191    pub fn active_pane(&self) -> Option<&Pane> {
192        self.pane(self.active_pane)
193    }
194
195    /// Returns the stable internal pane identity for a display index.
196    #[must_use]
197    pub fn pane_id(&self, pane_index: u32) -> Option<PaneId> {
198        self.pane(pane_index).map(Pane::id)
199    }
200
201    /// Returns the last selected named layout for the window.
202    #[must_use]
203    pub const fn layout(&self) -> LayoutName {
204        self.layout
205    }
206
207    /// Returns the terminal size currently backing the window.
208    #[must_use]
209    pub const fn size(&self) -> TerminalSize {
210        self.size
211    }
212
213    /// Returns the tmux-compatible serialized layout tree for the window.
214    #[must_use]
215    pub fn layout_dump(&self) -> String {
216        self.layout_tree
217            .as_ref()
218            .map_or_else(String::new, |tree| tree.dump(&self.panes))
219    }
220
221    /// Saves the current serialized layout as the tmux-compatible old layout.
222    pub fn save_old_layout(&mut self) {
223        self.old_layout = Some(self.layout_dump());
224    }
225
226    /// Returns the previously saved serialized layout when one exists.
227    #[must_use]
228    pub fn old_layout(&self) -> Option<&str> {
229        self.old_layout.as_deref()
230    }
231
232    /// Returns the explicit user-supplied window name when one exists.
233    #[must_use]
234    pub fn name(&self) -> Option<&str> {
235        self.name.as_deref()
236    }
237
238    /// Returns whether automatic renaming remains enabled for the window.
239    #[must_use]
240    pub const fn automatic_rename(&self) -> bool {
241        self.automatic_rename
242    }
243
244    /// Returns any queued runtime alert flags for the window.
245    #[must_use]
246    pub const fn alert_flags(&self) -> AlertFlags {
247        self.alert_flags
248    }
249
250    /// Returns whether alert processing is already queued for this window.
251    #[must_use]
252    pub const fn alerts_queued(&self) -> bool {
253        self.alerts_queued
254    }
255
256    /// Returns the number of panes in the window.
257    #[must_use]
258    pub fn pane_count(&self) -> usize {
259        self.panes.len()
260    }
261
262    /// Queues alert flags for later server-side processing.
263    pub fn queue_alerts(&mut self, flags: AlertFlags) {
264        self.alert_flags.insert(flags);
265    }
266
267    /// Drains and returns all queued alert flags.
268    pub fn take_alert_flags(&mut self) -> AlertFlags {
269        let flags = self.alert_flags;
270        self.alert_flags = AlertFlags::empty();
271        flags
272    }
273
274    /// Clears the provided queued alert flags.
275    pub fn clear_alert_flags(&mut self, flags: AlertFlags) {
276        self.alert_flags.remove(flags);
277    }
278
279    /// Sets whether alert processing is already queued.
280    pub fn set_alerts_queued(&mut self, queued: bool) {
281        self.alerts_queued = queued;
282    }
283
284    /// Resets this window to a single pane with a fresh pane ID, preserving the window id,
285    /// name, and size. Returns the new pane ID.
286    pub(crate) fn respawn(&mut self, pane_id: PaneId) -> PaneId {
287        let size = self.size;
288        self.panes = vec![Pane::new_with_id(
289            pane_id,
290            0,
291            PaneGeometry::new(0, 0, size.cols, size.rows),
292        )];
293        self.next_pane_index = 1;
294        self.active_pane = 0;
295        self.last_pane = None;
296        self.layout = LayoutName::MainVertical;
297        self.last_layout = None;
298        self.layout_tree = Some(LayoutTree::single(size));
299        self.custom_layout = false;
300        self.old_layout = None;
301        self.automatic_rename = true;
302        self.zoomed = false;
303        self.zoom_restore_pending = false;
304        self.alert_flags = AlertFlags::empty();
305        self.alerts_queued = false;
306        self.requested_main_width = None;
307        self.requested_main_height = None;
308        pane_id
309    }
310
311    pub(crate) fn set_size(&mut self, size: TerminalSize) {
312        self.size = size;
313        if self.zoomed {
314            self.apply_zoom_geometry();
315        } else {
316            self.recalculate_geometry();
317        }
318    }
319
320    pub(crate) fn set_name(&mut self, name: String) {
321        self.name = Some(name);
322        self.automatic_rename = false;
323    }
324
325    /// Re-enables runtime automatic renaming for this window.
326    pub fn enable_automatic_rename(&mut self) {
327        self.automatic_rename = true;
328    }
329
330    /// Updates the runtime window name while preserving automatic renaming.
331    pub fn set_automatic_name(&mut self, name: String) {
332        self.name = Some(name);
333        self.automatic_rename = true;
334    }
335
336    pub(crate) fn rotate_panes(&mut self, direction: RotateWindowDirection) {
337        self.rotate_panes_with_zoom(direction, false);
338    }
339
340    pub(crate) fn rotate_panes_with_zoom(
341        &mut self,
342        direction: RotateWindowDirection,
343        restore_zoom: bool,
344    ) {
345        if self.panes.len() <= 1 {
346            return;
347        }
348
349        self.push_zoom(restore_zoom);
350        let previous_active_pane_id = self
351            .active_pane()
352            .expect("active pane must exist before pane rotation")
353            .id();
354        let active_position = self
355            .panes
356            .iter()
357            .position(|pane| pane.index() == self.active_pane)
358            .expect("active pane must exist in window order");
359
360        match direction {
361            RotateWindowDirection::Down => self.panes.rotate_right(1),
362            RotateWindowDirection::Up => self.panes.rotate_left(1),
363        }
364        for (index, pane) in self.panes.iter_mut().enumerate() {
365            pane.set_index(index as u32);
366        }
367
368        self.apply_layout_tree();
369
370        // tmux keeps the selected slot stable while pane contents rotate, then
371        // tracks last-pane as the pane identity that was active before rotation.
372        self.active_pane = active_position as u32;
373        self.last_pane = self
374            .pane_index_for_id(previous_active_pane_id)
375            .filter(|pane_index| *pane_index != self.active_pane);
376        self.mark_pane_active(self.active_pane);
377
378        self.pop_zoom();
379    }
380
381    pub(crate) fn insert_pane_at_position(
382        &mut self,
383        position: usize,
384        pane: Pane,
385        direction: SplitDirection,
386    ) -> Result<(), RmuxError> {
387        if position > self.panes.len() {
388            return Err(RmuxError::Server(format!(
389                "cannot insert pane at position {position} in a {}-pane window",
390                self.panes.len()
391            )));
392        }
393
394        self.ensure_accepts_pane(&pane, None)?;
395        self.auto_unzoom();
396        self.layout = layout_for_split(direction);
397        self.bump_next_pane_index(pane.index());
398        let inserted_index = pane.index();
399        self.panes.insert(position, pane);
400        if self.panes.len() == 1 {
401            self.active_pane = inserted_index;
402            self.last_pane = None;
403        }
404        if self.panes.len() == 1 {
405            self.layout_tree = Some(LayoutTree::single(self.size));
406            self.apply_layout_tree();
407            return Ok(());
408        }
409
410        let (target_leaf, insert_before_target) = if position == 0 {
411            (0, true)
412        } else {
413            (position - 1, false)
414        };
415        let inserted = self.layout_tree.as_mut().is_some_and(|tree| {
416            tree.split_leaf(
417                target_leaf,
418                LayoutDirection::from_split_direction(direction),
419                insert_before_target,
420            )
421        });
422        if !inserted {
423            self.rebuild_named_layout_tree(self.layout);
424        } else {
425            self.apply_layout_tree();
426        }
427        Ok(())
428    }
429
430    pub(crate) fn move_pane_by_splitting_target(
431        &mut self,
432        source_position: usize,
433        target_position: usize,
434        final_insert_position: usize,
435        direction: SplitDirection,
436        insert_before_target: bool,
437    ) -> Result<PaneId, RmuxError> {
438        let pane_count = self.panes.len();
439        if source_position >= pane_count {
440            return Err(RmuxError::Server(format!(
441                "cannot move missing pane at position {source_position}"
442            )));
443        }
444        if target_position >= pane_count {
445            return Err(RmuxError::Server(format!(
446                "cannot split missing target pane at position {target_position}"
447            )));
448        }
449        if final_insert_position > pane_count.saturating_sub(1) {
450            return Err(RmuxError::Server(format!(
451                "cannot insert moved pane at position {final_insert_position} in a {}-pane window",
452                pane_count.saturating_sub(1)
453            )));
454        }
455
456        self.auto_unzoom();
457        self.layout = layout_for_split(direction);
458
459        let split_insert_position = if insert_before_target {
460            target_position
461        } else {
462            target_position + 1
463        };
464        let source_leaf_after_split = if split_insert_position <= source_position {
465            source_position + 1
466        } else {
467            source_position
468        };
469        let tree = self.layout_tree.as_mut().ok_or_else(|| {
470            RmuxError::Server("cannot move pane without a layout tree".to_owned())
471        })?;
472        if !tree.split_leaf(
473            target_position,
474            LayoutDirection::from_split_direction(direction),
475            insert_before_target,
476        ) {
477            return Err(RmuxError::Server(format!(
478                "cannot split target pane at position {target_position}"
479            )));
480        }
481        if !tree.remove_leaf(source_leaf_after_split) {
482            return Err(RmuxError::Server(format!(
483                "cannot remove source pane leaf at position {source_leaf_after_split}"
484            )));
485        }
486
487        let moved_pane = self.panes.remove(source_position);
488        let moved_pane_id = moved_pane.id();
489        self.panes.insert(final_insert_position, moved_pane);
490        self.apply_layout_tree();
491        Ok(moved_pane_id)
492    }
493
494    pub(crate) fn insert_pane_full_size(
495        &mut self,
496        pane: Pane,
497        direction: SplitDirection,
498        insert_before_target: bool,
499    ) -> Result<(), RmuxError> {
500        self.ensure_accepts_pane(&pane, None)?;
501        self.auto_unzoom();
502        self.layout = layout_for_split(direction);
503        self.bump_next_pane_index(pane.index());
504
505        if insert_before_target {
506            self.panes.insert(0, pane);
507        } else {
508            self.panes.push(pane);
509        }
510
511        let split = self.layout_tree.as_mut().is_some_and(|tree| {
512            tree.split_root(
513                LayoutDirection::from_split_direction(direction),
514                insert_before_target,
515            )
516        });
517        if !split {
518            self.rebuild_named_layout_tree(self.layout);
519        } else {
520            self.apply_layout_tree();
521        }
522        Ok(())
523    }
524
525    pub(crate) fn replace_pane(&mut self, pane_index: u32, pane: Pane) -> Result<(), RmuxError> {
526        let position = self.pane_position(pane_index).ok_or_else(|| {
527            RmuxError::Server(format!(
528                "cannot replace missing pane index {pane_index} in window {}",
529                self.id
530            ))
531        })?;
532        self.ensure_accepts_pane(&pane, Some(position))?;
533        self.bump_next_pane_index(pane.index());
534        self.panes[position] = pane;
535        self.apply_layout_tree();
536        Ok(())
537    }
538
539    pub(crate) fn swap_panes(&mut self, source_pane_index: u32, target_pane_index: u32) -> bool {
540        let Some(source_position) = self.pane_position(source_pane_index) else {
541            return false;
542        };
543        let Some(target_position) = self.pane_position(target_pane_index) else {
544            return false;
545        };
546        if source_position == target_position {
547            return true;
548        }
549
550        self.auto_unzoom();
551        let active_pane_id = self
552            .active_pane()
553            .expect("active pane must exist before pane swap")
554            .id();
555        let last_pane_id = self
556            .last_pane
557            .and_then(|pane_index| self.pane(pane_index).map(Pane::id));
558        self.panes.swap(source_position, target_position);
559        self.apply_layout_tree();
560        self.renumber_panes_by_position(active_pane_id, last_pane_id);
561        true
562    }
563}
564
565#[cfg(test)]
566#[path = "window/tests.rs"]
567mod tests;