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 insert_command_char(&mut self, char: char) {
        let byte = byte_index(&self.command, self.command_cursor);
        self.command.insert(byte, char);
        self.command_cursor += 1;
        self.command_palette_cursor = 0;
        self.normalize_command_input();
    }

    pub(super) fn delete_command_before_cursor(&mut self) {
        if self.command_cursor == 0 {
            return;
        }
        let start = byte_index(&self.command, self.command_cursor - 1);
        let end = byte_index(&self.command, self.command_cursor);
        self.command.replace_range(start..end, "");
        self.command_cursor -= 1;
        self.command_palette_cursor = 0;
        self.normalize_command_input();
    }

    pub(super) fn delete_command_at_cursor(&mut self) {
        if self.command_cursor >= char_len(&self.command) {
            return;
        }
        let start = byte_index(&self.command, self.command_cursor);
        let end = byte_index(&self.command, self.command_cursor + 1);
        self.command.replace_range(start..end, "");
        self.command_palette_cursor = 0;
        self.normalize_command_input();
    }

    pub(super) fn command_suggestions(&self) -> Vec<CommandChoice> {
        if self.focus != Focus::Command {
            return Vec::new();
        }
        let Some(query) = self.command.trim_start().strip_prefix('/') else {
            return Vec::new();
        };
        let query = query.to_lowercase();
        let trimmed = query.trim_start();
        let choices = self.command_choices();
        if trimmed.is_empty() {
            return self
                .default_command_inserts()
                .iter()
                .filter_map(|insert| choices.iter().find(|hint| hint.insert == *insert).cloned())
                .collect();
        }
        choices
            .into_iter()
            .filter(|hint| hint.insert.starts_with(trimmed))
            .collect()
    }

    pub(super) fn default_command_inserts(&self) -> &'static [&'static str] {
        match self.mode {
            AppMode::Home => &[
                "learn", "progress", "problems", "doctor", "profile", "help", "quit",
            ],
            AppMode::Problems => &[
                "run",
                "next",
                "back",
                "problems",
                "answer",
                "hint ",
                "generate ",
                "profile",
                "progress",
                "doctor",
                "home",
            ],
            AppMode::Learn => &[
                "run", "next", "back", "lesson", "progress", "hint ", "ask ", "learn", "problems",
                "profile", "doctor", "home",
            ],
        }
    }

    pub(super) fn command_choices(&self) -> Vec<CommandChoice> {
        let mut choices = Vec::new();
        for hint in COMMAND_HINTS {
            if hint.insert == "effort max" && self.state.settings.ai_provider != "claude" {
                continue;
            }
            if hint.insert == "model " {
                for model in self
                    .available_models
                    .iter()
                    .filter(|model| *model != "auto")
                {
                    choices.push(CommandChoice {
                        insert: format!("model {model}"),
                        display: format!("/model {model}"),
                        desc_key: "cmd_model_available",
                        keep_open: false,
                    });
                }
            }
            choices.push(CommandChoice {
                insert: hint.insert.to_string(),
                display: hint.display.to_string(),
                desc_key: hint.desc_key,
                keep_open: hint.keep_open,
            });
        }
        choices
    }

    pub(super) fn move_command_palette(&mut self, delta: isize) {
        let len = self.command_suggestions().len();
        if len == 0 {
            return;
        }
        let cursor = self.command_palette_cursor as isize;
        self.command_palette_cursor = ((cursor + delta).rem_euclid(len as isize)) as usize;
    }

    pub(super) fn accept_command_palette(&mut self) -> Result<bool> {
        let suggestions = self.command_suggestions();
        if suggestions.is_empty() {
            return Ok(false);
        }
        let hint = &suggestions[self.command_palette_cursor.min(suggestions.len() - 1)];
        if hint.keep_open {
            self.command = format!("/{}", hint.insert);
            self.command_cursor = char_len(&self.command);
            self.command_palette_cursor = 0;
            return Ok(true);
        }
        let value = hint.insert.clone();
        self.command.clear();
        self.command_cursor = 0;
        self.command_palette_cursor = 0;
        self.focus = Focus::None;
        self.submit_command(&value)?;
        Ok(true)
    }

    pub(super) fn normalize_command_input(&mut self) {
        let normalized = compose_hangul_jamo(&self.command);
        if normalized == self.command {
            self.command_cursor = self.command_cursor.min(char_len(&self.command));
            return;
        }
        let old_prefix = prefix(&self.command, self.command_cursor);
        self.command = normalized;
        self.command_cursor =
            char_len(&compose_hangul_jamo(&old_prefix)).min(char_len(&self.command));
    }
}