Skip to main content

muxr_core/protocol/
pane_layout.rs

1use std::collections::BTreeSet;
2use std::fmt;
3use std::num::NonZeroU32;
4
5use rootcause::report;
6use serde::Deserialize;
7use serde::Serialize;
8
9use super::ClientMousePosition;
10use super::TrackedProcessState;
11
12#[derive(
13    rkyv::Archive,
14    Clone,
15    Copy,
16    Debug,
17    Deserialize,
18    rkyv::Deserialize,
19    Eq,
20    Hash,
21    Ord,
22    PartialEq,
23    PartialOrd,
24    Serialize,
25    rkyv::Serialize,
26)]
27#[serde(transparent)]
28pub struct TabId(NonZeroU32);
29
30impl TabId {
31    /// Build a tab id for layout snapshots and persisted session state.
32    ///
33    /// # Errors
34    /// - The id is zero.
35    pub fn new(id: u32) -> rootcause::Result<Self> {
36        let Some(id) = NonZeroU32::new(id) else {
37            return Err(report!("invalid muxr tab id").attach("id=0"));
38        };
39        Ok(Self(id))
40    }
41
42    /// Return the numeric tab id.
43    #[must_use]
44    pub const fn get(self) -> u32 {
45        self.0.get()
46    }
47}
48
49impl fmt::Display for TabId {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        write!(f, "tab-{}", self.get())
52    }
53}
54
55#[derive(
56    rkyv::Archive,
57    Clone,
58    Copy,
59    Debug,
60    Deserialize,
61    rkyv::Deserialize,
62    Eq,
63    Hash,
64    Ord,
65    PartialEq,
66    PartialOrd,
67    Serialize,
68    rkyv::Serialize,
69)]
70#[serde(transparent)]
71pub struct PaneId(NonZeroU32);
72
73impl PaneId {
74    /// Build a pane id for layout snapshots and persisted session state.
75    ///
76    /// # Errors
77    /// - The id is zero.
78    pub fn new(id: u32) -> rootcause::Result<Self> {
79        let Some(id) = NonZeroU32::new(id) else {
80            return Err(report!("invalid muxr pane id").attach("id=0"));
81        };
82        Ok(Self(id))
83    }
84
85    /// Return the numeric pane id.
86    #[must_use]
87    pub const fn get(self) -> u32 {
88        self.0.get()
89    }
90}
91
92impl fmt::Display for PaneId {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        write!(f, "pane-{}", self.get())
95    }
96}
97
98#[derive(rkyv::Archive, Clone, Debug, Eq, PartialEq, Serialize, rkyv::Serialize)]
99pub struct LayoutSnapshot {
100    active_tab: TabId,
101    tabs: Vec<TabSnapshot>,
102}
103
104impl LayoutSnapshot {
105    /// Build a layout snapshot with active tab and pane invariants checked.
106    ///
107    /// # Errors
108    /// - Any tab or pane id is duplicated.
109    /// - The layout has no tabs.
110    /// - The active tab does not exist.
111    /// - Any tab has no panes or an active pane that does not exist.
112    pub fn new(active_tab: TabId, tabs: Vec<TabSnapshot>) -> rootcause::Result<Self> {
113        let snapshot = Self { active_tab, tabs };
114        snapshot.validate()?;
115        Ok(snapshot)
116    }
117
118    #[must_use]
119    pub const fn active_tab(&self) -> &TabId {
120        &self.active_tab
121    }
122
123    #[must_use]
124    pub fn tabs(&self) -> &[TabSnapshot] {
125        &self.tabs
126    }
127
128    fn validate(&self) -> rootcause::Result<()> {
129        if self.tabs.is_empty() {
130            return Err(report!("invalid muxr layout snapshot").attach("reason=tabs must not be empty"));
131        }
132        if !self.tabs.iter().any(|tab| tab.id == self.active_tab) {
133            return Err(report!("invalid muxr layout snapshot")
134                .attach("reason=active tab is missing")
135                .attach(format!("active_tab={}", self.active_tab)));
136        }
137
138        let mut seen_tab_ids = BTreeSet::new();
139        let mut seen_pane_ids = BTreeSet::new();
140        for tab in &self.tabs {
141            tab.validate()?;
142            if !seen_tab_ids.insert(tab.id) {
143                return Err(report!("invalid muxr layout snapshot")
144                    .attach("reason=duplicate tab id")
145                    .attach(format!("tab_id={}", tab.id)));
146            }
147
148            for pane in &tab.panes {
149                if !seen_pane_ids.insert(pane.id) {
150                    return Err(report!("invalid muxr layout snapshot")
151                        .attach("reason=duplicate pane id")
152                        .attach(format!("tab_id={}", tab.id))
153                        .attach(format!("pane_id={}", pane.id)));
154                }
155            }
156        }
157
158        Ok(())
159    }
160}
161
162impl<D> rkyv::Deserialize<LayoutSnapshot, D> for ArchivedLayoutSnapshot
163where
164    D: rkyv::rancor::Fallible + ?Sized,
165    D::Error: rkyv::rancor::Source,
166{
167    fn deserialize(&self, deserializer: &mut D) -> Result<LayoutSnapshot, D::Error> {
168        let active_tab = rkyv::Deserialize::<TabId, D>::deserialize(&self.active_tab, deserializer)?;
169        let tabs = rkyv::Deserialize::<Vec<TabSnapshot>, D>::deserialize(&self.tabs, deserializer)?;
170        LayoutSnapshot::new(active_tab, tabs).map_err(super::rkyv_deserialize_error::<D::Error>)
171    }
172}
173
174#[derive(rkyv::Archive, Clone, Debug, Eq, PartialEq, Serialize, rkyv::Serialize)]
175pub struct TabSnapshot {
176    active_pane: PaneId,
177    id: TabId,
178    panes: Vec<PaneSnapshot>,
179    title: String,
180}
181
182impl TabSnapshot {
183    /// Build a tab snapshot with active pane invariants checked.
184    ///
185    /// # Errors
186    /// - Any pane id is duplicated.
187    /// - The tab has no panes.
188    /// - The active pane does not exist.
189    pub fn new(
190        id: TabId,
191        title: impl Into<String>,
192        active_pane: PaneId,
193        panes: Vec<PaneSnapshot>,
194    ) -> rootcause::Result<Self> {
195        let snapshot = Self {
196            active_pane,
197            id,
198            panes,
199            title: title.into(),
200        };
201        snapshot.validate()?;
202        Ok(snapshot)
203    }
204
205    #[must_use]
206    pub const fn active_pane(&self) -> &PaneId {
207        &self.active_pane
208    }
209
210    #[must_use]
211    pub const fn id(&self) -> &TabId {
212        &self.id
213    }
214
215    #[must_use]
216    pub fn panes(&self) -> &[PaneSnapshot] {
217        &self.panes
218    }
219
220    #[must_use]
221    pub fn title(&self) -> &str {
222        &self.title
223    }
224
225    fn validate(&self) -> rootcause::Result<()> {
226        if self.panes.is_empty() {
227            return Err(report!("invalid muxr tab snapshot")
228                .attach("reason=panes must not be empty")
229                .attach(format!("tab_id={}", self.id)));
230        }
231        if !self.panes.iter().any(|pane| pane.id == self.active_pane) {
232            return Err(report!("invalid muxr tab snapshot")
233                .attach("reason=active pane is missing")
234                .attach(format!("tab_id={}", self.id))
235                .attach(format!("active_pane={}", self.active_pane)));
236        }
237
238        let mut seen_pane_ids = BTreeSet::new();
239        for pane in &self.panes {
240            if !seen_pane_ids.insert(pane.id) {
241                return Err(report!("invalid muxr tab snapshot")
242                    .attach("reason=duplicate pane id")
243                    .attach(format!("tab_id={}", self.id))
244                    .attach(format!("pane_id={}", pane.id)));
245            }
246        }
247
248        Ok(())
249    }
250}
251
252impl<D> rkyv::Deserialize<TabSnapshot, D> for ArchivedTabSnapshot
253where
254    D: rkyv::rancor::Fallible + ?Sized,
255    D::Error: rkyv::rancor::Source,
256{
257    fn deserialize(&self, deserializer: &mut D) -> Result<TabSnapshot, D::Error> {
258        let active_pane = rkyv::Deserialize::<PaneId, D>::deserialize(&self.active_pane, deserializer)?;
259        let id = rkyv::Deserialize::<TabId, D>::deserialize(&self.id, deserializer)?;
260        let panes = rkyv::Deserialize::<Vec<PaneSnapshot>, D>::deserialize(&self.panes, deserializer)?;
261        let title = rkyv::Deserialize::<String, D>::deserialize(&self.title, deserializer)?;
262        TabSnapshot::new(id, title, active_pane, panes).map_err(super::rkyv_deserialize_error::<D::Error>)
263    }
264}
265
266/// Tracked-process status rendered by the client tab bar.
267
268#[derive(rkyv::Archive, Clone, Debug, Eq, PartialEq, Serialize, rkyv::Serialize)]
269pub struct PaneSnapshot {
270    /// Tracked-process status used by the client tab bar.
271    pub tracked_process_state: TrackedProcessState,
272    /// Current pane working directory, used by the client tab bar.
273    pub cwd: String,
274    /// Shell-provided cmd label from the pane terminal title, used by the client tab bar.
275    pub cmd_label: Option<String>,
276    /// Last focus sequence assigned by the server, used to pick a representative pane.
277    pub focus_seq: u64,
278    /// Stable pane id.
279    pub id: PaneId,
280    /// Pane title displayed in tab and pane UI.
281    pub title: String,
282}
283
284impl<D> rkyv::Deserialize<PaneSnapshot, D> for ArchivedPaneSnapshot
285where
286    D: rkyv::rancor::Fallible + ?Sized,
287    D::Error: rkyv::rancor::Source,
288{
289    fn deserialize(&self, deserializer: &mut D) -> Result<PaneSnapshot, D::Error> {
290        let tracked_process_state =
291            rkyv::Deserialize::<TrackedProcessState, D>::deserialize(&self.tracked_process_state, deserializer)?;
292        let cwd = rkyv::Deserialize::<String, D>::deserialize(&self.cwd, deserializer)?;
293        let cmd_label = rkyv::Deserialize::<Option<String>, D>::deserialize(&self.cmd_label, deserializer)?;
294        let focus_seq = rkyv::Deserialize::<u64, D>::deserialize(&self.focus_seq, deserializer)?;
295        let id = rkyv::Deserialize::<PaneId, D>::deserialize(&self.id, deserializer)?;
296        let title = rkyv::Deserialize::<String, D>::deserialize(&self.title, deserializer)?;
297        Ok(PaneSnapshot {
298            tracked_process_state,
299            cwd,
300            cmd_label,
301            focus_seq,
302            id,
303            title,
304        })
305    }
306}
307
308/// Mouse tracking mode requested by the application running in a pane.
309#[derive(rkyv::Archive, Clone, Copy, Debug, rkyv::Deserialize, Eq, PartialEq, Serialize, rkyv::Serialize)]
310pub enum PaneMouseMode {
311    AnyMotion,
312    ButtonMotion,
313    None,
314    Press,
315    PressRelease,
316}
317
318/// Whether a terminal row ends normally or continues via a soft wrap.
319#[derive(
320    rkyv::Archive, Clone, Copy, Debug, Deserialize, rkyv::Deserialize, Eq, PartialEq, Serialize, rkyv::Serialize,
321)]
322pub enum RowWrap {
323    EndsBeforeSoftWrap,
324    EndsWithSoftWrap,
325}
326
327#[derive(rkyv::Archive, Clone, Debug, Eq, PartialEq, Serialize, rkyv::Serialize)]
328pub struct PaneRegionSnapshot {
329    id: PaneId,
330    col: u16,
331    row: u16,
332    cols: u16,
333    mouse_mode: PaneMouseMode,
334    rows: u16,
335    visible_top_row: u64,
336    wrapped_rows: Vec<RowWrap>,
337}
338
339impl PaneRegionSnapshot {
340    /// Build a visible pane region for the current rendered frame.
341    ///
342    /// # Errors
343    /// - The region has zero columns or rows.
344    /// - The region row or column range overflows.
345    pub fn new(
346        id: PaneId,
347        col: u16,
348        row: u16,
349        cols: u16,
350        rows: u16,
351        mouse_mode: PaneMouseMode,
352        visible_top_row: u64,
353    ) -> rootcause::Result<Self> {
354        let region = Self {
355            id,
356            col,
357            row,
358            cols,
359            mouse_mode,
360            rows,
361            visible_top_row,
362            wrapped_rows: vec![RowWrap::EndsBeforeSoftWrap; usize::from(rows)],
363        };
364        region.validate()?;
365        Ok(region)
366    }
367
368    /// Attach per-visible-row soft-wrap metadata from the pane terminal.
369    ///
370    /// # Errors
371    /// - The number of row flags does not match the region height.
372    pub fn with_wrapped_rows(mut self, wrapped_rows: Vec<RowWrap>) -> rootcause::Result<Self> {
373        self.wrapped_rows = wrapped_rows;
374        self.validate()?;
375        Ok(self)
376    }
377
378    #[must_use]
379    pub const fn col(&self) -> u16 {
380        self.col
381    }
382
383    #[must_use]
384    pub const fn cols(&self) -> u16 {
385        self.cols
386    }
387
388    #[must_use]
389    pub const fn id(&self) -> &PaneId {
390        &self.id
391    }
392
393    /// Return the pane application's current terminal mouse tracking mode.
394    #[must_use]
395    pub const fn mouse_mode(&self) -> PaneMouseMode {
396        self.mouse_mode
397    }
398
399    #[must_use]
400    pub const fn row(&self) -> u16 {
401        self.row
402    }
403
404    #[must_use]
405    pub const fn rows(&self) -> u16 {
406        self.rows
407    }
408
409    /// Return the stable content row rendered at the top of this pane's visible viewport.
410    #[must_use]
411    pub const fn visible_top_row(&self) -> u64 {
412        self.visible_top_row
413    }
414
415    /// Return the soft-wrap metadata for a stable content row in this rendered viewport.
416    #[must_use]
417    pub fn content_row_wrap(&self, content_row: u64) -> RowWrap {
418        let Some(local_row) = content_row.checked_sub(self.visible_top_row) else {
419            return RowWrap::EndsBeforeSoftWrap;
420        };
421        let Ok(local_row) = usize::try_from(local_row) else {
422            return RowWrap::EndsBeforeSoftWrap;
423        };
424        self.wrapped_rows
425            .get(local_row)
426            .copied()
427            .unwrap_or(RowWrap::EndsBeforeSoftWrap)
428    }
429
430    #[must_use]
431    pub const fn containment(&self, row: u16, col: u16) -> PaneRegionContainment {
432        let Some(end_row) = self.row.checked_add(self.rows) else {
433            return PaneRegionContainment::Outside;
434        };
435        let Some(end_col) = self.col.checked_add(self.cols) else {
436            return PaneRegionContainment::Outside;
437        };
438
439        if row >= self.row && row < end_row && col >= self.col && col < end_col {
440            PaneRegionContainment::Inside
441        } else {
442            PaneRegionContainment::Outside
443        }
444    }
445
446    fn validate(&self) -> rootcause::Result<()> {
447        if self.cols == 0 {
448            return Err(report!("invalid muxr pane region").attach("reason=cols must be nonzero"));
449        }
450        if self.rows == 0 {
451            return Err(report!("invalid muxr pane region").attach("reason=rows must be nonzero"));
452        }
453        if self.col.checked_add(self.cols).is_none() {
454            return Err(report!("invalid muxr pane region").attach("reason=column range overflowed"));
455        }
456        if self.row.checked_add(self.rows).is_none() {
457            return Err(report!("invalid muxr pane region").attach("reason=row range overflowed"));
458        }
459        if self.wrapped_rows.len() != usize::from(self.rows) {
460            return Err(report!("invalid muxr pane region")
461                .attach("reason=wrapped row count must match region height")
462                .attach(format!("expected={}", self.rows))
463                .attach(format!("actual={}", self.wrapped_rows.len())));
464        }
465        Ok(())
466    }
467}
468
469impl<D> rkyv::Deserialize<PaneRegionSnapshot, D> for ArchivedPaneRegionSnapshot
470where
471    D: rkyv::rancor::Fallible + ?Sized,
472    D::Error: rkyv::rancor::Source,
473{
474    fn deserialize(&self, deserializer: &mut D) -> Result<PaneRegionSnapshot, D::Error> {
475        let id = rkyv::Deserialize::<PaneId, D>::deserialize(&self.id, deserializer)?;
476        let col = rkyv::Deserialize::<u16, D>::deserialize(&self.col, deserializer)?;
477        let row = rkyv::Deserialize::<u16, D>::deserialize(&self.row, deserializer)?;
478        let cols = rkyv::Deserialize::<u16, D>::deserialize(&self.cols, deserializer)?;
479        let rows = rkyv::Deserialize::<u16, D>::deserialize(&self.rows, deserializer)?;
480        let mouse_mode = rkyv::Deserialize::<PaneMouseMode, D>::deserialize(&self.mouse_mode, deserializer)?;
481        let visible_top_row = rkyv::Deserialize::<u64, D>::deserialize(&self.visible_top_row, deserializer)?;
482        let wrapped_rows = rkyv::Deserialize::<Vec<RowWrap>, D>::deserialize(&self.wrapped_rows, deserializer)?;
483        PaneRegionSnapshot::new(id, col, row, cols, rows, mouse_mode, visible_top_row)
484            .and_then(|region| region.with_wrapped_rows(wrapped_rows))
485            .map_err(super::rkyv_deserialize_error::<D::Error>)
486    }
487}
488
489#[derive(rkyv::Archive, Clone, Debug, Eq, PartialEq, Serialize, rkyv::Serialize)]
490pub struct PaneRegionsSnapshot {
491    regions: Vec<PaneRegionSnapshot>,
492}
493
494impl PaneRegionsSnapshot {
495    /// Build the pane regions for the currently rendered tab.
496    ///
497    /// # Errors
498    /// - The region list is empty.
499    /// - Any region is invalid.
500    /// - Any pane id appears more than once.
501    pub fn new(regions: Vec<PaneRegionSnapshot>) -> rootcause::Result<Self> {
502        let snapshot = Self { regions };
503        snapshot.validate()?;
504        Ok(snapshot)
505    }
506
507    #[must_use]
508    pub fn regions(&self) -> &[PaneRegionSnapshot] {
509        &self.regions
510    }
511
512    #[must_use]
513    pub fn pane_at(&self, position: ClientMousePosition) -> Option<&PaneRegionSnapshot> {
514        self.regions()
515            .iter()
516            .find(|region| region.containment(position.row, position.col) == PaneRegionContainment::Inside)
517    }
518
519    fn validate(&self) -> rootcause::Result<()> {
520        if self.regions.is_empty() {
521            return Err(report!("invalid muxr pane regions snapshot").attach("reason=regions must not be empty"));
522        }
523
524        let mut seen_pane_ids = BTreeSet::new();
525        for region in &self.regions {
526            region.validate()?;
527            if !seen_pane_ids.insert(region.id) {
528                return Err(report!("invalid muxr pane regions snapshot")
529                    .attach("reason=duplicate pane id")
530                    .attach(format!("pane_id={}", region.id)));
531            }
532        }
533
534        Ok(())
535    }
536}
537
538#[derive(Clone, Copy, Debug, Eq, PartialEq)]
539pub enum PaneRegionContainment {
540    Inside,
541    Outside,
542}
543
544impl<D> rkyv::Deserialize<PaneRegionsSnapshot, D> for ArchivedPaneRegionsSnapshot
545where
546    D: rkyv::rancor::Fallible + ?Sized,
547    D::Error: rkyv::rancor::Source,
548{
549    fn deserialize(&self, deserializer: &mut D) -> Result<PaneRegionsSnapshot, D::Error> {
550        let regions = rkyv::Deserialize::<Vec<PaneRegionSnapshot>, D>::deserialize(&self.regions, deserializer)?;
551        PaneRegionsSnapshot::new(regions).map_err(super::rkyv_deserialize_error::<D::Error>)
552    }
553}
554
555#[cfg(test)]
556pub mod test_helpers {
557    use super::*;
558
559    pub const fn raw_layout_snapshot(active_tab: TabId, tabs: Vec<TabSnapshot>) -> LayoutSnapshot {
560        LayoutSnapshot { active_tab, tabs }
561    }
562
563    pub fn raw_tab_snapshot(
564        id: TabId,
565        title: impl Into<String>,
566        active_pane: PaneId,
567        panes: Vec<PaneSnapshot>,
568    ) -> TabSnapshot {
569        TabSnapshot {
570            active_pane,
571            id,
572            panes,
573            title: title.into(),
574        }
575    }
576}
577
578#[cfg(test)]
579mod tests {
580    use rootcause::report;
581    use rstest::rstest;
582    use test_that::prelude::*;
583
584    use super::*;
585
586    #[test]
587    fn test_layout_snapshot_single_pane_when_built_returns_stable_layout() -> rootcause::Result<()> {
588        let layout = self::layout_snapshot()?;
589
590        assert_that!(layout.active_tab().get(), eq(1));
591        assert_that!(layout.tabs().len(), eq(1));
592        let Some(tab) = layout.tabs().first() else {
593            return Err(report!("expected one tab"));
594        };
595        assert_that!(tab.active_pane().get(), eq(1));
596        assert_that!(tab.panes().len(), eq(1));
597        Ok(())
598    }
599
600    #[test]
601    fn test_layout_id_new_when_id_is_zero_returns_error() {
602        assert_that!(TabId::new(0), err(anything()));
603        assert_that!(PaneId::new(0), err(anything()));
604    }
605
606    #[test]
607    fn test_layout_id_deserialize_when_id_is_zero_returns_error() {
608        let raw = "0";
609
610        assert_that!(serde_json::from_str::<TabId>(raw), err(anything()));
611        assert_that!(serde_json::from_str::<PaneId>(raw), err(anything()));
612    }
613
614    #[test]
615    fn test_layout_id_display_when_formatted_returns_human_label() -> rootcause::Result<()> {
616        assert_that!(TabId::new(1)?.to_string(), eq("tab-1"));
617        assert_that!(PaneId::new(1)?.to_string(), eq("pane-1"));
618        Ok(())
619    }
620
621    #[rstest]
622    #[case::empty_tabs(test_helpers::raw_layout_snapshot(tab_id(1), Vec::new()))]
623    #[case::missing_active_tab(test_helpers::raw_layout_snapshot(
624            tab_id(99),
625            vec![raw_tab_snapshot(1, "default", 1, vec![raw_pane_snapshot(1, "shell")])],
626        ))]
627    #[case::empty_panes(test_helpers::raw_layout_snapshot(
628            tab_id(1),
629            vec![test_helpers::raw_tab_snapshot(tab_id(1), "default", pane_id(1), Vec::new())],
630        ))]
631    #[case::missing_active_pane(test_helpers::raw_layout_snapshot(
632            tab_id(1),
633            vec![test_helpers::raw_tab_snapshot(
634                tab_id(1),
635                "default",
636                pane_id(99),
637                vec![raw_pane_snapshot(1, "shell")]
638            )],
639        ))]
640    #[case::duplicate_tab(test_helpers::raw_layout_snapshot(
641            tab_id(1),
642            vec![
643                raw_tab_snapshot(1, "default", 1, vec![raw_pane_snapshot(1, "shell")]),
644                raw_tab_snapshot(1, "other", 2, vec![raw_pane_snapshot(2, "shell")]),
645            ],
646        ))]
647    #[case::duplicate_pane(test_helpers::raw_layout_snapshot(
648            tab_id(1),
649            vec![test_helpers::raw_tab_snapshot(
650                tab_id(1),
651                "default",
652                pane_id(1),
653                vec![raw_pane_snapshot(1, "shell"), raw_pane_snapshot(1, "other")]
654            )],
655        ))]
656    #[case::duplicate_pane_across_tabs(test_helpers::raw_layout_snapshot(
657            tab_id(1),
658            vec![
659                raw_tab_snapshot(1, "default", 1, vec![raw_pane_snapshot(1, "shell")]),
660                raw_tab_snapshot(2, "other", 2, vec![
661                    raw_pane_snapshot(1, "other"),
662                    raw_pane_snapshot(2, "shell"),
663                ]),
664            ],
665        ))]
666    fn test_layout_snapshot_validate_when_layout_is_invalid_returns_error(#[case] layout: LayoutSnapshot) {
667        assert_that!(
668            LayoutSnapshot::new(*layout.active_tab(), layout.tabs().to_vec()),
669            err(anything())
670        );
671    }
672
673    fn layout_snapshot() -> rootcause::Result<LayoutSnapshot> {
674        let active_tab = TabId::new(1)?;
675        let active_pane = PaneId::new(1)?;
676        let pane = PaneSnapshot {
677            tracked_process_state: TrackedProcessState::None,
678            cwd: "/tmp".to_owned(),
679            cmd_label: None,
680            focus_seq: 1,
681            id: active_pane,
682            title: "shell".to_owned(),
683        };
684        let tab = TabSnapshot::new(active_tab, "default", active_pane, vec![pane])?;
685        LayoutSnapshot::new(active_tab, vec![tab])
686    }
687
688    fn raw_tab_snapshot(id: u32, title: &str, active_pane: u32, panes: Vec<PaneSnapshot>) -> TabSnapshot {
689        test_helpers::raw_tab_snapshot(tab_id(id), title, pane_id(active_pane), panes)
690    }
691
692    fn raw_pane_snapshot(id: u32, title: &str) -> PaneSnapshot {
693        PaneSnapshot {
694            tracked_process_state: TrackedProcessState::None,
695            cwd: "/tmp".to_owned(),
696            cmd_label: None,
697            focus_seq: 1,
698            id: pane_id(id),
699            title: title.to_owned(),
700        }
701    }
702
703    fn tab_id(id: u32) -> TabId {
704        TabId::new(id).expect("test tab id should be valid")
705    }
706
707    fn pane_id(id: u32) -> PaneId {
708        PaneId::new(id).expect("test pane id should be valid")
709    }
710}