1pub 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
35pub trait Screen: Send {
37 fn title(&self) -> &str;
39
40 fn handle_key(&mut self, key: KeyEvent) -> bool;
42
43 fn render(&self, frame: &mut Frame, area: Rect);
45
46 fn tick(&mut self, client: &MockForgeClient, tx: &mpsc::UnboundedSender<Event>);
49
50 fn on_data(&mut self, payload: &str);
52
53 fn on_error(&mut self, message: &str);
55
56 fn status_hint(&self) -> &str {
58 ""
59 }
60
61 fn error(&self) -> Option<&str> {
64 None
65 }
66
67 fn force_refresh(&mut self) {}
69
70 fn push_log_line(&mut self, _line: String) {}
72}
73
74#[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 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 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}