#![warn(clippy::pedantic)]
use std::io::{self, stdout, Write};
use std::ops::ControlFlow;
#[cfg(feature = "clap")]
use clap::Parser;
use crossterm::event::KeyEvent;
#[cfg(feature = "ui")]
use crossterm::{
cursor,
event::{self, DisableMouseCapture, Event, KeyCode},
execute, terminal,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use flashed::App;
use flashed::CardType;
use flashed::Opts;
use flashed::DeckError;
#[cfg(feature = "ui")]
use tuviv::{
border::Border,
le::{
layout::{Rect, Vec2, TRBL},
Alignment, Orientation,
},
prelude::*,
widgets::{Flexbox, Paragraph as P},
CrosstermBackend, Widget,
};
#[cfg(feature = "ui")]
mod theme;
#[cfg(not(feature = "ui"))]
fn main() {
compile_error!("Really sorry but you don't have a ui enabled.");
}
#[cfg(feature = "ui")]
fn main() -> Result<(), DeckError> {
use flashed::Deck;
let opts = Opts::parse();
let deck = Deck::new_from_path(&opts.input)?;
let mut app = App::new(deck, opts);
if !app.opts.reset {
app.read_scores()?;
app.retain_undue();
app.order_playables();
}
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen, cursor::Hide)?;
enable_raw_mode()?;
let mut size = terminal::size()?;
let mut buffer = tuviv::Buffer::new(Vec2::new(size.0.into(), size.1.into()));
let backend = CrosstermBackend;
let mut finished = true;
loop {
if app.unfinished_count() == 0 {
break;
}
let new_size = terminal::size()?;
if new_size != size {
size = new_size;
buffer = tuviv::Buffer::new(Vec2::new(size.0.into(), size.1.into()));
}
buffer.clear();
ui(&app).render(Rect::new(0, 0, size.0.into(), size.1.into()), &mut buffer);
backend.finish(&buffer, &mut stdout)?;
stdout.flush()?;
if let Event::Key(key) = event::read()? {
if let ControlFlow::Break(()) = handle_input(&mut app, key)? {
finished = false;
break;
}
}
}
disable_raw_mode()?;
execute!(
stdout,
LeaveAlternateScreen,
DisableMouseCapture,
cursor::Show
)?;
app.write_scores()?;
if finished {
println!("All cards learned!");
} else {
println!("Exiting!");
}
Ok(())
}
fn handle_input(app: &mut App, key: KeyEvent) -> io::Result<ControlFlow<()>> {
match app.card().inner.question_type {
CardType::Memory => match key.code {
KeyCode::Char('j') => {
app.change_current_card_score(-1);
app.next_card();
}
KeyCode::Char('k') => {
app.change_current_card_score(1);
app.next_card();
}
KeyCode::Char(' ') => {
app.flip();
}
KeyCode::Enter => app.next_card(),
KeyCode::Char('w') => {
app.write_scores()?;
}
KeyCode::Char('q') => {
return Ok(ControlFlow::Break(()));
}
_ => (),
},
CardType::Input => {
if app.flipped {
match key.code {
KeyCode::Char('q') => {
return Ok(ControlFlow::Break(()));
}
KeyCode::Enter => {
app.change_current_card_score(if app.text == app.card().inner.answer {
1
} else {
-1
});
app.next_card();
}
_ => (),
}
} else {
match key.code {
KeyCode::Char(x) => app.text.push(x),
KeyCode::Backspace => {
let _ = app.text.pop();
}
KeyCode::Enter => app.flip(),
_ => (),
}
}
}
}
Ok(ControlFlow::Continue(()))
}
#[cfg(feature = "ui")]
fn ui(app: &App) -> Box<impl Widget + '_> {
Flexbox::new(Orientation::Vertical, false)
.child(create_instructions(app).to_flex_child())
.child(create_card(app).to_flex_child().expand(1))
.child(create_stats(app).to_flex_child())
}
#[cfg(feature = "ui")]
fn create_instructions(app: &App) -> Box<impl Widget> {
match app.card().inner.question_type {
CardType::Memory => Flexbox::new(Orientation::Horizontal, false)
.child(P::new("Press ".styled()).wrap(false).to_flex_child())
.child(
P::new("q".with_style(theme::KEY))
.wrap(false)
.to_flex_child(),
)
.child(P::new(" to exit, ".styled()).wrap(false).to_flex_child())
.child(
P::new("space".with_style(theme::KEY))
.wrap(false)
.to_flex_child(),
)
.child(P::new(" to flip, ".styled()).wrap(false).to_flex_child())
.child(
P::new("j".with_style(theme::KEY))
.wrap(false)
.to_flex_child(),
)
.child(P::new(" to demote, ".styled()).wrap(false).to_flex_child())
.child(
P::new("k".with_style(theme::KEY))
.wrap(false)
.to_flex_child(),
)
.child(P::new(" to promote.".styled()).wrap(false).to_flex_child()),
CardType::Input => Flexbox::new(Orientation::Horizontal, false)
.child(P::new("Press ".styled()).wrap(false).to_flex_child())
.child(
P::new("q".with_style(theme::KEY))
.wrap(false)
.to_flex_child(),
)
.child(P::new(" to exit, ".styled()).wrap(false).to_flex_child())
.child(
P::new("enter".with_style(theme::KEY))
.wrap(false)
.to_flex_child(),
)
.child(P::new(" to submit, ".styled()).wrap(false).to_flex_child()),
}
}
#[cfg(feature = "ui")]
fn create_card(app: &App) -> Box<impl Widget + '_> {
match app.card().inner.question_type {
CardType::Memory => {
let card = if app.flipped {
P::new((&app.card().inner.answer).with_style(theme::CARD_FLIPPED))
} else {
P::new((&app.card().inner.question).with_style(theme::CARD))
};
let card = card.prefered_ratio(8.0).text_align(Alignment::Center);
let card = card
.create_paragraphs()
.centered()
.to_box_sizing()
.padding(TRBL::new(1, 1, 1, 1))
.border(Border::MODERN);
card.centered() }
CardType::Input => {
let card = if app.flipped {
if app.text == app.card().inner.answer {
Flexbox::new(Orientation::Vertical, false)
.child(
P::new("[Correct]".with_style(theme::CORRECT))
.text_align(Alignment::Center)
.to_flex_child(),
)
.child(
P::new((&app.text).with_style(theme::YOUR_ANSWER))
.text_align(Alignment::Center)
.prefered_ratio(8.0)
.create_paragraphs()
.to_flex_child(),
)
} else {
let mut fb = Flexbox::new(Orientation::Vertical, false).child(
P::new("[Incorrect]".with_style(theme::INCORRECT))
.text_align(Alignment::Center)
.to_flex_child(),
);
if !app.text.is_empty() {
fb = fb
.child(P::new("You wrote:".styled()).to_flex_child())
.child(
P::new((&app.text).with_style(theme::YOUR_ANSWER))
.text_align(Alignment::Center)
.prefered_ratio(8.0)
.create_paragraphs()
.to_box_sizing()
.margin_left(4)
.to_flex_child(),
);
}
fb = fb.child(P::new("Answer:".styled()).to_flex_child()).child(
P::new((&app.card().inner.answer).with_style(theme::CARD_FLIPPED))
.text_align(Alignment::Center)
.prefered_ratio(8.0)
.create_paragraphs()
.to_box_sizing()
.margin_left(4)
.to_flex_child(),
);
fb
}
} else {
Flexbox::new(Orientation::Vertical, false)
.child(
P::new((&app.card().inner.question).with_style(theme::CARD))
.text_align(Alignment::Center)
.prefered_ratio(8.0)
.create_paragraphs()
.to_flex_child(),
)
.child(
P::new((&app.text).with_style(theme::YOUR_ANSWER))
.prefered_ratio(8.0)
.create_paragraphs()
.to_box_sizing()
.border(Border::MODERN)
.to_flex_child(),
)
};
let card = card
.centered()
.to_box_sizing()
.padding(TRBL::new(1, 1, 1, 1))
.border(Border::MODERN);
card.centered() }
}
}
#[cfg(feature = "ui")]
fn create_stats(app: &App) -> Box<impl Widget + '_> {
Flexbox::new(Orientation::Horizontal, false)
.child(P::new("Card Score: ".styled()).wrap(false).to_flex_child())
.child(
P::new(app.card_score().to_string().with_style(theme::STAT))
.wrap(false)
.to_flex_child(),
)
.child(P::new(", Correct: ".styled()).wrap(false).to_flex_child())
.child(
P::new(
(app.cards().len() - app.unfinished_count())
.to_string()
.with_style(theme::STAT),
)
.wrap(false)
.to_flex_child(),
)
.child(
P::new(", Still to go: ".styled())
.wrap(false)
.to_flex_child(),
)
.child(
P::new(app.unfinished_count().to_string().with_style(theme::STAT))
.wrap(false)
.to_flex_child(),
)
.child(P::new(", Total: ".styled()).wrap(false).to_flex_child())
.child(
P::new(app.cards().len().to_string().with_style(theme::STAT))
.wrap(false)
.to_flex_child(),
)
}