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 terminal size currently backing the window.
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        if self.zoomed {
324            self.apply_zoom_geometry();
325        } else {
326            self.recalculate_geometry();
327        }
328    }
329
330    pub(crate) fn set_name(&mut self, name: String) {
331        self.name = Some(name);
332        self.automatic_rename = false;
333    }
334
335    /// Re-enables runtime automatic renaming for this window.
336    pub fn enable_automatic_rename(&mut self) {
337        self.automatic_rename = true;
338    }
339
340    /// Updates the runtime window name while preserving automatic renaming.
341    pub fn set_automatic_name(&mut self, name: String) {
342        self.name = Some(name);
343        self.automatic_rename = true;
344    }
345
346    pub(crate) fn rotate_panes(&mut self, direction: RotateWindowDirection) {
347        self.rotate_panes_with_zoom(direction, false);
348    }
349
350    pub(crate) fn rotate_panes_with_zoom(
351        &mut self,
352        direction: RotateWindowDirection,
353        restore_zoom: bool,
354    ) {
355        if self.panes.len() <= 1 {
356            return;
357        }
358
359        self.push_zoom(restore_zoom);
360        let previous_active_pane_id = self
361            .active_pane()
362            .expect("active pane must exist before pane rotation")
363            .id();
364        let active_position = self
365            .panes
366            .iter()
367            .position(|pane| pane.index() == self.active_pane)
368            .expect("active pane must exist in window order");
369
370        match direction {
371            RotateWindowDirection::Down => self.panes.rotate_right(1),
372            RotateWindowDirection::Up => self.panes.rotate_left(1),
373        }
374        for (index, pane) in self.panes.iter_mut().enumerate() {
375            pane.set_index(index as u32);
376        }
377
378        self.apply_layout_tree();
379
380        // tmux keeps the selected slot stable while pane contents rotate, then
381        // tracks last-pane as the pane identity that was active before rotation.
382        self.active_pane = active_position as u32;
383        self.last_pane = self
384            .pane_index_for_id(previous_active_pane_id)
385            .filter(|pane_index| *pane_index != self.active_pane);
386        self.mark_pane_active(self.active_pane);
387
388        self.pop_zoom();
389    }
390
391    pub(crate) fn insert_pane_at_position(
392        &mut self,
393        position: usize,
394        pane: Pane,
395        direction: SplitDirection,
396    ) -> Result<(), RmuxError> {
397        if position > self.panes.len() {
398            return Err(RmuxError::Server(format!(
399                "cannot insert pane at position {position} in a {}-pane window",
400                self.panes.len()
401            )));
402        }
403
404        self.ensure_accepts_pane(&pane, None)?;
405        self.auto_unzoom();
406        self.layout = layout_for_split(direction);
407        self.bump_next_pane_index(pane.index());
408        let inserted_index = pane.index();
409        self.panes.insert(position, pane);
410        if self.panes.len() == 1 {
411            self.active_pane = inserted_index;
412            self.last_pane = None;
413        }
414        if self.panes.len() == 1 {
415            self.layout_tree = Some(LayoutTree::single(self.size));
416            self.apply_layout_tree();
417            return Ok(());
418        }
419
420        let (target_leaf, insert_before_target) = if position == 0 {
421            (0, true)
422        } else {
423            (position - 1, false)
424        };
425        let inserted = self.layout_tree.as_mut().is_some_and(|tree| {
426            tree.split_leaf(
427                target_leaf,
428                LayoutDirection::from_split_direction(direction),
429                insert_before_target,
430            )
431        });
432        if !inserted {
433            self.rebuild_named_layout_tree(self.layout);
434        } else {
435            self.apply_layout_tree();
436        }
437        Ok(())
438    }
439
440    pub(crate) fn move_pane_by_splitting_target(
441        &mut self,
442        source_position: usize,
443        target_position: usize,
444        final_insert_position: usize,
445        direction: SplitDirection,
446        insert_before_target: bool,
447    ) -> Result<PaneId, RmuxError> {
448        let pane_count = self.panes.len();
449        if source_position >= pane_count {
450            return Err(RmuxError::Server(format!(
451                "cannot move missing pane at position {source_position}"
452            )));
453        }
454        if target_position >= pane_count {
455            return Err(RmuxError::Server(format!(
456                "cannot split missing target pane at position {target_position}"
457            )));
458        }
459        if final_insert_position > pane_count.saturating_sub(1) {
460            return Err(RmuxError::Server(format!(
461                "cannot insert moved pane at position {final_insert_position} in a {}-pane window",
462                pane_count.saturating_sub(1)
463            )));
464        }
465
466        self.auto_unzoom();
467        self.layout = layout_for_split(direction);
468
469        let split_insert_position = if insert_before_target {
470            target_position
471        } else {
472            target_position + 1
473        };
474        let source_leaf_after_split = if split_insert_position <= source_position {
475            source_position + 1
476        } else {
477            source_position
478        };
479        let tree = self.layout_tree.as_mut().ok_or_else(|| {
480            RmuxError::Server("cannot move pane without a layout tree".to_owned())
481        })?;
482        if !tree.split_leaf(
483            target_position,
484            LayoutDirection::from_split_direction(direction),
485            insert_before_target,
486        ) {
487            return Err(RmuxError::Server(format!(
488                "cannot split target pane at position {target_position}"
489            )));
490        }
491        if !tree.remove_leaf(source_leaf_after_split) {
492            return Err(RmuxError::Server(format!(
493                "cannot remove source pane leaf at position {source_leaf_after_split}"
494            )));
495        }
496
497        let moved_pane = self.panes.remove(source_position);
498        let moved_pane_id = moved_pane.id();
499        self.panes.insert(final_insert_position, moved_pane);
500        self.apply_layout_tree();
501        Ok(moved_pane_id)
502    }
503
504    pub(crate) fn insert_pane_full_size(
505        &mut self,
506        pane: Pane,
507        direction: SplitDirection,
508        insert_before_target: bool,
509    ) -> Result<(), RmuxError> {
510        self.ensure_accepts_pane(&pane, None)?;
511        self.auto_unzoom();
512        self.layout = layout_for_split(direction);
513        self.bump_next_pane_index(pane.index());
514
515        if insert_before_target {
516            self.panes.insert(0, pane);
517        } else {
518            self.panes.push(pane);
519        }
520
521        let split = self.layout_tree.as_mut().is_some_and(|tree| {
522            tree.split_root(
523                LayoutDirection::from_split_direction(direction),
524                insert_before_target,
525            )
526        });
527        if !split {
528            self.rebuild_named_layout_tree(self.layout);
529        } else {
530            self.apply_layout_tree();
531        }
532        Ok(())
533    }
534
535    pub(crate) fn replace_pane_for_swap(
536        &mut self,
537        pane_index: u32,
538        pane: Pane,
539    ) -> Result<(), RmuxError> {
540        let position = self.pane_position(pane_index).ok_or_else(|| {
541            RmuxError::Server(format!(
542                "cannot replace missing pane index {pane_index} in window {}",
543                self.id
544            ))
545        })?;
546        for (existing_position, existing) in self.panes.iter().enumerate() {
547            if existing_position != position && existing.id() == pane.id() {
548                return Err(RmuxError::Server(format!(
549                    "pane id {} already exists in window {}",
550                    pane.id().as_u32(),
551                    self.id
552                )));
553            }
554        }
555        self.bump_next_pane_index(pane.index());
556        self.panes[position] = pane;
557        self.apply_layout_tree();
558        Ok(())
559    }
560
561    pub(crate) fn swap_panes(&mut self, source_pane_index: u32, target_pane_index: u32) -> bool {
562        let Some(source_position) = self.pane_position(source_pane_index) else {
563            return false;
564        };
565        let Some(target_position) = self.pane_position(target_pane_index) else {
566            return false;
567        };
568        if source_position == target_position {
569            return true;
570        }
571
572        self.auto_unzoom();
573        let active_pane_id = self
574            .active_pane()
575            .expect("active pane must exist before pane swap")
576            .id();
577        let last_pane_id = self
578            .last_pane
579            .and_then(|pane_index| self.pane(pane_index).map(Pane::id));
580        self.panes.swap(source_position, target_position);
581        self.apply_layout_tree();
582        self.renumber_panes_by_position(active_pane_id, last_pane_id);
583        true
584    }
585}
586
587fn current_unix_timestamp() -> i64 {
588    SystemTime::now()
589        .duration_since(UNIX_EPOCH)
590        .ok()
591        .and_then(|duration| i64::try_from(duration.as_secs()).ok())
592        .unwrap_or_default()
593}
594
595#[cfg(test)]
596#[path = "window/tests.rs"]
597mod tests;