practicode 0.2.5

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

impl PracticodeApp {
    pub(super) fn status_text_for_width(&self, width: u16) -> String {
        if self.task_rx.is_some()
            || self.editing_notes
            || self.focus == Focus::Command
            || self.list_cursor.is_some()
            || self.show_output
        {
            return format!(" {} ", self.mode_hint());
        }
        if width > 120 {
            return self.status_text();
        }
        if let Some(update) = self.update_status() {
            let hint = if self.mode == AppMode::Learn {
                self.learning_primary_hint()
            } else {
                ui_text(
                    &self.state.settings.ui_language,
                    match self.mode {
                        AppMode::Home => "hint_home_compact",
                        AppMode::Learn => "hint_learn_compact",
                        AppMode::Problems => "hint_problem_compact",
                    },
                )
            };
            return format!(" {update} | {hint} ");
        }
        if self.mode == AppMode::Learn {
            return format!(" {} ", self.learning_primary_hint());
        }
        let lang = &self.state.settings.ui_language;
        let key = match self.mode {
            AppMode::Home => "hint_home_compact",
            AppMode::Learn => "hint_learn_compact",
            AppMode::Problems => "hint_problem_compact",
        };
        format!(" {} ", ui_text(lang, key))
    }

    pub(super) fn status_text(&self) -> String {
        let lang = &self.state.settings.ui_language;
        if self.mode == AppMode::Home && !self.show_output {
            let tail = self
                .update_status()
                .unwrap_or_else(|| self.mode_hint().to_string());
            return format!(" PRACTICODE | {} | {} ", ui_text(lang, "mode_home"), tail);
        }
        if self.mode == AppMode::Learn {
            let primary = if self.task_rx.is_some()
                || self.editing_notes
                || self.focus == Focus::Command
                || self.list_cursor.is_some()
                || self.show_output
            {
                self.mode_hint()
            } else {
                self.learning_primary_hint()
            };
            let lesson = current_syntax_lesson(&self.state, &self.state.settings.language);
            let (done, total) =
                syntax_core_progress_count(&self.state, &self.state.settings.language);
            let selected_focus = match self.learning_session.view() {
                LearningView::Lesson => Focus::Left,
                LearningView::Code => Focus::Code,
                LearningView::Result => Focus::Output,
            };
            let focus = if !self.show_output && self.focus == selected_focus {
                format!(
                    " | {}: {}",
                    ui_text(lang, "focus_active"),
                    learning_view_label(lang, self.learning_session.view()),
                )
            } else {
                String::new()
            };
            let update = self
                .update_status()
                .map(|status| format!(" | {status}"))
                .unwrap_or_default();
            return format!(
                " {} | PRACTICODE | {} | {} | {} | {done}/{total}{focus} | {}:{}{update} ",
                primary,
                ui_text(lang, "mode_learn"),
                syntax_language_name(&self.state.settings.language),
                lesson.id,
                ui_text(lang, "status_code"),
                self.state.settings.language,
            );
        }
        let code_status = localized_status(lang, &self.submission_status(&self.problem).0);
        let activity = if self.busy_label.is_empty() {
            ui_text(lang, "status_idle").to_string()
        } else {
            let elapsed = self
                .busy_started
                .map(|started| started.elapsed().as_secs())
                .unwrap_or_default();
            format!(
                "{}{} {}",
                self.busy_text(),
                self.busy_dots(),
                self.elapsed_text(elapsed)
            )
        };
        let tail = if let Some(status) = self.update_status() {
            status
        } else if self.task_rx.is_some() {
            self.mode_hint().to_string()
        } else if let Some(status) = self.background_generation_status() {
            status
        } else {
            self.mode_hint().to_string()
        };
        format!(
            " PRACTICODE | {} | {} | {} | {} | {}:{} | {} | {} ",
            self.problem.id,
            localized_status(lang, &self.problem.difficulty),
            localized_status(lang, &self.problem_status(&self.problem)),
            activity,
            ui_text(lang, "status_code"),
            code_status,
            self.state.settings.language,
            tail,
        )
    }

    fn update_status(&self) -> Option<String> {
        self.update_notice.as_ref().map(|version| {
            format!(
                "{}:{version} /update",
                ui_text(&self.state.settings.ui_language, "update")
            )
        })
    }

    pub(super) fn next_source_help(&self) -> String {
        ui_text(&self.state.settings.ui_language, "next_source_help").to_string()
    }

    pub(super) fn background_generation_status(&self) -> Option<String> {
        if self.generate_rx.is_some() {
            let elapsed = self
                .generate_started
                .map(|started| started.elapsed().as_secs())
                .unwrap_or_default();
            Some(format!(
                "{} {}",
                ui_text(
                    &self.state.settings.ui_language,
                    "status_background_generation"
                ),
                self.elapsed_text(elapsed)
            ))
        } else {
            self.generate_notice
                .as_ref()
                .map(|notice| self.generation_notice_text(notice))
        }
    }

    pub(super) fn busy_text(&self) -> String {
        let lang = &self.state.settings.ui_language;
        match self.busy_label.as_str() {
            "ai" => ui_text(lang, "busy_ai_thinking").replace("{provider}", &self.busy_arg),
            "next" => ui_text(lang, "generating_next").to_string(),
            _ => self.busy_arg.clone(),
        }
    }

    pub(super) fn elapsed_text(&self, seconds: u64) -> String {
        ui_text(&self.state.settings.ui_language, "elapsed_seconds")
            .replace("{seconds}", &seconds.to_string())
    }

    pub(super) fn generation_notice_text(&self, notice: &GenerationNotice) -> String {
        let lang = &self.state.settings.ui_language;
        match notice {
            GenerationNotice::Started => ui_text(lang, "generation_started").to_string(),
            GenerationNotice::Duplicate => ui_text(lang, "generation_duplicate").to_string(),
            GenerationNotice::Generated(count) => {
                ui_text(lang, "generation_generated").replace("{count}", &count.to_string())
            }
            GenerationNotice::Failed {
                status,
                detail,
                added,
                reload_error,
            } => {
                let mut lines = vec![ui_text(lang, "generation_failed").to_string()];
                if let Some(status) = status {
                    lines.push(
                        ui_text(lang, "generation_exit_status")
                            .replace("{status}", &status.to_string()),
                    );
                }
                if !detail.is_empty() {
                    lines.push(detail.clone());
                }
                if *added > 0 {
                    lines.push(
                        ui_text(lang, "generation_partial_count")
                            .replace("{count}", &added.to_string()),
                    );
                }
                if let Some(error) = reload_error {
                    lines.push(ui_text(lang, "generation_reload_failed").to_string());
                    if !error.is_empty() {
                        lines.push(error.clone());
                    }
                }
                lines.join("\n")
            }
            GenerationNotice::Finished => ui_text(lang, "generation_finished").to_string(),
            GenerationNotice::ReloadFailed(detail) => {
                let mut text = ui_text(lang, "generation_reload_failed").to_string();
                if !detail.is_empty() {
                    text.push('\n');
                    text.push_str(detail);
                }
                text
            }
        }
    }

    pub(super) fn busy_dots(&self) -> String {
        ".".repeat((self.busy_frame / 8) % 4)
    }

    pub(super) fn busy_game_track(&self) -> String {
        let width = 9;
        let target = width / 2;
        let position = (self.busy_frame / 2) % width;
        let mut cells = vec!['-'; width];
        cells[target] = '|';
        cells[position] = if position == target { 'X' } else { '*' };
        format!("[{}]", cells.into_iter().collect::<String>())
    }

    pub(super) fn busy_game_on_target(&self) -> bool {
        (self.busy_frame / 2) % 9 == 4
    }

    pub(super) fn mode_hint(&self) -> &'static str {
        let lang = &self.state.settings.ui_language;
        if self.task_rx.is_some() {
            return if self.busy_label == "next" {
                ui_text(lang, "hint_busy_next")
            } else {
                ui_text(lang, "hint_busy")
            };
        }
        if self.editing_notes {
            return ui_text(lang, "hint_notes");
        }
        if self.mode == AppMode::Home && !self.show_output {
            return ui_text(lang, "home_help");
        }
        if self.mode == AppMode::Learn && !self.show_output && self.focus != Focus::Command {
            return self.learning_primary_hint();
        }
        if self.mode == AppMode::Problems && !self.show_output && self.focus != Focus::Command {
            return ui_text(lang, "hint_problem");
        }
        match (self.focus, self.list_cursor.is_some(), self.show_output) {
            (Focus::Command, _, _) => ui_text(lang, "hint_command"),
            (_, true, _) => ui_text(lang, "hint_list"),
            (_, _, true) if self.settings_cursor.is_some() => ui_text(lang, "hint_settings"),
            (_, _, true) => ui_text(lang, "hint_output"),
            (Focus::Code, _, _) => ui_text(lang, "hint_problem"),
            _ => ui_text(lang, "hint_idle"),
        }
    }

    fn learning_primary_hint(&self) -> &'static str {
        let key = if self.focus == Focus::Code
            && self.learning_session.step() != LearningStep::Exercise
        {
            "learning_primary_leave_code"
        } else {
            match self.learning_session.step() {
                LearningStep::Exercise if self.learning_session.view() == LearningView::Result => {
                    "learning_primary_edit"
                }
                LearningStep::Exercise => "learning_primary_run",
                LearningStep::Complete => "learning_primary_home",
                _ => "learning_primary_next",
            }
        };
        ui_text(&self.state.settings.ui_language, key)
    }

    pub(super) fn help_text(&self) -> String {
        let lang = &self.state.settings.ui_language;
        let choices = self.command_choices();
        let commands = self
            .default_command_inserts()
            .iter()
            .filter_map(|insert| choices.iter().find(|hint| hint.insert == *insert))
            .map(|hint| format!("- `{}` {}", hint.display, ui_text(lang, hint.desc_key)))
            .collect::<Vec<_>>()
            .join("\n");
        let (daily_loop, shortcuts) = match self.mode {
            AppMode::Home => (ui_text(lang, "help_home_loop"), ui_text(lang, "home_help")),
            AppMode::Learn => (
                ui_text(lang, "help_learn_loop"),
                ui_text(lang, "learning_shortcuts"),
            ),
            AppMode::Problems => (
                ui_text(lang, "help_problem_loop"),
                ui_text(lang, "practice_shortcuts"),
            ),
        };
        format!(
            "# {}\n\n## {}\n\n{}\n\n## {}\n\n{}\n\n## {}\n\n- {}\n- {}\n- {}\n- {}\n\n## {}\n\n- {}\n- {}",
            ui_text(lang, "help_title"),
            ui_text(lang, "daily_loop"),
            daily_loop,
            ui_text(lang, "commands"),
            commands,
            ui_text(lang, "keys"),
            shortcuts,
            ui_text(lang, "help_palette_open"),
            ui_text(lang, "help_palette_move"),
            ui_text(lang, "help_palette_close"),
            ui_text(lang, "debug_prints"),
            ui_text(lang, "help_stdout"),
            ui_text(lang, "help_stderr"),
        )
    }
}