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 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 #[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 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 #[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 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 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#[derive(rkyv::Archive, Clone, Debug, Eq, PartialEq, Serialize, rkyv::Serialize)]
269pub struct PaneSnapshot {
270 pub tracked_process_state: TrackedProcessState,
272 pub cwd: String,
274 pub cmd_label: Option<String>,
276 pub focus_seq: u64,
278 pub id: PaneId,
280 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#[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#[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 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 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 #[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 #[must_use]
411 pub const fn visible_top_row(&self) -> u64 {
412 self.visible_top_row
413 }
414
415 #[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 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}