1pub mod analytics;
5pub mod audit;
6pub mod behavioral_cloning;
7pub mod chains;
8pub mod chaos;
9pub mod config;
10pub mod conformance;
11pub mod contract_diff;
12pub mod dashboard;
13pub mod federation;
14pub mod fixtures;
15pub mod health;
16pub mod import;
17pub mod logs;
18pub mod metrics;
19pub mod plugins;
20pub mod recorder;
21pub mod routes;
22pub mod smoke_tests;
23pub mod time_travel;
24pub mod verification;
25pub mod workspaces;
26pub mod world_state;
27
28use crossterm::event::KeyEvent;
29use ratatui::layout::Rect;
30use ratatui::Frame;
31
32use crate::api::client::MockForgeClient;
33use crate::event::Event;
34use tokio::sync::mpsc;
35
36pub trait Screen: Send {
38 fn title(&self) -> &str;
40
41 fn handle_key(&mut self, key: KeyEvent) -> bool;
43
44 fn render(&self, frame: &mut Frame, area: Rect);
46
47 fn tick(&mut self, client: &MockForgeClient, tx: &mpsc::UnboundedSender<Event>);
50
51 fn on_data(&mut self, payload: &str);
53
54 fn on_error(&mut self, message: &str);
56
57 fn status_hint(&self) -> &str {
59 ""
60 }
61
62 fn error(&self) -> Option<&str> {
65 None
66 }
67
68 fn force_refresh(&mut self) {}
70
71 fn push_log_line(&mut self, _line: String) {}
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum ScreenId {
78 Dashboard,
79 Logs,
80 Routes,
81 Metrics,
82 Config,
83 Chaos,
84 Workspaces,
85 Plugins,
86 Fixtures,
87 Health,
88 SmokeTests,
89 TimeTravel,
90 Chains,
91 Verification,
92 Analytics,
93 Recorder,
94 Import,
95 Audit,
96 WorldState,
97 ContractDiff,
98 Federation,
99 BehavioralCloning,
100 Conformance,
101}
102
103impl ScreenId {
104 pub const ALL: &[Self] = &[
106 Self::Dashboard,
107 Self::Logs,
108 Self::Routes,
109 Self::Metrics,
110 Self::Config,
111 Self::Chaos,
112 Self::Workspaces,
113 Self::Plugins,
114 Self::Fixtures,
115 Self::Health,
116 Self::SmokeTests,
117 Self::TimeTravel,
118 Self::Chains,
119 Self::Verification,
120 Self::Analytics,
121 Self::Recorder,
122 Self::Import,
123 Self::Audit,
124 Self::WorldState,
125 Self::ContractDiff,
126 Self::Federation,
127 Self::BehavioralCloning,
128 Self::Conformance,
129 ];
130
131 pub fn label(self) -> &'static str {
132 match self {
133 Self::Dashboard => "Dashboard",
134 Self::Logs => "Logs",
135 Self::Routes => "Routes",
136 Self::Metrics => "Metrics",
137 Self::Config => "Config",
138 Self::Chaos => "Chaos",
139 Self::Workspaces => "Workspaces",
140 Self::Plugins => "Plugins",
141 Self::Fixtures => "Fixtures",
142 Self::Health => "Health",
143 Self::SmokeTests => "Smoke Tests",
144 Self::TimeTravel => "Time Travel",
145 Self::Chains => "Chains",
146 Self::Verification => "Verification",
147 Self::Analytics => "Analytics",
148 Self::Recorder => "Recorder",
149 Self::Import => "Import",
150 Self::Audit => "Audit",
151 Self::WorldState => "World State",
152 Self::ContractDiff => "Contract Diff",
153 Self::Federation => "Federation",
154 Self::BehavioralCloning => "VBR",
155 Self::Conformance => "Conformance",
156 }
157 }
158
159 pub fn data_key(self) -> &'static str {
160 match self {
161 Self::Dashboard => "dashboard",
162 Self::Logs => "logs",
163 Self::Routes => "routes",
164 Self::Metrics => "metrics",
165 Self::Config => "config",
166 Self::Chaos => "chaos",
167 Self::Workspaces => "workspaces",
168 Self::Plugins => "plugins",
169 Self::Fixtures => "fixtures",
170 Self::Health => "health",
171 Self::SmokeTests => "smoke_tests",
172 Self::TimeTravel => "time_travel",
173 Self::Chains => "chains",
174 Self::Verification => "verification",
175 Self::Analytics => "analytics",
176 Self::Recorder => "recorder",
177 Self::Import => "import",
178 Self::Audit => "audit",
179 Self::WorldState => "world_state",
180 Self::ContractDiff => "contract_diff",
181 Self::Federation => "federation",
182 Self::BehavioralCloning => "behavioral_cloning",
183 Self::Conformance => "conformance",
184 }
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn all_has_correct_count() {
194 assert_eq!(ScreenId::ALL.len(), 23);
196 }
197
198 #[test]
199 fn all_labels_are_non_empty() {
200 for screen_id in ScreenId::ALL {
201 let label = screen_id.label();
202 assert!(!label.is_empty(), "label() returned empty string for {screen_id:?}");
203 }
204 }
205
206 #[test]
207 fn all_data_keys_are_non_empty() {
208 for screen_id in ScreenId::ALL {
209 let key = screen_id.data_key();
210 assert!(!key.is_empty(), "data_key() returned empty string for {screen_id:?}");
211 }
212 }
213
214 #[test]
215 fn all_data_keys_are_snake_case() {
216 for screen_id in ScreenId::ALL {
217 let key = screen_id.data_key();
218 assert!(
219 !key.contains(' ') && !key.contains('-'),
220 "data_key() for {screen_id:?} contains spaces or hyphens: {key}"
221 );
222 assert_eq!(
223 key,
224 key.to_lowercase(),
225 "data_key() for {screen_id:?} is not lowercase: {key}"
226 );
227 }
228 }
229
230 #[test]
231 fn all_data_keys_are_unique() {
232 let keys: Vec<&str> = ScreenId::ALL.iter().map(|s| s.data_key()).collect();
233 let mut deduped = keys.clone();
234 deduped.sort_unstable();
235 deduped.dedup();
236 assert_eq!(keys.len(), deduped.len(), "Duplicate data_key() values found");
237 }
238
239 #[test]
240 fn all_labels_and_data_keys_consistent_count() {
241 let label_count = ScreenId::ALL.iter().filter(|s| !s.label().is_empty()).count();
242 let key_count = ScreenId::ALL.iter().filter(|s| !s.data_key().is_empty()).count();
243 assert_eq!(label_count, key_count);
244 assert_eq!(label_count, ScreenId::ALL.len());
245 }
246
247 #[test]
248 fn specific_labels() {
249 assert_eq!(ScreenId::Dashboard.label(), "Dashboard");
250 assert_eq!(ScreenId::Logs.label(), "Logs");
251 assert_eq!(ScreenId::SmokeTests.label(), "Smoke Tests");
252 assert_eq!(ScreenId::TimeTravel.label(), "Time Travel");
253 assert_eq!(ScreenId::WorldState.label(), "World State");
254 assert_eq!(ScreenId::ContractDiff.label(), "Contract Diff");
255 assert_eq!(ScreenId::BehavioralCloning.label(), "VBR");
256 }
257
258 #[test]
259 fn specific_data_keys() {
260 assert_eq!(ScreenId::Dashboard.data_key(), "dashboard");
261 assert_eq!(ScreenId::SmokeTests.data_key(), "smoke_tests");
262 assert_eq!(ScreenId::TimeTravel.data_key(), "time_travel");
263 assert_eq!(ScreenId::WorldState.data_key(), "world_state");
264 assert_eq!(ScreenId::ContractDiff.data_key(), "contract_diff");
265 assert_eq!(ScreenId::BehavioralCloning.data_key(), "behavioral_cloning");
266 }
267
268 #[test]
269 fn screen_id_equality() {
270 assert_eq!(ScreenId::Dashboard, ScreenId::Dashboard);
271 assert_ne!(ScreenId::Dashboard, ScreenId::Logs);
272 }
273
274 #[test]
275 fn screen_id_clone() {
276 let id = ScreenId::Dashboard;
277 let cloned = id;
278 assert_eq!(id, cloned);
279 }
280}