lsv 0.1.15

Three‑pane terminal file viewer (TUI) with preview and Lua configuration
Documentation
use ratatui::{
    layout::{
        Constraint,
        Direction,
        Layout,
        Rect,
    },
    style::{
        Color,
        Modifier,
        Style,
    },
    text::Span,
    widgets::{
        Block,
        Borders,
        Clear,
        List,
        ListItem,
        ListState,
        Paragraph,
    },
};
use unicode_width::UnicodeWidthStr;

pub fn draw_theme_picker_panel(
    f: &mut ratatui::Frame,
    area: Rect,
    app: &crate::App,
)
{
    let state = match app.overlay
    {
        crate::app::Overlay::ThemePicker(ref s) => s.as_ref(),
        _ => return,
    };
    if state.entries.is_empty()
    {
        return;
    }

    let max_name_width = state
        .entries
        .iter()
        .map(|e| UnicodeWidthStr::width(e.name.as_str()))
        .max()
        .unwrap_or(0);
    let (popup_width, popup_height) = if let Some(m) =
        app.config.ui.modals.as_ref()
    {
        let w = (area.width.saturating_mul(m.theme.width_pct.clamp(10, 100))
            / 100)
            .max(20);
        let h = (area.height.saturating_mul(m.theme.height_pct.clamp(10, 100))
            / 100)
            .max(5);
        (w, h)
    }
    else
    {
        let base_width = (max_name_width as u16).saturating_add(6);
        let desired_width = base_width.max(30);
        let w = desired_width
            .min(area.width.saturating_sub(4).max(20))
            .min(area.width)
            .max(10);
        let entries_len = state.entries.len() as u16;
        let desired_height = entries_len.saturating_add(4);
        let h = desired_height
            .min(area.height.saturating_sub(4).max(6))
            .min(area.height)
            .max(5);
        (w, h)
    };

    let popup = Rect::new(
        area.x + area.width.saturating_sub(popup_width) / 2,
        area.y + area.height.saturating_sub(popup_height) / 2,
        popup_width,
        popup_height,
    );

    f.render_widget(Clear, popup);

    let mut pane_bg = None;
    let mut border_fg = None;
    let mut title_fg = Color::Yellow;
    let mut title_bg = None;
    if let Some(th) = app.config.ui.theme.as_ref()
    {
        pane_bg =
            th.pane_bg.as_ref().and_then(|s| crate::ui::colors::parse_color(s));
        border_fg = th
            .border_fg
            .as_ref()
            .and_then(|s| crate::ui::colors::parse_color(s));
        if let Some(tf) =
            th.title_fg.as_ref().and_then(|s| crate::ui::colors::parse_color(s))
        {
            title_fg = tf;
        }
        title_bg = th
            .title_bg
            .as_ref()
            .and_then(|s| crate::ui::colors::parse_color(s));
    }

    let mut block = Block::default().borders(Borders::ALL);
    if let Some(bg) = pane_bg
    {
        block = block.style(Style::default().bg(bg));
    }
    if let Some(bfg) = border_fg
    {
        block = block.border_style(Style::default().fg(bfg));
    }
    let mut title_style =
        Style::default().fg(title_fg).add_modifier(Modifier::BOLD);
    if let Some(tb) = title_bg
    {
        title_style = title_style.bg(tb);
    }
    block = block.title(Span::styled("Select UI Theme", title_style));

    let inner = block.inner(popup);
    f.render_widget(block, popup);
    if inner.width == 0 || inner.height == 0
    {
        return;
    }

    let base_style = app
        .config
        .ui
        .theme
        .as_ref()
        .and_then(|th| th.item_fg.as_ref())
        .and_then(|s| crate::ui::colors::parse_color(s))
        .map(|fg| Style::default().fg(fg))
        .unwrap_or_else(|| Style::default().fg(Color::Gray));

    let mut highlight = Style::default().add_modifier(Modifier::BOLD);
    if let Some(th) = app.config.ui.theme.as_ref()
    {
        if let Some(fg) = th
            .selected_item_fg
            .as_ref()
            .and_then(|s| crate::ui::colors::parse_color(s))
        {
            highlight = highlight.fg(fg);
        }
        if let Some(bg) = th
            .selected_item_bg
            .as_ref()
            .and_then(|s| crate::ui::colors::parse_color(s))
        {
            highlight = highlight.bg(bg);
        }
        else if let Some(bg) =
            th.pane_bg.as_ref().and_then(|s| crate::ui::colors::parse_color(s))
        {
            highlight = highlight.bg(bg);
        }
    }

    let items: Vec<ListItem> = state
        .entries
        .iter()
        .map(|entry| {
            ListItem::new(ratatui::text::Line::from(entry.name.clone()))
        })
        .collect();

    let constraints: Vec<Constraint> = if inner.height > 3
    {
        vec![Constraint::Min(1), Constraint::Length(1)]
    }
    else
    {
        vec![Constraint::Min(1)]
    };
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints(constraints)
        .split(inner);
    let list_area = chunks[0];

    let mut list_state = ListState::default();
    list_state.select(Some(state.selected));
    let list = List::new(items).style(base_style).highlight_style(highlight);
    f.render_stateful_widget(list, list_area, &mut list_state);

    if chunks.len() > 1
    {
        let info_area = chunks[1];
        let mut info_style = Style::default().fg(Color::DarkGray);
        if let Some(th) = app.config.ui.theme.as_ref()
            && let Some(fg) = th
                .info_fg
                .as_ref()
                .and_then(|s| crate::ui::colors::parse_color(s))
        {
            info_style = info_style.fg(fg);
        }
        let hint = Paragraph::new("↑/↓ preview  Enter apply  Esc cancel")
            .style(info_style)
            .alignment(ratatui::layout::Alignment::Center);
        f.render_widget(hint, info_area);
    }
}