use crate::brick::{Brick, BrickAssertion, BrickBudget, BrickVerification};
use presentar_core::{Canvas, Color, Point, TextStyle};
use presentar_terminal::Theme;
use std::any::Any;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfigProfile {
pub name: String,
pub description: String,
pub is_active: bool,
}
impl ConfigProfile {
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: description.into(),
is_active: false,
}
}
pub fn active(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: description.into(),
is_active: true,
}
}
}
pub struct ConfigPanelBrick {
pub config_path: String,
pub profiles: Vec<ConfigProfile>,
pub selected_index: usize,
pub auto_save: bool,
pub load_last: bool,
pub theme: Theme,
}
impl ConfigPanelBrick {
pub fn new() -> Self {
Self {
config_path: "~/.config/cbtop/config.toml".to_string(),
profiles: vec![
ConfigProfile::active("inference", "LLM Inference"),
ConfigProfile::new("ml_training", "ML Training"),
ConfigProfile::new("stress_test", "Stress Testing"),
ConfigProfile::new("power_saving", "Power Saving"),
],
selected_index: 0,
auto_save: true,
load_last: true,
theme: Theme::tokyo_night(),
}
}
pub fn active_profile(&self) -> Option<&ConfigProfile> {
self.profiles.iter().find(|p| p.is_active)
}
pub fn next_profile(&mut self) {
if !self.profiles.is_empty() {
self.selected_index = (self.selected_index + 1) % self.profiles.len();
}
}
pub fn prev_profile(&mut self) {
if !self.profiles.is_empty() {
self.selected_index =
(self.selected_index + self.profiles.len() - 1) % self.profiles.len();
}
}
pub fn activate_selected(&mut self) {
for (i, profile) in self.profiles.iter_mut().enumerate() {
profile.is_active = i == self.selected_index;
}
}
pub fn toggle_auto_save(&mut self) {
self.auto_save = !self.auto_save;
}
pub fn toggle_load_last(&mut self) {
self.load_last = !self.load_last;
}
pub fn paint(&self, canvas: &mut dyn Canvas, _width: f32, _height: f32) {
let label_style = TextStyle {
color: self.theme.foreground,
..Default::default()
};
let dim_style = TextStyle {
color: self.theme.dim,
..Default::default()
};
let active_style = TextStyle {
color: Color::new(0.3, 1.0, 0.5, 1.0), ..Default::default()
};
let selected_style = TextStyle {
color: Color::new(0.3, 0.8, 1.0, 1.0), ..Default::default()
};
canvas.draw_text("Configuration", Point::new(2.0, 2.0), &label_style);
canvas.draw_text("Config:", Point::new(2.0, 4.0), &dim_style);
canvas.draw_text(&self.config_path, Point::new(10.0, 4.0), &label_style);
if let Some(profile) = self.active_profile() {
canvas.draw_text("Profile:", Point::new(2.0, 5.0), &dim_style);
canvas.draw_text(
&format!("{} ({})", profile.name, profile.description),
Point::new(10.0, 5.0),
&active_style,
);
}
let auto_save_check = if self.auto_save { "[x]" } else { "[ ]" };
let load_last_check = if self.load_last { "[x]" } else { "[ ]" };
canvas.draw_text(
&format!("{} Auto-save on exit", auto_save_check),
Point::new(2.0, 7.0),
&label_style,
);
canvas.draw_text(
&format!("{} Load last profile on start", load_last_check),
Point::new(2.0, 8.0),
&label_style,
);
canvas.draw_text("Profiles:", Point::new(2.0, 10.0), &dim_style);
for (i, profile) in self.profiles.iter().enumerate() {
let y = 11.0 + i as f32;
let prefix = if i == self.selected_index {
" > "
} else {
" "
};
let suffix = if profile.is_active { " (active)" } else { "" };
let style = if i == self.selected_index {
&selected_style
} else if profile.is_active {
&active_style
} else {
&label_style
};
canvas.draw_text(
&format!("{}{}{}", prefix, profile.name, suffix),
Point::new(2.0, y),
style,
);
}
let help_y = 11.0 + self.profiles.len() as f32 + 2.0;
canvas.draw_text(
"Press 'P' to activate profile",
Point::new(2.0, help_y),
&dim_style,
);
canvas.draw_text(
"Press 'S' to save current as new profile",
Point::new(2.0, help_y + 1.0),
&dim_style,
);
}
}
impl Default for ConfigPanelBrick {
fn default() -> Self {
Self::new()
}
}
impl Brick for ConfigPanelBrick {
fn brick_name(&self) -> &'static str {
"config_panel"
}
fn assertions(&self) -> Vec<BrickAssertion> {
vec![
BrickAssertion::MinWidth(45),
BrickAssertion::MinHeight(18),
BrickAssertion::max_latency_ms(8),
]
}
fn budget(&self) -> BrickBudget {
BrickBudget::FRAME_60FPS
}
fn verify(&self) -> BrickVerification {
let mut v = BrickVerification::new();
for assertion in self.assertions() {
v.check(&assertion);
}
v
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use presentar_core::RecordingCanvas;
#[test]
fn test_config_panel_brick_name() {
let panel = ConfigPanelBrick::new();
assert_eq!(panel.brick_name(), "config_panel");
}
#[test]
fn test_config_panel_paint_default() {
let panel = ConfigPanelBrick::new();
let mut canvas = RecordingCanvas::new();
panel.paint(&mut canvas, 80.0, 24.0);
assert!(!canvas.is_empty());
assert!(canvas.command_count() >= 10);
}
#[test]
fn test_config_panel_paint_profile_list() {
let panel = ConfigPanelBrick::new();
let mut canvas = RecordingCanvas::new();
panel.paint(&mut canvas, 80.0, 24.0);
assert!(canvas.command_count() >= 10);
}
#[test]
fn test_config_panel_paint_different_selections() {
let mut panel = ConfigPanelBrick::new();
for i in 0..panel.profiles.len() {
panel.selected_index = i;
let mut canvas = RecordingCanvas::new();
panel.paint(&mut canvas, 80.0, 24.0);
assert!(!canvas.is_empty());
}
}
#[test]
fn test_config_panel_paint_with_settings_toggled() {
let mut panel = ConfigPanelBrick::new();
panel.toggle_auto_save();
panel.toggle_load_last();
let mut canvas = RecordingCanvas::new();
panel.paint(&mut canvas, 80.0, 24.0);
assert!(!canvas.is_empty());
}
#[test]
fn test_config_panel_paint_no_active_profile() {
let mut panel = ConfigPanelBrick::new();
for profile in &mut panel.profiles {
profile.is_active = false;
}
let mut canvas = RecordingCanvas::new();
panel.paint(&mut canvas, 80.0, 24.0);
assert!(!canvas.is_empty());
}
#[test]
fn test_config_panel_has_assertions() {
let panel = ConfigPanelBrick::new();
assert!(!panel.assertions().is_empty());
}
#[test]
fn test_config_panel_default_profiles() {
let panel = ConfigPanelBrick::new();
assert_eq!(panel.profiles.len(), 4);
assert!(panel.active_profile().is_some());
assert_eq!(panel.active_profile().unwrap().name, "inference");
}
#[test]
fn test_profile_navigation() {
let mut panel = ConfigPanelBrick::new();
assert_eq!(panel.selected_index, 0);
panel.next_profile();
assert_eq!(panel.selected_index, 1);
panel.prev_profile();
assert_eq!(panel.selected_index, 0);
panel.prev_profile();
assert_eq!(panel.selected_index, 3); }
#[test]
fn test_activate_profile() {
let mut panel = ConfigPanelBrick::new();
panel.selected_index = 2;
panel.activate_selected();
assert!(!panel.profiles[0].is_active);
assert!(!panel.profiles[1].is_active);
assert!(panel.profiles[2].is_active);
assert!(!panel.profiles[3].is_active);
}
#[test]
fn test_toggle_settings() {
let mut panel = ConfigPanelBrick::new();
assert!(panel.auto_save);
assert!(panel.load_last);
panel.toggle_auto_save();
assert!(!panel.auto_save);
panel.toggle_load_last();
assert!(!panel.load_last);
}
#[test]
fn test_profile_creation() {
let profile = ConfigProfile::new("test", "Test Profile");
assert_eq!(profile.name, "test");
assert_eq!(profile.description, "Test Profile");
assert!(!profile.is_active);
let active = ConfigProfile::active("active", "Active Profile");
assert!(active.is_active);
}
}