use ratatui::{
Frame,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, List, ListItem, Paragraph},
};
use super::AppState;
pub fn draw(f: &mut Frame, state: &AppState, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Length(2)])
.split(area);
let content_area = chunks[0];
let footer_area = chunks[1];
let mut items: Vec<ListItem<'static>> = Vec::new();
let heading = |s: &str| -> ListItem<'static> {
ListItem::new(Line::from(vec![
Span::raw(" "),
Span::styled(
s.to_string(),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
]))
};
let dim = |s: String| -> ListItem<'static> {
ListItem::new(Line::from(vec![
Span::raw(" "),
Span::styled(s, Style::default().fg(Color::Rgb(65, 60, 95))),
]))
};
let blank = || ListItem::new(Line::raw(""));
items.push(blank());
items.push(heading(&format!("Profile: {} (active)", state.profile)));
items.push(blank());
items.push(heading("Settings"));
items.push(kv("endpoint", &state.endpoint));
items.push(kv("model", &state.model));
items.push(kv("context_tokens", &state.context_tokens.to_string()));
items.push(kv(
"api_key",
if state.endpoint.contains("localhost") || state.endpoint.contains("127.0.0.1") {
"(not required)"
} else {
"(set via config/env)"
},
));
items.push(kv(
"auto_commit",
if state.auto_commit { "true" } else { "false" },
));
items.push(kv(
"git_context",
if state.git_context_enabled {
"true"
} else {
"false"
},
));
items.push(blank());
items.push(heading("Hooks"));
let hook_status = if state.hooks_disabled_profile {
"disabled (hooks_disabled = true in profile)"
} else if !state.hooks_enabled {
"off (press h to re-enable)"
} else {
"on (press h to disable)"
};
items.push(kv("status", hook_status));
let hc = &state.hooks_config;
items.push(hook_line("on_edit", &hc.on_edit));
items.push(hook_line("on_task_done", &hc.on_task_done));
items.push(hook_line("on_plan_step_done", &hc.on_plan_step_done));
items.push(hook_line("on_session_start", &hc.on_session_start));
items.push(hook_line("on_session_end", &hc.on_session_end));
items.push(blank());
items.push(heading("MCP Servers"));
if state.mcp_server_names.is_empty() {
items.push(dim("(none configured)".to_string()));
} else {
for name in &state.mcp_server_names {
items.push(kv(name, "configured"));
}
}
items.push(blank());
items.push(heading("Conventions"));
let cwd = super::cwd_str();
let conv_path = std::path::Path::new(&cwd).join(".parecode/conventions.md");
if conv_path.exists() {
items.push(kv("file", &conv_path.display().to_string()));
if let Ok(content) = std::fs::read_to_string(&conv_path) {
for line in content.lines().take(8) {
items.push(dim(format!(" {line}")));
}
}
} else {
items.push(dim(format!(
"{} — not found",
conv_path.display()
)));
items.push(dim(" Use /init to generate one.".to_string()));
}
items.push(blank());
let total = items.len();
let visible = content_area.height as usize;
let max_scroll = total.saturating_sub(visible);
let scroll = state.config_scroll.min(max_scroll);
let visible_items: Vec<ListItem<'static>> = items
.into_iter()
.skip(scroll)
.take(visible)
.collect();
let list = List::new(visible_items).block(
Block::default().style(Style::default().bg(Color::Rgb(8, 8, 14))),
);
f.render_widget(list, content_area);
if max_scroll > 0 && scroll < max_scroll {
let indicator = Paragraph::new(Line::from(vec![
Span::styled(
" ↓ more (j/↓ to scroll) ",
Style::default().fg(Color::Rgb(60, 55, 90)),
),
]))
.style(Style::default().bg(Color::Rgb(8, 8, 14)));
let ind_area = Rect {
y: content_area.y + content_area.height.saturating_sub(1),
height: 1,
..content_area
};
f.render_widget(indicator, ind_area);
}
let footer_line = Line::from(vec![
Span::raw(" "),
Span::styled("e", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
Span::styled(" edit ", Style::default().fg(Color::Rgb(100, 95, 140))),
Span::styled("p", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
Span::styled(" profile ", Style::default().fg(Color::Rgb(100, 95, 140))),
Span::styled("h", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
Span::styled(" hooks ", Style::default().fg(Color::Rgb(100, 95, 140))),
Span::styled("j/k", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
Span::styled(" scroll", Style::default().fg(Color::Rgb(100, 95, 140))),
]);
let footer = Paragraph::new(footer_line)
.style(Style::default().bg(Color::Rgb(12, 12, 20)));
f.render_widget(footer, footer_area);
}
fn kv(k: &str, v: &str) -> ListItem<'static> {
ListItem::new(Line::from(vec![
Span::styled(
format!(" {k:<18}"),
Style::default().fg(Color::Rgb(100, 95, 140)),
),
Span::styled(v.to_string(), Style::default().fg(Color::White)),
]))
}
fn hook_line(label: &str, cmds: &[String]) -> ListItem<'static> {
if cmds.is_empty() {
ListItem::new(Line::from(vec![
Span::styled(
format!(" {label:<18}"),
Style::default().fg(Color::Rgb(100, 95, 140)),
),
Span::styled("—", Style::default().fg(Color::Rgb(55, 50, 80))),
]))
} else {
ListItem::new(Line::from(vec![
Span::styled(
format!(" {label:<18}"),
Style::default().fg(Color::Rgb(100, 95, 140)),
),
Span::styled(
cmds.join(" · "),
Style::default().fg(Color::Rgb(180, 200, 140)),
),
]))
}
}