gitlab-tracker 0.2.5

A fast terminal TUI dashboard for tracking GitLab Merge Requests across branches
use crate::config::AppConfig;
use crate::models::{GitlabMrState, MergeabilityStatus, TrackedMr};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span, Text};

pub fn create_chip_span(label: &str, config: &AppConfig) -> Span<'static> {
    let (bg, fg) = config.get_label_style(label);
    Span::styled(
        format!(" {} ", label),
        Style::default().bg(bg).fg(fg).add_modifier(Modifier::BOLD),
    )
}

pub fn render_safe_inspector_text(mr: &TrackedMr, config: &AppConfig) -> Text<'static> {
    // Format ISO 8601 timestamp to a more readable form (date + time, drop sub-seconds).
    let updated_at_display = mr
        .updated_at
        .as_deref()
        .map(|s| s.get(..19).unwrap_or(s).replace('T', " "))
        .unwrap_or_else(|| "Unknown".to_string());

    // Compute the activity badge (icon + color) based on configurable thresholds.
    let (badge_icon, badge_color) = config.activity_badge(mr.updated_at.as_deref());

    // Build the state badge using the same colour coding as the table column.
    // All labels are padded to 9 chars to match the table badge width.
    let (state_label, state_fg, state_bg) = match mr.state {
        GitlabMrState::Opened => ("  Open   ", Color::Black, Color::Green),
        GitlabMrState::Merged => (" Merged  ", Color::Black, Color::Magenta),
        GitlabMrState::Closed => (" Closed  ", Color::Black, Color::Red),
    };

    // For open MRs, display the mergeability status directly — no animation
    // needed here since the State field above already shows the MR lifecycle
    // state.
    let mergeability_line: Option<Line<'static>> = if mr.state == GitlabMrState::Opened {
        // Each label is wrapped with a single space on each side via format!(" {text} ").
        let (merge_text, merge_fg, merge_bg) = match mr.mergeability {
            MergeabilityStatus::Mergeable => ("Mergeable", Color::Black, Color::LightGreen),
            MergeabilityStatus::Conflict => ("Conflict", Color::White, Color::Red),
            MergeabilityStatus::NeedsRebase => ("Rebase", Color::Black, Color::Yellow),
            MergeabilityStatus::Unknown => ("Unknown", Color::DarkGray, Color::Black),
        };
        Some(Line::from(vec![
            Span::raw("Merge    : "),
            Span::styled(
                format!(" {merge_text} "),
                Style::default()
                    .fg(merge_fg)
                    .bg(merge_bg)
                    .add_modifier(Modifier::BOLD),
            ),
        ]))
    } else {
        None
    };

    let mut lines = vec![
        Line::from(vec![Span::styled(
            format!("MR ID    : !{}", mr.id),
            Style::default()
                .fg(ratatui::style::Color::Cyan)
                .add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
        )]),
        Line::from(vec![
            Span::raw("State    : "),
            Span::styled(
                state_label,
                Style::default()
                    .fg(state_fg)
                    .bg(state_bg)
                    .add_modifier(Modifier::BOLD),
            ),
        ]),
    ];

    if let Some(line) = mergeability_line {
        lines.push(line);
    }
    lines.push(Line::from(vec![
        Span::raw("Author   : "),
        Span::styled(
            format!("@{}", mr.author),
            Style::default().add_modifier(Modifier::BOLD),
        ),
    ]));
    lines.push(Line::from(vec![
        Span::raw("Assignee : "),
        Span::styled(
            format!("@{}", mr.assignee),
            Style::default().add_modifier(Modifier::BOLD),
        ),
    ]));
    lines.push(Line::from(vec![
        Span::raw("Milestone: "),
        Span::styled(
            mr.milestone.clone(),
            Style::default().fg(ratatui::style::Color::Cyan),
        ),
    ]));
    lines.push(Line::from(vec![
        Span::raw("Updated  : "),
        Span::styled(
            updated_at_display,
            Style::default().fg(ratatui::style::Color::Yellow),
        ),
        Span::raw("  "),
        Span::styled(
            badge_icon,
            Style::default()
                .fg(badge_color)
                .add_modifier(Modifier::BOLD),
        ),
    ]));

    if !mr.labels.is_empty() {
        let mut label_spans = vec![Span::raw("Labels   : ")];
        for label in &mr.labels {
            label_spans.push(create_chip_span(label, config));
            label_spans.push(Span::raw(" "));
        }
        lines.push(Line::from(label_spans));
    } else {
        lines.push(Line::from(vec![
            Span::raw("Labels   : "),
            Span::styled("None", Style::default().dark_gray()),
        ]));
    }

    lines.push(Line::from(vec![Span::styled(
        "───────────────────────────────────",
        Style::default().fg(ratatui::style::Color::DarkGray),
    )]));
    lines.push(Line::from(vec![Span::styled(
        "Description:",
        Style::default()
            .fg(ratatui::style::Color::Yellow)
            .add_modifier(Modifier::BOLD),
    )]));
    lines.push(Line::from(vec![Span::raw("")]));

    if mr.description.trim().is_empty() {
        lines.push(Line::from(vec![Span::styled(
            "No description text provided.",
            Style::default().dark_gray(),
        )]));
        return Text::from(lines);
    }

    for raw_line in mr.description.lines() {
        let trimmed = raw_line.trim();

        if trimmed.starts_with("# ") || trimmed.starts_with("## ") {
            let text = trimmed.trim_start_matches('#').trim();
            lines.push(Line::from(vec![Span::styled(
                text.to_string(),
                Style::default()
                    .fg(ratatui::style::Color::Cyan)
                    .add_modifier(Modifier::BOLD),
            )]));
        } else if trimmed.starts_with("### ") {
            let text = trimmed.trim_start_matches("### ");
            lines.push(Line::from(vec![Span::styled(
                text.to_string(),
                Style::default()
                    .fg(ratatui::style::Color::Yellow)
                    .add_modifier(Modifier::BOLD),
            )]));
        } else if trimmed == "---" {
            lines.push(Line::from(vec![Span::styled(
                "───────────────────────────────────",
                Style::default().fg(ratatui::style::Color::DarkGray),
            )]));
        } else {
            let mut spans = Vec::new();
            let mut line_content = raw_line.to_string();
            if trimmed.starts_with("- ") {
                spans.push(Span::styled(
                    "",
                    Style::default().fg(ratatui::style::Color::Yellow),
                ));
                line_content = raw_line.replacen("- ", "", 1);
            } else if trimmed.starts_with("* ") {
                spans.push(Span::styled(
                    "",
                    Style::default().fg(ratatui::style::Color::Yellow),
                ));
                line_content = raw_line.replacen("* ", "", 1);
            }

            let bold_parts = line_content.split("**");
            let mut is_bold = false;

            for bold_part in bold_parts {
                let code_parts = bold_part.split('`');
                let mut is_code = false;

                for code_part in code_parts {
                    let mut style = Style::default();
                    if is_bold {
                        style = style.add_modifier(Modifier::BOLD);
                    }
                    if is_code {
                        style = style.fg(ratatui::style::Color::Magenta);
                    }

                    spans.push(Span::styled(code_part.to_string(), style));
                    is_code = !is_code;
                }
                is_bold = !is_bold;
            }
            lines.push(Line::from(spans));
        }
    }

    Text::from(lines)
}