#[cfg(not(feature = "cli"))]
compile_error!("the cli binary must be built with the `cli` feature flag");
#[cfg(feature = "cli")]
fn main() -> anyhow::Result<()> {
use anyhow::Context;
use clap::Parser;
use forne::{Forne, Set};
use opts::{Args, Command};
use std::fs;
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
let args = Args::parse();
match args.command {
Command::New {
input,
output,
adapter,
method,
} => {
let contents =
fs::read_to_string(input).with_context(|| "failed to read from source file")?;
let adapter_script =
fs::read_to_string(adapter).with_context(|| "failed to read adapter script")?;
let method = method_from_string(method)?;
let forne = Forne::new_set(contents, &adapter_script, method)?;
let json = forne.save_set()?;
fs::write(output, json).with_context(|| "failed to write new set to output file")?;
println!("New set created!");
}
Command::Update {
set: set_file,
source,
adapter,
method,
} => {
let json =
fs::read_to_string(&set_file).with_context(|| "failed to read from set file")?;
let set = Set::from_json(&json)?;
let source =
fs::read_to_string(source).with_context(|| "failed to read from source file")?;
let adapter_script =
fs::read_to_string(adapter).with_context(|| "failed to read adapter script")?;
let method = method_from_string(method)?;
let mut forne = Forne::from_set(set);
forne.update(source, &adapter_script, method)?;
let new_json = forne.save_set()?;
fs::write(set_file, new_json)
.with_context(|| "failed to write updated set to output file")?;
println!("Set updated successfully!");
}
Command::Learn {
set: set_file,
method,
ty,
count,
reset,
} => {
let json =
fs::read_to_string(&set_file).with_context(|| "failed to read from set file")?;
let set = Set::from_json(&json)?;
let mut forne = Forne::from_set(set);
let method = method_from_string(method)?;
if reset && confirm("Are you absolutely certain you want to reset your learn progress? This action is IRREVERSIBLE!!!")? {
forne.reset_learn(method.clone())?;
} else {
println!("Continuing with previous progress...");
}
let mut driver = forne.learn(method)?;
driver.set_target(ty);
if let Some(count) = count {
driver.set_max_count(count);
}
let num_reviewed = drive(driver, &set_file)?;
println!(
"\nLearn session complete! You reviewed {} card(s).",
num_reviewed
);
}
Command::Test {
set: set_file,
static_test,
no_star,
no_unstar,
ty,
count,
reset,
} => {
let json =
fs::read_to_string(&set_file).with_context(|| "failed to read from set file")?;
let set = Set::from_json(&json)?;
let mut forne = Forne::from_set(set);
if reset && confirm("Are you sure you want to reset your test progress?")? {
forne.reset_test();
} else {
println!("Continuing with previous progress...");
}
let mut driver = forne.test();
driver.set_target(ty);
if let Some(count) = count {
driver.set_max_count(count);
}
if static_test {
driver.no_mark_starred().no_mark_unstarred();
} else if no_star {
driver.no_mark_starred();
} else if no_unstar {
driver.no_mark_unstarred();
}
let num_reviewed = drive(driver, &set_file)?;
println!("\nTest complete! You reviewed {} card(s).", num_reviewed);
}
Command::List { set, ty } => {
let json = fs::read_to_string(set).with_context(|| "failed to read from set file")?;
let set = Set::from_json(&json)?;
let mut yellow = ColorSpec::new();
yellow.set_fg(Some(Color::Yellow));
let mut green = ColorSpec::new();
green.set_fg(Some(Color::Green));
let mut stdout = StandardStream::stdout(ColorChoice::Always);
let mut num_printed = 0;
let list = set.list(ty);
for card in list.iter() {
stdout.set_color(&yellow)?;
println!(
"{}Q: {}",
if card.starred { "⦿ " } else { "" },
card.question
);
stdout.set_color(&green)?;
println!("A: {}", card.answer);
stdout.reset()?;
num_printed += 1;
if list.len() != num_printed {
println!("---");
}
}
}
};
Ok(())
}
#[cfg(feature = "cli")]
fn method_from_string(method_str: String) -> anyhow::Result<forne::RawMethod> {
use anyhow::bail;
use forne::RawMethod;
use std::{fs, path::PathBuf};
if RawMethod::is_inbuilt(&method_str) {
Ok(RawMethod::Inbuilt(method_str))
} else {
let method_path = PathBuf::from(&method_str);
if let Ok(contents) = fs::read_to_string(&method_path) {
let name = format!(
"{}/{}",
whoami::username(),
method_path.file_name().unwrap().to_string_lossy()
);
Ok(RawMethod::Custom {
name,
body: contents,
})
} else {
bail!("provided method is not inbuilt and does not represent a valid method file (or if it did, forne couldn't read it)")
}
}
}
#[cfg(feature = "cli")]
fn drive<'a>(mut driver: forne::Driver<'a, 'a>, set_file: &str) -> anyhow::Result<u32> {
use anyhow::{bail, Context};
use crossterm::{terminal, ExecutableCommand};
use std::{
fs,
io::{self, Write},
};
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
let mut yellow = ColorSpec::new();
yellow.set_fg(Some(Color::Yellow));
let mut green = ColorSpec::new();
green.set_fg(Some(Color::Green));
let stdin = io::stdin();
let mut stdout = StandardStream::stdout(ColorChoice::Always);
let mut card_option = driver.first()?;
while let Some(card) = card_option {
let json = driver.save_set_to_json()?;
fs::write(set_file, json).with_context(|| {
"failed to save set to json (progress up to the previous card was saved though)"
})?;
stdout.set_color(&yellow)?;
print!(
"{}Q: {}",
if card.starred { "⦿ " } else { "" },
card.question
);
stdout.flush()?;
let res = stdin.read_line(&mut String::new());
if let Ok(0) = res {
break;
}
stdout.set_color(&green)?;
println!("A: {}", card.answer);
stdout.reset()?;
let res = loop {
print!(
"How did you do? [{}] ",
driver.allowed_responses().join("/"),
);
stdout.flush()?;
let mut input = String::new();
match stdin.read_line(&mut input) {
Ok(_) => {
let input = input.strip_suffix('\n').unwrap_or(input.as_str());
if driver.allowed_responses().iter().any(|x| x == input) {
break input.to_string();
} else {
println!("Invalid option!");
continue;
}
}
Err(_) => bail!("failed to read from stdin"),
};
};
stdout.execute(terminal::Clear(terminal::ClearType::All))?;
card_option = driver.next(res)?;
}
stdout.reset()?;
let json = driver.save_set_to_json()?;
fs::write(set_file, json).with_context(|| {
"failed to save set to json (progress up to the previous card was saved though)"
})?;
Ok(driver.get_count())
}
#[cfg(feature = "cli")]
fn confirm(message: &str) -> anyhow::Result<bool> {
use anyhow::bail;
use std::io::{self, Write};
let stdin = io::stdin();
let mut stdout = io::stdout();
print!("{} [y/n] ", message);
stdout.flush()?;
let mut input = String::new();
let res = match stdin.read_line(&mut input) {
Ok(_) => {
let input = input.strip_suffix('\n').unwrap_or(&input);
if input == "y" {
true
} else if input == "n" {
false
} else {
println!("Invalid option!");
confirm(message)?
}
}
Err(_) => bail!("failed to read from stdin"),
};
Ok(res)
}
#[cfg(feature = "cli")]
mod opts {
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use forne::CardType;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Args {
#[clap(subcommand)]
pub command: Command,
}
#[derive(Subcommand, Debug)]
pub enum Command {
New {
input: String,
output: String,
#[arg(short, long)]
adapter: PathBuf,
#[arg(short, long)]
method: String, },
Update {
set: String,
#[arg(short, long)]
source: String,
#[arg(short, long)]
adapter: PathBuf,
#[arg(short, long)]
method: String, },
Learn {
set: String,
#[arg(short, long)]
method: String, #[arg(short, long = "type", value_enum, default_value = "all")]
ty: CardType,
#[arg(short, long)]
count: Option<u32>,
#[arg(long)]
reset: bool,
},
Test {
set: String,
#[arg(long = "static")]
static_test: bool,
#[arg(long)]
no_star: bool,
#[arg(long)]
no_unstar: bool,
#[arg(short, long = "type", value_enum, default_value = "all")]
ty: CardType,
#[arg(short, long)]
count: Option<u32>,
#[arg(long)]
reset: bool,
},
List {
set: String,
#[arg(short, long = "type", value_enum, default_value = "all")]
ty: CardType,
},
}
}