teamtalk 6.0.0

TeamTalk SDK for Rust
Documentation
use super::*;

impl Router {
    pub(super) fn push_command_route<F>(
        &mut self,
        command: String,
        command_pattern: Option<CommandPattern>,
        handler: F,
    ) where
        F: FnMut(&mut Context<'_>) -> Result<HandlerResult> + Send + 'static,
    {
        self.routes.push(Route {
            matcher: RouteMatcher::Command(command.clone()),
            command_pattern,
            dialog_filter: None,
            handler: Box::new(handler),
        });
    }

    pub(super) fn register_help(&mut self, usage: String, summary: Option<String>) {
        if usage.is_empty() {
            return;
        }

        if let Some(entry) = self
            .help_entries
            .iter_mut()
            .find(|entry| entry.usage.eq_ignore_ascii_case(&usage))
        {
            if summary.is_some() {
                entry.summary = summary;
            }
            return;
        }

        self.help_entries.push(HelpEntry { usage, summary });
    }

    pub(super) fn primary_command_prefix(&self) -> char {
        self.command_prefixes.first().copied().unwrap_or('/')
    }

    pub(super) fn canonicalize_command(&self, command: &Command) -> Command {
        let tokens = command.tokens();
        if tokens.is_empty() {
            return command.clone();
        }
        let normalized = tokens
            .iter()
            .map(|token| token.to_ascii_lowercase())
            .collect::<Vec<_>>();

        for prefix_len in (1..=tokens.len()).rev() {
            let alias = normalized[..prefix_len].join(" ");
            let Some(target) = self.command_aliases.get(&alias) else {
                continue;
            };
            let mut new_tokens = target
                .split_whitespace()
                .map(str::to_owned)
                .collect::<Vec<_>>();
            new_tokens.extend(tokens[prefix_len..].iter().map(|token| (*token).to_owned()));
            if new_tokens.is_empty() {
                return command.clone();
            }
            let name = new_tokens.remove(0);
            return Command {
                prefix: command.prefix,
                name,
                args: new_tokens,
                raw: command.raw.clone(),
            };
        }

        command.clone()
    }

    pub(super) fn suggest_commands(&self, query: &str) -> Vec<String> {
        if !self.suggestions.enabled || query.is_empty() {
            return Vec::new();
        }

        let mut commands = self
            .help_entries
            .iter()
            .map(|entry| {
                entry
                    .usage
                    .split_whitespace()
                    .take(2)
                    .collect::<Vec<_>>()
                    .join(" ")
            })
            .filter(|usage| !usage.is_empty())
            .collect::<Vec<_>>();
        commands.extend(self.command_aliases.keys().cloned());
        commands.sort();
        commands.dedup_by(|left, right| left.eq_ignore_ascii_case(right));

        let lower = query.to_ascii_lowercase();
        let max_distance = self.suggestions.max_distance.max(1);
        let mut ranked = commands
            .into_iter()
            .map(|candidate| {
                let candidate_lower = candidate.to_ascii_lowercase();
                let distance = edit_distance(&candidate_lower, &lower);
                let starts = candidate_lower.starts_with(&lower);
                (distance, !starts, candidate)
            })
            .filter(|(distance, _, candidate)| {
                *distance <= max_distance || candidate.to_ascii_lowercase().contains(&lower)
            })
            .collect::<Vec<_>>();
        ranked.sort();
        ranked
            .into_iter()
            .take(self.suggestions.limit)
            .map(|(_, _, candidate)| candidate)
            .collect()
    }

    pub(super) fn render_auto_help(&self, command: &Command) -> Option<String> {
        if !self.auto_help.enabled {
            return None;
        }

        let help_command = match_command_route(command, &self.auto_help.command)?;
        let query = help_command
            .args
            .iter()
            .map(String::as_str)
            .collect::<Vec<_>>()
            .join(" ")
            .to_ascii_lowercase();

        let mut entries = self.help_entries.clone();
        entries.push(HelpEntry {
            usage: format!("{} [command]", self.auto_help.command),
            summary: Some("Show command help".to_owned()),
        });

        entries.sort_by(|a, b| a.usage.cmp(&b.usage));
        entries.dedup_by(|a, b| a.usage.eq_ignore_ascii_case(&b.usage));

        let filtered = if query.is_empty() {
            entries
        } else {
            entries
                .into_iter()
                .filter(|entry| {
                    let usage = entry.usage.to_ascii_lowercase();
                    usage == query
                        || usage.starts_with(&format!("{query} "))
                        || usage.contains(&query)
                })
                .collect::<Vec<_>>()
        };

        let prefix = self.primary_command_prefix();
        let mut lines = Vec::new();
        if let Some(header) = &self.auto_help.header {
            lines.push(header.clone());
        } else {
            lines.push("Available commands:".to_owned());
        }

        if filtered.is_empty() {
            if query.is_empty() {
                lines.push("(no commands registered)".to_owned());
            } else {
                lines.push(format!("No commands found for '{query}'."));
            }
        } else {
            for entry in filtered {
                if let Some(summary) = &entry.summary {
                    lines.push(format!("- {prefix}{}  -  {summary}", entry.usage));
                } else {
                    lines.push(format!("- {prefix}{}", entry.usage));
                }
            }
        }

        if let Some(footer) = &self.auto_help.footer {
            lines.push(footer.clone());
        }

        Some(lines.join("\n"))
    }
}