randR method added for actual integration with X11 #7

Merged
JMR-dev merged 1 commits from feat-implement-x11-randR-method into main 2025-10-05 02:03:10 +00:00
6 changed files with 751 additions and 1 deletions
+1
View File
@@ -7,3 +7,4 @@ edition = "2021"
[dependencies]
clap = { version = "4.5", features = ["derive"] }
x11rb = { version = "0.13", features = ["randr"] }
+269
View File
@@ -0,0 +1,269 @@
/// X11 RandR gamma adjustment method
/// Ported from legacy/src/gamma-randr.c
use crate::colorramp::colorramp_fill;
use crate::gamma::GammaMethod;
use crate::types::ColorSetting;
use std::fmt;
use x11rb::connection::Connection;
use x11rb::protocol::randr;
use x11rb::protocol::xproto;
use x11rb::rust_connection::RustConnection;
const RANDR_VERSION_MAJOR: u32 = 1;
const RANDR_VERSION_MINOR: u32 = 3;
/// State for a single CRTC
struct CrtcState {
crtc: randr::Crtc,
ramp_size: u16,
saved_ramps: Vec<u16>, // R, G, B ramps concatenated (3 * ramp_size)
}
/// X11 RandR gamma adjustment method
pub struct RandrGammaMethod {
conn: Option<RustConnection>,
screen_num: Option<i32>,
preferred_screen: usize,
crtc_filter: Vec<usize>, // If non-empty, only adjust these CRTC indices
crtcs: Vec<CrtcState>,
}
impl RandrGammaMethod {
pub fn new() -> Self {
Self {
conn: None,
screen_num: None,
preferred_screen: 0,
crtc_filter: Vec::new(),
crtcs: Vec::new(),
}
}
/// Set which screen to use (None = use default)
pub fn set_screen(&mut self, screen: i32) {
self.screen_num = Some(screen);
}
/// Set which CRTCs to adjust (empty = all)
pub fn set_crtcs(&mut self, crtc_indices: Vec<usize>) {
self.crtc_filter = crtc_indices;
}
fn get_screen_root(&self) -> Result<xproto::Window, String> {
let conn = self.conn.as_ref().ok_or("Not connected to X server")?;
let screen_num = self.screen_num.unwrap_or(self.preferred_screen as i32);
let setup = conn.setup();
let screen = setup
.roots
.get(screen_num as usize)
.ok_or_else(|| format!("Screen {} could not be found", screen_num))?;
Ok(screen.root)
}
fn set_temperature_for_crtc(
&self,
crtc_state: &CrtcState,
setting: &ColorSetting,
preserve: bool,
) -> Result<(), String> {
let conn = self.conn.as_ref().ok_or("Not connected to X server")?;
let ramp_size = crtc_state.ramp_size as usize;
/* Create new gamma ramps */
let mut gamma_r = vec![0u16; ramp_size];
let mut gamma_g = vec![0u16; ramp_size];
let mut gamma_b = vec![0u16; ramp_size];
if preserve {
/* Initialize from saved state */
gamma_r.copy_from_slice(&crtc_state.saved_ramps[0..ramp_size]);
gamma_g.copy_from_slice(&crtc_state.saved_ramps[ramp_size..2 * ramp_size]);
gamma_b.copy_from_slice(&crtc_state.saved_ramps[2 * ramp_size..3 * ramp_size]);
} else {
/* Initialize to linear (pure state) */
for i in 0..ramp_size {
let value = ((i as f64 / ramp_size as f64) * 65536.0) as u16;
gamma_r[i] = value;
gamma_g[i] = value;
gamma_b[i] = value;
}
}
/* Apply color temperature adjustment */
colorramp_fill(&mut gamma_r, &mut gamma_g, &mut gamma_b, setting);
/* Set gamma ramps */
randr::set_crtc_gamma(
conn,
crtc_state.crtc,
&gamma_r,
&gamma_g,
&gamma_b,
)
.map_err(|e| format!("Failed to set CRTC gamma: {}", e))?
.check()
.map_err(|e| format!("RANDR Set CRTC Gamma returned error: {:?}", e))?;
Ok(())
}
}
impl Default for RandrGammaMethod {
fn default() -> Self {
Self::new()
}
}
impl GammaMethod for RandrGammaMethod {
fn init(&mut self) -> Result<(), String> {
/* Open X server connection */
let (conn, preferred_screen) = RustConnection::connect(None)
.map_err(|e| format!("Failed to connect to X server: {}", e))?;
self.preferred_screen = preferred_screen;
/* Query RandR version */
let ver_reply = randr::query_version(&conn, RANDR_VERSION_MAJOR, RANDR_VERSION_MINOR)
.map_err(|e| format!("Failed to query RANDR version: {}", e))?
.reply()
.map_err(|e| format!("RANDR Query Version returned error: {}", e))?;
if ver_reply.major_version != RANDR_VERSION_MAJOR
|| ver_reply.minor_version < RANDR_VERSION_MINOR
{
return Err(format!(
"Unsupported RANDR version ({}.{})",
ver_reply.major_version, ver_reply.minor_version
));
}
self.conn = Some(conn);
Ok(())
}
fn start(&mut self) -> Result<(), String> {
let conn = self.conn.as_ref().ok_or("Not initialized")?;
let root = self.get_screen_root()?;
/* Get screen resources (list of CRTCs) */
let res_reply = randr::get_screen_resources_current(conn, root)
.map_err(|e| format!("Failed to get screen resources: {}", e))?
.reply()
.map_err(|e| format!("RANDR Get Screen Resources Current returned error: {}", e))?;
let crtcs = res_reply.crtcs;
/* Save CRTC state and gamma ramps */
for crtc in crtcs {
/* Get gamma ramp size */
let gamma_size_reply = randr::get_crtc_gamma_size(conn, crtc)
.map_err(|e| format!("Failed to get CRTC gamma size: {}", e))?
.reply()
.map_err(|e| format!("RANDR Get CRTC Gamma Size returned error: {}", e))?;
let ramp_size = gamma_size_reply.size;
if ramp_size == 0 {
eprintln!("Warning: CRTC has gamma ramp size 0, skipping");
continue;
}
/* Get current gamma ramps */
let gamma_get_reply = randr::get_crtc_gamma(conn, crtc)
.map_err(|e| format!("Failed to get CRTC gamma: {}", e))?
.reply()
.map_err(|e| format!("RANDR Get CRTC Gamma returned error: {}", e))?;
/* Save gamma ramps (R, G, B concatenated) */
let mut saved_ramps = Vec::with_capacity(3 * ramp_size as usize);
saved_ramps.extend_from_slice(&gamma_get_reply.red);
saved_ramps.extend_from_slice(&gamma_get_reply.green);
saved_ramps.extend_from_slice(&gamma_get_reply.blue);
self.crtcs.push(CrtcState {
crtc,
ramp_size,
saved_ramps,
});
}
if self.crtcs.is_empty() {
return Err("No usable CRTCs found".to_string());
}
Ok(())
}
fn set_temperature(&mut self, setting: &ColorSetting, preserve: bool) -> Result<(), String> {
/* If no CRTC filter is set, adjust all CRTCs */
if self.crtc_filter.is_empty() {
for crtc_state in &self.crtcs {
self.set_temperature_for_crtc(crtc_state, setting, preserve)?;
}
} else {
/* Only adjust specified CRTCs */
for &crtc_idx in &self.crtc_filter {
if crtc_idx >= self.crtcs.len() {
return Err(format!(
"CRTC {} does not exist. Valid CRTCs are [0-{}]",
crtc_idx,
self.crtcs.len() - 1
));
}
self.set_temperature_for_crtc(&self.crtcs[crtc_idx], setting, preserve)?;
}
}
Ok(())
}
fn restore(&mut self) {
if let Some(conn) = &self.conn {
/* Restore original gamma ramps for all CRTCs */
for crtc_state in &self.crtcs {
let ramp_size = crtc_state.ramp_size as usize;
let gamma_r = &crtc_state.saved_ramps[0..ramp_size];
let gamma_g = &crtc_state.saved_ramps[ramp_size..2 * ramp_size];
let gamma_b = &crtc_state.saved_ramps[2 * ramp_size..3 * ramp_size];
match randr::set_crtc_gamma(conn, crtc_state.crtc, gamma_r, gamma_g, gamma_b) {
Ok(cookie) => {
if let Err(e) = cookie.check() {
eprintln!("Warning: Failed to restore CRTC gamma: {:?}", e);
}
}
Err(e) => {
eprintln!("Warning: Failed to send restore CRTC gamma request: {:?}", e);
}
}
}
}
}
fn name(&self) -> &str {
"randr"
}
fn print_help(&self) {
println!("Adjust gamma ramps with the X RANDR extension.");
println!();
println!(" screen=N X screen to apply adjustments to");
println!(" crtc=N List of comma separated CRTCs to apply adjustments to");
println!();
}
}
impl fmt::Display for RandrGammaMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "RandR")
}
}
impl Drop for RandrGammaMethod {
fn drop(&mut self) {
self.restore();
}
}
+1
View File
@@ -1,5 +1,6 @@
pub mod colorramp;
pub mod gamma;
pub mod gamma_randr;
pub mod location;
pub mod solar;
pub mod types;
+5 -1
View File
@@ -1,11 +1,13 @@
mod colorramp;
mod gamma;
mod gamma_randr;
mod location;
mod solar;
mod types;
use clap::{Parser, ValueEnum};
use gamma::{DummyGammaMethod, GammaMethod};
use gamma_randr::RandrGammaMethod;
use location::{LocationProvider, ManualLocationProvider};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use types::*;
@@ -19,6 +21,7 @@ const FADE_LENGTH: i32 = 40;
#[derive(Debug, Clone, Copy, ValueEnum)]
enum GammaMethodChoice {
Randr,
Dummy,
}
@@ -31,7 +34,7 @@ struct Args {
location: Option<String>,
/// Gamma adjustment method
#[arg(short = 'm', long, default_value = "dummy")]
#[arg(short = 'm', long, default_value = "randr")]
method: GammaMethodChoice,
/// One-shot mode (set temperature and exit)
@@ -239,6 +242,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
/* Set up gamma method */
let mut gamma_method: Box<dyn GammaMethod> = match args.method {
GammaMethodChoice::Randr => Box::new(RandrGammaMethod::new()),
GammaMethodChoice::Dummy => Box::new(DummyGammaMethod::new()),
};
+276
View File
@@ -0,0 +1,276 @@
use redshift_rebooted::gamma::GammaMethod;
use redshift_rebooted::gamma_randr::RandrGammaMethod;
use redshift_rebooted::types::*;
#[test]
fn test_randr_gamma_method_creation() {
// Test that RandrGammaMethod can be created
let method = RandrGammaMethod::new();
assert_eq!(method.name(), "randr", "RandrGammaMethod name should be 'randr'");
}
#[test]
fn test_randr_gamma_method_default() {
// Test that RandrGammaMethod can be created with Default trait
let method = RandrGammaMethod::default();
assert_eq!(method.name(), "randr", "Default RandrGammaMethod name should be 'randr'");
}
#[test]
fn test_randr_gamma_method_display_trait() {
let method = RandrGammaMethod::new();
let display_string = format!("{}", method);
assert_eq!(display_string, "RandR", "RandrGammaMethod should display as 'RandR'");
}
#[test]
fn test_randr_gamma_method_init_no_display() {
// Test init when DISPLAY is not set or X11 is not available
// This may fail gracefully or succeed depending on environment
let mut method = RandrGammaMethod::new();
// We don't assert success/failure here because it depends on environment
// Just verify it doesn't panic
let _ = method.init();
}
#[test]
fn test_randr_gamma_method_set_screen() {
// Test the set_screen configuration method
let mut method = RandrGammaMethod::new();
method.set_screen(0);
// If we got here without panicking, the method works
}
#[test]
fn test_randr_gamma_method_set_crtcs() {
// Test the set_crtcs configuration method
let mut method = RandrGammaMethod::new();
method.set_crtcs(vec![0, 1]);
// If we got here without panicking, the method works
}
#[test]
fn test_randr_gamma_method_restore_without_init() {
// Test that restore doesn't panic even if not initialized
let mut method = RandrGammaMethod::new();
method.restore();
// Should not panic
}
// Integration test - only runs if X11 is available
#[test]
#[ignore] // Use `cargo test -- --ignored` to run this
fn test_randr_gamma_method_full_lifecycle_x11() {
// This test requires X11 to be available
let mut method = RandrGammaMethod::new();
// Try to initialize
if method.init().is_err() {
eprintln!("X11 not available, skipping integration test");
return;
}
// Try to start
if method.start().is_err() {
eprintln!("Could not start RandR method, skipping");
return;
}
// Try to set a temperature
let setting = ColorSetting {
temperature: 5000,
gamma: [1.0, 1.0, 1.0],
brightness: 1.0,
};
if let Err(e) = method.set_temperature(&setting, false) {
eprintln!("Could not set temperature: {}", e);
}
// Restore
method.restore();
}
// Integration test - test preserve flag
#[test]
#[ignore]
fn test_randr_gamma_method_preserve_flag_x11() {
let mut method = RandrGammaMethod::new();
if method.init().is_err() || method.start().is_err() {
eprintln!("X11 not available, skipping integration test");
return;
}
let setting = ColorSetting {
temperature: 4000,
gamma: [1.0, 1.0, 1.0],
brightness: 0.9,
};
// Test without preserve
if let Err(e) = method.set_temperature(&setting, false) {
eprintln!("Could not set temperature without preserve: {}", e);
}
// Test with preserve
if let Err(e) = method.set_temperature(&setting, true) {
eprintln!("Could not set temperature with preserve: {}", e);
}
method.restore();
}
// Integration test - test multiple temperature changes
#[test]
#[ignore]
fn test_randr_gamma_method_multiple_changes_x11() {
let mut method = RandrGammaMethod::new();
if method.init().is_err() || method.start().is_err() {
eprintln!("X11 not available, skipping integration test");
return;
}
let temperatures = [6500, 5000, 3500, 4500, 6500];
for temp in temperatures {
let setting = ColorSetting {
temperature: temp,
gamma: [1.0, 1.0, 1.0],
brightness: 1.0,
};
if let Err(e) = method.set_temperature(&setting, false) {
eprintln!("Could not set temperature {}: {}", temp, e);
}
}
method.restore();
}
// Integration test - test extreme temperatures
#[test]
#[ignore]
fn test_randr_gamma_method_extreme_temperatures_x11() {
let mut method = RandrGammaMethod::new();
if method.init().is_err() || method.start().is_err() {
eprintln!("X11 not available, skipping integration test");
return;
}
// Test minimum temperature
let min_setting = ColorSetting {
temperature: MIN_TEMP,
gamma: [1.0, 1.0, 1.0],
brightness: 1.0,
};
if let Err(e) = method.set_temperature(&min_setting, false) {
eprintln!("Could not set minimum temperature: {}", e);
}
// Test maximum temperature
let max_setting = ColorSetting {
temperature: MAX_TEMP,
gamma: [1.0, 1.0, 1.0],
brightness: 1.0,
};
if let Err(e) = method.set_temperature(&max_setting, false) {
eprintln!("Could not set maximum temperature: {}", e);
}
// Restore to neutral
let neutral_setting = ColorSetting {
temperature: NEUTRAL_TEMP,
gamma: [1.0, 1.0, 1.0],
brightness: 1.0,
};
if let Err(e) = method.set_temperature(&neutral_setting, false) {
eprintln!("Could not restore neutral temperature: {}", e);
}
method.restore();
}
// Integration test - test various gamma values
#[test]
#[ignore]
fn test_randr_gamma_method_gamma_values_x11() {
let mut method = RandrGammaMethod::new();
if method.init().is_err() || method.start().is_err() {
eprintln!("X11 not available, skipping integration test");
return;
}
let gamma_values = [
[1.0, 1.0, 1.0],
[0.8, 0.8, 0.8],
[1.2, 1.2, 1.2],
[1.0, 0.9, 0.9],
];
for gamma in gamma_values {
let setting = ColorSetting {
temperature: 6500,
gamma,
brightness: 1.0,
};
if let Err(e) = method.set_temperature(&setting, false) {
eprintln!("Could not set gamma {:?}: {}", gamma, e);
}
}
method.restore();
}
// Integration test - test brightness values
#[test]
#[ignore]
fn test_randr_gamma_method_brightness_values_x11() {
let mut method = RandrGammaMethod::new();
if method.init().is_err() || method.start().is_err() {
eprintln!("X11 not available, skipping integration test");
return;
}
let brightness_values = [1.0, 0.8, 0.6, 0.9, 1.0];
for brightness in brightness_values {
let setting = ColorSetting {
temperature: 6500,
gamma: [1.0, 1.0, 1.0],
brightness,
};
if let Err(e) = method.set_temperature(&setting, false) {
eprintln!("Could not set brightness {}: {}", brightness, e);
}
}
method.restore();
}
#[test]
fn test_randr_gamma_method_as_trait_object() {
// Test that RandrGammaMethod can be used as a trait object
let method: Box<dyn GammaMethod> = Box::new(RandrGammaMethod::new());
assert_eq!(method.name(), "randr");
}
#[test]
fn test_randr_gamma_method_drop() {
// Test that RandrGammaMethod's Drop implementation doesn't panic
{
let _method = RandrGammaMethod::new();
// When _method goes out of scope, Drop should run
}
// If we got here, Drop didn't panic
}
+199
View File
@@ -0,0 +1,199 @@
use redshift_rebooted::gamma::*;
use redshift_rebooted::types::*;
#[test]
fn test_dummy_gamma_method_lifecycle() {
// Test that DummyGammaMethod can be initialized and started
let mut method = DummyGammaMethod::new();
// Init should succeed
assert!(method.init().is_ok(), "DummyGammaMethod init should succeed");
// Start should succeed
assert!(method.start().is_ok(), "DummyGammaMethod start should succeed");
// Name should be "dummy"
assert_eq!(method.name(), "dummy", "DummyGammaMethod name should be 'dummy'");
}
#[test]
fn test_dummy_gamma_method_set_temperature() {
let mut method = DummyGammaMethod::new();
method.init().unwrap();
method.start().unwrap();
// Create a color setting
let setting = ColorSetting {
temperature: 3500,
gamma: [1.0, 1.0, 1.0],
brightness: 1.0,
};
// Set temperature should succeed (even though it's a no-op)
assert!(
method.set_temperature(&setting, false).is_ok(),
"DummyGammaMethod set_temperature should succeed"
);
// Test with preserve flag
assert!(
method.set_temperature(&setting, true).is_ok(),
"DummyGammaMethod set_temperature with preserve should succeed"
);
}
#[test]
fn test_dummy_gamma_method_restore() {
let mut method = DummyGammaMethod::new();
method.init().unwrap();
method.start().unwrap();
// Restore should not panic (it's a no-op for dummy)
method.restore();
}
#[test]
fn test_dummy_gamma_method_various_temperatures() {
let mut method = DummyGammaMethod::new();
method.init().unwrap();
method.start().unwrap();
// Test with various temperature values
let temperatures = [1000, 2500, 3500, 5000, 6500, 10000, 25000];
for temp in temperatures {
let setting = ColorSetting {
temperature: temp,
gamma: [1.0, 1.0, 1.0],
brightness: 1.0,
};
assert!(
method.set_temperature(&setting, false).is_ok(),
"DummyGammaMethod should handle temperature {}K",
temp
);
}
}
#[test]
fn test_dummy_gamma_method_various_gamma_values() {
let mut method = DummyGammaMethod::new();
method.init().unwrap();
method.start().unwrap();
// Test with various gamma values
let gamma_values = [
[0.5, 0.5, 0.5],
[1.0, 1.0, 1.0],
[1.5, 1.5, 1.5],
[1.0, 0.8, 0.8],
];
for gamma in gamma_values {
let setting = ColorSetting {
temperature: 6500,
gamma,
brightness: 1.0,
};
assert!(
method.set_temperature(&setting, false).is_ok(),
"DummyGammaMethod should handle gamma {:?}",
gamma
);
}
}
#[test]
fn test_dummy_gamma_method_various_brightness_values() {
let mut method = DummyGammaMethod::new();
method.init().unwrap();
method.start().unwrap();
// Test with various brightness values
let brightness_values = [0.1, 0.5, 0.75, 1.0];
for brightness in brightness_values {
let setting = ColorSetting {
temperature: 6500,
gamma: [1.0, 1.0, 1.0],
brightness,
};
assert!(
method.set_temperature(&setting, false).is_ok(),
"DummyGammaMethod should handle brightness {}",
brightness
);
}
}
#[test]
fn test_dummy_gamma_method_multiple_calls() {
let mut method = DummyGammaMethod::new();
method.init().unwrap();
method.start().unwrap();
// Test calling set_temperature multiple times (simulating continual mode)
let setting1 = ColorSetting {
temperature: 6500,
gamma: [1.0, 1.0, 1.0],
brightness: 1.0,
};
let setting2 = ColorSetting {
temperature: 3500,
gamma: [1.0, 1.0, 1.0],
brightness: 1.0,
};
assert!(method.set_temperature(&setting1, false).is_ok());
assert!(method.set_temperature(&setting2, false).is_ok());
assert!(method.set_temperature(&setting1, false).is_ok());
assert!(method.set_temperature(&setting2, false).is_ok());
}
#[test]
fn test_gamma_method_trait_object() {
// Test that GammaMethod can be used as a trait object
let mut method: Box<dyn GammaMethod> = Box::new(DummyGammaMethod::new());
assert!(method.init().is_ok());
assert!(method.start().is_ok());
let setting = ColorSetting {
temperature: 4500,
gamma: [1.0, 1.0, 1.0],
brightness: 1.0,
};
assert!(method.set_temperature(&setting, false).is_ok());
method.restore();
}
#[test]
fn test_gamma_method_default_color_setting() {
let mut method = DummyGammaMethod::new();
method.init().unwrap();
method.start().unwrap();
// Test with default ColorSetting
let setting = ColorSetting::default();
assert_eq!(setting.temperature, NEUTRAL_TEMP);
assert_eq!(setting.gamma, [1.0, 1.0, 1.0]);
assert_eq!(setting.brightness, 1.0);
assert!(
method.set_temperature(&setting, false).is_ok(),
"DummyGammaMethod should handle default ColorSetting"
);
}
#[test]
fn test_dummy_gamma_method_display_trait() {
let method = DummyGammaMethod::new();
let display_string = format!("{}", method);
assert_eq!(display_string, "Dummy", "DummyGammaMethod should display as 'Dummy'");
}