ddctui 1.0.0

Terminal UI for controlling DDC/CI monitor properties
mod app;
mod cli;
mod config;
mod ddc;
mod ffi;
mod ui;

use anyhow::Result;
use app::{App, Focus, Mode};
use clap::Parser;
use cli::Cli;
use config::Favorites;
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use std::time::Duration;

fn main() -> Result<()> {
    let cli = Cli::parse();

    ddc::init()?;

    let config_dir = match &cli.config_dir {
        Some(dir) => dir.clone(),
        None => config::config_dir()?,
    };
    let favorites = Favorites::load(&config_dir)?;

    let monitors = ddc::detect_monitors()?;
    if monitors.is_empty() {
        println!("No DDC/CI capable monitors detected.");
        return Ok(());
    }

    let mut app = App::new(monitors, favorites)?;

    let mut terminal = ratatui::try_init()?;
    let result = run_event_loop(&mut terminal, &mut app);
    ratatui::try_restore()?;
    result
}

fn run_event_loop(terminal: &mut ratatui::DefaultTerminal, app: &mut App) -> Result<()> {
    while !app.should_quit {
        terminal.draw(|frame| ui::draw(frame, app))?;

        // While there are still feature values to fetch from the hardware, poll for input
        // instead of blocking on it, so the UI stays responsive and streams values in as they
        // arrive rather than freezing until every feature has been read.
        if app.has_pending_value_fetch() {
            if event::poll(Duration::from_millis(0))? {
                dispatch_event(app, event::read()?);
            } else {
                app.step_value_fetch();
            }
        } else {
            dispatch_event(app, event::read()?);
        }
    }
    Ok(())
}

fn dispatch_event(app: &mut App, event: Event) {
    let Event::Key(key) = event else {
        return;
    };
    if key.kind != KeyEventKind::Press {
        return;
    }
    match &app.mode {
        Mode::Normal => handle_normal_key(app, key.code),
        Mode::EditContinuous { .. } | Mode::EditChoice { .. } => handle_edit_key(app, key.code),
    }
}

fn handle_normal_key(app: &mut App, code: KeyCode) {
    app.status = None;
    match code {
        KeyCode::Char('q') | KeyCode::Esc => app.should_quit = true,
        KeyCode::Tab => app.toggle_focus(),
        KeyCode::Left => app.focus = Focus::Monitors,
        KeyCode::Right => app.focus = Focus::Capabilities,
        KeyCode::Char('j') | KeyCode::Down => match app.focus {
            Focus::Monitors => app.move_monitor(1),
            Focus::Capabilities => app.move_capability(1),
        },
        KeyCode::Char('k') | KeyCode::Up => match app.focus {
            Focus::Monitors => app.move_monitor(-1),
            Focus::Capabilities => app.move_capability(-1),
        },
        KeyCode::Char('f') => {
            if app.focus == Focus::Capabilities {
                app.toggle_favorite();
            }
        }
        KeyCode::Char('r') => app.refresh_current_values(),
        KeyCode::Enter => {
            if app.focus == Focus::Capabilities {
                app.start_edit();
            }
        }
        _ => {}
    }
}

fn handle_edit_key(app: &mut App, code: KeyCode) {
    match code {
        KeyCode::Esc => app.cancel_edit(),
        KeyCode::Enter => app.confirm_edit(),
        _ => {
            if let Some(delta) = edit_delta(&app.mode, code) {
                app.adjust_edit(delta);
            }
        }
    }
}

/// Continuous features use a horizontal slider (Left/Down decrease, Right/Up increase, matching
/// a volume-slider feel). Choice popups are a vertical list, so Up/Down must match this app's
/// other lists (Down moves to the next item, Up to the previous).
fn edit_delta(mode: &Mode, code: KeyCode) -> Option<i32> {
    match mode {
        Mode::EditContinuous { .. } => match code {
            KeyCode::Left | KeyCode::Char('h') | KeyCode::Down | KeyCode::Char('j') => Some(-1),
            KeyCode::Right | KeyCode::Char('l') | KeyCode::Up | KeyCode::Char('k') => Some(1),
            _ => None,
        },
        Mode::EditChoice { .. } => match code {
            KeyCode::Up | KeyCode::Char('k') | KeyCode::Left | KeyCode::Char('h') => Some(-1),
            KeyCode::Down | KeyCode::Char('j') | KeyCode::Right | KeyCode::Char('l') => Some(1),
            _ => None,
        },
        Mode::Normal => None,
    }
}