use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::Frame;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{ListItem, ListState};
use crate::app::{App, SettingsRow};
use super::chrome;
fn split_label(label: &'static str) -> (String, String) {
match label.split_once(" (") {
Some((name, rest)) => (name.to_string(), rest.trim_end_matches(')').to_string()),
None => (label.to_string(), String::new()),
}
}
#[allow(clippy::too_many_lines)]
pub fn render(f: &mut Frame, app: &App) {
use crate::app::{SETTINGS_GROUPS, SettingsField};
let area = crate::ui::centered(f.area(), 64, 60);
let dim = Style::default().fg(app.theme.fg_dim);
let name_w = SettingsField::ALL
.iter()
.map(|f| split_label(f.label()).0.chars().count())
.max()
.unwrap_or(0);
let toggle = |b: bool| -> Span<'static> {
if b {
Span::styled("● on ", Style::default().fg(app.theme.success))
} else {
Span::styled("○ off", dim)
}
};
let numeric = |s: &str| -> Span<'static> {
if s.trim().is_empty() {
Span::styled("default", dim.add_modifier(Modifier::ITALIC))
} else {
Span::styled(s.to_string(), Style::default().fg(app.theme.accent))
}
};
let value = |field: SettingsField| -> Span<'static> {
match field {
SettingsField::ShowStats => toggle(app.settings.show_stats),
SettingsField::ShowReasoning => toggle(app.settings.show_reasoning),
SettingsField::HideHints => toggle(app.settings.hide_hints),
SettingsField::Temperature => numeric(&app.settings_inputs[0]),
SettingsField::TopP => numeric(&app.settings_inputs[1]),
SettingsField::MaxTokens => numeric(&app.settings_inputs[2]),
SettingsField::MemoryModel => numeric(&app.memory_model),
SettingsField::CompactThreshold => numeric(&app.settings_inputs[3]),
SettingsField::SearxngUrl => numeric(&app.settings_inputs[4]),
SettingsField::Verbosity => {
Span::styled(app.verbosity.clone(), Style::default().fg(app.theme.accent))
}
SettingsField::LangsearchKey => numeric(&app.settings_inputs[5]),
SettingsField::SearchProvider => Span::styled(
app.search_provider.clone(),
Style::default().fg(app.theme.accent),
),
SettingsField::TranscriberModel => numeric(&app.transcriber_model),
SettingsField::OcrModel => numeric(&app.ocr_model),
SettingsField::OcrEngine => Span::styled(
app.ocr_engine.clone(),
Style::default().fg(app.theme.accent),
),
SettingsField::EmbeddingModel => numeric(&app.settings_inputs[6]),
SettingsField::BlockedDomains => numeric(&app.settings_inputs[7]),
SettingsField::ImageGenModel => numeric(&app.image_gen_model),
SettingsField::VideoGenModel => numeric(&app.video_gen_model),
}
};
let rows = app.settings_rows();
let items: Vec<ListItem> = rows
.iter()
.map(|row| match row {
SettingsRow::Group(i) => {
let g = &SETTINGS_GROUPS[*i];
let arrow = if app.settings_collapsed.contains(i) {
"▸"
} else {
"▾"
};
ListItem::new(Line::from(Span::styled(
format!("{arrow} {}", g.name),
Style::default()
.fg(app.theme.accent2)
.add_modifier(Modifier::BOLD),
)))
}
SettingsRow::Field(field) => {
let (name, _) = split_label(field.label());
let name = chrome::truncate(&name, name_w.max(1));
let v = value(*field);
let v_max = (area.width.saturating_sub(8)) as usize - name_w - 3; let v_span = Span::styled(chrome::truncate(&v.content, v_max.max(1)), v.style);
ListItem::new(Line::from(vec![
Span::raw(format!(" {name:<name_w$}")),
Span::raw(" "),
v_span,
]))
}
})
.collect();
let desc = match app.settings_row() {
SettingsRow::Field(f) => split_label(f.label()).1,
SettingsRow::Group(_) => String::new(),
};
let hint = format!(
"{}Space toggles/collapses · type numbers · ↑↓ · Esc saves",
chrome::count_hint(rows.len(), "setting")
);
let inner = chrome::render_hinted(
f,
area,
chrome::popup_title(app, "⚙", "nerd config"),
&hint,
app,
true,
chrome::Tone::Normal,
);
let (list_area, detail_area) = chrome::split_with_detail(inner, &desc);
let list = chrome::standard_list(items, &app.theme);
let mut state = ListState::default();
if !rows.is_empty() {
state.select(Some(app.settings_selected.min(rows.len() - 1)));
}
chrome::render_list(f, list, &mut state, list_area, rows.len(), 1, &app.theme);
chrome::render_detail(f, detail_area, &desc, &app.theme);
}
pub fn handle_key(app: &mut App, key: KeyEvent) -> Result<()> {
use crate::app::SettingsField;
let picker = matches!(
app.settings_field(),
Some(
SettingsField::MemoryModel
| SettingsField::TranscriberModel
| SettingsField::OcrModel
| SettingsField::ImageGenModel
| SettingsField::VideoGenModel
)
);
if picker {
match key.code {
KeyCode::Esc => app.save_settings()?,
KeyCode::Enter => match app.settings_field() {
Some(SettingsField::MemoryModel) => app.open_model_picker_for_memory(),
Some(SettingsField::OcrModel) => app.open_model_picker_for_ocr(),
Some(SettingsField::ImageGenModel) => app.open_model_picker_for_image_gen(),
Some(SettingsField::VideoGenModel) => app.open_model_picker_for_video_gen(),
_ => app.open_model_picker_for_transcriber(),
},
KeyCode::Backspace => match app.settings_field() {
Some(SettingsField::MemoryModel) => app.clear_memory_model()?,
Some(SettingsField::OcrModel) => app.clear_ocr_model()?,
Some(SettingsField::ImageGenModel) => app.clear_image_gen_model()?,
Some(SettingsField::VideoGenModel) => app.clear_video_gen_model()?,
_ => app.clear_transcriber_model()?,
},
KeyCode::Up => app.move_settings_selection(-1),
KeyCode::Down | KeyCode::Tab => app.move_settings_selection(1),
KeyCode::PageUp => app.move_settings_selection(-10),
KeyCode::PageDown => app.move_settings_selection(10),
KeyCode::Home => app.move_settings_selection(i32::MIN / 2),
KeyCode::End => app.move_settings_selection(i32::MAX / 2),
_ => {}
}
return Ok(());
}
match key.code {
KeyCode::Enter if matches!(app.settings_row(), SettingsRow::Group(_)) => {
app.toggle_settings_field();
}
KeyCode::Esc | KeyCode::Enter => app.save_settings()?,
KeyCode::Up => app.move_settings_selection(-1),
KeyCode::Down | KeyCode::Tab => app.move_settings_selection(1),
KeyCode::PageUp => app.move_settings_selection(-10),
KeyCode::PageDown => app.move_settings_selection(10),
KeyCode::Home => app.move_settings_selection(i32::MIN / 2),
KeyCode::End => app.move_settings_selection(i32::MAX / 2),
KeyCode::Char(' ') => app.toggle_settings_field(),
KeyCode::Char(c) => app.settings_input_char(c),
KeyCode::Backspace => app.settings_input_backspace(),
_ => {}
}
Ok(())
}