Skip to main content

mockforge_tui/screens/
mod.rs

1//! Screen trait and registry — each screen owns its own state and renders
2//! into the main content area.
3
4pub mod analytics;
5pub mod audit;
6pub mod behavioral_cloning;
7pub mod chains;
8pub mod chaos;
9pub mod config;
10pub mod contract_diff;
11pub mod dashboard;
12pub mod federation;
13pub mod fixtures;
14pub mod health;
15pub mod import;
16pub mod logs;
17pub mod metrics;
18pub mod plugins;
19pub mod recorder;
20pub mod routes;
21pub mod smoke_tests;
22pub mod time_travel;
23pub mod verification;
24pub mod workspaces;
25pub mod world_state;
26
27use crossterm::event::KeyEvent;
28use ratatui::layout::Rect;
29use ratatui::Frame;
30
31use crate::api::client::MockForgeClient;
32use crate::event::Event;
33use tokio::sync::mpsc;
34
35/// Every screen implements this trait.
36pub trait Screen: Send {
37    /// Display name shown in the tab bar.
38    fn title(&self) -> &str;
39
40    /// Handle a key event. Return `true` if the event was consumed.
41    fn handle_key(&mut self, key: KeyEvent) -> bool;
42
43    /// Render into the given area.
44    fn render(&self, frame: &mut Frame, area: Rect);
45
46    /// Called on tick to refresh data if needed. The screen can spawn
47    /// background fetches via the event sender.
48    fn tick(&mut self, client: &MockForgeClient, tx: &mpsc::UnboundedSender<Event>);
49
50    /// Ingest an event payload pushed by a background data fetcher.
51    fn on_data(&mut self, payload: &str);
52
53    /// Ingest an API error for this screen.
54    fn on_error(&mut self, message: &str);
55
56    /// Hint text for the status bar (screen-specific key hints).
57    fn status_hint(&self) -> &str {
58        ""
59    }
60
61    /// Return the current error message, if any. Used by the app to render
62    /// a persistent error banner while still showing stale data.
63    fn error(&self) -> Option<&str> {
64        None
65    }
66
67    /// Reset internal fetch timer so data is re-fetched on the next tick.
68    fn force_refresh(&mut self) {}
69
70    /// Push a single log line (only meaningful for the Logs screen).
71    fn push_log_line(&mut self, _line: String) {}
72}
73
74/// Screen identifiers used for tab ordering and data routing.
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum ScreenId {
77    Dashboard,
78    Logs,
79    Routes,
80    Metrics,
81    Config,
82    Chaos,
83    Workspaces,
84    Plugins,
85    Fixtures,
86    Health,
87    SmokeTests,
88    TimeTravel,
89    Chains,
90    Verification,
91    Analytics,
92    Recorder,
93    Import,
94    Audit,
95    WorldState,
96    ContractDiff,
97    Federation,
98    BehavioralCloning,
99}
100
101impl ScreenId {
102    /// All screens in tab order.
103    pub const ALL: &[Self] = &[
104        Self::Dashboard,
105        Self::Logs,
106        Self::Routes,
107        Self::Metrics,
108        Self::Config,
109        Self::Chaos,
110        Self::Workspaces,
111        Self::Plugins,
112        Self::Fixtures,
113        Self::Health,
114        Self::SmokeTests,
115        Self::TimeTravel,
116        Self::Chains,
117        Self::Verification,
118        Self::Analytics,
119        Self::Recorder,
120        Self::Import,
121        Self::Audit,
122        Self::WorldState,
123        Self::ContractDiff,
124        Self::Federation,
125        Self::BehavioralCloning,
126    ];
127
128    pub fn label(self) -> &'static str {
129        match self {
130            Self::Dashboard => "Dashboard",
131            Self::Logs => "Logs",
132            Self::Routes => "Routes",
133            Self::Metrics => "Metrics",
134            Self::Config => "Config",
135            Self::Chaos => "Chaos",
136            Self::Workspaces => "Workspaces",
137            Self::Plugins => "Plugins",
138            Self::Fixtures => "Fixtures",
139            Self::Health => "Health",
140            Self::SmokeTests => "Smoke Tests",
141            Self::TimeTravel => "Time Travel",
142            Self::Chains => "Chains",
143            Self::Verification => "Verification",
144            Self::Analytics => "Analytics",
145            Self::Recorder => "Recorder",
146            Self::Import => "Import",
147            Self::Audit => "Audit",
148            Self::WorldState => "World State",
149            Self::ContractDiff => "Contract Diff",
150            Self::Federation => "Federation",
151            Self::BehavioralCloning => "VBR",
152        }
153    }
154
155    pub fn data_key(self) -> &'static str {
156        match self {
157            Self::Dashboard => "dashboard",
158            Self::Logs => "logs",
159            Self::Routes => "routes",
160            Self::Metrics => "metrics",
161            Self::Config => "config",
162            Self::Chaos => "chaos",
163            Self::Workspaces => "workspaces",
164            Self::Plugins => "plugins",
165            Self::Fixtures => "fixtures",
166            Self::Health => "health",
167            Self::SmokeTests => "smoke_tests",
168            Self::TimeTravel => "time_travel",
169            Self::Chains => "chains",
170            Self::Verification => "verification",
171            Self::Analytics => "analytics",
172            Self::Recorder => "recorder",
173            Self::Import => "import",
174            Self::Audit => "audit",
175            Self::WorldState => "world_state",
176            Self::ContractDiff => "contract_diff",
177            Self::Federation => "federation",
178            Self::BehavioralCloning => "behavioral_cloning",
179        }
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn all_has_correct_count() {
189        // Count the enum variants: 22 total
190        assert_eq!(ScreenId::ALL.len(), 22);
191    }
192
193    #[test]
194    fn all_labels_are_non_empty() {
195        for screen_id in ScreenId::ALL {
196            let label = screen_id.label();
197            assert!(!label.is_empty(), "label() returned empty string for {screen_id:?}");
198        }
199    }
200
201    #[test]
202    fn all_data_keys_are_non_empty() {
203        for screen_id in ScreenId::ALL {
204            let key = screen_id.data_key();
205            assert!(!key.is_empty(), "data_key() returned empty string for {screen_id:?}");
206        }
207    }
208
209    #[test]
210    fn all_data_keys_are_snake_case() {
211        for screen_id in ScreenId::ALL {
212            let key = screen_id.data_key();
213            assert!(
214                !key.contains(' ') && !key.contains('-'),
215                "data_key() for {screen_id:?} contains spaces or hyphens: {key}"
216            );
217            assert_eq!(
218                key,
219                key.to_lowercase(),
220                "data_key() for {screen_id:?} is not lowercase: {key}"
221            );
222        }
223    }
224
225    #[test]
226    fn all_data_keys_are_unique() {
227        let keys: Vec<&str> = ScreenId::ALL.iter().map(|s| s.data_key()).collect();
228        let mut deduped = keys.clone();
229        deduped.sort_unstable();
230        deduped.dedup();
231        assert_eq!(keys.len(), deduped.len(), "Duplicate data_key() values found");
232    }
233
234    #[test]
235    fn all_labels_and_data_keys_consistent_count() {
236        let label_count = ScreenId::ALL.iter().filter(|s| !s.label().is_empty()).count();
237        let key_count = ScreenId::ALL.iter().filter(|s| !s.data_key().is_empty()).count();
238        assert_eq!(label_count, key_count);
239        assert_eq!(label_count, ScreenId::ALL.len());
240    }
241
242    #[test]
243    fn specific_labels() {
244        assert_eq!(ScreenId::Dashboard.label(), "Dashboard");
245        assert_eq!(ScreenId::Logs.label(), "Logs");
246        assert_eq!(ScreenId::SmokeTests.label(), "Smoke Tests");
247        assert_eq!(ScreenId::TimeTravel.label(), "Time Travel");
248        assert_eq!(ScreenId::WorldState.label(), "World State");
249        assert_eq!(ScreenId::ContractDiff.label(), "Contract Diff");
250        assert_eq!(ScreenId::BehavioralCloning.label(), "VBR");
251    }
252
253    #[test]
254    fn specific_data_keys() {
255        assert_eq!(ScreenId::Dashboard.data_key(), "dashboard");
256        assert_eq!(ScreenId::SmokeTests.data_key(), "smoke_tests");
257        assert_eq!(ScreenId::TimeTravel.data_key(), "time_travel");
258        assert_eq!(ScreenId::WorldState.data_key(), "world_state");
259        assert_eq!(ScreenId::ContractDiff.data_key(), "contract_diff");
260        assert_eq!(ScreenId::BehavioralCloning.data_key(), "behavioral_cloning");
261    }
262
263    #[test]
264    fn screen_id_equality() {
265        assert_eq!(ScreenId::Dashboard, ScreenId::Dashboard);
266        assert_ne!(ScreenId::Dashboard, ScreenId::Logs);
267    }
268
269    #[test]
270    fn screen_id_clone() {
271        let id = ScreenId::Dashboard;
272        let cloned = id;
273        assert_eq!(id, cloned);
274    }
275}