acorn-tui 0.3.4

ACORN terminal user interface (TUI)
use crate::{
    app::App,
    event::{to_internal_event, Event},
    theme::Theme,
    widgets::{
        detail::render_detail,
        list::{list_item, render_list},
    },
    Screen,
};
use acorn_host::terminal::Label;
use crossterm::event;
use ratatui::{
    layout::{Alignment, Constraint, Direction, Layout},
    style::Style,
    text::{Line, Span},
    widgets::{Block, Borders, Paragraph},
    Frame,
};

pub fn render(frame: &mut Frame, app: &App) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Min(1), Constraint::Length(3)])
        .split(frame.area());
    let panels = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Percentage(45), Constraint::Percentage(55)])
        .split(chunks[0]);
    let items = app
        .theme_picker
        .data
        .names
        .iter()
        .enumerate()
        .map(|(index, name)| {
            list_item(
                name,
                index == app.theme_picker.selected,
                app.theme.text,
                app.theme.selection_fg,
                app.theme.selection_bg,
            )
        })
        .collect::<Vec<_>>();
    render_list(frame, panels[0], &items, "Themes", app.theme.border, 0);
    let preview = app
        .theme_picker
        .data
        .names
        .get(app.theme_picker.selected)
        .and_then(|name| Theme::named(name))
        .unwrap_or_else(|| app.theme.clone());
    let details = vec![
        Line::from(Span::styled(" Theme preview", Style::default().fg(preview.text))),
        Line::from(Span::styled(
            format!(" {}PASS  Operation succeeded", Label::CHECKMARK),
            Style::default().fg(preview.success),
        )),
        Line::from(Span::styled(" ! CAUTION  Review recommended", Style::default().fg(preview.warning))),
        Line::from(Span::styled(" ✗ FAIL  Operation failed", Style::default().fg(preview.error))),
        Line::from(Span::styled(
            " > SELECTED  Current choice",
            Style::default().fg(preview.selection_fg).bg(preview.selection_bg),
        )),
    ];
    render_detail(frame, panels[1], "Preview", details, preview.accent, preview.border);
    let controls = Line::from(vec![
        Span::styled(" [Up/Down or j/k] Navigate  ", Style::default().fg(app.theme.text_muted)),
        Span::styled("[Enter] Apply  ", Style::default().fg(app.theme.text_muted)),
        Span::styled("[Esc] Cancel", Style::default().fg(app.theme.text_muted)),
    ]);
    frame.render_widget(
        Paragraph::new(controls)
            .block(Block::default().borders(Borders::TOP).border_style(Style::default().fg(app.theme.border)))
            .alignment(Alignment::Center),
        chunks[1],
    );
}

pub fn handle_event(app: &mut App, event: event::Event) {
    let event = match event {
        | event::Event::Key(key) => to_internal_event(key),
        | _ => None,
    };
    match event {
        | Some(Event::Up) => app.theme_picker.selected = app.theme_picker.selected.saturating_sub(1),
        | Some(Event::Down) => {
            app.theme_picker.selected = app
                .theme_picker
                .selected
                .saturating_add(1)
                .min(app.theme_picker.data.names.len().saturating_sub(1));
        }
        | Some(Event::Enter) => {
            if let Some(name) = app.theme_picker.data.names.get(app.theme_picker.selected).copied() {
                app.set_theme(name);
            }
            app.navigate_to(Screen::Dashboard);
        }
        | Some(Event::Back | Event::Quit) => app.navigate_to(Screen::Dashboard),
        | _ => {}
    }
}

#[cfg(test)]
mod tests;