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/layout_cycle.rs"]
108mod layout_cycle;
109#[path = "window/layout_ops.rs"]
110mod layout_ops;
111#[path = "window/panes.rs"]
112mod panes;
113#[path = "window/zoom.rs"]
114mod zoom;
115
116use panes::layout_for_split;
117
118impl Window {
119    /// Creates the single V1 window with its initial pane.
120    #[must_use]
121    pub fn new(size: TerminalSize) -> Self {
122        Self::new_with_initial_pane(size, PaneId::new(0), WindowId::new(0))
123    }
124
125    pub(crate) fn new_with_initial_pane(size: TerminalSize, pane_id: PaneId, id: WindowId) -> Self {
126        let now = current_unix_timestamp();
127        let mut window = Self {
128            id,
129            panes: vec![Pane::new_with_id(
130                pane_id,
131                0,
132                PaneGeometry::new(0, 0, size.cols, size.rows),
133            )],
134            next_pane_index: 1,
135            active_pane: 0,
136            last_pane: None,
137            layout: LayoutName::MainVertical,
138            last_layout: None,
139            layout_tree: Some(LayoutTree::single(size)),
140            custom_layout: false,
141            old_layout: None,
142            size,
143            name: None,
144            automatic_rename: true,
145            zoomed: false,
146            zoom_restore_pending: false,
147            alert_flags: AlertFlags::empty(),
148            alerts_queued: false,
149            created_at: now,
150            activity_at: now,
151            requested_main_width: None,
152            requested_main_height: None,
153        };
154        window.recalculate_geometry();
155        window
156    }
157
158    /// Returns the stable internal window identity.
159    #[must_use]
160    pub const fn id(&self) -> WindowId {
161        self.id
162    }
163
164    /// Returns the window creation timestamp as Unix seconds.
165    #[must_use]
166    pub const fn created_at(&self) -> i64 {
167        self.created_at
168    }
169
170    /// Returns the last window activity timestamp as Unix seconds.
171    #[must_use]
172    pub const fn activity_at(&self) -> i64 {
173        self.activity_at
174    }
175
176    /// Records output activity for a specific pane in this window.
177    pub fn touch_activity_for_pane(&mut self, pane_index: u32) -> bool {
178        let Some(position) = self
179            .panes
180            .iter()
181            .position(|pane| pane.index() == pane_index)
182        else {
183            return false;
184        };
185        self.activity_at = current_unix_timestamp();
186        self.panes[position].touch_activity();
187        true
188    }
189
190    /// Returns the panes in window order.
191    #[must_use]
192    pub fn panes(&self) -> &[Pane] {
193        &self.panes
194    }
195
196    /// Returns the pane with the given stable pane index.
197    #[must_use]
198    pub fn pane(&self, pane_index: u32) -> Option<&Pane> {
199        self.panes.iter().find(|pane| pane.index() == pane_index)
200    }
201
202    /// Returns a mutable pane reference for the given stable pane index.
203    #[must_use]
204    pub fn pane_mut(&mut self, pane_index: u32) -> Option<&mut Pane> {
205        self.panes
206            .iter_mut()
207            .find(|pane| pane.index() == pane_index)
208    }
209
210    /// Returns the active pane index owned by the window.
211    #[must_use]
212    pub const fn active_pane_index(&self) -> u32 {
213        self.active_pane
214    }
215
216    /// Returns the previously active pane index when one exists.
217    #[must_use]
218    pub const fn last_pane_index(&self) -> Option<u32> {
219        self.last_pane
220    }
221
222    /// Returns the active pane when the window invariant is satisfied.
223    #[must_use]
224    pub fn active_pane(&self) -> Option<&Pane> {
225        self.pane(self.active_pane)
226    }
227
228    /// Returns the stable internal pane identity for a display index.
229    #[must_use]
230    pub fn pane_id(&self, pane_index: u32) -> Option<PaneId> {
231        self.pane(pane_index).map(Pane::id)
232    }
233
234    /// Returns the last selected named layout for the window.
235    #[must_use]
236    pub const fn layout(&self) -> LayoutName {
237        self.layout
238    }
239
240    /// Returns the terminal size currently backing the window.
241    #[must_use]
242    pub const fn size(&self) -> TerminalSize {
243        self.size
244    }
245
246    /// Returns the tmux-compatible serialized layout tree for the window.
247    #[must_use]
248    pub fn layout_dump(&self) -> String {
249        self.layout_tree
250            .as_ref()
251            .map_or_else(String::new, |tree| tree.dump(&self.panes))
252    }
253
254    /// Saves the current serialized layout as the tmux-compatible old layout.
255    pub fn save_old_layout(&mut self) {
256        self.old_layout = Some(self.layout_dump());
257    }
258
259    /// Returns the previously saved serialized layout when one exists.
260    #[must_use]
261    pub fn old_layout(&self) -> Option<&str> {
262        self.old_layout.as_deref()
263    }
264
265    /// Returns the explicit user-supplied window name when one exists.
266    #[must_use]
267    pub fn name(&self) -> Option<&str> {
268        self.name.as_deref()
269    }
270
271    /// Returns whether automatic renaming remains enabled for the window.
272    #[must_use]
273    pub const fn automatic_rename(&self) -> bool {
274        self.automatic_rename
275    }
276
277    /// Returns any queued runtime alert flags for the window.
278    #[must_use]
279    pub const fn alert_flags(&self) -> AlertFlags {
280        self.alert_flags
281    }
282
283    /// Returns whether alert processing is already queued for this window.
284    #[must_use]
285    pub const fn alerts_queued(&self) -> bool {
286        self.alerts_queued
287    }
288
289    /// Returns the number of panes in the window.
290    #[must_use]
291    pub fn pane_count(&self) -> usize {
292        self.panes.len()
293    }
294
295    /// Queues alert flags for later server-side processing.
296    pub fn queue_alerts(&mut self, flags: AlertFlags) {
297        self.alert_flags.insert(flags);
298    }
299
300    /// Drains and returns all queued alert flags.
301    pub fn take_alert_flags(&mut self) -> AlertFlags {
302        let flags = self.alert_flags;
303        self.alert_flags = AlertFlags::empty();
304        flags
305    }
306
307    /// Clears the provided queued alert flags.
308    pub fn clear_alert_flags(&mut self, flags: AlertFlags) {
309        self.alert_flags.remove(flags);
310    }
311
312    /// Sets whether alert processing is already queued.
313    pub fn set_alerts_queued(&mut self, queued: bool) {
314        self.alerts_queued = queued;
315    }
316
317    /// Resets this window to a single pane with a fresh pane ID, preserving the window id,
318    /// name, and size. Returns the new pane ID.
319    pub(crate) fn respawn(&mut self, pane_id: PaneId) -> PaneId {
320        let size = self.size;
321        self.panes = vec![Pane::new_with_id(
322            pane_id,
323            0,
324            PaneGeometry::new(0, 0, size.cols, size.rows),
325        )];
326        self.next_pane_index = 1;
327        self.active_pane = 0;
328        self.last_pane = None;
329        self.layout = LayoutName::MainVertical;
330        self.last_layout = None;
331        self.layout_tree = Some(LayoutTree::single(size));
332        self.custom_layout = false;
333        self.old_layout = None;
334        self.automatic_rename = true;
335        self.zoomed = false;
336        self.zoom_restore_pending = false;
337        self.alert_flags = AlertFlags::empty();
338        self.alerts_queued = false;
339        self.requested_main_width = None;
340        self.requested_main_height = None;
341        pane_id
342    }
343
344    pub(crate) fn set_size(&mut self, size: TerminalSize) {
345        self.size = size;
346        if self.zoomed {
347            self.apply_zoom_geometry();
348        } else {
349            self.recalculate_geometry();
350        }
351    }
352
353    pub(crate) fn set_name(&mut self, name: String) {
354        self.name = Some(name);
355        self.automatic_rename = false;
356    }
357
358    /// Re-enables runtime automatic renaming for this window.
359    pub fn enable_automatic_rename(&mut self) {
360        self.automatic_rename = true;
361    }
362
363    /// Updates the runtime window name while preserving automatic renaming.
364    pub fn set_automatic_name(&mut self, name: String) {
365        self.name = Some(name);
366        self.automatic_rename = true;
367    }
368
369    pub(crate) fn rotate_panes(&mut self, direction: RotateWindowDirection) {
370        self.rotate_panes_with_zoom(direction, false);
371    }
372
373    pub(crate) fn rotate_panes_with_zoom(
374        &mut self,
375        direction: RotateWindowDirection,
376        restore_zoom: bool,
377    ) {
378        if self.panes.len() <= 1 {
379            return;
380        }
381
382        self.push_zoom(restore_zoom);
383        let previous_active_pane_id = self
384            .active_pane()
385            .expect("active pane must exist before pane rotation")
386            .id();
387        let active_position = self
388            .panes
389            .iter()
390            .position(|pane| pane.index() == self.active_pane)
391            .expect("active pane must exist in window order");
392
393        match direction {
394            RotateWindowDirection::Down => self.panes.rotate_right(1),
395            RotateWindowDirection::Up => self.panes.rotate_left(1),
396        }
397        for (index, pane) in self.panes.iter_mut().enumerate() {
398            pane.set_index(index as u32);
399        }
400
401        self.apply_layout_tree();
402
403        // tmux keeps the selected slot stable while pane contents rotate, then
404        // tracks last-pane as the pane identity that was active before rotation.
405        self.active_pane = active_position as u32;
406        self.last_pane = self
407            .pane_index_for_id(previous_active_pane_id)
408            .filter(|pane_index| *pane_index != self.active_pane);
409        self.mark_pane_active(self.active_pane);
410
411        self.pop_zoom();
412    }
413
414    pub(crate) fn insert_pane_at_position(
415        &mut self,
416        position: usize,
417        pane: Pane,
418        direction: SplitDirection,
419    ) -> Result<(), RmuxError> {
420        if position > self.panes.len() {
421            return Err(RmuxError::Server(format!(
422                "cannot insert pane at position {position} in a {}-pane window",
423                self.panes.len()
424            )));
425        }
426
427        self.ensure_accepts_pane(&pane, None)?;
428        self.auto_unzoom();
429        self.layout = layout_for_split(direction);
430        self.bump_next_pane_index(pane.index());
431        let inserted_index = pane.index();
432        self.panes.insert(position, pane);
433        if self.panes.len() == 1 {
434            self.active_pane = inserted_index;
435            self.last_pane = None;
436        }
437        if self.panes.len() == 1 {
438            self.layout_tree = Some(LayoutTree::single(self.size));
439            self.apply_layout_tree();
440            return Ok(());
441        }
442
443        let (target_leaf, insert_before_target) = if position == 0 {
444            (0, true)
445        } else {
446            (position - 1, false)
447        };
448        let inserted = self.layout_tree.as_mut().is_some_and(|tree| {
449            tree.split_leaf(
450                target_leaf,
451                LayoutDirection::from_split_direction(direction),
452                insert_before_target,
453            )
454        });
455        if !inserted {
456            self.rebuild_named_layout_tree(self.layout);
457        } else {
458            self.apply_layout_tree();
459        }
460        Ok(())
461    }
462
463    pub(crate) fn move_pane_by_splitting_target(
464        &mut self,
465        source_position: usize,
466        target_position: usize,
467        final_insert_position: usize,
468        direction: SplitDirection,
469        insert_before_target: bool,
470    ) -> Result<PaneId, RmuxError> {
471        let pane_count = self.panes.len();
472        if source_position >= pane_count {
473            return Err(RmuxError::Server(format!(
474                "cannot move missing pane at position {source_position}"
475            )));
476        }
477        if target_position >= pane_count {
478            return Err(RmuxError::Server(format!(
479                "cannot split missing target pane at position {target_position}"
480            )));
481        }
482        if final_insert_position > pane_count.saturating_sub(1) {
483            return Err(RmuxError::Server(format!(
484                "cannot insert moved pane at position {final_insert_position} in a {}-pane window",
485                pane_count.saturating_sub(1)
486            )));
487        }
488
489        self.auto_unzoom();
490        self.layout = layout_for_split(direction);
491
492        let split_insert_position = if insert_before_target {
493            target_position
494        } else {
495            target_position + 1
496        };
497        let source_leaf_after_split = if split_insert_position <= source_position {
498            source_position + 1
499        } else {
500            source_position
501        };
502        let tree = self.layout_tree.as_mut().ok_or_else(|| {
503            RmuxError::Server("cannot move pane without a layout tree".to_owned())
504        })?;
505        if !tree.split_leaf(
506            target_position,
507            LayoutDirection::from_split_direction(direction),
508            insert_before_target,
509        ) {
510            return Err(RmuxError::Server(format!(
511                "cannot split target pane at position {target_position}"
512            )));
513        }
514        if !tree.remove_leaf(source_leaf_after_split) {
515            return Err(RmuxError::Server(format!(
516                "cannot remove source pane leaf at position {source_leaf_after_split}"
517            )));
518        }
519
520        let moved_pane = self.panes.remove(source_position);
521        let moved_pane_id = moved_pane.id();
522        self.panes.insert(final_insert_position, moved_pane);
523        self.apply_layout_tree();
524        Ok(moved_pane_id)
525    }
526
527    pub(crate) fn insert_pane_full_size(
528        &mut self,
529        pane: Pane,
530        direction: SplitDirection,
531        insert_before_target: bool,
532    ) -> Result<(), RmuxError> {
533        self.ensure_accepts_pane(&pane, None)?;
534        self.auto_unzoom();
535        self.layout = layout_for_split(direction);
536        self.bump_next_pane_index(pane.index());
537
538        if insert_before_target {
539            self.panes.insert(0, pane);
540        } else {
541            self.panes.push(pane);
542        }
543
544        let split = self.layout_tree.as_mut().is_some_and(|tree| {
545            tree.split_root(
546                LayoutDirection::from_split_direction(direction),
547                insert_before_target,
548            )
549        });
550        if !split {
551            self.rebuild_named_layout_tree(self.layout);
552        } else {
553            self.apply_layout_tree();
554        }
555        Ok(())
556    }
557
558    pub(crate) fn replace_pane_for_swap(
559        &mut self,
560        pane_index: u32,
561        pane: Pane,
562    ) -> Result<(), RmuxError> {
563        let position = self.pane_position(pane_index).ok_or_else(|| {
564            RmuxError::Server(format!(
565                "cannot replace missing pane index {pane_index} in window {}",
566                self.id
567            ))
568        })?;
569        for (existing_position, existing) in self.panes.iter().enumerate() {
570            if existing_position != position && existing.id() == pane.id() {
571                return Err(RmuxError::Server(format!(
572                    "pane id {} already exists in window {}",
573                    pane.id().as_u32(),
574                    self.id
575                )));
576            }
577        }
578        self.bump_next_pane_index(pane.index());
579        self.panes[position] = pane;
580        self.apply_layout_tree();
581        Ok(())
582    }
583
584    pub(crate) fn swap_panes(&mut self, source_pane_index: u32, target_pane_index: u32) -> bool {
585        let Some(source_position) = self.pane_position(source_pane_index) else {
586            return false;
587        };
588        let Some(target_position) = self.pane_position(target_pane_index) else {
589            return false;
590        };
591        if source_position == target_position {
592            return true;
593        }
594
595        self.auto_unzoom();
596        let active_pane_id = self
597            .active_pane()
598            .expect("active pane must exist before pane swap")
599            .id();
600        let last_pane_id = self
601            .last_pane
602            .and_then(|pane_index| self.pane(pane_index).map(Pane::id));
603        self.panes.swap(source_position, target_position);
604        self.apply_layout_tree();
605        self.renumber_panes_by_position(active_pane_id, last_pane_id);
606        true
607    }
608}
609
610fn current_unix_timestamp() -> i64 {
611    SystemTime::now()
612        .duration_since(UNIX_EPOCH)
613        .ok()
614        .and_then(|duration| i64::try_from(duration.as_secs()).ok())
615        .unwrap_or_default()
616}
617
618#[cfg(test)]
619#[path = "window/tests.rs"]
620mod tests;