use crate::icons;
use crate::theme::Theme;
use arct_core::{Lesson, LessonStep, StepType, ValidationResult, LessonValidator};
use ratatui::{
layout::Rect,
style::Style,
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Wrap},
Frame,
};
pub struct LessonPanel {
pub current_lesson: Option<Lesson>,
current_step_index: usize,
user_input: String,
validator: LessonValidator,
last_validation: Option<ValidationResult>,
completed_steps: Vec<usize>,
}
impl LessonPanel {
pub fn new() -> Self {
Self {
current_lesson: None,
current_step_index: 0,
user_input: String::new(),
validator: LessonValidator::new(),
last_validation: None,
completed_steps: Vec::new(),
}
}
pub fn load_lesson(&mut self, lesson: Lesson) {
self.current_lesson = Some(lesson);
self.current_step_index = 0;
self.user_input.clear();
self.last_validation = None;
self.completed_steps.clear();
}
fn current_step(&self) -> Option<&LessonStep> {
self.current_lesson
.as_ref()
.and_then(|lesson| lesson.steps.get(self.current_step_index))
}
pub fn validate_current_step(&mut self, input: &str) -> ValidationResult {
if let Some(step) = self.current_step() {
let result = match &step.step_type {
StepType::CommandExercise {
expected_command,
validation,
success_message,
} => {
let validation_result =
self.validator.validate_command(input, expected_command, validation);
if validation_result.is_success() {
ValidationResult::Success {
message: success_message.clone(),
}
} else {
validation_result
}
}
StepType::MultipleChoice {
correct_index, ..
} => {
if let Ok(choice) = input.parse::<usize>() {
self.validator.validate_multiple_choice(choice, *correct_index)
} else {
ValidationResult::Failure {
message: "Please enter a number.".to_string(),
hint: None,
}
}
}
StepType::Information { .. } => {
ValidationResult::Success {
message: "Continue to next step.".to_string(),
}
}
_ => ValidationResult::Success {
message: "Continue.".to_string(),
},
};
self.last_validation = Some(result.clone());
result
} else {
ValidationResult::Failure {
message: "No active step.".to_string(),
hint: None,
}
}
}
pub fn next_step(&mut self) -> bool {
if let Some(lesson) = &self.current_lesson {
if !self.completed_steps.contains(&self.current_step_index) {
self.completed_steps.push(self.current_step_index);
}
if self.current_step_index + 1 < lesson.steps.len() {
self.current_step_index += 1;
self.user_input.clear();
self.last_validation = None;
true
} else {
false }
} else {
false
}
}
pub fn previous_step(&mut self) {
if self.current_step_index > 0 {
self.current_step_index -= 1;
self.user_input.clear();
self.last_validation = None;
}
}
pub fn completion_percentage(&self) -> f32 {
if let Some(lesson) = &self.current_lesson {
let total = lesson.steps.len();
if total == 0 {
return 0.0;
}
(self.completed_steps.len() as f32 / total as f32) * 100.0
} else {
0.0
}
}
pub fn render(&self, frame: &mut Frame, area: Rect, focused: bool, theme: &Theme) {
let border_style = if focused {
theme.style_border_focused()
} else {
theme.style_border()
};
if let Some(lesson) = &self.current_lesson {
if let Some(step) = self.current_step() {
self.render_step(frame, area, lesson, step, theme, border_style);
}
} else {
self.render_lesson_selection(frame, area, theme, border_style);
}
}
fn render_step(
&self,
frame: &mut Frame,
area: Rect,
lesson: &Lesson,
step: &LessonStep,
theme: &Theme,
border_style: Style,
) {
let mut lines = Vec::new();
lines.push(Line::from(vec![
Span::styled(format!("Step {}: ", step.step_number), theme.style_accent()),
Span::styled(&step.title, theme.style_header()),
]));
match &step.step_type {
StepType::CommandExercise { .. } => {
lines.push(Line::from(vec![
Span::styled("▶ Type command in Shell → Enter", theme.style_warning()),
]));
if !step.instruction.is_empty() {
lines.push(Line::from(vec![
Span::styled("Task: ", theme.style_accent()),
Span::styled(&step.instruction, theme.style_normal()),
]));
}
if let Some(hint) = &step.hint {
lines.push(Line::from(vec![
icons::hint(),
Span::styled(hint, theme.style_dim()),
]));
}
if let Some(validation) = &self.last_validation {
match validation {
ValidationResult::Success { message } => {
lines.push(Line::from(vec![
icons::success(),
Span::styled(message, theme.style_success()),
]));
}
ValidationResult::Failure { message, hint } => {
lines.push(Line::from(vec![
icons::error(),
Span::styled(message, theme.style_error()),
]));
if let Some(h) = hint {
lines.push(Line::from(vec![
icons::hint(),
Span::styled(h, theme.style_dim()),
]));
}
}
ValidationResult::Partial { message, progress } => {
lines.push(Line::from(vec![
icons::warning(),
Span::styled(
format!("{} ({:.0}%)", message, progress),
theme.style_warning(),
),
]));
}
}
}
}
StepType::MultipleChoice {
question,
options,
explanation,
..
} => {
lines.push(Line::from(vec![
Span::styled(format!("▶ Type 0-{} → ", options.len() - 1), theme.style_warning()),
icons::question(),
Span::styled(question, theme.style_normal()),
]));
for (i, option) in options.iter().enumerate() {
lines.push(Line::from(vec![
Span::styled(format!(" {}. ", i), theme.style_accent()),
Span::styled(option, theme.style_normal()),
]));
}
if let Some(ValidationResult::Success { .. }) = &self.last_validation {
lines.push(Line::from(vec![
icons::success(),
Span::styled(explanation, theme.style_success()),
]));
}
}
StepType::Information { content } => {
lines.push(Line::from(vec![
Span::styled("▶ Press Enter to continue", theme.style_warning()),
]));
for line in content.lines() {
lines.push(Line::from(vec![Span::styled(line, theme.style_normal())]));
}
}
StepType::FillInBlank { template, .. } => {
lines.push(Line::from(vec![
Span::styled("▶ Fill blank, type in Shell → Enter", theme.style_warning()),
]));
lines.push(Line::from(vec![
icons::note(),
Span::styled(&step.instruction, theme.style_normal()),
]));
lines.push(Line::from(vec![
Span::styled("Template: ", theme.style_accent()),
Span::styled(template, theme.style_dim()),
]));
}
StepType::Practice { goal, hints, .. } => {
lines.push(Line::from(vec![
Span::styled("▶ Try commands → ", theme.style_warning()),
icons::target(),
Span::styled(goal, theme.style_normal()),
]));
if !hints.is_empty() {
for hint in hints {
lines.push(Line::from(vec![
icons::hint(),
Span::styled(hint, theme.style_dim()),
]));
}
}
}
}
let progress = self.completion_percentage();
let title = format!(
" {} {}/{} | {:.0}% ",
lesson.title,
self.current_step_index + 1,
lesson.steps.len(),
progress
);
let block = Block::default()
.title(title)
.borders(Borders::ALL)
.border_style(border_style)
.style(theme.style_block());
let paragraph = Paragraph::new(lines)
.block(block)
.wrap(Wrap { trim: false });
frame.render_widget(paragraph, area);
}
fn render_lesson_selection(
&self,
frame: &mut Frame,
area: Rect,
theme: &Theme,
border_style: Style,
) {
let block = Block::default()
.title(format!(" {}Interactive Lessons ", icons::lesson().content))
.borders(Borders::ALL)
.border_style(border_style)
.style(theme.style_block());
let paragraph = Paragraph::new(vec![
Line::from(""),
Line::from(vec![
icons::welcome(),
Span::styled("Welcome to Interactive Lessons!", theme.style_accent()),
]),
Line::from(""),
Line::from(vec![
Span::styled("🎓 ", theme.style_accent()),
Span::styled("10 comprehensive lessons", theme.style_normal()),
Span::styled(" available", theme.style_dim()),
]),
Line::from(vec![
Span::styled("🏆 ", theme.style_accent()),
Span::styled("Track progress & earn achievements", theme.style_normal()),
]),
Line::from(vec![
Span::styled("🛡️ ", theme.style_accent()),
Span::styled("Safe virtual filesystem", theme.style_normal()),
Span::styled(" for hands-on practice", theme.style_dim()),
]),
Line::from(""),
Line::from(""),
Line::from(vec![
Span::styled("📚 Select a lesson:", theme.style_header()),
]),
Line::from(""),
Line::from(vec![
Span::styled(" Press ", theme.style_dim()),
Span::styled("m", theme.style_accent()),
Span::styled(" to open the lesson menu", theme.style_dim()),
]),
Line::from(vec![
Span::styled(" Use ", theme.style_dim()),
Span::styled("↑/↓", theme.style_accent()),
Span::styled(" or ", theme.style_dim()),
Span::styled("1-9,0", theme.style_accent()),
Span::styled(" to select", theme.style_dim()),
]),
Line::from(vec![
Span::styled(" Press ", theme.style_dim()),
Span::styled("Enter", theme.style_accent()),
Span::styled(" to start learning!", theme.style_dim()),
]),
Line::from(""),
Line::from(""),
Line::from(vec![
Span::styled("💡 Tip: ", theme.style_accent()),
Span::styled("Complete lessons to unlock achievements", theme.style_dim()),
]),
Line::from(vec![
Span::styled(" and build your learning streak!", theme.style_dim()),
]),
])
.block(block)
.wrap(Wrap { trim: false });
frame.render_widget(paragraph, area);
}
}
impl Default for LessonPanel {
fn default() -> Self {
Self::new()
}
}