practicode 0.2.5

Offline-first programming language lessons and coding practice in a Rust terminal UI.
Documentation
use super::*;

pub const LANGUAGES: &[&str] = &["python", "ts", "java", "rust"];
pub const THEMES: &[&str] = &["dark", "light"];
pub const AI_PROVIDERS: &[&str] = &["codex", "claude"];
pub const CODEX_AI_EFFORTS: &[&str] = &["auto", "low", "medium", "high", "xhigh"];
pub const CLAUDE_AI_EFFORTS: &[&str] = &["auto", "low", "medium", "high", "xhigh", "max"];
pub const BANK_PATH: &str = "problem_bank.json";
pub const STATE_PATH: &str = "problem-state.json";
pub const PROBLEM_NOTES_PATH: &str = "problem_notes.md";

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Settings {
    #[serde(default = "default_language")]
    pub language: String,
    #[serde(default = "default_ui_language")]
    pub ui_language: String,
    #[serde(default = "default_theme")]
    pub theme: String,
    #[serde(default = "default_start_mode")]
    pub start_mode: String,
    #[serde(default = "default_difficulty")]
    pub difficulty: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub topics: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub avoid_topics: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub generate_languages: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub generate_ui_languages: Vec<String>,
    #[serde(default = "default_editor")]
    pub editor: String,
    #[serde(default = "default_next_source")]
    pub next_source: String,
    #[serde(default = "default_ai_provider")]
    pub ai_provider: String,
    #[serde(default = "default_ai_model")]
    pub ai_model: String,
    #[serde(default = "default_ai_effort")]
    pub ai_effort: String,
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub ai_next_command: String,
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub skipped_update_version: String,
}

impl Default for Settings {
    fn default() -> Self {
        Self {
            language: default_language(),
            ui_language: default_ui_language(),
            theme: default_theme(),
            start_mode: default_start_mode(),
            difficulty: default_difficulty(),
            topics: Vec::new(),
            avoid_topics: Vec::new(),
            generate_languages: Vec::new(),
            generate_ui_languages: Vec::new(),
            editor: default_editor(),
            next_source: default_next_source(),
            ai_provider: default_ai_provider(),
            ai_model: default_ai_model(),
            ai_effort: default_ai_effort(),
            ai_next_command: String::new(),
            skipped_update_version: String::new(),
        }
    }
}

impl Settings {
    pub fn next_ai_command(&self) -> &str {
        &self.ai_next_command
    }

    pub fn model_arg(&self) -> Option<&str> {
        let model = self.ai_model.trim();
        if model.is_empty() || model == "auto" {
            None
        } else {
            Some(model)
        }
    }

    pub fn effort_arg(&self) -> Option<&str> {
        let effort = self.ai_effort.trim();
        if effort.is_empty() || effort == "auto" {
            None
        } else {
            Some(effort)
        }
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HistoryItem {
    pub id: String,
    pub status: String,
}

#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MasteryStage {
    #[default]
    New,
    Practiced,
    Retained,
    Mastered,
}

#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct LessonMastery {
    #[serde(default)]
    pub stage: MasteryStage,
    #[serde(default)]
    pub review_due_at: u64,
    #[serde(default)]
    pub attempts: u64,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AppState {
    pub current_problem: String,
    #[serde(default)]
    pub settings: Settings,
    #[serde(default)]
    pub solved: Vec<String>,
    #[serde(default)]
    pub history: Vec<HistoryItem>,
    #[serde(default = "default_suggested_difficulty")]
    pub suggested_next_difficulty: String,
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub syntax_progress: HashMap<String, Vec<String>>,
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub current_syntax_lesson: HashMap<String, String>,
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub syntax_mastery: HashMap<String, HashMap<String, LessonMastery>>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub completed_syntax_courses: Vec<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Problem {
    pub id: String,
    pub slug: String,
    pub difficulty: String,
    pub topics: Vec<String>,
    pub title: HashMap<String, String>,
    pub statement: HashMap<String, String>,
    pub input: HashMap<String, String>,
    pub output: HashMap<String, String>,
    pub examples: Vec<IoCase>,
    pub cases: Vec<IoCase>,
    pub answers: HashMap<String, String>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct IoCase {
    pub input: String,
    pub output: String,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum JudgeFailureKind {
    Compile,
    TypeCheck,
    Runtime,
    Timeout,
    Output,
}

#[derive(Clone, Debug)]
pub struct JudgeResult {
    pub passed: bool,
    pub passed_cases: usize,
    pub total_cases: usize,
    pub failure_kind: Option<JudgeFailureKind>,
    pub output: String,
}

pub fn default_language() -> String {
    "python".to_string()
}

pub fn default_ui_language() -> String {
    "en".to_string()
}

pub fn default_theme() -> String {
    "dark".to_string()
}

pub fn default_start_mode() -> String {
    "home".to_string()
}

pub fn default_editor() -> String {
    "vim".to_string()
}

pub fn default_next_source() -> String {
    "bank".to_string()
}

pub fn default_ai_provider() -> String {
    "codex".to_string()
}

pub fn default_ai_model() -> String {
    "auto".to_string()
}

pub fn default_ai_effort() -> String {
    "auto".to_string()
}

pub fn default_suggested_difficulty() -> String {
    "easy".to_string()
}