collet 0.1.0

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
//! `Info` and `Select` popup arm renderers extracted from `mod.rs::render`.

use std::cell::RefCell;

use ratatui::prelude::*;
use ratatui::widgets::{Block, Paragraph, Wrap};

use super::{render_info_line, render_search_bar, truncate_str};
use crate::tui::state::PopupState;
use crate::tui::theme::Theme;
use crate::tui::widgets::diff_view::try_render_with_delta;

// Cache delta output to avoid re-spawning the subprocess on every render frame.
// Key: (content hash, terminal width, dark mode) — invalidates when popup content changes.
thread_local! {
    static DELTA_CACHE: RefCell<Option<(u64, u16, bool, Text<'static>)>> = const { RefCell::new(None) };
}

/// Render the `PopupKind::Info` body (text/diff content with scroll).
pub(super) fn render_info(
    popup: &PopupState,
    popup_area: Rect,
    block: Block<'_>,
    theme: &Theme,
    buf: &mut Buffer,
) {
    let inner = block.inner(popup_area);
    block.render(popup_area, buf);

    let looks_like_diff = popup.content.contains("\n+++ ")
        || popup.content.contains("\n--- ")
        || popup.content.starts_with("diff ")
        || popup.content.starts_with("--- ");

    let content_hash = if looks_like_diff {
        use std::hash::{Hash, Hasher};
        let mut hasher = std::collections::hash_map::DefaultHasher::new();
        popup.content.hash(&mut hasher);
        hasher.finish()
    } else {
        0
    };

    let cached_delta = if looks_like_diff {
        DELTA_CACHE.with(|cell| {
            let c = cell.borrow();
            if let Some((hash, w, dark, ref text)) = *c
                && hash == content_hash
                && w == inner.width
                && dark == theme.is_dark
            {
                return Some(text.clone());
            }
            None
        })
    } else {
        None
    };

    let paragraph = if looks_like_diff {
        let delta_text = cached_delta.or_else(|| {
            let result = try_render_with_delta(&popup.content, theme.is_dark, inner.width);
            if let Some(ref text) = result {
                DELTA_CACHE.with(|cell| {
                    *cell.borrow_mut() =
                        Some((content_hash, inner.width, theme.is_dark, text.clone()));
                });
            }
            result
        });
        if let Some(text) = delta_text {
            Paragraph::new(text)
                .wrap(Wrap { trim: false })
                .scroll((popup.scroll, 0))
        } else {
            let lines: Vec<Line> = popup
                .content
                .lines()
                .map(|line| render_info_line(line, theme))
                .collect();
            Paragraph::new(lines)
                .wrap(Wrap { trim: false })
                .scroll((popup.scroll, 0))
        }
    } else {
        let lines: Vec<Line> = popup
            .content
            .lines()
            .map(|line| render_info_line(line, theme))
            .collect();
        Paragraph::new(lines)
            .wrap(Wrap { trim: false })
            .scroll((popup.scroll, 0))
    };

    paragraph.render(inner, buf);
}

/// Render the `PopupKind::Select` body (filterable list of strings).
pub(super) fn render_select(
    popup: &PopupState,
    items: &[String],
    selected: usize,
    popup_area: Rect,
    block: Block<'_>,
    theme: &Theme,
    buf: &mut Buffer,
) {
    let q = popup.search.to_lowercase();
    let filtered: Vec<&String> = items
        .iter()
        .filter(|s| s.to_lowercase().contains(&q))
        .collect();

    let inner = block.inner(popup_area);
    block.render(popup_area, buf);

    render_search_bar(
        inner,
        buf,
        &popup.search,
        filtered.len(),
        items.len(),
        theme,
    );

    let list_area = Rect::new(
        inner.x,
        inner.y + 1,
        inner.width,
        inner.height.saturating_sub(1),
    );
    let skip = popup.scroll as usize;
    let mut row_y = list_area.y;

    for (i, item) in filtered.iter().enumerate() {
        if i < skip {
            continue;
        }
        if row_y >= list_area.y + list_area.height {
            break;
        }

        let is_sel = i == selected;
        let row_area = Rect::new(list_area.x, row_y, list_area.width, 1);

        if is_sel {
            Paragraph::new(Span::styled(
                " ".repeat(list_area.width as usize),
                Style::default().bg(theme.accent),
            ))
            .render(row_area, buf);
            Paragraph::new(Span::styled(
                format!(
                    "  {}",
                    truncate_str(item, list_area.width.saturating_sub(2) as usize)
                ),
                Style::default()
                    .fg(theme.bg)
                    .bg(theme.accent)
                    .add_modifier(Modifier::BOLD),
            ))
            .render(row_area, buf);
        } else {
            Paragraph::new(Span::styled(
                format!(
                    "  {}",
                    truncate_str(item, list_area.width.saturating_sub(2) as usize)
                ),
                Style::default().fg(theme.text_dim),
            ))
            .render(row_area, buf);
        }
        row_y += 1;
    }

    if filtered.is_empty() {
        let no_match_area = Rect::new(list_area.x, list_area.y, list_area.width, 1);
        Paragraph::new(Span::styled(
            "  no matches",
            Style::default().fg(theme.text_muted),
        ))
        .render(no_match_area, buf);
    }
}