use crate::{
command::CommandResult,
evt, fs,
languages::{programming, spoken},
models::{lesson, workshop, Error as ModelError, LessonData},
ui::tui::{
self,
screens::{self, Screens},
widgets::{LessonBox, LessonBoxState},
Screen,
},
Error, Status,
};
use crossterm::event::{self, KeyCode};
use ratatui::{
buffer::Buffer,
layout::{Alignment, Constraint, Flex, Layout, Rect},
style::{Color, Style},
symbols::border::Set,
text::{Line, Span},
widgets::{block::Position, Block, Borders, Padding, StatefulWidget, Widget},
};
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc::Sender;
use tracing::{debug, info};
const TOP_BORDER: Set = Set {
top_left: "┌",
top_right: "┐",
bottom_left: "│",
bottom_right: "│",
vertical_left: "│",
vertical_right: "│",
horizontal_top: "─",
horizontal_bottom: " ",
};
const STATUS_BORDER: Set = Set {
top_left: " ",
top_right: " ",
bottom_left: "└",
bottom_right: "┘",
vertical_left: "│",
vertical_right: "│",
horizontal_top: " ",
horizontal_bottom: "─",
};
#[derive(Clone, Debug, Default)]
pub struct Lesson {
workshop_title: String,
lesson_title: String,
lesson_state: LessonBoxState,
spoken_language: Option<spoken::Code>,
programming_language: Option<programming::Code>,
}
impl Lesson {
async fn init<S: AsRef<str>>(
&mut self,
workshop_title: S,
lesson_title: S,
text: S,
spoken_language: Option<spoken::Code>,
programming_language: Option<programming::Code>,
) -> Result<(), Error> {
self.workshop_title = workshop_title.as_ref().to_string();
self.lesson_title = lesson_title.as_ref().to_string();
self.lesson_state = LessonBoxState::from_markdown(text.as_ref());
self.spoken_language = spoken_language;
self.programming_language = programming_language;
Ok(())
}
async fn check_all_lessons_completed(
&self,
lessons: &std::collections::HashMap<String, LessonData>,
) -> Result<bool, Error> {
for lesson_data in lessons.values() {
let lesson = lesson_data.get_metadata().await?;
if !matches!(lesson.status, lesson::Status::Completed) {
return Ok(false);
}
}
Ok(true)
}
fn render_lesson(&mut self, area: Rect, buf: &mut Buffer) {
let title = Line::from(vec![
Span::styled("─", Style::default().fg(Color::DarkGray)),
Span::styled(
format!("/ {} /", self.lesson_title),
Style::default().fg(Color::White),
),
]);
let block = Block::default()
.title(title)
.title_style(Style::default().fg(Color::White))
.padding(Padding::uniform(1))
.style(Style::default().fg(Color::DarkGray))
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT)
.border_set(TOP_BORDER);
let lesson_widget = LessonBox::new()
.block(block)
.style(Style::default().fg(Color::White));
StatefulWidget::render(lesson_widget, area, buf, &mut self.lesson_state);
}
fn render_status(&mut self, area: Rect, buf: &mut Buffer) {
let [keys_area, langs_area] =
Layout::horizontal([Constraint::Min(1), Constraint::Length(46)]).areas(area);
self.render_keys(keys_area, buf);
self.render_langs(langs_area, buf);
}
fn render_keys(&mut self, area: Rect, buf: &mut Buffer) {
let title = Line::from(vec![
Span::styled("─", Style::default().fg(Color::DarkGray)),
Span::styled(
"/ j,k scroll / ↵ expand hint / c check / b back / q quit /",
Style::default().fg(Color::White),
),
]);
let block = Block::default()
.title(title)
.title_style(Style::default().fg(Color::White))
.title_position(Position::Bottom)
.title_alignment(Alignment::Left)
.style(Style::default().fg(Color::DarkGray))
.borders(Borders::LEFT | Borders::BOTTOM)
.border_set(STATUS_BORDER)
.padding(Padding::horizontal(1));
Widget::render(block, area, buf);
}
fn render_langs(&mut self, area: Rect, buf: &mut Buffer) {
let spoken = match self.spoken_language {
Some(code) => code.get_name_in_english().to_string(),
None => "All".to_string(),
};
let programming = match self.programming_language {
Some(code) => code.get_name().to_string(),
None => "All".to_string(),
};
let title = Line::from(vec![
Span::styled(
format!("/ {} / {spoken} / {programming} /", self.workshop_title),
Style::default().fg(Color::White),
),
Span::styled("─", Style::default().fg(Color::DarkGray)),
]);
let block = Block::default()
.title(title)
.title_style(Style::default().fg(Color::White))
.title_position(Position::Bottom)
.title_alignment(Alignment::Right)
.style(Style::default().fg(Color::DarkGray))
.borders(Borders::RIGHT | Borders::BOTTOM)
.border_set(STATUS_BORDER)
.padding(Padding::horizontal(1));
Widget::render(block, area, buf);
}
pub async fn handle_ui_event(
&mut self,
event: tui::Event,
to_ui: Sender<screens::Event>,
status: Arc<Mutex<Status>>,
) -> Result<(), Error> {
match event {
tui::Event::LoadLesson => {
debug!("Loading lessons");
let (spoken, programming, workshop, lesson) = {
let status = status
.lock()
.map_err(|e| Error::StatusLock(e.to_string()))?;
(
status.spoken_language(),
status.programming_language(),
status
.workshop()
.map(String::from)
.ok_or(ModelError::NoWorkshopSpecified)?,
status
.lesson()
.map(String::from)
.ok_or(ModelError::NoLessonSpecified)?,
)
};
if let Some(workshop_data) = fs::workshops::load(&workshop) {
debug!("Loading lessons for workshop: {}", &workshop);
let lessons = workshop_data.get_lessons_data(spoken, programming).await?;
let workshop_title = workshop_data.get_metadata(spoken).await?.title;
let lesson_data = lessons
.get(&lesson)
.ok_or(ModelError::NoLessonData(lesson.to_string()))?;
let lesson_text = lesson_data.get_text().await?;
let lesson_metadata = lesson_data.get_metadata().await?;
let lesson_title = lesson_metadata.title.clone();
if matches!(lesson_metadata.status, lesson::Status::NotStarted) {
lesson_data
.update_status(lesson::Status::InProgress)
.await?;
debug!("Updated lesson status to InProgress: {}", lesson_title);
}
self.init(
&workshop_title,
&lesson_title,
&lesson_text,
spoken,
programming,
)
.await?;
to_ui
.send((None, tui::Event::Show(screens::Screens::Lesson)).into())
.await?;
} else {
info!("Failed to load workshop data for: {}", &workshop);
}
}
tui::Event::SolutionComplete => {
let (spoken, programming, workshop, lesson) = {
let status = status
.lock()
.map_err(|e| Error::StatusLock(e.to_string()))?;
(
status.spoken_language(),
status.programming_language(),
status
.workshop()
.map(String::from)
.ok_or(ModelError::NoWorkshopSpecified)?,
status
.lesson()
.map(String::from)
.ok_or(ModelError::NoLessonSpecified)?,
)
};
if let Some(workshop_data) = fs::workshops::load(&workshop) {
let lessons = workshop_data.get_lessons_data(spoken, programming).await?;
if let Some(lesson_data) = lessons.get(&lesson) {
lesson_data.update_status(lesson::Status::Completed).await?;
debug!("Updated lesson status to Completed: {}", lesson);
let all_completed = self.check_all_lessons_completed(&lessons).await?;
if all_completed {
workshop_data
.update_status(spoken, workshop::Status::Completed)
.await?;
let set_workshop = evt!(
None,
tui::Event::SetWorkshop(None, std::collections::HashMap::default())
);
let hide_log = evt!(None, tui::Event::HideLog(Some(set_workshop)));
let workshop_complete = evt!(
Screens::Log,
tui::Event::CommandCompleted(
CommandResult {
success: true,
exit_code: 0,
last_line: "All lessons completed!".to_string()
},
Some(hide_log),
None
)
);
to_ui.send(workshop_complete.into()).await?;
} else {
let load_lessons = evt!(Screens::Lessons, tui::Event::LoadLessons);
let hide_log = evt!(None, tui::Event::HideLog(Some(load_lessons)));
to_ui.send(hide_log.into()).await?;
}
}
}
}
tui::Event::SolutionIncomplete => {
let load_lesson = evt!(Screens::Lesson, tui::Event::LoadLesson);
let hide_log = evt!(None, tui::Event::HideLog(Some(load_lesson)));
to_ui.send(hide_log.into()).await?;
}
_ => {
info!("Ignoring UI event: {:?}", event);
}
}
Ok(())
}
pub async fn handle_input_event(
&mut self,
event: event::Event,
to_ui: Sender<screens::Event>,
_status: Arc<Mutex<Status>>,
) -> Result<(), Error> {
if let event::Event::Key(key) = event {
match key.code {
KeyCode::PageUp => self.lesson_state.scroll_top(),
KeyCode::PageDown => self.lesson_state.scroll_bottom(),
KeyCode::Char('j') | KeyCode::Char('J') | KeyCode::Down => {
self.lesson_state.highlight_down()
}
KeyCode::Char('k') | KeyCode::Char('K') | KeyCode::Up => {
self.lesson_state.highlight_up()
}
KeyCode::Enter => {
self.lesson_state.toggle_highlighted_hint(80); }
KeyCode::Char('c') | KeyCode::Char('C') => {
let success = evt!(Screens::Lesson, tui::Event::SolutionComplete);
let failure = evt!(Screens::Lesson, tui::Event::SolutionIncomplete);
let check_solution = evt!(
None,
tui::Event::CheckSolution(Some(success), Some(failure)),
);
to_ui.send(check_solution.into()).await?;
}
KeyCode::Char('b') | KeyCode::Esc => {
to_ui
.send((None, tui::Event::SetLesson(None)).into())
.await?;
}
_ => {}
}
}
Ok(())
}
}
#[async_trait::async_trait]
impl Screen for Lesson {
async fn handle_event(
&mut self,
event: screens::Event,
to_ui: Sender<screens::Event>,
status: Arc<Mutex<Status>>,
) -> Result<(), Error> {
match event {
screens::Event::Input(input_event) => {
self.handle_input_event(input_event, to_ui, status).await
}
screens::Event::Ui(_, ui_event) => self.handle_ui_event(ui_event, to_ui, status).await,
}
}
fn render_screen(&mut self, area: Rect, buf: &mut Buffer) -> Result<(), Error> {
let [lesson_area, status_area] =
Layout::vertical([Constraint::Percentage(100), Constraint::Min(1)])
.flex(Flex::End)
.areas(area);
self.render_lesson(lesson_area, buf);
self.render_status(status_area, buf);
Ok(())
}
}