nighterrors 0.1.0

Ultra-light Rust Wayland blue-light filter daemon for Hyprland/Niri and wlroots compositors
use std::ffi::CString;
use std::io;
use std::os::fd::BorrowedFd;
use std::os::fd::RawFd;

use wayland_client::protocol::wl_output;
use wayland_client::{Dispatch, QueueHandle};

use crate::color;
use crate::protocols::wlr_gamma::zwlr_gamma_control_manager_v1::ZwlrGammaControlManagerV1;
use crate::protocols::wlr_gamma::zwlr_gamma_control_v1::ZwlrGammaControlV1;

#[derive(Debug, Clone)]
pub struct WlrControlState {
    pub control: ZwlrGammaControlV1,
    pub gamma_size: Option<u32>,
    pub failed: bool,
}

impl WlrControlState {
    pub fn new(control: ZwlrGammaControlV1) -> Self {
        Self {
            control,
            gamma_size: None,
            failed: false,
        }
    }
}

pub fn create_control<D>(
    manager: &ZwlrGammaControlManagerV1,
    output: &wl_output::WlOutput,
    qh: &QueueHandle<D>,
    output_global_id: u32,
) -> WlrControlState
where
    D: Dispatch<ZwlrGammaControlV1, u32> + 'static,
{
    let control = manager.get_gamma_control(output, qh, output_global_id);
    WlrControlState::new(control)
}

pub fn destroy_control(control: &WlrControlState) {
    control.control.destroy();
}

pub fn apply_control(control: &WlrControlState, multipliers: [f64; 3]) -> Result<(), String> {
    if control.failed {
        return Err("wlr gamma control object is marked as failed".to_string());
    }

    let gamma_size = control
        .gamma_size
        .ok_or_else(|| "gamma size has not been announced yet".to_string())?;

    if gamma_size < 2 {
        return Err(format!(
            "invalid gamma ramp size {gamma_size}, expected at least 2"
        ));
    }

    let table = color::build_gamma_lut(gamma_size as usize, multipliers);
    let fd = memfd_create("nighterrors-gamma")?;

    let apply_result = (|| {
        write_all_fd(fd, &table)?;

        let seek_result = unsafe { libc::lseek(fd, 0, libc::SEEK_SET) };
        if seek_result < 0 {
            return Err(io::Error::last_os_error().to_string());
        }

        let borrowed = unsafe { BorrowedFd::borrow_raw(fd) };
        control.control.set_gamma(borrowed);

        Ok(())
    })();

    let _ = unsafe { libc::close(fd) };
    apply_result
}

fn memfd_create(name: &str) -> Result<RawFd, String> {
    let cname = CString::new(name).map_err(|e| e.to_string())?;
    let fd = unsafe { libc::memfd_create(cname.as_ptr(), libc::MFD_CLOEXEC) };
    if fd < 0 {
        return Err(io::Error::last_os_error().to_string());
    }
    Ok(fd)
}

fn write_all_fd(fd: RawFd, mut data: &[u8]) -> Result<(), String> {
    while !data.is_empty() {
        let written = unsafe { libc::write(fd, data.as_ptr().cast(), data.len()) };
        if written < 0 {
            return Err(io::Error::last_os_error().to_string());
        }

        let written = written as usize;
        if written == 0 {
            return Err("short write while writing gamma table".to_string());
        }

        data = &data[written..];
    }

    Ok(())
}