use ratatui::{
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
Frame,
};
use super::{App, Mode};
use super::markdown;
const FG: Color = Color::Rgb(213, 196, 161); const BG_DARK: Color = Color::Rgb(40, 40, 40); const BLUE: Color = Color::Rgb(131, 165, 152); const GREEN: Color = Color::Rgb(184, 187, 38); const YELLOW: Color = Color::Rgb(250, 189, 47); const RED: Color = Color::Rgb(251, 73, 52); const PURPLE: Color = Color::Rgb(211, 134, 155); const GRAY: Color = Color::Rgb(146, 131, 116);
pub fn draw(f: &mut Frame, app: &mut App) {
let input_height = if app.permission_details.is_some() {
let detail_lines = app.permission_details.as_ref().map(|d| d.len()).unwrap_or(0);
(detail_lines as u16 + 4).min(f.area().height / 2) } else {
3
};
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), Constraint::Min(1), Constraint::Length(input_height), Constraint::Length(1), ])
.split(f.area());
let header = Paragraph::new(Line::from(vec![
Span::styled(" claux ", Style::default().fg(PURPLE).add_modifier(Modifier::BOLD)),
Span::styled(
format!("v{}", env!("CARGO_PKG_VERSION")),
Style::default().fg(GRAY),
),
]));
f.render_widget(header, chunks[0]);
let msg_area = chunks[1];
let _msg_width = msg_area.width.saturating_sub(2) as usize;
let mut lines: Vec<Line> = Vec::new();
for msg in &app.messages {
if !lines.is_empty() {
lines.push(Line::from(""));
}
match msg.role.as_str() {
"user" => {
lines.push(Line::from(vec![
Span::styled("● ", Style::default().fg(BLUE)),
Span::styled("You", Style::default().fg(BLUE).add_modifier(Modifier::BOLD)),
]));
for line in msg.content.lines() {
lines.push(Line::from(Span::styled(
format!(" {}", line),
Style::default().fg(BLUE),
)));
}
}
"assistant" => {
lines.push(Line::from(Span::styled("● ", Style::default().fg(PURPLE))));
let rendered = markdown::render(&msg.content, Style::default().fg(FG));
for line in rendered {
let mut indented = vec![Span::raw(" ")];
indented.extend(line.spans);
lines.push(Line::from(indented));
}
}
"system" => {
lines.push(Line::from(Span::styled("● ", Style::default().fg(YELLOW))));
for line in msg.content.lines() {
lines.push(Line::from(Span::styled(
format!(" {}", line),
Style::default().fg(YELLOW),
)));
}
}
"error" => {
lines.push(Line::from(Span::styled("● ", Style::default().fg(RED))));
for line in msg.content.lines() {
lines.push(Line::from(Span::styled(
format!(" {}", line),
Style::default().fg(RED),
)));
}
}
_ => {
lines.push(Line::from(Span::styled("● ", Style::default().fg(GRAY))));
for line in msg.content.lines() {
lines.push(Line::from(Span::styled(
format!(" {}", line),
Style::default().fg(FG),
)));
}
}
}
}
if !app.stream_buffer.is_empty() {
if !lines.is_empty() {
lines.push(Line::from(""));
}
lines.push(Line::from(Span::styled("● ", Style::default().fg(GREEN))));
let rendered = markdown::render(&app.stream_buffer, Style::default().fg(GREEN));
for line in rendered {
let mut indented = vec![Span::raw(" ")];
indented.extend(line.spans);
lines.push(Line::from(indented));
}
lines.push(Line::from(Span::styled(" ▊", Style::default().fg(GREEN))));
}
app.total_lines = lines.len() as u16;
let visible_height = msg_area.height.saturating_sub(2);
let max_scroll = app.total_lines.saturating_sub(visible_height);
if !app.manual_scroll {
app.scroll = 0; }
let scroll_offset = if app.manual_scroll {
max_scroll.saturating_sub(app.scroll.min(max_scroll))
} else {
max_scroll
};
let messages_widget = Paragraph::new(lines)
.block(
Block::default()
.borders(Borders::LEFT | Borders::RIGHT)
.border_style(Style::default().fg(GRAY)),
)
.scroll((scroll_offset, 0));
f.render_widget(messages_widget, msg_area);
let input_style = match app.mode {
Mode::Input => Style::default().fg(FG),
Mode::Permission => Style::default().fg(YELLOW),
Mode::Streaming => Style::default().fg(GRAY),
};
if let (Some(ref prompt), Some(ref details)) = (&app.permission_prompt, &app.permission_details) {
let mut perm_lines: Vec<Line> = Vec::new();
perm_lines.push(Line::from(vec![
Span::styled("⚡ ", Style::default().fg(YELLOW)),
Span::styled(prompt.as_str(), Style::default().fg(YELLOW).add_modifier(Modifier::BOLD)),
]));
perm_lines.push(Line::from(""));
for detail in details {
let style = if detail.starts_with(" +") {
Style::default().fg(GREEN)
} else if detail.starts_with(" -") {
Style::default().fg(RED)
} else if detail.ends_with(':') {
Style::default().fg(FG).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(GRAY)
};
perm_lines.push(Line::from(Span::styled(detail.clone(), style)));
}
perm_lines.push(Line::from(""));
perm_lines.push(Line::from(vec![
Span::styled(" (y)es ", Style::default().fg(GREEN)),
Span::styled("(n)o ", Style::default().fg(RED)),
Span::styled("(a)lways allow", Style::default().fg(YELLOW)),
]));
let perm_widget = Paragraph::new(perm_lines)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(YELLOW))
.title(" Permission Required "),
);
f.render_widget(perm_widget, chunks[2]);
} else {
let input_text = if app.mode == Mode::Streaming {
"...".to_string()
} else {
app.input.clone()
};
let input_widget = Paragraph::new(input_text)
.style(input_style)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(if app.mode == Mode::Input {
BLUE
} else {
GRAY
}))
.title(" > "),
);
f.render_widget(input_widget, chunks[2]);
}
if app.mode == Mode::Input {
f.set_cursor_position((
chunks[2].x + app.cursor as u16 + 1,
chunks[2].y + 1,
));
}
let status = Paragraph::new(Line::from(vec![
Span::styled(format!(" {} ", app.status), Style::default().fg(GRAY)),
]));
f.render_widget(status, chunks[3]);
}