oo-ide 0.0.4

∞ is a terminal IDE focused on low distraction, high usability.
Documentation
pub mod args;
pub mod builder;
pub mod id;
pub mod meta;

pub use args::{ArgKind, ArgValue, CommandArg};
pub use builder::command;
pub use id::CommandId;
pub use meta::CommandMeta;

use std::collections::{HashMap, VecDeque};
use std::time::Instant;

use crate::app_state::AppState;
use crate::operation::Operation;

pub type CommandHandler =
    Box<dyn Fn(&AppState, &HashMap<String, ArgValue>) -> Vec<Operation>>;

pub struct Command {
    pub meta: CommandMeta,
    /// Read-only context — commands declare intent via operations, never mutate.
    pub handler: CommandHandler,
}

const HISTORY_LIMIT: usize = 50;

#[derive(Debug, Clone)]
pub struct HistoryEntry {
    pub id: CommandId,
    pub args: HashMap<String, ArgValue>,
    pub timestamp: Instant,
    pub count: u32,
}

pub struct CommandRegistry {
    commands: HashMap<CommandId, Command>,
    history: VecDeque<HistoryEntry>,
}

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

    pub fn register(&mut self, command: Command) {
        self.commands.insert(command.meta.id.clone(), command);
    }

    /// Like `resolve_shortcut` but only returns the command if it is active in
    /// at least one of the given contexts (or has no context restriction).
    pub fn select_command_in_context<'a>(
        &self,
        ids: &'a Vec<CommandId>,
        active_contexts: &std::collections::HashSet<String>,
    ) -> Option<&'a CommandId> {
        log::debug!("Select commands in context {ids:?} {active_contexts:?}");
        for id in ids {
            if let Some(cmd) = self.commands.get(id) {
                let active = cmd.meta.contexts.is_empty()
                    || cmd
                        .meta
                        .contexts
                        .iter()
                        .any(|c| active_contexts.contains(c.as_ref()));
                log::debug!("{id:?} {active:?}");
                if active {
                    return Some(id);
                }
            }
        }
        None
    }

    #[allow(dead_code)]
    pub fn active_commands<'a>(
        &'a self,
        app: &'a AppState,
    ) -> impl Iterator<Item = &'a Command> + 'a {
        self.commands.values().filter(move |cmd| {
            cmd.meta.contexts.is_empty()
                || cmd
                    .meta
                    .contexts
                    .iter()
                    .any(|c| app.active_contexts.contains(c.as_ref()))
        })
    }

    #[allow(dead_code)]
    pub fn all_commands_sorted(&self) -> Vec<&Command> {
        let mut cmds: Vec<&Command> = self.commands.values().collect();
        cmds.sort_by(|a, b| {
            a.meta
                .id
                .group
                .cmp(&b.meta.id.group)
                .then(a.meta.id.name.cmp(&b.meta.id.name))
        });
        cmds
    }

    pub fn user_commands_sorted(&self) -> Vec<&Command> {
        let mut cmds: Vec<&Command> = self
            .commands
            .values()
            .filter(|c| matches!(c.meta.visibility, meta::Visibility::UserVisible))
            .collect();
        cmds.sort_by(|a, b| {
            a.meta
                .id
                .group
                .cmp(&b.meta.id.group)
                .then(a.meta.id.name.cmp(&b.meta.id.name))
        });
        cmds
    }

    pub fn history(&self) -> &VecDeque<HistoryEntry> {
        &self.history
    }

    /// Execute a command: returns the operations it produced.
    /// Unknown ID → returns empty, logs to stderr (no AppState mutation allowed here).
    pub fn execute(
        &mut self,
        id: &CommandId,
        args: HashMap<String, ArgValue>,
        app: &AppState,
    ) -> Vec<Operation> {
        if let Some(cmd) = self.commands.get(id) {
            
            // Do not record history here — callers decide whether to record.
            (cmd.handler)(app, &args)
        } else {
            log::warn!("Unknown command: {}", id);
            vec![]
        }
    }

    fn push_history(&mut self, id: CommandId, args: HashMap<String, ArgValue>) {
        // Do not record history for non-user-visible commands.
        if let Some(cmd) = self.commands.get(&id) {
            if !matches!(cmd.meta.visibility, meta::Visibility::UserVisible) {
                return;
            }
        } else {
            // Unknown command; don't record.
            return;
        }

        if let Some(front) = self.history.front_mut()
            && front.id == id
            && front.args == args
        {
            front.count += 1;
            front.timestamp = Instant::now();
            return;
        }
        self.history.push_front(HistoryEntry {
            id,
            args,
            timestamp: Instant::now(),
            count: 1,
        });
        if self.history.len() > HISTORY_LIMIT {
            self.history.pop_back();
        }
    }

    /// Record a history entry for commands explicitly invoked via the command picker.
    /// This honors command visibility (hidden commands are not recorded).
    pub fn record_palette_selection(&mut self, id: CommandId, args: HashMap<String, ArgValue>) {
        self.push_history(id, args);
    }
}

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