1use crate::brick::{Brick, BrickAssertion, BrickBudget, BrickVerification};
6use presentar_core::{Canvas, Color, Point, TextStyle};
7use presentar_terminal::Theme;
8use std::any::Any;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct ConfigProfile {
13 pub name: String,
15 pub description: String,
17 pub is_active: bool,
19}
20
21impl ConfigProfile {
22 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
24 Self {
25 name: name.into(),
26 description: description.into(),
27 is_active: false,
28 }
29 }
30
31 pub fn active(name: impl Into<String>, description: impl Into<String>) -> Self {
33 Self {
34 name: name.into(),
35 description: description.into(),
36 is_active: true,
37 }
38 }
39}
40
41pub struct ConfigPanelBrick {
43 pub config_path: String,
45 pub profiles: Vec<ConfigProfile>,
47 pub selected_index: usize,
49 pub auto_save: bool,
51 pub load_last: bool,
53 pub theme: Theme,
55}
56
57impl ConfigPanelBrick {
58 pub fn new() -> Self {
60 Self {
61 config_path: "~/.config/cbtop/config.toml".to_string(),
62 profiles: vec![
63 ConfigProfile::active("inference", "LLM Inference"),
64 ConfigProfile::new("ml_training", "ML Training"),
65 ConfigProfile::new("stress_test", "Stress Testing"),
66 ConfigProfile::new("power_saving", "Power Saving"),
67 ],
68 selected_index: 0,
69 auto_save: true,
70 load_last: true,
71 theme: Theme::tokyo_night(),
72 }
73 }
74
75 pub fn active_profile(&self) -> Option<&ConfigProfile> {
77 self.profiles.iter().find(|p| p.is_active)
78 }
79
80 pub fn next_profile(&mut self) {
82 if !self.profiles.is_empty() {
83 self.selected_index = (self.selected_index + 1) % self.profiles.len();
84 }
85 }
86
87 pub fn prev_profile(&mut self) {
89 if !self.profiles.is_empty() {
90 self.selected_index =
91 (self.selected_index + self.profiles.len() - 1) % self.profiles.len();
92 }
93 }
94
95 pub fn activate_selected(&mut self) {
97 for (i, profile) in self.profiles.iter_mut().enumerate() {
98 profile.is_active = i == self.selected_index;
99 }
100 }
101
102 pub fn toggle_auto_save(&mut self) {
104 self.auto_save = !self.auto_save;
105 }
106
107 pub fn toggle_load_last(&mut self) {
109 self.load_last = !self.load_last;
110 }
111
112 pub fn paint(&self, canvas: &mut dyn Canvas, _width: f32, _height: f32) {
114 let label_style = TextStyle {
115 color: self.theme.foreground,
116 ..Default::default()
117 };
118 let dim_style = TextStyle {
119 color: self.theme.dim,
120 ..Default::default()
121 };
122 let active_style = TextStyle {
123 color: Color::new(0.3, 1.0, 0.5, 1.0), ..Default::default()
125 };
126 let selected_style = TextStyle {
127 color: Color::new(0.3, 0.8, 1.0, 1.0), ..Default::default()
129 };
130
131 canvas.draw_text("Configuration", Point::new(2.0, 2.0), &label_style);
132
133 canvas.draw_text("Config:", Point::new(2.0, 4.0), &dim_style);
135 canvas.draw_text(&self.config_path, Point::new(10.0, 4.0), &label_style);
136
137 if let Some(profile) = self.active_profile() {
139 canvas.draw_text("Profile:", Point::new(2.0, 5.0), &dim_style);
140 canvas.draw_text(
141 &format!("{} ({})", profile.name, profile.description),
142 Point::new(10.0, 5.0),
143 &active_style,
144 );
145 }
146
147 let auto_save_check = if self.auto_save { "[x]" } else { "[ ]" };
149 let load_last_check = if self.load_last { "[x]" } else { "[ ]" };
150
151 canvas.draw_text(
152 &format!("{} Auto-save on exit", auto_save_check),
153 Point::new(2.0, 7.0),
154 &label_style,
155 );
156 canvas.draw_text(
157 &format!("{} Load last profile on start", load_last_check),
158 Point::new(2.0, 8.0),
159 &label_style,
160 );
161
162 canvas.draw_text("Profiles:", Point::new(2.0, 10.0), &dim_style);
164
165 for (i, profile) in self.profiles.iter().enumerate() {
166 let y = 11.0 + i as f32;
167 let prefix = if i == self.selected_index {
168 " > "
169 } else {
170 " "
171 };
172 let suffix = if profile.is_active { " (active)" } else { "" };
173
174 let style = if i == self.selected_index {
175 &selected_style
176 } else if profile.is_active {
177 &active_style
178 } else {
179 &label_style
180 };
181
182 canvas.draw_text(
183 &format!("{}{}{}", prefix, profile.name, suffix),
184 Point::new(2.0, y),
185 style,
186 );
187 }
188
189 let help_y = 11.0 + self.profiles.len() as f32 + 2.0;
191 canvas.draw_text(
192 "Press 'P' to activate profile",
193 Point::new(2.0, help_y),
194 &dim_style,
195 );
196 canvas.draw_text(
197 "Press 'S' to save current as new profile",
198 Point::new(2.0, help_y + 1.0),
199 &dim_style,
200 );
201 }
202}
203
204impl Default for ConfigPanelBrick {
205 fn default() -> Self {
206 Self::new()
207 }
208}
209
210impl Brick for ConfigPanelBrick {
211 fn brick_name(&self) -> &'static str {
212 "config_panel"
213 }
214
215 fn assertions(&self) -> Vec<BrickAssertion> {
216 vec![
217 BrickAssertion::MinWidth(45),
218 BrickAssertion::MinHeight(18),
219 BrickAssertion::max_latency_ms(8),
220 ]
221 }
222
223 fn budget(&self) -> BrickBudget {
224 BrickBudget::FRAME_60FPS
225 }
226
227 fn verify(&self) -> BrickVerification {
228 let mut v = BrickVerification::new();
229 for assertion in self.assertions() {
230 v.check(&assertion);
231 }
232 v
233 }
234
235 fn as_any(&self) -> &dyn Any {
236 self
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use presentar_core::RecordingCanvas;
244
245 #[test]
246 fn test_config_panel_brick_name() {
247 let panel = ConfigPanelBrick::new();
248 assert_eq!(panel.brick_name(), "config_panel");
249 }
250
251 #[test]
252 fn test_config_panel_paint_default() {
253 let panel = ConfigPanelBrick::new();
254 let mut canvas = RecordingCanvas::new();
255
256 panel.paint(&mut canvas, 80.0, 24.0);
257
258 assert!(!canvas.is_empty());
260 assert!(canvas.command_count() >= 10);
261 }
262
263 #[test]
264 fn test_config_panel_paint_profile_list() {
265 let panel = ConfigPanelBrick::new();
266 let mut canvas = RecordingCanvas::new();
267
268 panel.paint(&mut canvas, 80.0, 24.0);
269
270 assert!(canvas.command_count() >= 10);
274 }
275
276 #[test]
277 fn test_config_panel_paint_different_selections() {
278 let mut panel = ConfigPanelBrick::new();
279
280 for i in 0..panel.profiles.len() {
282 panel.selected_index = i;
283 let mut canvas = RecordingCanvas::new();
284 panel.paint(&mut canvas, 80.0, 24.0);
285 assert!(!canvas.is_empty());
286 }
287 }
288
289 #[test]
290 fn test_config_panel_paint_with_settings_toggled() {
291 let mut panel = ConfigPanelBrick::new();
292
293 panel.toggle_auto_save();
295 panel.toggle_load_last();
296
297 let mut canvas = RecordingCanvas::new();
298 panel.paint(&mut canvas, 80.0, 24.0);
299
300 assert!(!canvas.is_empty());
302 }
303
304 #[test]
305 fn test_config_panel_paint_no_active_profile() {
306 let mut panel = ConfigPanelBrick::new();
307
308 for profile in &mut panel.profiles {
310 profile.is_active = false;
311 }
312
313 let mut canvas = RecordingCanvas::new();
314 panel.paint(&mut canvas, 80.0, 24.0);
315
316 assert!(!canvas.is_empty());
318 }
319
320 #[test]
321 fn test_config_panel_has_assertions() {
322 let panel = ConfigPanelBrick::new();
323 assert!(!panel.assertions().is_empty());
324 }
325
326 #[test]
327 fn test_config_panel_default_profiles() {
328 let panel = ConfigPanelBrick::new();
329 assert_eq!(panel.profiles.len(), 4);
330 assert!(panel.active_profile().is_some());
331 assert_eq!(panel.active_profile().unwrap().name, "inference");
332 }
333
334 #[test]
335 fn test_profile_navigation() {
336 let mut panel = ConfigPanelBrick::new();
337 assert_eq!(panel.selected_index, 0);
338
339 panel.next_profile();
340 assert_eq!(panel.selected_index, 1);
341
342 panel.prev_profile();
343 assert_eq!(panel.selected_index, 0);
344
345 panel.prev_profile();
346 assert_eq!(panel.selected_index, 3); }
348
349 #[test]
350 fn test_activate_profile() {
351 let mut panel = ConfigPanelBrick::new();
352
353 panel.selected_index = 2;
354 panel.activate_selected();
355
356 assert!(!panel.profiles[0].is_active);
357 assert!(!panel.profiles[1].is_active);
358 assert!(panel.profiles[2].is_active);
359 assert!(!panel.profiles[3].is_active);
360 }
361
362 #[test]
363 fn test_toggle_settings() {
364 let mut panel = ConfigPanelBrick::new();
365 assert!(panel.auto_save);
366 assert!(panel.load_last);
367
368 panel.toggle_auto_save();
369 assert!(!panel.auto_save);
370
371 panel.toggle_load_last();
372 assert!(!panel.load_last);
373 }
374
375 #[test]
376 fn test_profile_creation() {
377 let profile = ConfigProfile::new("test", "Test Profile");
378 assert_eq!(profile.name, "test");
379 assert_eq!(profile.description, "Test Profile");
380 assert!(!profile.is_active);
381
382 let active = ConfigProfile::active("active", "Active Profile");
383 assert!(active.is_active);
384 }
385}