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()
}
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 {
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,
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,
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)
}