chabeau 0.7.3

A full-screen terminal chat interface that connects to various AI APIs for real-time conversations
Documentation
use crate::character::CharacterCard;
use crate::core::builtin_providers::BuiltinProvider;
use crate::core::config::data::CustomProvider;
use crate::ui::builtin_themes::ThemeSpec;

use super::{sanitize_picker_metadata, sanitize_picker_metadata_for_inspect};

#[derive(Debug, Clone, Copy)]
pub(super) enum ThemeSource {
    Builtin,
    Custom,
}

impl ThemeSource {
    fn label(self) -> &'static str {
        match self {
            ThemeSource::Builtin => "Built-in theme",
            ThemeSource::Custom => "Custom theme (config.toml)",
        }
    }
}

pub(super) fn theme_metadata(
    spec: &ThemeSpec,
    source: ThemeSource,
    is_default: bool,
) -> (String, String) {
    let mut summary_parts = vec![source.label().to_string(), format!("ID: {}", spec.id)];
    if is_default {
        summary_parts.push("Default from config".to_string());
    }
    let summary = sanitize_picker_metadata(&summary_parts.join(""));

    let mut lines = vec![
        format!("Theme: {} (ID: {})", spec.display_name, spec.id),
        format!("Source: {}", source.label()),
    ];

    if is_default {
        lines.push("Status: Default theme from config".to_string());
    }

    append_theme_sections(&mut lines, spec);

    let inspect = build_inspect_text(lines);
    (summary, inspect)
}

fn append_theme_sections(lines: &mut Vec<String>, spec: &ThemeSpec) {
    lines.push(String::new());
    append_theme_section(
        lines,
        "General",
        &[
            "Background",
            "Selection highlight",
            "Title",
            "Streaming indicator",
        ],
        &[
            &spec.background,
            &spec.selection_highlight,
            &spec.title,
            &spec.streaming_indicator,
        ],
    );
    append_theme_section(
        lines,
        "Chat",
        &["User prefix", "User text", "Assistant text", "System text"],
        &[
            &spec.user_prefix,
            &spec.user_text,
            &spec.assistant_text,
            &spec.system_text,
        ],
    );
    append_theme_section(
        lines,
        "App messages",
        &[
            "Info prefix",
            "Info prefix style",
            "Info text",
            "Warning prefix",
            "Warning prefix style",
            "Warning text",
            "Error prefix",
            "Error prefix style",
            "Error text",
        ],
        &[
            &spec.app_info_prefix,
            &spec.app_info_prefix_style,
            &spec.app_info_text,
            &spec.app_warning_prefix,
            &spec.app_warning_prefix_style,
            &spec.app_warning_text,
            &spec.app_error_prefix,
            &spec.app_error_prefix_style,
            &spec.app_error_text,
        ],
    );
    append_theme_section(
        lines,
        "Input",
        &[
            "Border",
            "Title",
            "Text",
            "Cursor modifiers",
            "Cursor color",
        ],
        &[
            &spec.input_border,
            &spec.input_title,
            &spec.input_text,
            &spec.input_cursor_modifiers,
            &spec.cursor_color,
        ],
    );

    append_theme_section(
        lines,
        "Markdown",
        &[
            "Heading 1",
            "Heading 2",
            "Heading 3",
            "Heading 4",
            "Heading 5",
            "Heading 6",
            "Paragraph",
            "Inline code",
            "Link",
            "Rule",
            "Blockquote",
            "List marker",
            "Code block text",
            "Code block background",
        ],
        &[
            &spec.md_h1,
            &spec.md_h2,
            &spec.md_h3,
            &spec.md_h4,
            &spec.md_h5,
            &spec.md_h6,
            &spec.md_paragraph,
            &spec.md_inline_code,
            &spec.md_link,
            &spec.md_rule,
            &spec.md_blockquote_text,
            &spec.md_list_marker,
            &spec.md_codeblock_text,
            &spec.md_codeblock_bg,
        ],
    );
}

fn append_theme_section(
    lines: &mut Vec<String>,
    heading: &str,
    labels: &[&str],
    values: &[&Option<String>],
) {
    if let Some(section) = build_theme_section(heading, labels, values) {
        if !lines.last().map(|s| s.is_empty()).unwrap_or(false) {
            lines.push(String::new());
        }
        lines.extend(section.lines().map(|line| line.to_string()));
    }
}

fn build_theme_section(
    heading: &str,
    labels: &[&str],
    values: &[&Option<String>],
) -> Option<String> {
    let mut section_lines = Vec::new();
    for (label, value) in labels.iter().zip(values.iter()) {
        if let Some(value) = value {
            section_lines.push(format!("  {}: {}", label, value));
        }
    }

    if section_lines.is_empty() {
        None
    } else {
        let mut result = Vec::with_capacity(section_lines.len() + 1);
        result.push(heading.to_string());
        result.extend(section_lines);
        Some(result.join("\n"))
    }
}

fn trim_trailing_blank_lines(mut lines: Vec<String>) -> Vec<String> {
    while matches!(lines.last(), Some(line) if line.trim().is_empty()) {
        lines.pop();
    }
    lines
}

pub(crate) fn build_inspect_text(lines: Vec<String>) -> String {
    sanitize_picker_metadata_for_inspect(&trim_trailing_blank_lines(lines).join("\n"))
}

pub(super) fn provider_metadata_builtin(
    provider: &BuiltinProvider,
    is_default: bool,
) -> (String, String) {
    provider_metadata(
        &provider.display_name,
        &provider.id,
        &provider.base_url,
        "Built-in provider",
        Some(provider.auth_mode()),
        is_default,
    )
}

pub(super) fn provider_metadata_custom(
    provider: &CustomProvider,
    is_default: bool,
) -> (String, String) {
    provider_metadata(
        &provider.display_name,
        &provider.id,
        &provider.base_url,
        "Custom provider (config.toml)",
        provider.mode.as_deref(),
        is_default,
    )
}

fn provider_metadata(
    display_name: &str,
    id: &str,
    base_url: &str,
    source: &str,
    auth_mode: Option<&str>,
    is_default: bool,
) -> (String, String) {
    let mut summary_parts = vec![
        source.to_string(),
        format!("ID: {}", id),
        base_url.to_string(),
    ];
    if is_default {
        summary_parts.push("Default from config".to_string());
    }
    let summary = sanitize_picker_metadata(&summary_parts.join(""));

    let mut lines = vec![
        format!("Provider: {}", display_name),
        format!("ID: {}", id),
        format!("Source: {}", source),
        format!("Base URL: {}", base_url),
    ];

    if let Some(mode) = auth_mode {
        lines.push(format!("Authentication mode: {}", mode));
    }

    if is_default {
        lines.push("Status: Default provider from config".to_string());
    }

    let inspect = build_inspect_text(lines);
    (summary, inspect)
}

pub(super) fn character_inspect(card: &CharacterCard) -> String {
    let mut lines = vec![
        format!("Character: {}", card.data.name),
        format!("Spec: {} (version {})", card.spec, card.spec_version),
    ];

    append_character_block(&mut lines, "Description", &card.data.description);
    append_character_block(&mut lines, "Personality", &card.data.personality);
    append_character_block(&mut lines, "Scenario", &card.data.scenario);
    append_character_block(&mut lines, "First message", &card.data.first_mes);
    append_character_block(&mut lines, "Example dialogue", &card.data.mes_example);
    append_character_block_optional(
        &mut lines,
        "System prompt",
        card.data.system_prompt.as_deref(),
    );
    append_character_block_optional(
        &mut lines,
        "Creator notes",
        card.data.creator_notes.as_deref(),
    );
    append_character_block_optional(
        &mut lines,
        "Post-history instructions",
        card.data.post_history_instructions.as_deref(),
    );
    append_character_list(
        &mut lines,
        "Alternate greetings",
        card.data.alternate_greetings.as_ref(),
    );

    if let Some(tags) = card.data.tags.as_ref() {
        if !tags.is_empty() {
            lines.push(String::new());
            lines.push(format!("Tags: {}", tags.join(", ")));
        }
    }

    if let Some(creator) = card.data.creator.as_ref() {
        if !creator.trim().is_empty() {
            lines.push(String::new());
            lines.push(format!("Creator: {}", creator.trim()));
        }
    }

    if let Some(version) = card.data.character_version.as_ref() {
        if !version.trim().is_empty() {
            lines.push(String::new());
            lines.push(format!("Version: {}", version.trim()));
        }
    }

    build_inspect_text(lines)
}

fn append_character_block(lines: &mut Vec<String>, heading: &str, content: &str) {
    let trimmed = content.trim();
    if trimmed.is_empty() {
        return;
    }

    lines.push(String::new());
    lines.push(format!("{}:", heading));
    for line in trimmed.lines() {
        lines.push(format!("  {}", line.trim_end()));
    }
}

fn append_character_block_optional(lines: &mut Vec<String>, heading: &str, content: Option<&str>) {
    if let Some(content) = content {
        append_character_block(lines, heading, content);
    }
}

fn append_character_list(lines: &mut Vec<String>, heading: &str, items: Option<&Vec<String>>) {
    if let Some(items) = items {
        let filtered: Vec<&str> = items
            .iter()
            .map(|item| item.trim())
            .filter(|item| !item.is_empty())
            .collect();
        if filtered.is_empty() {
            return;
        }

        lines.push(String::new());
        lines.push(format!("{}:", heading));
        for item in filtered {
            for (idx, line) in item.lines().enumerate() {
                if idx == 0 {
                    lines.push(format!("  - {}", line.trim_end()));
                } else {
                    lines.push(format!("    {}", line.trim_end()));
                }
            }
        }
    }
}