use crate::app::App;
use crate::panels::{
context::ContextPanel,
explanation::ExplanationPanel,
help::HelpPanel,
lesson::LessonPanel,
PanelId,
};
use crate::theme::Theme;
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::Modifier,
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Wrap},
Frame,
};
pub fn draw(frame: &mut Frame, app: &App) {
let size = frame.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Min(0), ])
.split(size);
draw_header(frame, chunks[0], app);
draw_content(frame, chunks[1], app);
if let Some(ref wizard) = app.onboarding {
wizard.render(frame, &app.theme);
return; }
if app.show_help {
let help_panel = HelpPanel::new();
help_panel.render(frame, &app.theme);
return; }
if let Some(ref panel) = app.settings_panel {
panel.render(frame, &app.theme, &app.config);
}
}
fn draw_header(frame: &mut Frame, area: Rect, app: &App) {
let title = " ⚡ ARC ACADEMY TERMINAL ";
let mode_indicator = if app.lesson_mode {
" 📖 LESSON MODE "
} else if app.ai_mode {
" 🤖 AI MODE "
} else {
""
};
let version = format!("v0.1.0-alpha | {} | arcacademy.sh ", app.theme.name);
let help_text = " [? help] [^L lessons] [^A AI] [^T theme] [^S settings] [q quit] ";
let version_len = version.len();
let mode_len = mode_indicator.len();
let mut spans = vec![
Span::styled(title, app.theme.style_accent().add_modifier(Modifier::BOLD)),
];
if !mode_indicator.is_empty() {
spans.push(Span::styled(mode_indicator, app.theme.style_success().add_modifier(Modifier::BOLD)));
}
spans.push(Span::styled(version, app.theme.style_info()));
spans.push(Span::raw(" ".repeat(area.width.saturating_sub(
title.len() as u16 + mode_len as u16 + version_len as u16 + help_text.len() as u16
) as usize)));
spans.push(Span::styled(help_text, app.theme.style_dim()));
let header_text = Line::from(spans);
let header = Paragraph::new(header_text)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(app.theme.style_border_focused()),
);
frame.render_widget(header, area);
}
fn draw_content(frame: &mut Frame, area: Rect, app: &App) {
let main_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(30), Constraint::Percentage(70), ])
.split(area);
let context_panel = ContextPanel::new();
let analytics_summary = app.analytics.as_ref()
.and_then(|a| a.get_summary().ok());
context_panel.render(
frame,
main_chunks[0],
app.active_panel == PanelId::Context,
&app.context,
&app.theme,
app.config.ai.enabled,
analytics_summary.as_ref(),
app.lesson_mode,
app.virtual_fs.as_ref(),
);
let right_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Percentage(40), Constraint::Min(0), ])
.split(main_chunks[1]);
draw_shell_panel(
frame,
right_chunks[0],
app.active_panel == PanelId::Shell,
&app.command_buffer,
&app.completion_suggestions,
&app.theme,
app.ai_mode,
&app.ai_input_buffer,
app.ai_loading,
);
draw_output_panel(
frame,
right_chunks[1],
app.active_panel == PanelId::Output,
&app.last_output,
app.output_scroll,
&app.theme,
);
if app.lesson_mode {
if let Some(ref lesson_panel) = app.lesson_panel {
lesson_panel.render(
frame,
right_chunks[2],
app.active_panel == PanelId::Explanation,
&app.theme,
);
}
} else {
let explanation_panel = ExplanationPanel::new();
explanation_panel.render(
frame,
right_chunks[2],
app.active_panel == PanelId::Explanation,
app.last_explanation.as_ref(),
&app.theme,
app.ai_mode,
app.ai_response.as_deref(),
);
}
}
fn draw_shell_panel(
frame: &mut Frame,
area: Rect,
focused: bool,
command: &str,
completions: &[String],
theme: &Theme,
ai_mode: bool,
ai_input: &str,
ai_loading: bool,
) {
let border_style = if focused {
theme.style_border_focused()
} else {
theme.style_border()
};
let title = if ai_mode {
if focused {
" 🤖 AI Assistant (Active - Ctrl+A to exit, Enter to ask) "
} else {
" 🤖 AI Assistant "
}
} else if focused {
" ⚡ Shell (Active - Ctrl+A for AI, Tab to complete, ↑↓ for history) "
} else {
" ⚡ Shell "
};
let block = Block::default()
.title(title)
.borders(Borders::ALL)
.border_style(border_style);
let mut lines = Vec::new();
if ai_mode {
let prompt = Span::styled("🤖 ", theme.style_info());
let input_text = if ai_loading {
Span::styled("⏳ Thinking...", theme.style_dim())
} else {
Span::styled(ai_input, theme.style_normal())
};
let mut input_line = vec![prompt, input_text];
if focused && !ai_loading {
input_line.push(Span::styled("█", theme.style_accent()));
}
lines.push(Line::from(input_line));
} else {
let prompt = Span::styled("$ ", theme.style_accent());
let command_text = Span::styled(command, theme.style_normal());
let mut command_line = vec![prompt, command_text];
if focused {
command_line.push(Span::styled("█", theme.style_accent()));
}
lines.push(Line::from(command_line));
}
if !ai_mode && !completions.is_empty() {
lines.push(Line::from("")); lines.push(Line::from(vec![
Span::styled("💡 ", theme.style_info()),
Span::styled("Suggestions:", theme.style_dim()),
]));
for completion in completions.iter().take(5) {
lines.push(Line::from(vec![
Span::styled(" • ", theme.style_dim()),
Span::styled(completion, theme.style_success()),
]));
}
if completions.len() > 5 {
lines.push(Line::from(vec![
Span::styled(format!(" ...and {} more", completions.len() - 5), theme.style_dim()),
]));
}
}
let paragraph = Paragraph::new(lines)
.block(block)
.wrap(Wrap { trim: false });
frame.render_widget(paragraph, area);
}
fn draw_output_panel(
frame: &mut Frame,
area: Rect,
focused: bool,
output: &str,
scroll_offset: usize,
theme: &Theme,
) {
let border_style = if focused {
theme.style_border_focused()
} else {
theme.style_border()
};
let title = if focused {
" 📟 Output (Active - ↑↓ to scroll) "
} else {
" 📟 Output "
};
let block = Block::default()
.title(title)
.borders(Borders::ALL)
.border_style(border_style);
let inner_height = area.height.saturating_sub(2) as usize;
let text = if output.is_empty() {
vec![
Line::from(""),
Line::from(vec![
Span::styled(" Ready to execute commands!", theme.style_dim()),
]),
Line::from(""),
Line::from(vec![
Span::styled(" Type a command above and press ", theme.style_dim()),
Span::styled("Enter", theme.style_accent()),
]),
]
} else {
let all_lines: Vec<Line> = crate::ansi::parse_ansi(output);
let total_lines = all_lines.len();
let mut visible_lines: Vec<Line> = all_lines
.into_iter()
.skip(scroll_offset)
.take(inner_height)
.collect();
if scroll_offset + inner_height < total_lines {
let remaining = total_lines - (scroll_offset + inner_height);
visible_lines.push(Line::from(vec![
Span::styled(
format!(" ↓ {} more lines (press ↓ or j to scroll)", remaining),
theme.style_dim(),
),
]));
}
if scroll_offset > 0 {
visible_lines.insert(0, Line::from(vec![
Span::styled(
format!(" ↑ {} lines above (press ↑ or k to scroll)", scroll_offset),
theme.style_dim(),
),
]));
}
visible_lines
};
let paragraph = Paragraph::new(text)
.block(block)
.wrap(Wrap { trim: false });
frame.render_widget(paragraph, area);
}