chabeau 0.7.3

A full-screen terminal chat interface that connects to various AI APIs for real-time conversations
Documentation
use super::{required_arg, usage_status};
use crate::commands::registry::CommandInvocation;
use crate::commands::CommandResult;
use crate::core::app::App;

const USAGE_MARKDOWN: &str = "Usage: /markdown [on|off|toggle]";
const USAGE_SYNTAX: &str = "Usage: /syntax [on|off|toggle]";

pub(crate) fn handle_theme(app: &mut App, invocation: CommandInvocation<'_>) -> CommandResult {
    if invocation.args_len() == 0 {
        return CommandResult::OpenThemePicker;
    }

    let Some(id) = required_arg(app, &invocation, 0, "Usage: /theme <id>") else {
        return CommandResult::Continue;
    };

    let res = {
        let mut controller = app.theme_controller();
        controller.apply_theme_by_id(id)
    };
    match res {
        Ok(_) => {
            app.conversation().set_status(format!("Theme set: {}", id));
            CommandResult::Continue
        }
        Err(_) => usage_status(app, "Theme error"),
    }
}

pub(crate) fn handle_model(app: &mut App, invocation: CommandInvocation<'_>) -> CommandResult {
    if invocation.args_len() == 0 {
        return CommandResult::OpenModelPicker;
    }

    let Some(model_id) = required_arg(app, &invocation, 0, "Usage: /model <id>") else {
        return CommandResult::Continue;
    };

    {
        let mut controller = app.provider_controller();
        controller.apply_model_by_id(model_id);
    }
    app.conversation()
        .set_status(format!("Model set: {}", model_id));
    CommandResult::Continue
}

pub(crate) fn handle_provider(app: &mut App, invocation: CommandInvocation<'_>) -> CommandResult {
    if invocation.args_len() == 0 {
        return CommandResult::OpenProviderPicker;
    }

    let Some(provider_id) = required_arg(app, &invocation, 0, "Usage: /provider <id>") else {
        return CommandResult::Continue;
    };

    let (result, should_open_model_picker) = {
        let mut controller = app.provider_controller();
        controller.apply_provider_by_id(provider_id)
    };

    match result {
        Ok(_) => {
            app.conversation()
                .set_status(format!("Provider set: {}", provider_id));
            if should_open_model_picker {
                CommandResult::OpenModelPicker
            } else {
                CommandResult::Continue
            }
        }
        Err(e) => {
            app.conversation()
                .set_status(format!("Provider error: {}", e));
            CommandResult::Continue
        }
    }
}

pub(crate) fn handle_markdown(app: &mut App, invocation: CommandInvocation<'_>) -> CommandResult {
    handle_toggle_command(
        app,
        invocation,
        app.ui.markdown_enabled,
        ToggleText {
            usage: USAGE_MARKDOWN,
            feature: "Markdown",
            on_word: "enabled",
            off_word: "disabled",
        },
        |app, new_state| app.ui.markdown_enabled = new_state,
        |cfg, new_state| cfg.markdown = Some(new_state),
    )
}

pub(crate) fn handle_syntax(app: &mut App, invocation: CommandInvocation<'_>) -> CommandResult {
    handle_toggle_command(
        app,
        invocation,
        app.ui.syntax_enabled,
        ToggleText {
            usage: USAGE_SYNTAX,
            feature: "Syntax",
            on_word: "on",
            off_word: "off",
        },
        |app, new_state| app.ui.syntax_enabled = new_state,
        |cfg, new_state| cfg.syntax = Some(new_state),
    )
}

pub(crate) fn handle_character(app: &mut App, invocation: CommandInvocation<'_>) -> CommandResult {
    if invocation.args_text().is_empty() {
        CommandResult::OpenCharacterPicker
    } else {
        let character_name = invocation.args_text();
        match app.character_service.resolve(character_name) {
            Ok(card) => {
                let name = card.data.name.clone();
                app.session.set_character(card);
                app.conversation()
                    .set_status(format!("Character set: {}", name));
                CommandResult::Continue
            }
            Err(e) => {
                app.conversation()
                    .set_status(format!("Character error: {}", e));
                CommandResult::Continue
            }
        }
    }
}

pub(crate) fn handle_persona(app: &mut App, invocation: CommandInvocation<'_>) -> CommandResult {
    if invocation.args_len() == 0 {
        return CommandResult::OpenPersonaPicker;
    }

    let Some(persona_id) = required_arg(app, &invocation, 0, "Usage: /persona <id>") else {
        return CommandResult::Continue;
    };

    match app.persona_manager.set_active_persona(persona_id) {
        Ok(()) => {
            let active_persona_name = app
                .persona_manager
                .get_active_persona()
                .map(|p| p.display_name.clone());
            let persona_name = active_persona_name
                .clone()
                .unwrap_or_else(|| "Unknown".to_string());

            if active_persona_name.is_some() {
                let display_name = app.persona_manager.get_display_name();
                app.ui.update_user_display_name(display_name);
            } else {
                app.ui.update_user_display_name("You".to_string());
            }
            app.conversation()
                .set_status(format!("Persona activated: {}", persona_name));
            CommandResult::Continue
        }
        Err(e) => {
            app.conversation()
                .set_status(format!("Persona error: {}", e));
            CommandResult::Continue
        }
    }
}

pub(crate) fn handle_preset(app: &mut App, invocation: CommandInvocation<'_>) -> CommandResult {
    if invocation.args_len() == 0 {
        return CommandResult::OpenPresetPicker;
    }

    let Some(preset_id) = required_arg(app, &invocation, 0, "Usage: /preset <id>") else {
        return CommandResult::Continue;
    };

    if preset_id.eq_ignore_ascii_case("off") || preset_id == "[turn_off_preset]" {
        app.preset_manager.clear_active_preset();
        app.conversation()
            .set_status("Preset deactivated".to_string());
        CommandResult::Continue
    } else {
        match app.preset_manager.set_active_preset(preset_id) {
            Ok(()) => {
                app.conversation()
                    .set_status(format!("Preset activated: {}", preset_id));
                CommandResult::Continue
            }
            Err(e) => {
                app.conversation()
                    .set_status(format!("Preset error: {}", e));
                CommandResult::Continue
            }
        }
    }
}

struct ToggleText {
    usage: &'static str,
    feature: &'static str,
    on_word: &'static str,
    off_word: &'static str,
}

fn handle_toggle_command<F, G>(
    app: &mut App,
    invocation: CommandInvocation<'_>,
    current_state: bool,
    text: ToggleText,
    mut apply_ui: F,
    mut persist_config: G,
) -> CommandResult
where
    F: FnMut(&mut App, bool),
    G: FnMut(&mut crate::core::config::data::Config, bool),
{
    let action = match invocation.toggle_action() {
        Ok(action) => action,
        Err(_) => return usage_status(app, text.usage),
    };

    let new_state = action.apply(current_state);
    apply_ui(app, new_state);
    let state_word = if new_state {
        text.on_word
    } else {
        text.off_word
    };

    match crate::core::config::data::Config::load() {
        Ok(mut cfg) => {
            persist_config(&mut cfg, new_state);
            let status = if cfg.save().is_ok() {
                format!("{} {}", text.feature, state_word)
            } else {
                format!("{} {} (unsaved)", text.feature, state_word)
            };
            app.conversation().set_status(status);
        }
        Err(_) => {
            app.conversation()
                .set_status(format!("{} {}", text.feature, state_word));
        }
    }

    CommandResult::Continue
}