magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::*;

#[derive(Debug, Clone)]
pub(crate) struct ThemeRoles {
    pub(crate) surface_background: OpalineColor,
    pub(crate) surface_panel: OpalineColor,
    pub(crate) surface_panel_alt: OpalineColor,
    pub(crate) surface_overlay: OpalineColor,
    pub(crate) surface_selection: OpalineColor,
    pub(crate) surface_code: OpalineColor,
    pub(crate) surface_success: OpalineColor,
    pub(crate) surface_pending: OpalineColor,
    pub(crate) surface_error: OpalineColor,
    pub(crate) text_primary: OpalineColor,
    pub(crate) text_muted: OpalineColor,
    pub(crate) text_dim: OpalineColor,
    pub(crate) text_accent: OpalineColor,
    pub(crate) text_title: OpalineColor,
    pub(crate) text_prompt: OpalineColor,
    pub(crate) text_status: OpalineColor,
    pub(crate) text_help: OpalineColor,
    pub(crate) text_metadata: OpalineColor,
    pub(crate) text_command: OpalineColor,
    pub(crate) text_command_active: OpalineColor,
    pub(crate) border_default: OpalineColor,
    pub(crate) border_active: OpalineColor,
    pub(crate) border_alternate: OpalineColor,
    pub(crate) border_command: OpalineColor,
    pub(crate) border_command_active: OpalineColor,
    pub(crate) border_error: OpalineColor,
    pub(crate) status_queued: OpalineColor,
    pub(crate) status_running: OpalineColor,
    pub(crate) status_writing: OpalineColor,
    pub(crate) status_success: OpalineColor,
    pub(crate) status_failed: OpalineColor,
    pub(crate) status_canceled: OpalineColor,
    pub(crate) status_tool: OpalineColor,
    pub(crate) status_subagent: OpalineColor,
    pub(crate) status_assistant: OpalineColor,
    pub(crate) status_diagnostic: OpalineColor,
    pub(crate) transcript_user: OpalineColor,
    pub(crate) transcript_assistant: OpalineColor,
    pub(crate) transcript_thinking: OpalineColor,
    pub(crate) transcript_tool: OpalineColor,
    pub(crate) transcript_session: OpalineColor,
    pub(crate) transcript_diagnostic: OpalineColor,
    pub(crate) transcript_heading: OpalineColor,
    pub(crate) transcript_section: OpalineColor,
    pub(crate) transcript_error_label: OpalineColor,
    pub(crate) transcript_error_body: OpalineColor,
    pub(crate) diff_inserted: OpalineColor,
    pub(crate) diff_removed: OpalineColor,
    pub(crate) diff_changed: OpalineColor,
    pub(crate) diff_context: OpalineColor,
    pub(crate) diff_hunk_header: OpalineColor,
    pub(crate) diff_file_header: OpalineColor,
    pub(crate) diff_metadata: OpalineColor,
    pub(crate) syntax_plain: OpalineColor,
    pub(crate) syntax_keyword: OpalineColor,
    pub(crate) syntax_string: OpalineColor,
    pub(crate) syntax_comment: OpalineColor,
    pub(crate) syntax_number: OpalineColor,
    pub(crate) syntax_function: OpalineColor,
    pub(crate) syntax_type: OpalineColor,
    pub(crate) syntax_operator: OpalineColor,
    pub(crate) syntax_punctuation: OpalineColor,
    pub(crate) syntax_code_fence: OpalineColor,
    pub(crate) syntax_language_label: OpalineColor,
    pub(crate) syntax_gutter: OpalineColor,
    pub(crate) syntax_fallback: OpalineColor,
    pub(crate) heading_style: OpalineStyle,
    pub(crate) user_heading_style: OpalineStyle,
    pub(crate) assistant_heading_style: OpalineStyle,
    pub(crate) thinking_heading_style: OpalineStyle,
    pub(crate) tool_heading_style: OpalineStyle,
    pub(crate) standard_styles: ThemeStandardStyles,
    pub(crate) gradients: ThemeGradients,
}

#[derive(Debug, Clone)]
pub(crate) struct ThemeGradients {
    pub(crate) fast: Gradient,
}

#[derive(Debug, Clone)]
pub(crate) struct ThemeStandardStyles {
    pub(crate) keyword: OpalineStyle,
    pub(crate) line_number: OpalineStyle,
    pub(crate) selected: OpalineStyle,
    pub(crate) active_selected: OpalineStyle,
    pub(crate) focused_border: OpalineStyle,
    pub(crate) unfocused_border: OpalineStyle,
    pub(crate) success: OpalineStyle,
    pub(crate) error: OpalineStyle,
    pub(crate) warning: OpalineStyle,
    pub(crate) info: OpalineStyle,
    pub(crate) dimmed: OpalineStyle,
    pub(crate) muted: OpalineStyle,
    pub(crate) inline_code: OpalineStyle,
}

impl ThemeRoles {
    pub(super) fn from_theme(theme: &Theme) -> Self {
        let text_primary = role_color(theme, "magi.text.primary", &[tokens::TEXT_PRIMARY]);
        let text_muted = role_color(theme, "magi.text.muted", &[tokens::TEXT_MUTED]);
        let text_dim = role_color(theme, "magi.text.dim", &[tokens::TEXT_DIM]);
        let text_accent = role_color(theme, "magi.text.accent", &[tokens::ACCENT_PRIMARY]);
        let text_title = role_color(
            theme,
            "magi.text.title",
            &["magi.text.accent", tokens::ACCENT_PRIMARY],
        );
        let text_prompt = role_color(theme, "magi.text.prompt", &[tokens::TEXT_PRIMARY]);
        let text_status = role_color(theme, "magi.text.status", &[tokens::TEXT_SECONDARY]);
        let text_help = role_color(theme, "magi.text.help", &[tokens::TEXT_SECONDARY]);
        let text_metadata = role_color(theme, "magi.text.metadata", &[tokens::TEXT_MUTED]);
        let text_command = role_color(theme, "magi.text.command", &[tokens::ACCENT_SECONDARY]);
        let text_command_active =
            role_color(theme, "magi.text.command_active", &[tokens::ACCENT_PRIMARY]);

        let surface_background = role_color(theme, "magi.surface.background", &[tokens::BG_BASE]);
        let surface_panel = role_color(
            theme,
            "magi.surface.panel",
            &[tokens::BG_PANEL, tokens::BG_BASE],
        );
        let surface_panel_alt = role_color(
            theme,
            "magi.surface.panel_alt",
            &[tokens::BG_HIGHLIGHT, tokens::BG_PANEL],
        );
        let surface_overlay = role_color(
            theme,
            "magi.surface.overlay",
            &["bg.elevated", tokens::BG_HIGHLIGHT, tokens::BG_PANEL],
        );
        let surface_selection = role_color(
            theme,
            "magi.surface.selection",
            &[tokens::BG_SELECTION, tokens::BG_HIGHLIGHT],
        );
        let surface_code = role_color(
            theme,
            "magi.surface.code",
            &[tokens::BG_CODE, tokens::BG_BASE],
        );
        let surface_success = role_color(
            theme,
            "magi.surface.success",
            &[tokens::BG_HIGHLIGHT, tokens::BG_PANEL],
        );
        let surface_pending = role_color(
            theme,
            "magi.surface.pending",
            &[tokens::BG_HIGHLIGHT, tokens::BG_PANEL],
        );
        let surface_error = role_color(
            theme,
            "magi.surface.error",
            &[tokens::BG_HIGHLIGHT, tokens::BG_PANEL],
        );

        let border_default = role_color(theme, "magi.border.default", &[tokens::BORDER_UNFOCUSED]);
        let border_active = role_color(theme, "magi.border.active", &[tokens::BORDER_FOCUSED]);
        let border_alternate =
            role_color(theme, "magi.border.alternate", &[tokens::ACCENT_SECONDARY]);
        let border_command = role_color(theme, "magi.border.command", &[tokens::BORDER_UNFOCUSED]);
        let border_command_active = role_color(
            theme,
            "magi.border.command_active",
            &[tokens::BORDER_FOCUSED],
        );
        let border_error = role_color(theme, "magi.border.error", &[tokens::ERROR]);

        let status_queued = role_color(theme, "magi.status.queued", &[tokens::TEXT_MUTED]);
        let status_running = role_color(theme, "magi.status.running", &[tokens::WARNING]);
        let status_writing = role_color(theme, "magi.status.writing", &[tokens::WARNING]);
        let status_success = role_color(theme, "magi.status.success", &[tokens::SUCCESS]);
        let status_failed = role_color(theme, "magi.status.failed", &[tokens::ERROR]);
        let status_canceled = role_color(theme, "magi.status.canceled", &[tokens::TEXT_MUTED]);
        let status_tool = role_color(theme, "magi.status.tool", &[tokens::ACCENT_SECONDARY]);
        let status_subagent = role_color(theme, "magi.status.subagent", &[tokens::ACCENT_TERTIARY]);
        let status_assistant =
            role_color(theme, "magi.status.assistant", &[tokens::ACCENT_PRIMARY]);
        let status_diagnostic = role_color(theme, "magi.status.diagnostic", &[tokens::INFO]);

        let transcript_user =
            role_color(theme, "magi.transcript.user", &[tokens::ACCENT_SECONDARY]);
        let transcript_assistant =
            role_color(theme, "magi.transcript.assistant", &[tokens::TEXT_PRIMARY]);
        let transcript_thinking = role_color(
            theme,
            "magi.transcript.thinking",
            &[tokens::ACCENT_TERTIARY],
        );
        let transcript_tool =
            role_color(theme, "magi.transcript.tool", &[tokens::ACCENT_SECONDARY]);
        let transcript_session =
            role_color(theme, "magi.transcript.session", &[tokens::TEXT_MUTED]);
        let transcript_diagnostic =
            role_color(theme, "magi.transcript.diagnostic", &[tokens::INFO]);
        let transcript_heading =
            role_color(theme, "magi.transcript.heading", &[tokens::ACCENT_PRIMARY]);
        let transcript_section =
            role_color(theme, "magi.transcript.section", &[tokens::TEXT_PRIMARY]);
        let transcript_error_label =
            role_color(theme, "magi.transcript.error_label", &[tokens::ERROR]);
        let transcript_error_body =
            role_color(theme, "magi.transcript.error_body", &[tokens::TEXT_PRIMARY]);

        let diff_inserted = role_color(theme, "magi.diff.inserted", &[tokens::SUCCESS]);
        let diff_removed = role_color(theme, "magi.diff.removed", &[tokens::ERROR]);
        let diff_changed = role_color(theme, "magi.diff.changed", &[tokens::WARNING]);
        let diff_context = role_color(theme, "magi.diff.context", &[tokens::TEXT_PRIMARY]);
        let diff_hunk_header =
            role_color(theme, "magi.diff.hunk_header", &[tokens::ACCENT_SECONDARY]);
        let diff_file_header =
            role_color(theme, "magi.diff.file_header", &[tokens::ACCENT_PRIMARY]);
        let diff_metadata = role_color(theme, "magi.diff.metadata", &[tokens::TEXT_MUTED]);

        let syntax_plain = role_color(theme, "magi.syntax.plain", &[tokens::TEXT_PRIMARY]);
        let syntax_keyword = role_color(theme, "magi.syntax.keyword", &[tokens::CODE_KEYWORD]);
        let syntax_string = role_color(theme, "magi.syntax.string", &[tokens::CODE_STRING]);
        let syntax_comment = role_color(theme, "magi.syntax.comment", &[tokens::CODE_COMMENT]);
        let syntax_number = role_color(theme, "magi.syntax.number", &[tokens::CODE_NUMBER]);
        let syntax_function = role_color(theme, "magi.syntax.function", &[tokens::CODE_FUNCTION]);
        let syntax_type = role_color(theme, "magi.syntax.type", &[tokens::CODE_TYPE]);
        let syntax_operator =
            role_color(theme, "magi.syntax.operator", &[tokens::ACCENT_SECONDARY]);
        let syntax_punctuation =
            role_color(theme, "magi.syntax.punctuation", &[tokens::TEXT_SECONDARY]);
        let syntax_code_fence =
            role_color(theme, "magi.syntax.code_fence", &[tokens::ACCENT_PRIMARY]);
        let syntax_language_label = role_color(
            theme,
            "magi.syntax.language_label",
            &[tokens::ACCENT_SECONDARY],
        );
        let syntax_gutter = role_color(theme, "magi.syntax.gutter", &[tokens::CODE_LINE_NUMBER]);
        let syntax_fallback = role_color(theme, "magi.syntax.fallback", &[tokens::TEXT_SECONDARY]);

        let default_heading_style = heading_style(theme, styles::INFO_STYLE, transcript_heading);
        let user_heading_style = heading_style(theme, styles::INFO_STYLE, transcript_user);
        let assistant_heading_style =
            heading_style(theme, styles::INFO_STYLE, transcript_assistant);
        let thinking_heading_style = heading_style(theme, styles::INFO_STYLE, transcript_thinking);
        let tool_heading_style = heading_style(theme, styles::INFO_STYLE, transcript_tool);
        let standard_styles = ThemeStandardStyles {
            keyword: standard_style(theme, styles::KEYWORD, syntax_keyword),
            line_number: standard_style(theme, styles::LINE_NUMBER, syntax_gutter),
            selected: standard_style(theme, styles::SELECTED, text_primary),
            active_selected: standard_style(theme, styles::ACTIVE_SELECTED, text_primary),
            focused_border: standard_style(theme, styles::FOCUSED_BORDER, border_active),
            unfocused_border: standard_style(theme, styles::UNFOCUSED_BORDER, border_default),
            success: standard_style(theme, styles::SUCCESS_STYLE, status_success),
            error: standard_style(theme, styles::ERROR_STYLE, status_failed),
            warning: standard_style(theme, styles::WARNING_STYLE, status_running),
            info: standard_style(theme, styles::INFO_STYLE, status_diagnostic),
            dimmed: standard_style(theme, styles::DIMMED, text_dim),
            muted: standard_style(theme, styles::MUTED, text_muted),
            inline_code: standard_style(theme, styles::INLINE_CODE, syntax_plain),
        };

        let fast = fast_gradient(theme, Gradient::new(vec![text_accent, text_primary]));

        Self {
            surface_background,
            surface_panel,
            surface_panel_alt,
            surface_overlay,
            surface_selection,
            surface_code,
            surface_success,
            surface_pending,
            surface_error,
            text_primary,
            text_muted,
            text_dim,
            text_accent,
            text_title,
            text_prompt,
            text_status,
            text_help,
            text_metadata,
            text_command,
            text_command_active,
            border_default,
            border_active,
            border_alternate,
            border_command,
            border_command_active,
            border_error,
            status_queued,
            status_running,
            status_writing,
            status_success,
            status_failed,
            status_canceled,
            status_tool,
            status_subagent,
            status_assistant,
            status_diagnostic,
            transcript_user,
            transcript_assistant,
            transcript_thinking,
            transcript_tool,
            transcript_session,
            transcript_diagnostic,
            transcript_heading,
            transcript_section,
            transcript_error_label,
            transcript_error_body,
            diff_inserted,
            diff_removed,
            diff_changed,
            diff_context,
            diff_hunk_header,
            diff_file_header,
            diff_metadata,
            syntax_plain,
            syntax_keyword,
            syntax_string,
            syntax_comment,
            syntax_number,
            syntax_function,
            syntax_type,
            syntax_operator,
            syntax_punctuation,
            syntax_code_fence,
            syntax_language_label,
            syntax_gutter,
            syntax_fallback,
            heading_style: default_heading_style,
            user_heading_style,
            assistant_heading_style,
            thinking_heading_style,
            tool_heading_style,
            gradients: ThemeGradients { fast },
            standard_styles,
        }
    }
}

fn role_color(theme: &Theme, extension: &str, fallbacks: &[&str]) -> OpalineColor {
    token_color(theme, extension)
        .or_else(|| fallbacks.iter().find_map(|token| token_color(theme, token)))
        .unwrap_or(OpalineColor::FALLBACK)
}

fn token_color(theme: &Theme, name: &str) -> Option<OpalineColor> {
    theme
        .token_names()
        .contains(&name)
        .then(|| theme.try_color(name))
        .flatten()
}

fn standard_style(theme: &Theme, standard_style: &str, fallback: OpalineColor) -> OpalineStyle {
    let mut style = theme
        .try_style(standard_style)
        .cloned()
        .unwrap_or_else(|| OpalineStyle::fg(fallback));
    if style.fg.is_none() {
        style.fg = Some(fallback);
    }
    style
}

fn heading_style(theme: &Theme, standard_style: &str, fallback: OpalineColor) -> OpalineStyle {
    let mut style = theme
        .try_style(standard_style)
        .cloned()
        .unwrap_or_else(|| OpalineStyle::fg(fallback));
    if style.fg.is_none() {
        style.fg = Some(fallback);
    }
    style.bold = true;
    style
}

fn fast_gradient(theme: &Theme, fallback: Gradient) -> Gradient {
    theme
        .get_gradient(FAST_GRADIENT_NAME)
        .cloned()
        .unwrap_or(fallback)
}