Migrate to Gitea

This commit is contained in:
2025-04-17 15:53:45 -06:00
parent da86957915
commit 7a0b0cd465
46 changed files with 6083 additions and 0 deletions

3
src/course/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub(crate) mod units;
pub(crate) mod story;
pub(crate) mod tracker;

218
src/course/story.rs Normal file
View File

@@ -0,0 +1,218 @@
use serde::{Deserialize, Serialize};
use crate::{files, learner::profile, printers::{self, clear_console, print_boxed, print_screen}, utilities::questions::{ask_mcq, ask_multi_select, ask_plaintext}, Module};
use super::units;
const REQUIRED_FOR_MASTERY: f64 = 0.1;
#[derive(Serialize, Deserialize, Debug)]
pub struct UnitStory {
unit: Module,
title: String,
screens: Vec<Screen>
}
#[derive(Serialize, Deserialize, Debug)]
enum ScreenType {
Text,
Mcq,
Checkboxes,
FreeResponse,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Screen {
screen_type: ScreenType,
text: String,
question: Option<String>,
options: Option<Vec<String>>,
correct_indices: Option<Vec<usize>>,
correct_text: Option<Vec<String>>,
hints: Option<Vec<String>>,
}
pub fn outer_loop(learner: &mut profile::Learner) {
loop {
let next_unit = select_next_unit(&learner);
if next_unit.is_none() {
print_boxed("Congratulations! You have completed all the modules. You are now a master of the galaxy! I have nothing new to teach you. Well done.", printers::StatementType::CorrectFeedback);
println!("Saving your progress...");
files::save_to_file(&learner.filename, &learner)
.expect("Failed to save profile file");
println!("Progress saved. Goodbye!");
return;
}
let next_unit = next_unit.unwrap();
inner_loop(next_unit.clone(), learner);
if learner.progress.get(&next_unit).map_or(true, |p| p.get_probability() >= 1.0) {
print_boxed(&format!("You have completed {:?}", next_unit), printers::StatementType::GeneralFeedback);
} else {
print_boxed(&format!("I think we still need to practice {:?}. Let's keep practicing before we move on.", next_unit), printers::StatementType::GeneralFeedback);
}
let answer = ask_mcq("Do you want to keep going or leave and save?", &[
"Keep working (and save the galaxy)",
"Leave and save (risking all life in the galaxy)",
]).unwrap();
if answer == "Keep working (and save the galaxy)" {
print_boxed(&format!("Great! Let's carry on."), printers::StatementType::GeneralFeedback);
println!("Saving your progress...");
files::save_to_file(&learner.filename, &learner)
.expect("Failed to save profile file");
println!("Progress saved. Let's keep going!");
learner.display_progress();
continue;
} else {
println!("Bummer. We were really counting on you, but I guess you probably have human needs to attend to.");
println!("Saving your progress...");
files::save_to_file(&learner.filename, &learner)
.expect("Failed to save profile file");
println!("Progress saved. Goodbye!");
return;
}
}
}
pub fn inner_loop(unit: Module, learner: &mut profile::Learner) {
print_boxed(&format!("The unit is {}", unit), printers::StatementType::GeneralFeedback);
let filename = unit.get_filename();
let unit_story = files::parse_unit(&filename).unwrap();
let mut milestones = 0;
printers::unit_screen(&unit_story.title);
for screen in &unit_story.screens {
match screen.screen_type {
ScreenType::Text => { }
_ => {
milestones += 1;
}
}
}
if let Some(progress) = learner.progress.get_mut(&unit) {
progress.set_milestones(milestones);
progress.set_completed_milestones(0);
}
for screen in unit_story.screens {
let mut attempts = 0;
match screen.screen_type {
ScreenType::Text => {
clear_console();
print_screen(&screen.text);
}
ScreenType::Mcq => {
let options: Vec<&str> = screen.options.as_ref().unwrap().iter().map(|s| s.as_str()).collect();
let mut options = screen.options.clone().unwrap();
options.push("Get hint".to_string());
let options: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
loop {
let answer = ask_mcq(&screen.question.clone().unwrap(), &options).unwrap();
let correct = if screen.correct_indices.as_ref().unwrap().contains(&options.iter().position(|x| *x == answer).unwrap_or(usize::MAX)) {
print_boxed("Correct!", printers::StatementType::CorrectFeedback);
true
} else {
if screen.hints.is_some() {
if attempts == screen.hints.iter().len() {
let correct_indices_text: Vec<_> = screen.correct_indices.unwrap().iter().map(|&i| options[i]).collect();
print_boxed(&format!("It seems like these hints aren't working. The answer is: {:?}", correct_indices_text), printers::StatementType::IncorrectFeedback);
break;
} else {
let hint = screen.hints.as_ref().unwrap()[attempts].clone();
print_boxed(&format!("Please try the question again. Here's a hint: {}", hint), printers::StatementType::IncorrectFeedback);
println!("Please try the question again. Here's a hint: {}", hint);
attempts += 1;
}
}
false
};
learner.update_progress(unit.clone(), correct);
learner.progress.get_mut(&unit).unwrap().print_progress();
if correct {
break;
}
}
}
ScreenType::Checkboxes => {
let options: Vec<&str> = screen.options.as_ref().unwrap().iter().map(|s| s.as_str()).collect();
let mut options = screen.options.clone().unwrap();
options.push("Get hint".to_string());
let options: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
loop {
let answers = ask_multi_select(&screen.question.clone().unwrap(), &options).unwrap();
let correct_indices: Vec<usize> = screen.correct_indices.as_ref().unwrap().to_vec();
let selected_indices: Vec<usize> = answers.iter().filter_map(|answer| options.iter().position(|x| *x == *answer)).collect();
let correct = selected_indices.iter().all(|&index| correct_indices.contains(&index)) && selected_indices.len() == correct_indices.len();
if correct {
print_boxed("Correct!", printers::StatementType::CorrectFeedback);
learner.update_progress(unit.clone(), true);
learner.progress.get_mut(&unit).unwrap().print_progress();
break;
} else {
if screen.hints.is_some() {
if attempts == screen.hints.iter().len() {
print_boxed(&format!("It seems like these hints aren't working. The correct answers are: {:?}", correct_indices.iter().map(|&i| options[i]).collect::<Vec<_>>()), printers::StatementType::IncorrectFeedback);
break;
} else {
let hint = screen.hints.as_ref().unwrap()[attempts].clone();
let hint = format!("{}\nThere are {} correct answers.", hint, correct_indices.len());
print_boxed(&format!("Please try the question again. Here's a hint: {}", hint), printers::StatementType::IncorrectFeedback);
println!("Please try the question again. Here's a hint: {}", hint);
attempts += 1;
}
}
learner.update_progress(unit.clone(), false);
learner.progress.get_mut(&unit).unwrap().print_progress();
}
}
}
ScreenType::FreeResponse => {
loop {
let answer= ask_plaintext(&screen.question.clone().unwrap()).unwrap();
let correct_text: Vec<String> = screen.correct_text.as_ref().unwrap().to_vec();
let normalized_answer = answer.trim().to_lowercase().replace(|c: char| !c.is_alphanumeric(), "");
let correct = screen.correct_text.as_ref().unwrap().iter().any(|i| {
let normalized_correct_answer = i.trim().to_lowercase().replace(|c: char| !c.is_alphanumeric(), "");
normalized_answer == normalized_correct_answer
});
if correct {
print_boxed("Correct!", printers::StatementType::CorrectFeedback);
learner.update_progress(unit.clone(), true);
learner.progress.get_mut(&unit).unwrap().print_progress();
break;
} else {
if screen.hints.is_some() {
if attempts == screen.hints.iter().len() {
print_boxed(&format!("It seems like these hints aren't working. The correct answers are: {:?}", correct_text), printers::StatementType::IncorrectFeedback);
break;
} else {
let hint = screen.hints.as_ref().unwrap()[attempts].clone();
print_boxed(&format!("Please try the question again. Here's a hint: {}", hint), printers::StatementType::IncorrectFeedback);
println!("Please try the question again. Here's a hint: {}", hint);
attempts += 1;
}
}
learner.update_progress(unit.clone(), false);
learner.progress.get_mut(&unit).unwrap().print_progress();
}
}
}
}
}
}
pub fn select_next_unit(learner: &profile::Learner) -> Option<units::Module> {
let mut modules = units::Module::iter();
if learner.progress.get(&Module::Introduction).map_or(true, |p| p.get_probability() == 0.0) {
return Some(crate::Module::Introduction);
}
for module in modules {
if let Some(progress) = learner.progress.get(&module) {
if progress.get_probability() < REQUIRED_FOR_MASTERY {
if progress.get_probability() != 0.0 {
print_boxed(&format!("It looks like you haven't mastered {} yet. Let's work on that.", module), printers::StatementType::GeneralFeedback);
}
return Some(module);
}
// if progress.get_milestones_completed() == 0 {
// return Some(module);
// }
}
}
return None;
}

141
src/course/tracker.rs Normal file
View File

@@ -0,0 +1,141 @@
use crossterm::{style::Stylize, terminal::size};
use serde::{Deserialize, Serialize};
use super::units::Module;
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub(crate) struct Tracker {
pub(crate) unit: Module,
questions_answered: u64,
questions_correct: u64,
probability_known: f64,
results: Vec<bool>, // Vector to store correct/incorrect results
milestones: usize,
milestones_completed: usize,
}
impl Tracker {
pub(crate) fn new(unit: Module) -> Self {
Self {
unit,
questions_answered: 0,
questions_correct: 0,
probability_known: 0.3, // Start with a 30% chance of knowing the answer
results: Vec::new(),
milestones: 0,
milestones_completed: 0,
}
}
pub(crate) fn quiz_result(&mut self, correct: bool) -> f64 {
self.questions_answered += 1;
self.results.push(correct);
if correct {
self.milestones_completed = (self.milestones_completed + 1).min(self.milestones);
self.questions_correct += 1;
}
// Calculate streak of correct answers
let mut streak = 0;
for &result in self.results.iter().rev() {
if result {
streak += 1;
} else {
break;
}
}
let prior = self.probability_known;
let slip = if self.questions_answered < 10 {
0.05 // Lower slip rate for beginners
} else if self.probability_known > 0.8 {
0.15 // Higher slip rate for advanced students
} else {
0.1 // Default slip rate
};
let guess = if self.questions_answered < 10 {
0.2 // Lower guess rate for beginners
} else if self.probability_known > 0.8 {
0.3 // Higher guess rate for advanced students
} else {
0.25 // Default guess rate (average of 4 options)
};
let likelihood = if correct {
1.0 - slip
} else {
guess
};
// Adjust posterior based on streak
let streak_multiplier = 1.0 + (streak as f64 * 0.1); // Increase probability for streaks
let posterior = if prior == 0.0 {
likelihood * streak_multiplier // Start with likelihood if prior is zero
} else {
((likelihood * prior) / ((likelihood * prior) + ((1.0 - likelihood) * (1.0 - prior)))) * streak_multiplier
};
self.probability_known = posterior.min(1.0).max(0.0);
self.probability_known
}
pub(crate) fn get_probability(&self) -> f64 {
// self.probability_known
if self.milestones == 0 {
return 0.0;
}
if self.milestones_completed == 0 {
return 0.0;
}
self.milestones_completed as f64 / self.milestones as f64
}
pub fn get_milestones(&self) -> usize {
self.milestones
}
pub fn get_milestones_completed(&self) -> usize {
self.milestones_completed
}
pub(crate) fn print_progress(&self) {
let (cols, _) = size().unwrap();
// let percent = self.probability_known * 100.0; // Assuming `probability_known` is a method in `LessonTracker`
let percent = if self.milestones_completed > 0 {
(self.milestones_completed as f64) / (self.milestones as f64) * 100.0 // Assuming `probability_known` is a method in `LessonTracker`
} else {0.0};
let percent_name = format!("{:>4.0}%", percent).dark_blue();
let unit_name = format!("{} {} ", percent_name, self.unit);
let dots = ".".repeat((cols as usize).saturating_sub(unit_name.len() + 11));
match percent {
percent if percent > 0.0 => {
let progress = percent as usize;
let filled_percent = progress / 5; // Each '=' represents 5%
let bars = format!(
"|{}{}|",
"".repeat(filled_percent),
" ".repeat(20_usize.saturating_sub(filled_percent)),
);
println!(
"{} {} {}",
unit_name,
dots,
bars,
);
}
_ => {
println!(
"{} {} |{}|",
unit_name,
dots,
" ".repeat(20),
);
}
}
}
pub fn set_milestones(&mut self, milestones: usize) {
self.milestones = milestones;
}
pub fn set_completed_milestones(&mut self, completed_milestones: usize) {
self.milestones_completed = completed_milestones;
}
}

382
src/course/units.rs Normal file
View File

@@ -0,0 +1,382 @@
use serde::{Deserialize, Serialize};
use std::fmt;
use rand::{rng, seq::IndexedRandom};
use std::fs;
use std::path::Path;
// use strum_macros::EnumIter;
/// Module defines the structure and data for the course units, including lessons and modules.
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Clone)]
pub(crate) enum Module {
Introduction,
Logic(Logic),
Fallacy(Fallacy),
Bias(Bias),
Appraisal(Appraisal),
}
impl Default for Module {
fn default() -> Self {
Module::Introduction
}
}
impl fmt::Display for Module {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Module::Introduction => write!(f, "Introduction"),
Module::Logic(logic) => write!(f, "Logic: {}", logic),
Module::Fallacy(fallacy) => write!(f, "Fallacy: {}", fallacy),
Module::Bias(bias) => write!(f, "Bias: {}", bias),
Module::Appraisal(appraisal) => write!(f, "Appraisal: {}", appraisal),
}
}
}
impl Module {
pub(crate) fn get_filename(&self) -> String {
match self {
Module::Introduction => format!("data/introduction.json"),
Module::Logic(logic) => format!("data/logic/{}.json", logic.get_filename()),
Module::Fallacy(fallacy) => format!("data/fallacy/{}.json", fallacy.get_filename()),
Module::Bias(bias) => format!("data/bias/{}.json", bias.get_filename()),
Module::Appraisal(appraisal) => format!("data/appraisal/{}.json", appraisal.get_filename()),
}
}
pub(crate) fn random() -> Self {
let mut rng = rng();
let variants = [
Module::Logic(Logic::random()),
Module::Fallacy(Fallacy::random()),
Module::Bias(Bias::random()),
Module::Appraisal(Appraisal::random()),
];
variants.choose(&mut rng).unwrap().clone()
}
pub fn iter() -> impl Iterator<Item = Self> {
vec![
Module::Introduction,
Module::Logic(Logic::LogicalOperations),
Module::Logic(Logic::BooleanAlgebra),
Module::Fallacy(Fallacy::FormalFallacy(FormalFallacy::PropositionalFallacy)),
Module::Fallacy(Fallacy::FormalFallacy(FormalFallacy::ProbabilisticFallacy)),
Module::Fallacy(Fallacy::FormalFallacy(FormalFallacy::SyllogisticFallacy)),
Module::Fallacy(Fallacy::FormalFallacy(FormalFallacy::QuantificationalFallacy)),
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::PostHocErgoPropterHoc)),
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::SlipperySlope)),
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::TexasSharpshooter)),
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::HastyGeneralization)),
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::OverGeneralization)),
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::NoTrueScotsman)),
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::QuotingOutOfContext)),
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::AdHominem)),
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::TuQuoque)),
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::Bandwagon)),
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::StrawMan)),
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::AdIgnorantiam)),
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::SpecialPleading)),
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::BeggingTheQuestion)),
Module::Bias(Bias::ConfirmationBias),
Module::Bias(Bias::TheHaloEffect),
Module::Bias(Bias::FundamentalAttributionError),
Module::Bias(Bias::InGroupBias),
Module::Bias(Bias::DunningKrugerEffect),
Module::Bias(Bias::BarnumEffect),
Module::Appraisal(Appraisal::ConversionToPropositional),
Module::Appraisal(Appraisal::CounterArgument),
]
.into_iter()
}
// pub(crate) fn create_empty_files() -> std::io::Result<()> {
// for module in Module::iter() {
// let filename = module.get_filename();
// let path = Path::new(&filename);
// if let Some(parent) = path.parent() {
// fs::create_dir_all(parent)?;
// }
// fs::File::create(path)?;
// }
// Ok(())
// }
}
/// Logic module, which includes various lessons related to logical reasoning and operations.
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
pub(crate) enum Logic {
LogicalOperations,
BooleanAlgebra,
}
impl Default for Logic {
fn default() -> Self {
Logic::LogicalOperations
}
}
impl std::fmt::Display for Logic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Logic::LogicalOperations => write!(f, "Logical Operations"),
Logic::BooleanAlgebra => write!(f, "Boolean Algebra"),
}
}
}
impl Logic {
pub(crate) fn get_filename(&self) -> String {
match self {
Logic::LogicalOperations => "logical_operations".to_string(),
Logic::BooleanAlgebra => "boolean_algebra".to_string(),
}
}
pub(crate) fn random() -> Self {
let mut rng = rng();
let variants = [Logic::LogicalOperations, Logic::BooleanAlgebra];
variants.choose(&mut rng).unwrap().clone()
}
}
/// Fallacy module, which includes various lessons related to logical fallacies and errors in reasoning.
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
pub(crate) enum Fallacy {
FormalFallacy(FormalFallacy),
InformalFallacy(InformalFallacy),
}
impl Default for Fallacy {
fn default() -> Self {
Fallacy::FormalFallacy(FormalFallacy::PropositionalFallacy)
}
}
impl std::fmt::Display for Fallacy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Fallacy::FormalFallacy(formal) => write!(f, "Formal: {}", formal),
Fallacy::InformalFallacy(informal) => write!(f, "Informal: {}", informal),
}
}
}
impl Fallacy {
pub(crate) fn get_filename(&self) -> String {
match self {
Fallacy::FormalFallacy(formal) => format!("formal/{}", formal.get_filename()),
Fallacy::InformalFallacy(informal) => format!("informal/{}", informal.get_filename()),
}
}
pub(crate) fn random() -> Self {
let mut rng = rng();
let variants = [
Fallacy::FormalFallacy(FormalFallacy::random()),
Fallacy::InformalFallacy(InformalFallacy::random()),
];
variants.choose(&mut rng).unwrap().clone()
}
}
/// Formal fallacies, which are errors in the structure of an argument.
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
pub(crate) enum FormalFallacy {
PropositionalFallacy,
ProbabilisticFallacy,
SyllogisticFallacy,
QuantificationalFallacy
}
impl Default for FormalFallacy {
fn default() -> Self {
FormalFallacy::PropositionalFallacy
}
}
impl std::fmt::Display for FormalFallacy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FormalFallacy::PropositionalFallacy => write!(f, "Propositional Fallacy"),
FormalFallacy::ProbabilisticFallacy => write!(f, "Probabilistic Fallacy"),
FormalFallacy::SyllogisticFallacy => write!(f, "Syllogistic Fallacy"),
FormalFallacy::QuantificationalFallacy => write!(f, "Quantificational Fallacy"),
}
}
}
impl FormalFallacy {
pub(crate) fn get_filename(&self) -> String {
match self {
FormalFallacy::PropositionalFallacy => "propositional".to_string(),
FormalFallacy::ProbabilisticFallacy => "probabilistic".to_string(),
FormalFallacy::SyllogisticFallacy => "syllogistic".to_string(),
FormalFallacy::QuantificationalFallacy => "quantificational".to_string(),
}
}
pub(crate) fn random() -> Self {
let mut rng = rng();
let variants = [
FormalFallacy::PropositionalFallacy,
FormalFallacy::ProbabilisticFallacy,
FormalFallacy::SyllogisticFallacy,
FormalFallacy::QuantificationalFallacy,
];
variants.choose(&mut rng).unwrap().clone()
}
}
/// Informal fallacies, which are errors in reasoning that do not involve the structure of the argument.
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
pub(crate) enum InformalFallacy {
PostHocErgoPropterHoc,
SlipperySlope,
TexasSharpshooter,
HastyGeneralization,
OverGeneralization,
NoTrueScotsman,
QuotingOutOfContext,
AdHominem,
TuQuoque,
Bandwagon,
StrawMan,
AdIgnorantiam,
SpecialPleading,
BeggingTheQuestion
}
impl Default for InformalFallacy {
fn default() -> Self {
InformalFallacy::PostHocErgoPropterHoc
}
}
impl std::fmt::Display for InformalFallacy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InformalFallacy::PostHocErgoPropterHoc => write!(f, "Post Hoc Ergo Propter Hoc"),
InformalFallacy::SlipperySlope => write!(f, "Slippery Slope"),
InformalFallacy::TexasSharpshooter => write!(f, "Texas Sharpshooter"),
InformalFallacy::HastyGeneralization => write!(f, "Hasty Generalization"),
InformalFallacy::OverGeneralization => write!(f, "Over Generalization"),
InformalFallacy::NoTrueScotsman => write!(f, "No True Scotsman"),
InformalFallacy::QuotingOutOfContext => write!(f, "Quoting Out Of Context"),
InformalFallacy::AdHominem => write!(f, "Ad Hominem"),
InformalFallacy::TuQuoque => write!(f, "Tu Quoque"),
InformalFallacy::Bandwagon => write!(f, "Bandwagon"),
InformalFallacy::StrawMan => write!(f, "Straw Man"),
InformalFallacy::AdIgnorantiam => write!(f, "Ad Ignorantiam"),
InformalFallacy::SpecialPleading => write!(f, "Special Pleading"),
InformalFallacy::BeggingTheQuestion => write!(f, "Begging The Question"),
}
}
}
impl InformalFallacy {
pub(crate) fn get_filename(&self) -> String {
match self {
InformalFallacy::PostHocErgoPropterHoc => "post_hoc_ergo_propter_hoc".to_string(),
InformalFallacy::SlipperySlope => "slippery_slope".to_string(),
InformalFallacy::TexasSharpshooter => "texas_sharpshooter".to_string(),
InformalFallacy::HastyGeneralization => "hasty_generalization".to_string(),
InformalFallacy::OverGeneralization => "over_generalization".to_string(),
InformalFallacy::NoTrueScotsman => "no_true_scotsman".to_string(),
InformalFallacy::QuotingOutOfContext => "quoting_out_of_context".to_string(),
InformalFallacy::AdHominem => "ad_hominem".to_string(),
InformalFallacy::TuQuoque => "tu_quoque".to_string(),
InformalFallacy::Bandwagon => "bandwagon".to_string(),
InformalFallacy::StrawMan => "straw_man".to_string(),
InformalFallacy::AdIgnorantiam => "ad_ignorantiam".to_string(),
InformalFallacy::SpecialPleading => "special_pleading".to_string(),
InformalFallacy::BeggingTheQuestion => "begging_the_question".to_string(),
}
}
pub(crate) fn random() -> Self {
let mut rng = rng();
let variants = [
InformalFallacy::PostHocErgoPropterHoc,
InformalFallacy::SlipperySlope,
InformalFallacy::TexasSharpshooter,
InformalFallacy::HastyGeneralization,
InformalFallacy::OverGeneralization,
InformalFallacy::NoTrueScotsman,
InformalFallacy::QuotingOutOfContext,
InformalFallacy::AdHominem,
InformalFallacy::TuQuoque,
InformalFallacy::Bandwagon,
InformalFallacy::StrawMan,
InformalFallacy::AdIgnorantiam,
InformalFallacy::SpecialPleading,
InformalFallacy::BeggingTheQuestion,
];
variants.choose(&mut rng).unwrap().clone()
}
}
/// Bias module, which includes various lessons related to cognitive biases and their impact on reasoning.
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
pub(crate) enum Bias {
ConfirmationBias,
TheHaloEffect,
FundamentalAttributionError,
InGroupBias,
DunningKrugerEffect,
BarnumEffect
}
impl Default for Bias {
fn default() -> Self {
Bias::ConfirmationBias
}
}
impl std::fmt::Display for Bias {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Bias::ConfirmationBias => write!(f, "Confirmation Bias"),
Bias::TheHaloEffect => write!(f, "The Halo Effect"),
Bias::FundamentalAttributionError => write!(f, "Fundamental Attribution Error"),
Bias::InGroupBias => write!(f, "In Group Bias"),
Bias::DunningKrugerEffect => write!(f, "Dunning Kruger Effect"),
Bias::BarnumEffect => write!(f, "Barnum Effect"),
}
}
}
impl Bias {
pub(crate) fn get_filename(&self) -> String {
match self {
Bias::ConfirmationBias => "confirmation_bias".to_string(),
Bias::TheHaloEffect => "the_halo_effect".to_string(),
Bias::FundamentalAttributionError => "fundamental_attribution_error".to_string(),
Bias::InGroupBias => "in_group_bias".to_string(),
Bias::DunningKrugerEffect => "dunning_kruger_effect".to_string(),
Bias::BarnumEffect => "barnum_effect".to_string(),
}
}
pub(crate) fn random() -> Self {
let mut rng = rng();
let variants = [
Bias::ConfirmationBias,
Bias::TheHaloEffect,
Bias::FundamentalAttributionError,
Bias::InGroupBias,
Bias::DunningKrugerEffect,
Bias::BarnumEffect,
];
variants.choose(&mut rng).unwrap().clone()
}
}
/// Appraisal module, which includes various lessons related to the evaluation and appraisal of arguments.
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
pub(crate) enum Appraisal {
ConversionToPropositional,
CounterArgument,
}
impl Default for Appraisal {
fn default() -> Self {
Appraisal::ConversionToPropositional
}
}
impl std::fmt::Display for Appraisal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Appraisal::ConversionToPropositional => write!(f, "Conversion to Propositional"),
Appraisal::CounterArgument => write!(f, "Counter Argument"),
}
}
}
impl Appraisal {
pub(crate) fn get_filename(&self) -> String {
match self {
Appraisal::ConversionToPropositional => "conversion_to_propositional".to_string(),
Appraisal::CounterArgument => "counter_argument".to_string(),
}
}
pub(crate) fn random() -> Self {
let mut rng = rng();
let variants = [
Appraisal::ConversionToPropositional,
Appraisal::CounterArgument,
];
variants.choose(&mut rng).unwrap().clone()
}
}

1
src/learner/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub(crate) mod profile;

94
src/learner/profile.rs Normal file
View File

@@ -0,0 +1,94 @@
use std::collections::HashMap;
use crossterm::style::Stylize;
use crossterm::terminal::size;
use serde::{Deserialize, Serialize};
use crate::course::{units, tracker};
use crate::{files, printers};
use crate::printers::{clear_console, wait_for_input};
use crate::utilities::questions;
#[serde_with::serde_as]
#[derive(Serialize, Deserialize, Debug)]
pub struct Learner {
pub name: String,
pub filename: String,
#[serde_as(as = "HashMap<serde_with::json::JsonString, _>")]
pub progress: HashMap<units::Module, tracker::Tracker>,
}
impl Learner {
pub fn new() -> Self {
let previous_saves = files::get_previous_saves();
let user = questions::ask_mcq(
"Please select or create a user profile.",
&previous_saves.iter().map(String::as_str).collect::<Vec<&str>>(),
).unwrap();
let learner;
if user == "Create New" {
let name = questions::ask_name();
let sanitized_name = files::sanitize_name(name.as_str());
let filename = format!("saves/{}.json", sanitized_name);
let modules = units::Module::iter();
let mut progress = HashMap::new();
for module in modules {
progress.insert(module.clone(), tracker::Tracker::new(module));
}
learner = Self {
name: name.clone(),
filename: filename.clone(),
progress: progress.clone(),
};
files::save_to_file(&filename, &learner).expect("Failed to create new profile file");
printers::print_boxed(&format!("Profile created for {}. Your progress will be saved in {}.", name, filename), printers::StatementType::Default);
printers::print_boxed("You won't have any progress yet, but the next screen shows what your progress report will look like. Fill all the progress bars to win.", printers::StatementType::Default);
} else {
let sanitized_name = files::sanitize_name(&user);
let filename = format!("saves/{}.json", sanitized_name);
learner = files::load_from_file(&filename).expect("Failed to load profile file");
printers::print_boxed(&format!("Profile loaded for {}.", user), printers::StatementType::Default);
}
learner.display_progress();
return learner
}
pub fn display_progress(&self) {
clear_console();
let (cols, _) = size().unwrap();
let dashes = "-".repeat((cols as usize));
let dots = " ".repeat((cols as usize).saturating_sub(42));
println!("Progress Report for {}:", self.name);
println!("{}", dashes);
println!("{}", format!(" Unit {} | Mastery |", dots).bold());
let mut modules = units::Module::iter();
for module in modules {
if let Some(progress) = self.progress.get(&module) {
self.progress.get(&module).unwrap().print_progress();
}
}
// let mut sorted_progress: Vec<_> = self.progress.order();
// // sorted_progress.sort_by_key(|(module, _)| module.to_string());
// for (_, tracker) in sorted_progress {
// tracker.print_progress();
// }
println!("{}", dashes);
wait_for_input();
}
pub fn update_progress(&mut self, unit: units::Module, question_correct: bool) {
if let Some(tracker) = self.progress.get_mut(&unit) {
tracker.quiz_result(question_correct);
}
}
}

19
src/main.rs Normal file
View File

@@ -0,0 +1,19 @@
mod learner;
mod utilities;
mod course;
use course::{story, units::Module};
use learner::profile::Learner;
use utilities::{files, printers, printers::StatementType};
fn main() {
let mut learner = Learner::new();
printers::print_boxed(
&format!("Welcome, {}! Thanks for joining me.", learner.name),
StatementType::Default );
story::outer_loop(&mut learner);
}

122
src/utilities/files.rs Normal file
View File

@@ -0,0 +1,122 @@
use std::{fs::File, io::Read};
use serde::de::DeserializeOwned;
use crate::{course::story, learner::profile::{self, Learner}};
// pub fn save_learner_progress(learner: &Learner, file: &str) -> Result<(), std::io::Error> {
// learner.clone().save_to_file(file)?;
// Ok(())
// }
pub fn get_previous_saves() -> Vec<String> {
let mut previous_saves = std::fs::read_dir("saves/")
.unwrap_or_else(|_| {
std::fs::create_dir_all("saves/").expect("Failed to create the saves/ directory");
std::fs::read_dir("saves/").expect("Failed to read the saves/ directory")
})
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
if path.extension()?.to_str()? == "json" {
let file_content = std::fs::read_to_string(&path).ok()?;
let learner: Learner = serde_json::from_str(&file_content).ok()?;
Some(learner.name)
} else {
None
}
})
.collect::<Vec<String>>();
previous_saves.push("Create New".to_string());
previous_saves
}
pub fn save_to_file(file: &str, learner: &Learner) -> Result<String, std::io::Error> {
match serde_json::to_string_pretty(learner) {
Ok(json_data) => match std::fs::write(file, json_data) {
Ok(_) => Ok(format!("File saved to {}", file.to_string())),
Err(e) => {
eprintln!("Error writing to file: {}", e);
Err(e)
}
},
Err(e) => {
eprintln!("Error serializing learner: {}", e);
Err(e.into())
}
}
}
pub fn load_from_file<T: DeserializeOwned>(filename: &str) -> Result<T, Box<dyn std::error::Error>> {
let mut file = File::open(filename)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let data = serde_json::from_str(&contents)?;
Ok(data)
}
// pub fn load_learner_progress() -> Result<Learner, std::io::Error> {
// let mut new_session: bool = false;
// let mut learner = {
// // Initialize the learner's progress
// // Check if a save file exists to load previous progress
// if std::path::Path::new(SAVE_FILE).exists() {
// match Learner::load_from_file(SAVE_FILE) {
// Ok(progress) => {
// print_boxed(&format!("Welcome back, {}! Let's continue your journey in FABLE.", progress.name), StatementType::Default );
// progress
// }
// Err(e) => {
// new_session = true;
// print_boxed(&format!("Couldn't load your save file due to error: {}.\nStarting a new learning experience.", e), StatementType::IncorrectFeedback );
// Learner::initialize_learner()
// }
// }
// } else {
// new_session = true;
// // Start a new session
// print_boxed("
// Welcome to FABLE, an interactive tutor for formal reasoning.
// Fallacy ~~~~~~~~~~~~
// Awareness and ~~~~~~
// Bias ~~~~~~~~~~~~~~~
// Learning ~~~~~~~~~~~
// Environment ~~~~~~~~", StatementType::Default);
// print_boxed("
// My name is Francis, and I am here to help you learn about formal reasoning.\n
// You see, many years ago, my people were nearly wiped out because of a lack of critical thinking.
// I have been programmed to help you avoid the same fate.\n
// In the next screen, you can select what you want to study.
// Choose wisely. The fate of your people depends on it.", StatementType::Default);
// Learner::initialize_learner()
// }
// };
// if !new_session {
// learner.display_progress();
// learner.confirm_course_selection(); // Confirm the course selection after reading the progress
// }
// let json_data = std::fs::read_to_string(SAVE_FILE)?;
// let progress: Learner = serde_json::from_str(&json_data)?;
// Ok(progress)
// }
// pub fn load_learner_progress() -> Result<Learner, std::io::Error> {
// let json_data = fs::read_to_string(SAVE_FILE)?;
// let progress: Learner = serde_json::from_str(&json_data)?;
// Ok(progress)
// }
pub fn sanitize_name(name: &str) -> String {
name.replace(|c: char| !c.is_alphanumeric(), "_").to_lowercase()
}
pub fn parse_unit(filename: &str) -> Result<story::UnitStory, Box<dyn std::error::Error>> {
let mut file = File::open(filename)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let data = serde_json::from_str(&contents)?;
Ok(data)
}

123
src/utilities/generators.rs Normal file
View File

@@ -0,0 +1,123 @@
use rand::Rng;
/// Syllables that can make up a name.
/// WARNING: Later in this file, there is a list of offensive terms that should not be included in the name.
/// If you add more syllables, make sure they do not form any offensive terms.
const SYLLABLES: [&str; 295] = [
"ba", "be", "bi", "bo", "bu",
"ca", "ce", "ci", "co", "cu",
"da", "de", "di", "do", "du",
"fa", "fe", "fi", "fo", "fu",
"ga", "ge", "gi", "go", "gu",
"ha", "he", "hi", "ho", "hu",
"ja", "je", "ji", "jo", "ju",
"ka", "ke", "ki", "ko", "ku",
"la", "le", "li", "lo", "lu",
"ma", "me", "mi", "mo", "mu",
"na", "ne", "ni", "no", "nu",
"pa", "pe", "pi", "po", "pu",
"qa", "qe", "qi", "qo", "qu",
"ra", "re", "ri", "ro", "ru",
"sa", "se", "si", "so", "su",
"ta", "te", "ti", "to", "tu",
"va", "ve", "vi", "vo", "vu",
"wa", "we", "wi", "wo", "wu",
"xa", "xe", "xi", "xo", "xu",
"ya", "ye", "yi", "yo", "yu",
"za", "ze", "zi", "zo", "zu",
"bai", "bau", "bei", "beu", "boi", "bou",
"cai", "cau", "cei", "ceu", "coi", "cou",
"dai", "dau", "dei", "deu", "doi", "dou",
"fai", "fau", "fei", "feu", "foi", "fou",
"gai", "gau", "gei", "geu", "goi", "gou",
"hai", "hau", "hei", "heu", "hoi", "hou",
"jai", "jau", "jei", "jeu", "joi", "jou",
"kai", "kau", "kei", "keu", "koi", "kou",
"lai", "lau", "lei", "leu", "loi", "lou",
"mai", "mau", "mei", "meu", "moi", "mou",
"nai", "nau", "nei", "neu", "noi", "nou",
"pai", "pau", "pei", "peu", "poi", "pou",
"qai", "qau", "qei", "qeu", "qoi", "qou",
"rai", "rau", "rei", "reu", "roi", "rou",
"sai", "sau", "sei", "seu", "soi", "sou",
"tai", "tau", "tei", "teu", "toi", "tou",
"vai", "vau", "vei", "veu", "voi", "vou",
"wai", "wau", "wei", "weu", "woi", "wou",
"xai", "xau", "xei", "xeu", "xoi", "xou",
"yai", "yau", "yei", "yeu", "yoi", "you",
"zai", "zau", "zei", "zeu", "zoi", "zou",
"cha", "che", "chi", "cho", "chu",
"sha", "she", "shi", "sho", "shu",
"tha", "the", "thi", "tho", "thu",
"wha", "whe", "whi", "who", "whu",
"pha", "phe", "phi", "pho", "phu",
"zha", "zhe", "zhi", "zho", "zhu",
"tra", "tre", "tri", "tro", "tru",
"kha", "khe", "khi", "kho", "khu",
"gha", "ghe", "ghi", "gho", "ghu",
"sha", "she", "shi", "sho", "shu",
"twa", "twe", "twi", "two", "twu",
"qua", "que", "qui", "quo",
"sla", "sle", "sli", "slo", "slu",
];
const OFFENSIVE_TERMS: [&str; 20] = [
"shit", "fat", "whore", "nig",
"jig", "cum", "fag", "twat",
"jiz", "kum", "anal", "quere",
"phuk", "fuk", "arab", "dik",
"jew", "slut", "tit", "phag"
];
const affirmations: [&str; 8] = [
"You're doing great!",
"Keep up the good work!",
"Fantastic effort!",
"You're on the right track!",
"Excellent job!",
"You're making progress!",
"Keep it up!",
"Great work!",
];
pub fn random_affirmation() -> String {
let mut rng = rand::rng();
let index = rng.random_range(0..affirmations.len());
affirmations[index].to_string()
}
pub fn generate_name() -> String {
let mut rng = rand::rng();
let name_length = rng.random_range(2..5); // Generate a name with 2 to 4 syllables
let mut name = String::new();
for _ in 0..name_length {
let syllable = SYLLABLES[rng.random_range(0..SYLLABLES.len())];
name.push_str(syllable);
}
if OFFENSIVE_TERMS.iter().any(|&term| name.contains(term)) {
return generate_name(); // Regenerate the name if it contains offensive terms
}
if name.len() > 8 {
return generate_name(); // Regenerate the name if it contains too many characters
}
name[..1].to_ascii_uppercase() + &name[1..]
}
pub fn random_unit_intro() -> String {
let mut rng = rand::rng();
let unit_intros: [&str; 5] = [
"It's time to learn about",
"Let's dive into",
"Get ready to explore",
"Prepare to discover",
"Let's embark on a journey to learn about",
];
let index = rng.random_range(0..unit_intros.len());
unit_intros[index].to_string()
}

4
src/utilities/mod.rs Normal file
View File

@@ -0,0 +1,4 @@
pub(crate) mod generators;
pub(crate) mod files;
pub(crate) mod printers;
pub(crate) mod questions;

271
src/utilities/printers.rs Normal file
View File

@@ -0,0 +1,271 @@
use crossterm::{execute, style::Stylize, terminal::{size, Clear, ClearType}};
use std::io::stdout;
use super::generators;
const DELAY_SECONDS: u64 = 1;
#[derive(PartialEq)]
pub enum StatementType {
Default,
DelayExplanation,
Question,
GeneralFeedback,
CorrectFeedback,
IncorrectFeedback,
}
/// Clear the console
pub fn clear_console() {
execute!(stdout(), Clear(ClearType::All)).unwrap();
}
/// Pretty-print an instruction centered on the screen in an outlined box
pub fn print_boxed(instruction: &str, statement_type: StatementType) {
let (cols, rows) = size().unwrap();
let padding = 4;
let max_width = 100;
let instruction = instruction
.lines()
.flat_map(|line| {
let mut split_lines = vec![];
let mut current_line = String::new();
for word in line.split_whitespace() {
let word_width = word.chars().map(|c| if c.len_utf8() > 1 { 2 } else { 1 }).sum::<usize>();
if current_line.chars().map(|c| if c.len_utf8() > 1 { 2 } else { 1 }).sum::<usize>() + word_width + 1 > std::cmp::min(max_width, cols as usize) {
split_lines.push(current_line.trim_end().to_string());
current_line = String::new();
}
current_line.push_str(word);
current_line.push(' ');
}
if !current_line.is_empty() {
split_lines.push(current_line.trim_end().to_string());
}
split_lines
})
.collect::<Vec<_>>()
.join("\n");
let box_width = std::cmp::min(
instruction.lines().map(|line| line.chars().map(|c| c.len_utf8()).sum::<usize>()).max().unwrap_or(0) + padding * 2,
max_width,
);
let box_height = instruction.lines().count() + 2;
let start_col = (cols.saturating_sub(box_width as u16)) / 2;
let start_row = (rows.saturating_sub(box_height as u16)) / 2;
let top_border;
let bottom_border;
let empty_line;
match statement_type {
StatementType::Question => {
top_border = format!("{}{}{}", "".dark_blue(), "".repeat(box_width).dark_blue(), "".dark_blue());
bottom_border = format!("{}{}{}", "".dark_blue(), "".repeat(box_width).dark_blue(), "".dark_blue());
empty_line = format!("{}{}{}", "".dark_blue(), " ".repeat(box_width).dark_blue(), "".dark_blue());
}
StatementType::GeneralFeedback => {
top_border = format!("{}{}{}", "".dark_magenta(), "".repeat(box_width).dark_magenta(), "".dark_magenta());
bottom_border = format!("{}{}{}", "".dark_magenta(), "".repeat(box_width).dark_magenta(), "".dark_magenta());
empty_line = format!("{}{}{}", "".dark_magenta(), " ".repeat(box_width).dark_magenta(), "".dark_magenta());
}
StatementType::CorrectFeedback => {
top_border = format!("{}{}{}", "".green(), "".repeat(box_width).green(), "".green());
bottom_border = format!("{}{}{}", "".green(), "".repeat(box_width).green(), "".green());
empty_line = format!("{}{}{}", "".green(), " ".repeat(box_width).green(), "".green());
}
StatementType::IncorrectFeedback => {
top_border = format!("{}{}{}", "".red(), "".repeat(box_width).red(), "".red());
bottom_border = format!("{}{}{}", "".red(), "".repeat(box_width).red(), "".red());
empty_line = format!("{}{}{}", "".red(), " ".repeat(box_width).red(), "".red());
}
_ => {
top_border = format!("{}", "".repeat(box_width));
bottom_border = format!("{}", "".repeat(box_width));
empty_line = format!("{}", " ".repeat(box_width));
}
}
clear_console();
for i in 0..box_height {
let line = match i {
0 => &top_border,
i if i == box_height - 1 => &bottom_border,
i if i > 0 && i < box_height - 1 => {
let line_content = instruction
.lines()
.nth(i - 1)
.unwrap_or("")
.trim();
let line_width = line_content.chars().map(|c| if c.len_utf8() > 1 { 2 } else { 1 }).sum::<usize>();
match statement_type {
StatementType::Question => {
&format!(
"{}{}{}{}{}",
"".dark_blue(),
" ".repeat((box_width.saturating_sub(line_width)) / 2),
line_content,
" ".repeat((box_width.saturating_sub(line_width) + 1) / 2),
"".dark_blue(),
)
}
StatementType::GeneralFeedback => {
&format!(
"{}{}{}{}{}",
"".dark_magenta(),
" ".repeat((box_width.saturating_sub(line_width)) / 2),
line_content,
" ".repeat((box_width.saturating_sub(line_width) + 1) / 2),
"".dark_magenta(),
)
}
StatementType::CorrectFeedback => {
&format!(
"{}{}{}{}{}",
"".green(),
" ".repeat((box_width.saturating_sub(line_width)) / 2),
line_content,
" ".repeat((box_width.saturating_sub(line_width) + 1) / 2),
"".green(),
)
}
StatementType::IncorrectFeedback => {
&format!(
"{}{}{}{}{}",
"".red(),
" ".repeat((box_width.saturating_sub(line_width)) / 2),
line_content,
" ".repeat((box_width.saturating_sub(line_width) + 1) / 2),
"".red(),
)
}
_ => {
&format!(
"{}{}{}",
" ".repeat((box_width.saturating_sub(line_width)) / 2),
line_content,
" ".repeat((box_width.saturating_sub(line_width) + 1) / 2),
)
}
}
}
_ => &empty_line,
};
println!("\x1B[{};{}H{}", start_row + i as u16, start_col + 1, line);
}
match statement_type {
StatementType::Question => {
return;
}
StatementType::DelayExplanation => {
wait_for_input_delay(DELAY_SECONDS);
print_boxed(&instruction, StatementType::Default);
}
_ => {
wait_for_input();
}
}
}
/// Prints a full-width ruler of designated characters
pub fn print_rule(character: char, padding: (bool, bool)) {
let (cols, _) = size().unwrap();
println!(
"{}{}{}",
if padding.0 { "\n" } else { "" },
character.to_string().repeat(cols as usize),
if padding.1 { "\n" } else { "" }
);
}
/// Horizontally center the desired text on the screen
pub fn print_centered(text: &str) {
let (cols, _) = size().unwrap();
let text_width = text.len() as u16;
let start_col = (cols.saturating_sub(text_width)) / 2;
println!(
"\x1B[{}C{}",
start_col,
text
);
}
/// Wait for input from the user
pub fn wait_for_input() {
let prompt = "Press enter to continue";
let (cols, _) = size().unwrap();
let text_width = prompt.len() as u16;
let start_col = (cols.saturating_sub(text_width)) / 2 + 1;
println!(
"\x1B[{}C{}",
start_col,
prompt.italic().dark_blue()
);
// println!("{}", prompt.italic().dark_blue());
crossterm::event::read().unwrap();
}
/// Wait for input from the user
pub fn print_title(title: &str) {
let (cols, _) = size().unwrap();
let text_width = title.len() as u16;
let start_col = (cols.saturating_sub(text_width)) / 2 + 1;
println!(
"\x1B[{}C{}",
start_col,
title.bold()
);
wait_for_input();
}
/// Wait for input from the user
pub fn wait_for_input_delay(seconds: u64) {
let prompt = format!("Remember to be mindful. Advance in {} seconds.", seconds);
let (cols, _) = size().unwrap();
let text_width = prompt.len() as u16;
let start_col = (cols.saturating_sub(text_width)) / 2 + 1;
println!(
"\x1B[{}C{}",
start_col,
prompt.italic().dark_blue()
);
std::thread::sleep(std::time::Duration::from_secs(DELAY_SECONDS));
}
/// Wait for input from the user
pub fn inform_delay(seconds: u64) {
let prompt = format!("Remember to be mindful. Answers appear in {} seconds.", seconds);
let (cols, _) = size().unwrap();
let text_width = prompt.len() as u16;
let start_col = (cols.saturating_sub(text_width)) / 2 + 1;
println!(
"\x1B[{}C{}",
start_col,
prompt.italic().dark_blue()
);
}
pub fn unit_screen(title: &str) {
clear_console();
print_boxed(title, StatementType::GeneralFeedback);
// print_boxed(&format!("{}", description), StatementType::Default);
}
pub fn print_screen(text: &str) {
clear_console();
print_boxed(text, StatementType::Default);
}

View File

@@ -0,0 +1,97 @@
use inquire::{MultiSelect, Select, Text};
use crossterm::terminal;
use super::printers::{clear_console, inform_delay, print_boxed, StatementType};
const DELAY_SECONDS: u64 = 1;
pub fn ask_mcq_delayed(question: &str, options: &[&str]) -> Option<String> {
print_boxed(question, StatementType::Question);
inform_delay(DELAY_SECONDS);
std::thread::sleep(std::time::Duration::from_secs(DELAY_SECONDS));
ask_mcq(question, options)
}
/// Print a quiz question in an attractive format and get the user's choice
pub fn ask_mcq(question: &str, options: &[&str]) -> Option<String> {
clear_console();
// Calculate vertical centering
let rows = terminal::size().map(|(_, height)| height as usize).unwrap_or(0) - options.len() - 2;
print_boxed(&format!("{}", question), StatementType::Question);
for _ in 0..(rows/2) {
println!();
}
let ans = Select::new("Your response:", options.to_vec())
.prompt();
match ans {
Ok(choice) => Some(String::from(choice)),
Err(_) => None,
}
}
/// Print a quiz question in an attractive format and get the user's choices
pub fn ask_multi_select(question: &str, options: &[&str]) -> Option<Vec<String>> {
clear_console();
// Calculate vertical centering
let rows = terminal::size().map(|(_, height)| height as usize).unwrap_or(0) - options.len() - 2;
print_boxed(&format!("{}", question), StatementType::Question);
for _ in 0..(rows/2) {
println!();
}
let ans = MultiSelect::new("Select all applicable answers with SPACEBAR, then press RETURN to submit:", options.to_vec())
.prompt();
match ans {
Ok(choices) => Some(choices.into_iter().map(String::from).collect()),
Err(_) => None,
}
}
/// Print a question in an attractive format and get the user's text input
pub fn ask_plaintext(question: &str) -> Option<String> {
clear_console();
// Calculate vertical centering
let rows = terminal::size().map(|(_, height)| height as usize).unwrap_or(0) - 3;
print_boxed(&format!("{}", question), StatementType::Question);
for _ in 0..(rows/2) {
println!();
}
let ans = Text::new("Please provide your input:")
.prompt();
match ans {
Ok(choice) => Some(choice),
Err(_) => None,
}
}
pub fn ask_name() -> String {
let name = ask_plaintext("What is your name?").unwrap_or_else(|| {
let default_name = super::generators::generate_name();
print_boxed(&format!("Failed to get name. I'll give you a random one: {}.", default_name), StatementType::IncorrectFeedback);
default_name
});
if name .is_empty() {
let default_name = super::generators::generate_name();
print_boxed(&format!("You didn't provide a name. I'll give you a random one: {}.", default_name), StatementType::IncorrectFeedback);
return default_name;
}
print_boxed(&format!("Welcome, {}! Thanks for joining me.", name), StatementType::Default);
name
}