use rustix::event::EventfdFlags;
use rustix::event::eventfd;
use rustix::event::{PollFd, PollFlags, poll};
use rustix::fd::AsFd;
use std::fs::File;
use std::sync::mpsc::{self, Sender};
use std::thread;
use std::thread::JoinHandle;
use wayland_client::protocol::wl_output;
use wayland_client::{Connection, Dispatch, QueueHandle, protocol::wl_registry};
use wayland_protocols_wlr::gamma_control::v1::client::{
zwlr_gamma_control_manager_v1, zwlr_gamma_control_v1,
};
#[derive(Debug)]
pub struct WlrGammaBackend {
tx: Sender<Cmd>,
wake_fd: rustix::fd::OwnedFd,
handle: Option<JoinHandle<()>>,
}
impl WlrGammaBackend {
pub fn new() -> Self {
let wake_fd = eventfd(0, EventfdFlags::NONBLOCK).expect("eventfd");
let wake_fd_thread = wake_fd.try_clone().expect("eventfd clone");
let (tx, rx) = mpsc::channel::<Cmd>();
let handle = thread::spawn(move || run(rx, wake_fd_thread));
Self {
tx,
wake_fd,
handle: Some(handle),
}
}
pub fn set_gamma(&self, gamma: u8) {
self.send(Cmd::SetGamma(gamma));
}
pub fn set_temperature(&self, temp: u32) {
self.send(Cmd::SetTemperature(temp));
}
pub fn stop(&mut self) {
self.send(Cmd::Quit);
if let Some(handle) = self.handle.take() {
handle.join().ok();
}
}
fn send(&self, cmd: Cmd) {
self.tx.send(cmd).expect("backend thread died");
rustix::io::write(&self.wake_fd, &1u64.to_ne_bytes()).ok();
}
}
impl Default for WlrGammaBackend {
fn default() -> Self {
Self::new()
}
}
enum Cmd {
SetGamma(u8),
SetTemperature(u32),
Quit,
}
struct State {
outputs: Vec<wl_output::WlOutput>,
gamma_control_manager: Option<zwlr_gamma_control_manager_v1::ZwlrGammaControlManagerV1>,
gamma_controls: Vec<zwlr_gamma_control_v1::ZwlrGammaControlV1>,
gamma_ramp_sizes: Vec<usize>,
current_gamma: u8,
current_temp: u32,
}
impl State {
fn set_gamma(&mut self, gamma: u8) {
self.current_gamma = gamma.clamp(1, 100);
}
fn set_temperature(&mut self, temp: u32) {
self.current_temp = temp.clamp(1000, 40000);
}
fn apply_changes(&mut self) {
for i in 0..self.gamma_controls.len() {
let file = create_gamma_fd(
self.gamma_ramp_sizes[i],
self.current_gamma,
self.current_temp,
);
self.gamma_controls[i].set_gamma(file.as_fd());
}
}
}
fn run(rx: mpsc::Receiver<Cmd>, wake_fd: rustix::fd::OwnedFd) {
let conn = Connection::connect_to_env().expect("wayland connection");
let display = conn.display();
let mut event_queue = conn.new_event_queue();
let qh = event_queue.handle();
let _registry = display.get_registry(&qh, ());
let mut state = State {
outputs: Vec::new(),
gamma_control_manager: None,
gamma_controls: Vec::new(),
gamma_ramp_sizes: Vec::new(),
current_gamma: 100,
current_temp: 6500,
};
event_queue.roundtrip(&mut state).expect("roundtrip");
if let Some(ref manager) = state.gamma_control_manager {
for output in &state.outputs {
let control = manager.get_gamma_control(output, &qh, ());
state.gamma_controls.push(control);
}
}
event_queue.roundtrip(&mut state).expect("roundtrip");
loop {
let mut buf = [0u8; 8];
let _ = rustix::io::read(&wake_fd, &mut buf);
while let Ok(cmd) = rx.try_recv() {
match cmd {
Cmd::SetGamma(g) => state.set_gamma(g),
Cmd::SetTemperature(t) => state.set_temperature(t),
Cmd::Quit => return,
}
state.apply_changes();
}
event_queue.flush().expect("flush");
let guard = if let Some(g) = event_queue.prepare_read() {
g
} else {
event_queue.dispatch_pending(&mut state).expect("dispatch");
continue;
};
let wayland_pollfd = PollFd::new(&conn, PollFlags::IN);
let wake_pollfd = PollFd::new(&wake_fd, PollFlags::IN);
poll(&mut [wayland_pollfd, wake_pollfd], None).expect("poll");
guard.read().ok();
event_queue.dispatch_pending(&mut state).expect("dispatch");
}
}
impl Dispatch<wl_registry::WlRegistry, ()> for State {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
(): &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_registry::Event::Global {
name,
interface,
version,
} = event
{
match interface.as_str() {
"wl_output" => {
let output = registry.bind::<wl_output::WlOutput, _, _>(name, version, qh, ());
state.outputs.push(output);
},
"zwlr_gamma_control_manager_v1" => {
let manager = registry
.bind::<zwlr_gamma_control_manager_v1::ZwlrGammaControlManagerV1, _, _>(
name,
version,
qh,
(),
);
state.gamma_control_manager = Some(manager);
},
_ => {},
}
}
}
}
impl Dispatch<zwlr_gamma_control_v1::ZwlrGammaControlV1, ()> for State {
fn event(
state: &mut Self,
proxy: &zwlr_gamma_control_v1::ZwlrGammaControlV1,
event: zwlr_gamma_control_v1::Event,
(): &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
zwlr_gamma_control_v1::Event::GammaSize { size } => {
state.gamma_ramp_sizes.push(size as usize);
},
zwlr_gamma_control_v1::Event::Failed => {
eprintln!("gamma control failed for {proxy:?}");
},
_ => {},
}
}
}
impl Dispatch<zwlr_gamma_control_manager_v1::ZwlrGammaControlManagerV1, ()> for State {
fn event(
_: &mut Self,
_: &zwlr_gamma_control_manager_v1::ZwlrGammaControlManagerV1,
_: zwlr_gamma_control_manager_v1::Event,
(): &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<wl_output::WlOutput, ()> for State {
fn event(
_: &mut Self,
_: &wl_output::WlOutput,
_: wl_output::Event,
(): &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
fn kelvin_to_rgb(temp: u32) -> (f64, f64, f64) {
let t = f64::from(temp.clamp(1000, 40000) / 100);
let r = if t <= 66.0 {
1.0
} else {
(329.698727446 * (t - 60.0).powf(-0.1332047592) / 255.0).clamp(0.0, 1.0)
};
let g = if t <= 66.0 {
99.4708025861f64.mul_add(t.ln(), -161.1195681661) / 255.0
} else {
288.1221695283 * (t - 60.0).powf(-0.0755148492) / 255.0
}
.clamp(0.0, 1.0);
let b = if t >= 66.0 {
1.0
} else if t <= 19.0 {
0.0
} else {
138.5177312231f64.mul_add((t - 10.0).ln(), -305.0447927307) / 255.0
}
.clamp(0.0, 1.0);
(r, g, b)
}
fn generate_ramp_channel(ramp_size: usize, gamma: u8, color_mul: f64) -> Vec<u16> {
let brightness = f64::from(gamma.clamp(1, 100)) / 100.0;
let max_index = (ramp_size - 1).max(1) as f64;
(0..ramp_size)
.map(|i| {
let t = i as f64 / max_index;
let val = (t * brightness * color_mul).mul_add(65535.0, 0.5);
val.clamp(0.0, 65535.0) as u16
})
.collect()
}
fn create_gamma_fd(ramp_size: usize, gamma: u8, temp: u32) -> File {
let total_bytes = 3 * ramp_size * 2;
let mfd = memfd::MemfdOptions::default()
.create("gamma")
.expect("memfd");
let file = mfd.into_file();
file.set_len(total_bytes as u64).unwrap();
let mut buffer = unsafe {
memmap2::MmapOptions::new()
.len(total_bytes)
.map_mut(&file)
.expect("mmap")
};
let buffer_u16: &mut [u16] =
unsafe { std::slice::from_raw_parts_mut(buffer.as_mut_ptr() as *mut u16, total_bytes / 2) };
let (r_mul, g_mul, b_mul) = kelvin_to_rgb(temp);
let red = generate_ramp_channel(ramp_size, gamma, r_mul);
let green = generate_ramp_channel(ramp_size, gamma, g_mul);
let blue = generate_ramp_channel(ramp_size, gamma, b_mul);
buffer_u16[..ramp_size].copy_from_slice(&red);
buffer_u16[ramp_size..2 * ramp_size].copy_from_slice(&green);
buffer_u16[2 * ramp_size..].copy_from_slice(&blue);
buffer.flush().unwrap();
file
}