ddctui 1.0.0

Terminal UI for controlling DDC/CI monitor properties
use crate::ffi;
use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize};
use std::ffi::CStr;
use std::os::raw::{c_char, c_void};
use std::sync::OnceLock;

unsafe extern "C" {
    fn free(ptr: *mut c_void);
}

static INIT: OnceLock<Result<(), String>> = OnceLock::new();

fn status_message(context: &str, rc: ffi::DDCA_Status) -> String {
    unsafe {
        let name = ffi::ddca_rc_name(rc);
        let desc = ffi::ddca_rc_desc(rc);
        let name = if name.is_null() {
            "?".to_string()
        } else {
            CStr::from_ptr(name).to_string_lossy().into_owned()
        };
        let desc = if desc.is_null() {
            "?".to_string()
        } else {
            CStr::from_ptr(desc).to_string_lossy().into_owned()
        };
        format!("{context}: {name} - {desc} (rc={rc})")
    }
}

fn check(context: &str, rc: ffi::DDCA_Status) -> Result<()> {
    if rc == 0 {
        Ok(())
    } else {
        Err(anyhow!(status_message(context, rc)))
    }
}

fn cchar_array_to_string(arr: &[c_char]) -> String {
    let bytes: Vec<u8> = arr
        .iter()
        .take_while(|&&c| c != 0)
        .map(|&c| c as u8)
        .collect();
    String::from_utf8_lossy(&bytes).into_owned()
}

/// Initializes libddcutil. Safe to call more than once; only the first call
/// actually performs initialization.
pub fn init() -> Result<()> {
    let result = INIT.get_or_init(|| {
        let libopts = std::env::var("DDCTUI_LIBOPTS").unwrap_or_default();
        let libopts_c = std::ffi::CString::new(libopts).unwrap();
        let rc = unsafe {
            ffi::ddca_init2(
                libopts_c.as_ptr(),
                ffi::DDCA_Syslog_Level_DDCA_SYSLOG_NEVER,
                ffi::DDCA_Init_Options_DDCA_INIT_OPTIONS_NONE,
                std::ptr::null_mut(),
            )
        };
        if rc == 0 {
            Ok(())
        } else {
            Err(status_message("ddca_init2", rc))
        }
    });
    result.clone().map_err(|e| anyhow!(e))
}

#[derive(Debug, Clone)]
pub struct Monitor {
    pub dispno: i32,
    pub mfg_id: String,
    pub model_name: String,
    pub sn: String,
    dref: ffi::DDCA_Display_Ref,
}

impl Monitor {
    /// Stable identifier across runs (ddcutil's dispno can change between
    /// invocations), used as the capabilities cache key.
    pub fn cache_key(&self) -> String {
        format!("{}:{}:{}", self.mfg_id, self.model_name, self.sn)
    }
}

pub fn detect_monitors() -> Result<Vec<Monitor>> {
    let mut list_ptr: *mut ffi::DDCA_Display_Info_List = std::ptr::null_mut();
    let rc = unsafe { ffi::ddca_get_display_info_list2(false, &mut list_ptr) };
    check("ddca_get_display_info_list2", rc)?;

    let mut monitors = Vec::new();
    if !list_ptr.is_null() {
        unsafe {
            let ct = (*list_ptr).ct as usize;
            let infos = (*list_ptr).info.as_slice(ct);
            for info in infos {
                monitors.push(Monitor {
                    dispno: info.dispno,
                    mfg_id: cchar_array_to_string(&info.mfg_id),
                    model_name: cchar_array_to_string(&info.model_name),
                    sn: cchar_array_to_string(&info.sn),
                    dref: info.dref,
                });
            }
            ffi::ddca_free_display_info_list(list_ptr);
        }
    }
    Ok(monitors)
}

pub struct DisplayHandle(ffi::DDCA_Display_Handle);

impl DisplayHandle {
    pub fn open(monitor: &Monitor) -> Result<Self> {
        let mut dh: ffi::DDCA_Display_Handle = std::ptr::null_mut();
        let rc = unsafe { ffi::ddca_open_display2(monitor.dref, false, &mut dh) };
        check("ddca_open_display2", rc)?;
        Ok(Self(dh))
    }
}

impl Drop for DisplayHandle {
    fn drop(&mut self) {
        unsafe {
            ffi::ddca_close_display(self.0);
        }
    }
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CapabilityDef {
    pub feature_code: u8,
    /// Declared allowed values for non-continuous features; empty for continuous ones.
    pub values: Vec<u8>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MonitorCapabilities {
    pub mccs_major: u8,
    pub mccs_minor: u8,
    pub features: Vec<CapabilityDef>,
}

pub fn query_capabilities(dh: &DisplayHandle) -> Result<MonitorCapabilities> {
    let mut caps_ptr: *mut c_char = std::ptr::null_mut();
    let rc = unsafe { ffi::ddca_get_capabilities_string(dh.0, &mut caps_ptr) };
    check("ddca_get_capabilities_string", rc)?;

    let mut parsed_ptr: *mut ffi::DDCA_Capabilities = std::ptr::null_mut();
    let rc = unsafe { ffi::ddca_parse_capabilities_string(caps_ptr, &mut parsed_ptr) };
    unsafe {
        free(caps_ptr as *mut c_void);
    }
    check("ddca_parse_capabilities_string", rc)?;

    let result = unsafe {
        let parsed = &*parsed_ptr;
        let vcp_codes = if parsed.vcp_code_ct > 0 && !parsed.vcp_codes.is_null() {
            std::slice::from_raw_parts(parsed.vcp_codes, parsed.vcp_code_ct as usize)
        } else {
            &[]
        };
        let features = vcp_codes
            .iter()
            .map(|cap_vcp| {
                let values = if cap_vcp.value_ct > 0 && !cap_vcp.values.is_null() {
                    std::slice::from_raw_parts(cap_vcp.values, cap_vcp.value_ct as usize).to_vec()
                } else {
                    Vec::new()
                };
                CapabilityDef {
                    feature_code: cap_vcp.feature_code,
                    values,
                }
            })
            .collect();
        MonitorCapabilities {
            mccs_major: parsed.version_spec.major,
            mccs_minor: parsed.version_spec.minor,
            features,
        }
    };

    unsafe {
        ffi::ddca_free_parsed_capabilities(parsed_ptr);
    }
    Ok(result)
}

#[derive(Debug, Clone)]
pub struct FeatureMeta {
    pub name: String,
    pub is_continuous: bool,
    /// Named values for simple non-continuous features (value code -> label).
    pub sl_values: Vec<(u8, String)>,
}

pub fn feature_metadata(dh: &DisplayHandle, feature_code: u8) -> Result<FeatureMeta> {
    let mut meta_ptr: *mut ffi::DDCA_Feature_Metadata = std::ptr::null_mut();
    let rc =
        unsafe { ffi::ddca_get_feature_metadata_by_dh(feature_code, dh.0, true, &mut meta_ptr) };
    check("ddca_get_feature_metadata_by_dh", rc)?;

    let result = unsafe {
        let meta = &*meta_ptr;
        let flags = meta.feature_flags as u32;
        let is_continuous = flags & (ffi::DDCA_STD_CONT | ffi::DDCA_COMPLEX_CONT) != 0;
        let is_simple_nc = flags & ffi::DDCA_SIMPLE_NC != 0;

        let name = if meta.feature_name.is_null() {
            format!("Feature 0x{feature_code:02X}")
        } else {
            CStr::from_ptr(meta.feature_name)
                .to_string_lossy()
                .into_owned()
        };

        let mut sl_values = Vec::new();
        if is_simple_nc && !meta.sl_values.is_null() {
            let mut i: isize = 0;
            loop {
                let entry = &*meta.sl_values.offset(i);
                if entry.value_name.is_null() {
                    break;
                }
                sl_values.push((
                    entry.value_code,
                    CStr::from_ptr(entry.value_name)
                        .to_string_lossy()
                        .into_owned(),
                ));
                i += 1;
            }
        }

        FeatureMeta {
            name,
            is_continuous,
            sl_values,
        }
    };

    unsafe {
        ffi::ddca_free_feature_metadata(meta_ptr);
    }
    Ok(result)
}

#[derive(Debug, Clone, Copy)]
pub struct VcpValue {
    pub current: u16,
    pub max: u16,
}

pub fn get_vcp_value(dh: &DisplayHandle, feature_code: u8) -> Result<VcpValue> {
    let mut valrec = ffi::DDCA_Non_Table_Vcp_Value::default();
    let rc = unsafe { ffi::ddca_get_non_table_vcp_value(dh.0, feature_code, &mut valrec) };
    check("ddca_get_non_table_vcp_value", rc)?;
    Ok(VcpValue {
        current: ((valrec.sh as u16) << 8) | valrec.sl as u16,
        max: ((valrec.mh as u16) << 8) | valrec.ml as u16,
    })
}

pub fn set_vcp_value(dh: &DisplayHandle, feature_code: u8, new_value: u16) -> Result<()> {
    let hi = (new_value >> 8) as u8;
    let lo = (new_value & 0xFF) as u8;
    let rc = unsafe { ffi::ddca_set_non_table_vcp_value2(dh.0, feature_code, hi, lo) };
    check("ddca_set_non_table_vcp_value2", rc)
}