//! Frontend-local, read-only observer presentation state (`SMH-SPEC-SPEC0001`, Observer mode). #![deny(unsafe_code)] /// Maximum retained visible records and UTF-8 bytes per row. pub const ROWS: usize = 12; /// Byte budget for each visible line, including label and clipping marker. pub const ROW_BYTES: usize = 512; /// Prepared, bounded visible row; never an owned session frame. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Row { /// Entry identity for explicit fork/search results. pub id: String, /// One clipped UTF-8 line. pub text: String, } /// Navigation lives only in the frontend, never in persisted selection. #[derive(Debug, Default)] pub struct Observer { /// First visible record index in selected history. pub start: usize, /// Total records in most recent published snapshot. pub total: usize, /// Whether a worker has reported writer activity yet. pub status_known: bool, /// Last complete snapshot's writer activity. pub writer_active: bool, /// Follow must be explicitly enabled; browsing disables it. pub follow: bool, /// Only bounded prepared rows are retained. pub rows: Vec, /// Search query being entered, local to this frontend. pub search: String, /// Search entry, if found in most recent query. pub found: Option, /// Recent operation status. pub notice: String, } impl Observer { /// Navigate back without changing selected history. pub const fn back(&mut self) { self.follow = false; self.start = self.start.saturating_sub(ROWS); } /// Navigate forward without enabling follow. pub fn forward(&mut self) { self.follow = false; self.start = self .start .saturating_add(ROWS) .min(self.total.saturating_sub(ROWS)); } /// Jump to last page and toggle explicit live following. pub const fn toggle_follow(&mut self) { self.follow = !self.follow; if self.follow { self.start = self.total.saturating_sub(ROWS); } } /// Replace the prepared snapshot without jumping while browsing. pub fn publish(&mut self, total: usize, writer_active: bool, rows: Vec) { self.total = total; self.writer_active = writer_active; self.status_known = true; if self.follow { self.start = total.saturating_sub(ROWS); } self.rows = rows; } } #[cfg(test)] mod tests { use super::*; #[test] fn published_live_rows_never_jump_while_browsing() { let mut ui = Observer::default(); ui.publish(30, true, vec![]); assert_eq!(ui.start, 0); ui.toggle_follow(); assert_eq!(ui.start, 18); ui.publish(32, true, vec![]); assert_eq!(ui.start, 20); ui.back(); assert_eq!(ui.start, 8); ui.publish(80, false, vec![]); assert_eq!(ui.start, 8); assert!(!ui.follow); } #[test] fn state_is_small_without_payloads() { assert!(std::mem::size_of::() <= 256); } }