ddctui 1.0.0

Terminal UI for controlling DDC/CI monitor properties
use crate::app::{App, Focus, Mode};
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph};

pub fn draw(frame: &mut Frame, app: &App) {
    let area = frame.area();
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Min(3), Constraint::Length(1)])
        .split(area);
    let panes = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Percentage(35), Constraint::Percentage(65)])
        .split(chunks[0]);

    render_monitors(frame, panes[0], app);
    render_capabilities(frame, panes[1], app);
    render_help(frame, chunks[1], app);

    match &app.mode {
        Mode::EditContinuous { value, max } => render_continuous_popup(frame, area, *value, *max),
        Mode::EditChoice { index } => render_choice_popup(frame, area, app, *index),
        Mode::Normal => {}
    }
}

fn render_monitors(frame: &mut Frame, area: Rect, app: &App) {
    let items: Vec<ListItem> = app
        .monitors
        .iter()
        .map(|m| ListItem::new(m.label()))
        .collect();
    let mut state = ListState::default().with_selected(Some(app.selected_monitor));
    let focused = app.focus == Focus::Monitors;
    let block = Block::default()
        .title(" Monitors ")
        .borders(Borders::ALL)
        .border_style(focus_style(focused));
    let list = List::new(items)
        .block(block)
        .highlight_style(Style::default().add_modifier(Modifier::REVERSED));
    frame.render_stateful_widget(list, area, &mut state);
}

fn render_capabilities(frame: &mut Frame, area: Rect, app: &App) {
    let monitor = app.selected_monitor_entry();
    let items: Vec<ListItem> = monitor
        .features
        .iter()
        .map(|f| {
            let star = if f.favorite { "* " } else { "  " };
            let value = match f.value {
                Some(v) => {
                    if let Some((_, name)) = f.choices.iter().find(|(c, _)| *c == v.current as u8) {
                        name.clone()
                    } else {
                        format!("{}", v.current)
                    }
                }
                None if !f.value_fetched => "...".to_string(),
                None => "?".to_string(),
            };
            ListItem::new(format!(
                "{star}0x{:02X}  {:<28} {}",
                f.feature_code, f.name, value
            ))
        })
        .collect();
    let mut state = ListState::default().with_selected(Some(app.selected_capability));
    let focused = app.focus == Focus::Capabilities;
    let block = Block::default()
        .title(format!(" Capabilities: {} ", monitor.label()))
        .borders(Borders::ALL)
        .border_style(focus_style(focused));
    let list = List::new(items)
        .block(block)
        .highlight_style(Style::default().add_modifier(Modifier::REVERSED));
    frame.render_stateful_widget(list, area, &mut state);
}

fn render_help(frame: &mut Frame, area: Rect, app: &App) {
    let text = if let Some(status) = &app.status {
        status.clone()
    } else {
        "q: quit  Tab/\u{2190}\u{2192}: switch pane  j/k: move  f: favorite  Enter: edit  r: refresh"
            .to_string()
    };
    frame.render_widget(Paragraph::new(text), area);
}

fn focus_style(focused: bool) -> Style {
    if focused {
        Style::default().fg(Color::Cyan)
    } else {
        Style::default()
    }
}

fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
    let width = width.min(area.width);
    let height = height.min(area.height);
    let x = area.x + (area.width.saturating_sub(width)) / 2;
    let y = area.y + (area.height.saturating_sub(height)) / 2;
    Rect::new(x, y, width, height)
}

fn render_continuous_popup(frame: &mut Frame, area: Rect, value: u16, max: u16) {
    let popup = centered_rect(40, 5, area);
    frame.render_widget(Clear, popup);
    let ratio = if max == 0 {
        0.0
    } else {
        value as f64 / max as f64
    };
    let filled = (ratio * 30.0).round() as usize;
    let bar = format!(
        "[{}{}] {value}/{max}",
        "#".repeat(filled),
        "-".repeat(30 - filled)
    );
    let text = format!("{bar}\n\n\u{2190}/\u{2192}: adjust  Enter: confirm  Esc: cancel");
    let block = Block::default().title(" Edit value ").borders(Borders::ALL);
    frame.render_widget(Paragraph::new(text).block(block), popup);
}

fn render_choice_popup(frame: &mut Frame, area: Rect, app: &App, index: usize) {
    let popup = centered_rect(40, 10, area);
    frame.render_widget(Clear, popup);
    let choices = app
        .selected_capability_entry()
        .map(|e| e.choices.clone())
        .unwrap_or_default();
    let items: Vec<ListItem> = choices
        .iter()
        .map(|(code, name)| ListItem::new(format!("0x{code:02X}  {name}")))
        .collect();
    let mut state = ListState::default().with_selected(Some(index));
    let block = Block::default()
        .title(" Select value (Enter: confirm, Esc: cancel) ")
        .borders(Borders::ALL);
    let list = List::new(items)
        .block(block)
        .highlight_style(Style::default().add_modifier(Modifier::REVERSED));
    frame.render_stateful_widget(list, popup, &mut state);
}