use crate::icons;
use crate::theme::Theme;
use anyhow::Result;
use ratatui::{
layout::Rect,
text::{Line, Span},
widgets::{Block, Borders, Clear, Paragraph, Wrap},
Frame,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SettingField {
UserName,
AIEnabled,
AIProvider,
Theme,
}
impl SettingField {
pub fn next(self) -> Self {
match self {
Self::UserName => Self::AIEnabled,
Self::AIEnabled => Self::AIProvider,
Self::AIProvider => Self::Theme,
Self::Theme => Self::UserName,
}
}
pub fn previous(self) -> Self {
match self {
Self::UserName => Self::Theme,
Self::AIEnabled => Self::UserName,
Self::AIProvider => Self::AIEnabled,
Self::Theme => Self::AIProvider,
}
}
pub fn name(&self) -> &str {
match self {
Self::UserName => "Name",
Self::AIEnabled => "AI Assistant",
Self::AIProvider => "AI Provider",
Self::Theme => "Theme",
}
}
}
pub struct SettingsPanel {
pub selected_field: SettingField,
pub editing: bool,
pub edit_buffer: String,
}
impl SettingsPanel {
pub fn new() -> Self {
Self {
selected_field: SettingField::UserName,
editing: false,
edit_buffer: String::new(),
}
}
pub fn previous_field(&mut self) {
self.selected_field = self.selected_field.previous();
}
pub fn next_field(&mut self) {
self.selected_field = self.selected_field.next();
}
pub fn start_editing(&mut self, config: &arct_config::Config) {
self.editing = true;
self.edit_buffer = match self.selected_field {
SettingField::UserName => {
config.general.user_name.clone().unwrap_or_default()
}
SettingField::AIEnabled => {
if config.ai.enabled { "true" } else { "false" }.to_string()
}
SettingField::AIProvider => {
config.ai.provider.clone()
}
SettingField::Theme => {
config.theme.default_theme.clone()
}
};
}
pub fn cancel_editing(&mut self) {
self.editing = false;
self.edit_buffer.clear();
}
pub fn save_edit(&mut self, config: &mut arct_config::Config) -> Result<()> {
match self.selected_field {
SettingField::UserName => {
if self.edit_buffer.is_empty() {
config.general.user_name = None;
} else {
config.general.user_name = Some(self.edit_buffer.clone());
}
}
SettingField::AIEnabled => {
let value = self.edit_buffer.to_lowercase();
let enabling = value == "true" || value == "yes" || value == "1" || value == "enabled";
if enabling && !config.ai.enabled {
if config.ai.provider.is_empty() || config.ai.provider == "anthropic" {
if arct_ai::claude_cli::ClaudeCLIProvider::is_available() {
config.ai.provider = "claude-cli".to_string();
config.ai.model = Some("claude-sonnet-4".to_string());
} else {
config.ai.provider = "local".to_string();
config.ai.endpoint = Some("http://localhost:11434".to_string());
config.ai.model = Some("llama3.2".to_string());
}
}
}
config.ai.enabled = enabling;
}
SettingField::AIProvider => {
config.ai.provider = self.edit_buffer.clone();
}
SettingField::Theme => {
config.theme.default_theme = self.edit_buffer.clone();
}
}
config.save()?;
self.editing = false;
self.edit_buffer.clear();
Ok(())
}
pub fn push_char(&mut self, c: char) {
if self.editing {
self.edit_buffer.push(c);
}
}
pub fn pop_char(&mut self) {
if self.editing {
self.edit_buffer.pop();
}
}
pub fn render(&self, frame: &mut Frame, theme: &Theme, config: &arct_config::Config) {
let area = frame.size();
let modal_width = 80.min(area.width - 4);
let modal_height = 28.min(area.height - 4);
let horizontal_margin = (area.width.saturating_sub(modal_width)) / 2;
let vertical_margin = (area.height.saturating_sub(modal_height)) / 2;
let modal_area = Rect {
x: horizontal_margin,
y: vertical_margin,
width: modal_width,
height: modal_height,
};
frame.render_widget(Clear, modal_area);
let title = if self.editing {
format!(" Settings - Editing: {} ", self.selected_field.name())
} else {
" Settings - Use ↑↓ to navigate, Enter to edit ".to_string()
};
let block = Block::default()
.title(title)
.borders(Borders::ALL)
.border_style(theme.style_border_focused())
.style(theme.style_block());
let inner = block.inner(modal_area);
frame.render_widget(block, modal_area);
let user_name = config.general.user_name.as_deref().unwrap_or("Not set");
let ai_status = if config.ai.enabled { "Enabled" } else { "Disabled" };
let ai_provider = &config.ai.provider;
let theme_name = &config.theme.default_theme;
let mut lines = vec![
Line::from(""),
Line::from(vec![
Span::styled(" User Profile", theme.style_header()),
]),
];
self.add_field_line(
&mut lines,
SettingField::UserName,
"Name",
if self.editing && self.selected_field == SettingField::UserName {
&self.edit_buffer
} else {
user_name
},
theme,
);
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::styled(" AI Assistant", theme.style_header()),
]));
self.add_field_line(
&mut lines,
SettingField::AIEnabled,
"Status",
if self.editing && self.selected_field == SettingField::AIEnabled {
&self.edit_buffer
} else {
ai_status
},
theme,
);
self.add_field_line(
&mut lines,
SettingField::AIProvider,
"Provider",
if self.editing && self.selected_field == SettingField::AIProvider {
&self.edit_buffer
} else {
ai_provider
},
theme,
);
if config.ai.enabled {
let note = match ai_provider.as_str() {
"claude-cli" => format!(" {}Using Claude Code CLI - no API key needed", icons::hint().content),
"anthropic" | "openai" => format!(" {}Set API key in config.toml", icons::hint().content),
"local" => format!(" {}Make sure your local LLM server is running", icons::hint().content),
"managed" => format!(" {}Using Arc Academy managed AI", icons::hint().content),
_ => format!(" {}Invalid provider - press Enter to configure", icons::warning().content),
};
lines.push(Line::from(vec![
Span::styled(note, theme.style_dim()),
]));
} else {
lines.push(Line::from(vec![
Span::styled(format!(" {}Enable to use AI features (auto-configures provider)", icons::hint().content), theme.style_dim()),
]));
}
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::styled(" Appearance", theme.style_header()),
]));
self.add_field_line(
&mut lines,
SettingField::Theme,
"Theme",
if self.editing && self.selected_field == SettingField::Theme {
&self.edit_buffer
} else {
theme_name
},
theme,
);
lines.push(Line::from(""));
lines.push(Line::from(""));
if self.editing {
lines.push(Line::from(vec![
Span::styled(" Press ", theme.style_dim()),
Span::styled("Enter", theme.style_accent()),
Span::styled(" to save, ", theme.style_dim()),
Span::styled("Esc", theme.style_accent()),
Span::styled(" to cancel", theme.style_dim()),
]));
} else {
lines.push(Line::from(vec![
Span::styled(" Controls", theme.style_header()),
]));
lines.push(Line::from(vec![
Span::styled(" • Press ", theme.style_dim()),
Span::styled("↑↓", theme.style_accent()),
Span::styled(" to navigate fields", theme.style_dim()),
]));
lines.push(Line::from(vec![
Span::styled(" • Press ", theme.style_dim()),
Span::styled("Enter", theme.style_accent()),
Span::styled(" to edit selected field", theme.style_dim()),
]));
lines.push(Line::from(vec![
Span::styled(" • Press ", theme.style_dim()),
Span::styled("Ctrl+T", theme.style_accent()),
Span::styled(" to cycle themes", theme.style_dim()),
]));
lines.push(Line::from(vec![
Span::styled(" • Press ", theme.style_dim()),
Span::styled("Ctrl+A", theme.style_accent()),
Span::styled(" to toggle AI", theme.style_dim()),
]));
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::styled(" Press ", theme.style_dim()),
Span::styled("Esc", theme.style_accent()),
Span::styled(" or ", theme.style_dim()),
Span::styled("Ctrl+S", theme.style_accent()),
Span::styled(" to close", theme.style_dim()),
]));
}
let paragraph = Paragraph::new(lines).wrap(Wrap { trim: false });
frame.render_widget(paragraph, inner);
}
fn add_field_line<'a>(
&self,
lines: &mut Vec<Line<'a>>,
field: SettingField,
label: &str,
value: &'a str,
theme: &Theme,
) {
let is_selected = self.selected_field == field;
let is_editing = self.editing && is_selected;
let selector = if is_selected { " ▶ " } else { " " };
let mut spans = vec![
Span::styled(selector, theme.style_accent()),
Span::styled(format!("{}: ", label), theme.style_dim()),
];
if is_editing {
spans.push(Span::styled(value, theme.style_accent()));
spans.push(Span::styled("█", theme.style_accent()));
} else if is_selected {
spans.push(Span::styled(value, theme.style_success()));
} else {
spans.push(Span::styled(value, theme.style_normal()));
}
lines.push(Line::from(spans));
}
}
impl Default for SettingsPanel {
fn default() -> Self {
Self::new()
}
}