Skip to main content

rmux_core/
window.rs

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