1use crate::icons;
4use crate::theme::Theme;
5use anyhow::Result;
6use ratatui::{
7 layout::Rect,
8 text::{Line, Span},
9 widgets::{Block, Borders, Clear, Paragraph, Wrap},
10 Frame,
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum SettingField {
16 UserName,
17 AIEnabled,
18 AIProvider,
19 Theme,
20}
21
22impl SettingField {
23 pub fn next(self) -> Self {
25 match self {
26 Self::UserName => Self::AIEnabled,
27 Self::AIEnabled => Self::AIProvider,
28 Self::AIProvider => Self::Theme,
29 Self::Theme => Self::UserName,
30 }
31 }
32
33 pub fn previous(self) -> Self {
35 match self {
36 Self::UserName => Self::Theme,
37 Self::AIEnabled => Self::UserName,
38 Self::AIProvider => Self::AIEnabled,
39 Self::Theme => Self::AIProvider,
40 }
41 }
42
43 pub fn name(&self) -> &str {
45 match self {
46 Self::UserName => "Name",
47 Self::AIEnabled => "AI Assistant",
48 Self::AIProvider => "AI Provider",
49 Self::Theme => "Theme",
50 }
51 }
52}
53
54pub struct SettingsPanel {
56 pub selected_field: SettingField,
58
59 pub editing: bool,
61
62 pub edit_buffer: String,
64}
65
66impl SettingsPanel {
67 pub fn new() -> Self {
68 Self {
69 selected_field: SettingField::UserName,
70 editing: false,
71 edit_buffer: String::new(),
72 }
73 }
74
75 pub fn previous_field(&mut self) {
77 self.selected_field = self.selected_field.previous();
78 }
79
80 pub fn next_field(&mut self) {
82 self.selected_field = self.selected_field.next();
83 }
84
85 pub fn start_editing(&mut self, config: &arct_config::Config) {
87 self.editing = true;
88 self.edit_buffer = match self.selected_field {
89 SettingField::UserName => {
90 config.general.user_name.clone().unwrap_or_default()
91 }
92 SettingField::AIEnabled => {
93 if config.ai.enabled { "true" } else { "false" }.to_string()
94 }
95 SettingField::AIProvider => {
96 config.ai.provider.clone()
97 }
98 SettingField::Theme => {
99 config.theme.default_theme.clone()
100 }
101 };
102 }
103
104 pub fn cancel_editing(&mut self) {
106 self.editing = false;
107 self.edit_buffer.clear();
108 }
109
110 pub fn save_edit(&mut self, config: &mut arct_config::Config) -> Result<()> {
112 match self.selected_field {
113 SettingField::UserName => {
114 if self.edit_buffer.is_empty() {
115 config.general.user_name = None;
116 } else {
117 config.general.user_name = Some(self.edit_buffer.clone());
118 }
119 }
120 SettingField::AIEnabled => {
121 let value = self.edit_buffer.to_lowercase();
122 let enabling = value == "true" || value == "yes" || value == "1" || value == "enabled";
123
124 if enabling && !config.ai.enabled {
126 if config.ai.provider.is_empty() || config.ai.provider == "anthropic" {
128 if arct_ai::claude_cli::ClaudeCLIProvider::is_available() {
130 config.ai.provider = "claude-cli".to_string();
131 config.ai.model = Some("claude-sonnet-4".to_string());
132 } else {
133 config.ai.provider = "local".to_string();
135 config.ai.endpoint = Some("http://localhost:11434".to_string());
136 config.ai.model = Some("llama3.2".to_string());
137 }
138 }
139 }
140
141 config.ai.enabled = enabling;
142 }
143 SettingField::AIProvider => {
144 config.ai.provider = self.edit_buffer.clone();
145 }
146 SettingField::Theme => {
147 config.theme.default_theme = self.edit_buffer.clone();
148 }
149 }
150
151 config.save()?;
152 self.editing = false;
153 self.edit_buffer.clear();
154 Ok(())
155 }
156
157 pub fn push_char(&mut self, c: char) {
159 if self.editing {
160 self.edit_buffer.push(c);
161 }
162 }
163
164 pub fn pop_char(&mut self) {
166 if self.editing {
167 self.edit_buffer.pop();
168 }
169 }
170
171 pub fn render(&self, frame: &mut Frame, theme: &Theme, config: &arct_config::Config) {
173 let area = frame.size();
174
175 let modal_width = 80.min(area.width - 4);
177 let modal_height = 28.min(area.height - 4);
178
179 let horizontal_margin = (area.width.saturating_sub(modal_width)) / 2;
180 let vertical_margin = (area.height.saturating_sub(modal_height)) / 2;
181
182 let modal_area = Rect {
183 x: horizontal_margin,
184 y: vertical_margin,
185 width: modal_width,
186 height: modal_height,
187 };
188
189 frame.render_widget(Clear, modal_area);
191
192 let title = if self.editing {
193 format!(" Settings - Editing: {} ", self.selected_field.name())
194 } else {
195 " Settings - Use ↑↓ to navigate, Enter to edit ".to_string()
196 };
197
198 let block = Block::default()
199 .title(title)
200 .borders(Borders::ALL)
201 .border_style(theme.style_border_focused())
202 .style(theme.style_block()); let inner = block.inner(modal_area);
205 frame.render_widget(block, modal_area);
206
207 let user_name = config.general.user_name.as_deref().unwrap_or("Not set");
209 let ai_status = if config.ai.enabled { "Enabled" } else { "Disabled" };
210 let ai_provider = &config.ai.provider;
211 let theme_name = &config.theme.default_theme;
212
213 let mut lines = vec![
214 Line::from(""),
215 Line::from(vec![
216 Span::styled(" User Profile", theme.style_header()),
217 ]),
218 ];
219
220 self.add_field_line(
222 &mut lines,
223 SettingField::UserName,
224 "Name",
225 if self.editing && self.selected_field == SettingField::UserName {
226 &self.edit_buffer
227 } else {
228 user_name
229 },
230 theme,
231 );
232
233 lines.push(Line::from(""));
234 lines.push(Line::from(vec![
235 Span::styled(" AI Assistant", theme.style_header()),
236 ]));
237
238 self.add_field_line(
240 &mut lines,
241 SettingField::AIEnabled,
242 "Status",
243 if self.editing && self.selected_field == SettingField::AIEnabled {
244 &self.edit_buffer
245 } else {
246 ai_status
247 },
248 theme,
249 );
250
251 self.add_field_line(
253 &mut lines,
254 SettingField::AIProvider,
255 "Provider",
256 if self.editing && self.selected_field == SettingField::AIProvider {
257 &self.edit_buffer
258 } else {
259 ai_provider
260 },
261 theme,
262 );
263
264 if config.ai.enabled {
266 let note = match ai_provider.as_str() {
267 "claude-cli" => format!(" {}Using Claude Code CLI - no API key needed", icons::hint().content),
268 "anthropic" | "openai" => format!(" {}Set API key in config.toml", icons::hint().content),
269 "local" => format!(" {}Make sure your local LLM server is running", icons::hint().content),
270 "managed" => format!(" {}Using Arc Academy managed AI", icons::hint().content),
271 _ => format!(" {}Invalid provider - press Enter to configure", icons::warning().content),
272 };
273 lines.push(Line::from(vec![
274 Span::styled(note, theme.style_dim()),
275 ]));
276 } else {
277 lines.push(Line::from(vec![
278 Span::styled(format!(" {}Enable to use AI features (auto-configures provider)", icons::hint().content), theme.style_dim()),
279 ]));
280 }
281
282 lines.push(Line::from(""));
283 lines.push(Line::from(vec![
284 Span::styled(" Appearance", theme.style_header()),
285 ]));
286
287 self.add_field_line(
289 &mut lines,
290 SettingField::Theme,
291 "Theme",
292 if self.editing && self.selected_field == SettingField::Theme {
293 &self.edit_buffer
294 } else {
295 theme_name
296 },
297 theme,
298 );
299
300 lines.push(Line::from(""));
301 lines.push(Line::from(""));
302
303 if self.editing {
304 lines.push(Line::from(vec![
305 Span::styled(" Press ", theme.style_dim()),
306 Span::styled("Enter", theme.style_accent()),
307 Span::styled(" to save, ", theme.style_dim()),
308 Span::styled("Esc", theme.style_accent()),
309 Span::styled(" to cancel", theme.style_dim()),
310 ]));
311 } else {
312 lines.push(Line::from(vec![
313 Span::styled(" Controls", theme.style_header()),
314 ]));
315 lines.push(Line::from(vec![
316 Span::styled(" • Press ", theme.style_dim()),
317 Span::styled("↑↓", theme.style_accent()),
318 Span::styled(" to navigate fields", theme.style_dim()),
319 ]));
320 lines.push(Line::from(vec![
321 Span::styled(" • Press ", theme.style_dim()),
322 Span::styled("Enter", theme.style_accent()),
323 Span::styled(" to edit selected field", theme.style_dim()),
324 ]));
325 lines.push(Line::from(vec![
326 Span::styled(" • Press ", theme.style_dim()),
327 Span::styled("Ctrl+T", theme.style_accent()),
328 Span::styled(" to cycle themes", theme.style_dim()),
329 ]));
330 lines.push(Line::from(vec![
331 Span::styled(" • Press ", theme.style_dim()),
332 Span::styled("Ctrl+A", theme.style_accent()),
333 Span::styled(" to toggle AI", theme.style_dim()),
334 ]));
335 lines.push(Line::from(""));
336 lines.push(Line::from(vec![
337 Span::styled(" Press ", theme.style_dim()),
338 Span::styled("Esc", theme.style_accent()),
339 Span::styled(" or ", theme.style_dim()),
340 Span::styled("Ctrl+S", theme.style_accent()),
341 Span::styled(" to close", theme.style_dim()),
342 ]));
343 }
344
345 let paragraph = Paragraph::new(lines).wrap(Wrap { trim: false });
346 frame.render_widget(paragraph, inner);
347 }
348
349 fn add_field_line<'a>(
351 &self,
352 lines: &mut Vec<Line<'a>>,
353 field: SettingField,
354 label: &str,
355 value: &'a str,
356 theme: &Theme,
357 ) {
358 let is_selected = self.selected_field == field;
359 let is_editing = self.editing && is_selected;
360
361 let selector = if is_selected { " ▶ " } else { " " };
362
363 let mut spans = vec![
364 Span::styled(selector, theme.style_accent()),
365 Span::styled(format!("{}: ", label), theme.style_dim()),
366 ];
367
368 if is_editing {
369 spans.push(Span::styled(value, theme.style_accent()));
370 spans.push(Span::styled("█", theme.style_accent()));
371 } else if is_selected {
372 spans.push(Span::styled(value, theme.style_success()));
373 } else {
374 spans.push(Span::styled(value, theme.style_normal()));
375 }
376
377 lines.push(Line::from(spans));
378 }
379}
380
381impl Default for SettingsPanel {
382 fn default() -> Self {
383 Self::new()
384 }
385}