ddctui 1.0.0

Terminal UI for controlling DDC/CI monitor properties
use crate::config::Favorites;
use crate::ddc::{self, DisplayHandle, Monitor, MonitorCapabilities, VcpValue};
use anyhow::Result;

pub struct CapabilityEntry {
    pub feature_code: u8,
    pub name: String,
    pub is_continuous: bool,
    /// Named choices for non-continuous features, restricted to the values this
    /// monitor actually declared support for (falls back to all named values if
    /// the capabilities string didn't enumerate any).
    pub choices: Vec<(u8, String)>,
    pub value: Option<VcpValue>,
    /// Whether an initial value read has been attempted (successful or not). Distinct from
    /// `value.is_some()` so a failed read doesn't get retried forever by the background fetch.
    pub value_fetched: bool,
    pub favorite: bool,
}

pub struct MonitorEntry {
    pub monitor: Monitor,
    pub capabilities: MonitorCapabilities,
    pub features: Vec<CapabilityEntry>,
    pub features_loaded: bool,
    handle: Option<DisplayHandle>,
}

impl MonitorEntry {
    pub fn label(&self) -> String {
        format!(
            "{} {} (#{})",
            self.monitor.mfg_id, self.monitor.model_name, self.monitor.dispno
        )
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Focus {
    Monitors,
    Capabilities,
}

pub enum Mode {
    Normal,
    EditContinuous { value: u16, max: u16 },
    EditChoice { index: usize },
}

pub struct App {
    pub monitors: Vec<MonitorEntry>,
    pub selected_monitor: usize,
    pub selected_capability: usize,
    pub focus: Focus,
    pub favorites: Favorites,
    pub mode: Mode,
    pub status: Option<String>,
    pub should_quit: bool,
}

impl App {
    pub fn new(monitors: Vec<Monitor>, favorites: Favorites) -> Result<Self> {
        let mut entries = Vec::with_capacity(monitors.len());
        for monitor in monitors {
            let dh = DisplayHandle::open(&monitor)?;
            let capabilities = ddc::query_capabilities(&dh)?;
            entries.push(MonitorEntry {
                monitor,
                capabilities,
                features: Vec::new(),
                features_loaded: false,
                handle: None,
            });
        }

        let mut app = Self {
            monitors: entries,
            selected_monitor: 0,
            selected_capability: 0,
            focus: Focus::Monitors,
            favorites,
            mode: Mode::Normal,
            status: None,
            should_quit: false,
        };
        // Load every monitor's feature metadata up front (cheap, static lookups) so
        // `step_value_fetch` can stream in live values for *all* monitors in the background
        // from the start, rather than only once each monitor is selected.
        for idx in 0..app.monitors.len() {
            if let Err(e) = app.ensure_features_loaded(idx) {
                app.status = Some(format!(
                    "failed to load capabilities for {}: {e}",
                    app.monitors[idx].label()
                ));
            }
        }
        Ok(app)
    }

    fn ensure_features_loaded(&mut self, idx: usize) -> Result<()> {
        if self.monitors[idx].features_loaded {
            return Ok(());
        }
        let entry = &mut self.monitors[idx];
        let monitor_key = entry.monitor.cache_key();
        let handle = match &entry.handle {
            Some(_) => entry.handle.as_ref().unwrap(),
            None => {
                entry.handle = Some(DisplayHandle::open(&entry.monitor)?);
                entry.handle.as_ref().unwrap()
            }
        };

        // Feature metadata (name, type, named choices) is a cheap, static lookup, so it's
        // fetched eagerly here. Current *values* require a live DDC round trip per feature
        // (tens of ms each) and are deliberately left unset: they're streamed in afterwards
        // by `step_value_fetch`, so the UI can appear immediately instead of blocking on
        // every feature read up front.
        let mut features = Vec::with_capacity(entry.capabilities.features.len());
        for def in &entry.capabilities.features {
            let meta = match ddc::feature_metadata(handle, def.feature_code) {
                Ok(m) => m,
                Err(_) => continue,
            };
            let choices = if def.values.is_empty() {
                meta.sl_values.clone()
            } else {
                meta.sl_values
                    .iter()
                    .filter(|(code, _)| def.values.contains(code))
                    .cloned()
                    .collect()
            };
            features.push(CapabilityEntry {
                feature_code: def.feature_code,
                name: meta.name,
                is_continuous: meta.is_continuous,
                choices,
                value: None,
                value_fetched: false,
                favorite: self.favorites.is_favorite(&monitor_key, def.feature_code),
            });
        }
        let entry = &mut self.monitors[idx];
        entry.features = features;
        entry.features_loaded = true;
        self.sort_features(idx);
        Ok(())
    }

    /// Whether any loaded monitor still has a feature awaiting its initial value read.
    pub fn has_pending_value_fetch(&self) -> bool {
        self.monitors
            .iter()
            .any(|m| m.features.iter().any(|f| !f.value_fetched))
    }

    /// Fetches the current value for a single not-yet-fetched feature, prioritizing the
    /// selected monitor so what's on screen populates first. Does nothing if there's no
    /// pending work. Intended to be called once per idle tick of the event loop.
    pub fn step_value_fetch(&mut self) {
        let selected = self.selected_monitor;
        let order =
            std::iter::once(selected).chain((0..self.monitors.len()).filter(|&i| i != selected));
        for idx in order {
            let entry = &self.monitors[idx];
            if !entry.features_loaded {
                continue;
            }
            let Some(pos) = entry.features.iter().position(|f| !f.value_fetched) else {
                continue;
            };
            let Some(handle) = entry.handle.as_ref() else {
                continue;
            };
            let code = entry.features[pos].feature_code;
            let value = ddc::get_vcp_value(handle, code).ok();
            let feature = &mut self.monitors[idx].features[pos];
            feature.value = value;
            feature.value_fetched = true;
            return;
        }
    }

    fn sort_features(&mut self, idx: usize) {
        self.monitors[idx]
            .features
            .sort_by_key(|f| (!f.favorite, f.feature_code));
    }

    pub fn selected_monitor_entry(&self) -> &MonitorEntry {
        &self.monitors[self.selected_monitor]
    }

    pub fn selected_capability_entry(&self) -> Option<&CapabilityEntry> {
        self.monitors[self.selected_monitor]
            .features
            .get(self.selected_capability)
    }

    pub fn move_monitor(&mut self, delta: i32) {
        if self.monitors.is_empty() {
            return;
        }
        let len = self.monitors.len() as i32;
        let new_idx = (self.selected_monitor as i32 + delta).rem_euclid(len) as usize;
        self.selected_monitor = new_idx;
        self.selected_capability = 0;
        if let Err(e) = self.ensure_features_loaded(new_idx) {
            self.status = Some(format!("failed to load capabilities: {e}"));
        }
    }

    pub fn move_capability(&mut self, delta: i32) {
        let len = self.monitors[self.selected_monitor].features.len();
        if len == 0 {
            return;
        }
        let new_idx = (self.selected_capability as i32 + delta).rem_euclid(len as i32) as usize;
        self.selected_capability = new_idx;
    }

    pub fn toggle_focus(&mut self) {
        self.focus = match self.focus {
            Focus::Monitors => Focus::Capabilities,
            Focus::Capabilities => Focus::Monitors,
        };
    }

    pub fn toggle_favorite(&mut self) {
        let idx = self.selected_monitor;
        let Some(entry) = self.monitors[idx].features.get(self.selected_capability) else {
            return;
        };
        let code = entry.feature_code;
        let monitor_key = self.monitors[idx].monitor.cache_key();
        self.favorites.toggle(&monitor_key, code);
        if let Err(e) = self.favorites.save() {
            self.status = Some(format!("failed to save favorites: {e}"));
        }
        let selected_code = code;
        for f in &mut self.monitors[idx].features {
            if f.feature_code == code {
                f.favorite = self.favorites.is_favorite(&monitor_key, code);
            }
        }
        self.sort_features(idx);
        if let Some(pos) = self.monitors[idx]
            .features
            .iter()
            .position(|f| f.feature_code == selected_code)
        {
            self.selected_capability = pos;
        }
    }

    pub fn start_edit(&mut self) {
        let Some(entry) = self.selected_capability_entry() else {
            return;
        };
        if entry.is_continuous {
            let Some(v) = entry.value else { return };
            self.mode = Mode::EditContinuous {
                value: v.current,
                max: v.max,
            };
        } else if !entry.choices.is_empty() {
            // Non-continuous features only use the low byte (SL) of the reported value;
            // the high byte is meaningless here, so compare against it rather than the
            // full 16-bit VcpValue::current.
            let current = entry.value.map(|v| v.current as u8);
            let index = current
                .and_then(|cur| entry.choices.iter().position(|(code, _)| *code == cur))
                .unwrap_or(0);
            self.mode = Mode::EditChoice { index };
        }
    }

    pub fn cancel_edit(&mut self) {
        self.mode = Mode::Normal;
    }

    pub fn adjust_edit(&mut self, delta: i32) {
        let choice_len = self
            .selected_capability_entry()
            .map(|e| e.choices.len())
            .unwrap_or(0);
        match &mut self.mode {
            Mode::EditContinuous { value, max } => {
                let new_value = (*value as i32 + delta).clamp(0, *max as i32);
                *value = new_value as u16;
            }
            Mode::EditChoice { index } => {
                if choice_len > 0 {
                    *index = (*index as i32 + delta).clamp(0, choice_len as i32 - 1) as usize;
                }
            }
            Mode::Normal => {}
        }
    }

    /// Re-reads the current value of every loaded feature on the selected monitor from
    /// the monitor itself, e.g. to pick up changes made via the monitor's own OSD.
    pub fn refresh_current_values(&mut self) {
        let idx = self.selected_monitor;
        let entry = &mut self.monitors[idx];
        let Some(handle) = entry.handle.as_ref() else {
            return;
        };
        let mut failures = 0;
        for feature in &mut entry.features {
            feature.value_fetched = true;
            match ddc::get_vcp_value(handle, feature.feature_code) {
                Ok(v) => feature.value = Some(v),
                Err(_) => failures += 1,
            }
        }
        self.status = Some(if failures == 0 {
            "Refreshed values".to_string()
        } else {
            format!("Refreshed values ({failures} feature(s) failed to read)")
        });
    }

    pub fn confirm_edit(&mut self) {
        let idx = self.selected_monitor;
        let cap_idx = self.selected_capability;
        let new_value = match &self.mode {
            Mode::EditContinuous { value, .. } => Some(*value),
            Mode::EditChoice { index } => self.monitors[idx]
                .features
                .get(cap_idx)
                .and_then(|f| f.choices.get(*index))
                .map(|(code, _)| *code as u16),
            Mode::Normal => None,
        };
        self.mode = Mode::Normal;
        let Some(new_value) = new_value else { return };

        let entry = &mut self.monitors[idx];
        let Some(handle) = entry.handle.as_ref() else {
            return;
        };
        let Some(feature) = entry.features.get_mut(cap_idx) else {
            return;
        };
        match ddc::set_vcp_value(handle, feature.feature_code, new_value) {
            Ok(()) => {
                if let Some(v) = feature.value.as_mut() {
                    v.current = new_value;
                }
            }
            Err(e) => {
                self.status = Some(format!("failed to set value: {e}"));
            }
        }
    }
}