stynx-code-commands 3.7.0

Slash commands and file reference expansion
Documentation
use std::collections::HashMap;

use stynx_code_errors::AppResult;

use crate::domain::{CommandDefinition, CommandHandler, CommandOutput, CommandType};
use crate::application::parse_command::parse_command;
use crate::application::execute_command::execute_command;
use crate::domain::command::CommandResult;

pub struct CommandRegistry {
    commands: Vec<CommandDefinition>,

    index: HashMap<String, usize>,
}

impl CommandRegistry {
    pub fn new() -> Self {
        Self {
            commands: Vec::new(),
            index: HashMap::new(),
        }
    }

    pub fn register(&mut self, def: CommandDefinition) {
        let idx = self.commands.len();
        self.index.insert(def.name.clone(), idx);
        for alias in &def.aliases {
            self.index.insert(alias.clone(), idx);
        }
        self.commands.push(def);
    }

    pub fn get(&self, name: &str) -> Option<&CommandDefinition> {
        self.index.get(name).map(|&i| &self.commands[i])
    }

    pub fn completions(&self, prefix: &str) -> Vec<String> {
        let mut results: Vec<String> = self.commands.iter()
            .filter(|cmd| !cmd.is_hidden)
            .flat_map(|cmd| {
                let mut names = vec![cmd.name.clone()];
                names.extend(cmd.aliases.iter().cloned());
                names
            })
            .filter(|name| name.starts_with(prefix))
            .collect();
        results.sort();
        results.dedup();
        results
    }

    pub fn list_commands(&self) -> Vec<(&str, &str, CommandType)> {
        self.commands.iter()
            .filter(|c| !c.is_hidden)
            .map(|c| (c.name.as_str(), c.description.as_str(), c.command_type))
            .collect()
    }

    pub async fn dispatch(&self, input: &str) -> Option<AppResult<CommandOutput>> {
        let trimmed = input.trim();

        let cmd_name = if let Some(space_idx) = trimmed.find(' ') {
            &trimmed[..space_idx]
        } else {
            trimmed
        };

        let args = trimmed.strip_prefix(cmd_name).unwrap_or("").trim();

        if let Some(def) = self.get(cmd_name) {
            return Some((def.handler)(args).await);
        }

        if let Some(cmd) = parse_command(trimmed) {
            let result = execute_command(cmd).await;
            return Some(Ok(match result {
                CommandResult::Output(text) => CommandOutput::Text(text),
                CommandResult::ReplaceConversation(_) => CommandOutput::None,
                CommandResult::Quit => CommandOutput::Quit,
            }));
        }

        None
    }

    pub fn len(&self) -> usize {
        self.commands.len()
    }

    pub fn is_empty(&self) -> bool {
        self.commands.is_empty()
    }
}

impl Default for CommandRegistry {
    fn default() -> Self {
        Self::new()
    }
}

pub fn local_handler<F>(f: F) -> CommandHandler
where
    F: Fn(&str) -> AppResult<CommandOutput> + Send + Sync + 'static,
{
    std::sync::Arc::new(move |args: &str| {
        let result = f(args);
        Box::pin(async move { result })
    })
}

pub fn prompt_handler<F>(f: F) -> CommandHandler
where
    F: Fn(&str) -> String + Send + Sync + 'static,
{
    std::sync::Arc::new(move |args: &str| {
        let prompt = f(args);
        Box::pin(async move { Ok(CommandOutput::Prompt(prompt)) })
    })
}