@@ -0,0 +1,357 @@ |
| | 1 | +//! RandR manager for querying and applying display configurations. |
| | 2 | + |
| | 3 | +use gartk_x11::Connection; |
| | 4 | +use x11rb::connection::Connection as X11Connection; |
| | 5 | +use x11rb::protocol::randr::{self, ConnectionExt as RandrExt}; |
| | 6 | + |
| | 7 | +use super::error::{RandrError, Result}; |
| | 8 | +use super::types::{ModeInfo, OutputInfo}; |
| | 9 | +use crate::config::MonitorConfig; |
| | 10 | + |
| | 11 | +/// Manager for RandR operations. |
| | 12 | +pub struct RandrManager { |
| | 13 | + conn: Connection, |
| | 14 | + root: u32, |
| | 15 | +} |
| | 16 | + |
| | 17 | +impl RandrManager { |
| | 18 | + /// Create a new RandR manager. |
| | 19 | + pub fn new(conn: Connection) -> Result<Self> { |
| | 20 | + let root = conn.root(); |
| | 21 | + |
| | 22 | + // Verify RandR is available |
| | 23 | + conn.inner() |
| | 24 | + .randr_query_version(1, 5)? |
| | 25 | + .reply()?; |
| | 26 | + |
| | 27 | + Ok(Self { conn, root }) |
| | 28 | + } |
| | 29 | + |
| | 30 | + /// Get screen resources (outputs, CRTCs, modes). |
| | 31 | + fn get_resources(&self) -> Result<randr::GetScreenResourcesCurrentReply> { |
| | 32 | + Ok(self |
| | 33 | + .conn |
| | 34 | + .inner() |
| | 35 | + .randr_get_screen_resources_current(self.root)? |
| | 36 | + .reply()?) |
| | 37 | + } |
| | 38 | + |
| | 39 | + /// Get information about all outputs. |
| | 40 | + #[allow(dead_code)] // Used for mode selection UI |
| | 41 | + pub fn get_outputs(&self) -> Result<Vec<OutputInfo>> { |
| | 42 | + let resources = self.get_resources()?; |
| | 43 | + let mut outputs = Vec::new(); |
| | 44 | + |
| | 45 | + for &output in &resources.outputs { |
| | 46 | + let info = self |
| | 47 | + .conn |
| | 48 | + .inner() |
| | 49 | + .randr_get_output_info(output, resources.config_timestamp)? |
| | 50 | + .reply()?; |
| | 51 | + |
| | 52 | + let name = String::from_utf8_lossy(&info.name).to_string(); |
| | 53 | + let connected = info.connection == randr::Connection::CONNECTED; |
| | 54 | + |
| | 55 | + // Get available modes |
| | 56 | + let modes: Vec<ModeInfo> = info |
| | 57 | + .modes |
| | 58 | + .iter() |
| | 59 | + .filter_map(|&mode_id| self.get_mode_info(&resources, mode_id)) |
| | 60 | + .collect(); |
| | 61 | + |
| | 62 | + // Get current mode and position if CRTC is set |
| | 63 | + let (crtc, current_mode, position) = if info.crtc != 0 { |
| | 64 | + let crtc_info = self |
| | 65 | + .conn |
| | 66 | + .inner() |
| | 67 | + .randr_get_crtc_info(info.crtc, resources.config_timestamp)? |
| | 68 | + .reply()?; |
| | 69 | + |
| | 70 | + let current = if crtc_info.mode != 0 { |
| | 71 | + self.get_mode_info(&resources, crtc_info.mode) |
| | 72 | + } else { |
| | 73 | + None |
| | 74 | + }; |
| | 75 | + |
| | 76 | + ( |
| | 77 | + Some(info.crtc), |
| | 78 | + current, |
| | 79 | + Some((crtc_info.x, crtc_info.y)), |
| | 80 | + ) |
| | 81 | + } else { |
| | 82 | + (None, None, None) |
| | 83 | + }; |
| | 84 | + |
| | 85 | + outputs.push(OutputInfo { |
| | 86 | + name, |
| | 87 | + output, |
| | 88 | + crtc, |
| | 89 | + connected, |
| | 90 | + modes, |
| | 91 | + current_mode, |
| | 92 | + position, |
| | 93 | + width_mm: info.mm_width, |
| | 94 | + height_mm: info.mm_height, |
| | 95 | + }); |
| | 96 | + } |
| | 97 | + |
| | 98 | + Ok(outputs) |
| | 99 | + } |
| | 100 | + |
| | 101 | + /// Get mode information by ID. |
| | 102 | + fn get_mode_info( |
| | 103 | + &self, |
| | 104 | + resources: &randr::GetScreenResourcesCurrentReply, |
| | 105 | + mode_id: randr::Mode, |
| | 106 | + ) -> Option<ModeInfo> { |
| | 107 | + resources.modes.iter().find(|m| m.id == mode_id).map(|m| { |
| | 108 | + let refresh = if m.htotal > 0 && m.vtotal > 0 { |
| | 109 | + (m.dot_clock as f64) / (m.htotal as f64 * m.vtotal as f64) |
| | 110 | + } else { |
| | 111 | + 0.0 |
| | 112 | + }; |
| | 113 | + |
| | 114 | + ModeInfo { |
| | 115 | + id: m.id, |
| | 116 | + width: m.width, |
| | 117 | + height: m.height, |
| | 118 | + refresh, |
| | 119 | + } |
| | 120 | + }) |
| | 121 | + } |
| | 122 | + |
| | 123 | + /// Get the name of the primary output. |
| | 124 | + #[allow(dead_code)] // Used for mode selection UI |
| | 125 | + pub fn get_primary_name(&self) -> Result<Option<String>> { |
| | 126 | + let reply = self.conn.inner().randr_get_output_primary(self.root)?.reply()?; |
| | 127 | + |
| | 128 | + if reply.output == 0 { |
| | 129 | + return Ok(None); |
| | 130 | + } |
| | 131 | + |
| | 132 | + let resources = self.get_resources()?; |
| | 133 | + let info = self |
| | 134 | + .conn |
| | 135 | + .inner() |
| | 136 | + .randr_get_output_info(reply.output, resources.config_timestamp)? |
| | 137 | + .reply()?; |
| | 138 | + |
| | 139 | + Ok(Some(String::from_utf8_lossy(&info.name).to_string())) |
| | 140 | + } |
| | 141 | + |
| | 142 | + /// Set the primary output by name. |
| | 143 | + pub fn set_primary(&self, name: &str) -> Result<()> { |
| | 144 | + let output = self.find_output_by_name(name)?; |
| | 145 | + self.conn.inner().randr_set_output_primary(self.root, output)?; |
| | 146 | + tracing::info!("set primary output to {}", name); |
| | 147 | + Ok(()) |
| | 148 | + } |
| | 149 | + |
| | 150 | + /// Find an output by name. |
| | 151 | + fn find_output_by_name(&self, name: &str) -> Result<randr::Output> { |
| | 152 | + let resources = self.get_resources()?; |
| | 153 | + |
| | 154 | + for &output in &resources.outputs { |
| | 155 | + let info = self |
| | 156 | + .conn |
| | 157 | + .inner() |
| | 158 | + .randr_get_output_info(output, resources.config_timestamp)? |
| | 159 | + .reply()?; |
| | 160 | + |
| | 161 | + let output_name = String::from_utf8_lossy(&info.name); |
| | 162 | + if output_name == name { |
| | 163 | + return Ok(output); |
| | 164 | + } |
| | 165 | + } |
| | 166 | + |
| | 167 | + Err(RandrError::OutputNotFound(name.to_string())) |
| | 168 | + } |
| | 169 | + |
| | 170 | + /// Apply a monitor configuration. |
| | 171 | + pub fn apply_monitor(&self, config: &MonitorConfig) -> Result<()> { |
| | 172 | + if !config.enabled { |
| | 173 | + return self.disable_output(&config.name); |
| | 174 | + } |
| | 175 | + |
| | 176 | + let resources = self.get_resources()?; |
| | 177 | + let output = self.find_output_by_name(&config.name)?; |
| | 178 | + |
| | 179 | + let output_info = self |
| | 180 | + .conn |
| | 181 | + .inner() |
| | 182 | + .randr_get_output_info(output, resources.config_timestamp)? |
| | 183 | + .reply()?; |
| | 184 | + |
| | 185 | + // Find matching mode |
| | 186 | + let mode_id = self.find_mode_id(&resources, &output_info.modes, config)?; |
| | 187 | + |
| | 188 | + // Find available CRTC |
| | 189 | + let crtc = self.find_available_crtc(&resources, &output_info, output)?; |
| | 190 | + |
| | 191 | + // Convert rotation |
| | 192 | + let rotation = match config.rotation { |
| | 193 | + 90 => randr::Rotation::ROTATE90, |
| | 194 | + 180 => randr::Rotation::ROTATE180, |
| | 195 | + 270 => randr::Rotation::ROTATE270, |
| | 196 | + _ => randr::Rotation::ROTATE0, |
| | 197 | + }; |
| | 198 | + |
| | 199 | + // Apply configuration |
| | 200 | + let result = self |
| | 201 | + .conn |
| | 202 | + .inner() |
| | 203 | + .randr_set_crtc_config( |
| | 204 | + crtc, |
| | 205 | + resources.timestamp, |
| | 206 | + resources.config_timestamp, |
| | 207 | + config.x as i16, |
| | 208 | + config.y as i16, |
| | 209 | + mode_id, |
| | 210 | + rotation, |
| | 211 | + &[output], |
| | 212 | + )? |
| | 213 | + .reply()?; |
| | 214 | + |
| | 215 | + if result.status != randr::SetConfig::SUCCESS { |
| | 216 | + return Err(RandrError::ConfigFailed(format!( |
| | 217 | + "set_crtc_config returned {:?}", |
| | 218 | + result.status |
| | 219 | + ))); |
| | 220 | + } |
| | 221 | + |
| | 222 | + tracing::info!( |
| | 223 | + "applied config for {}: {}x{} at ({}, {})", |
| | 224 | + config.name, |
| | 225 | + config.width, |
| | 226 | + config.height, |
| | 227 | + config.x, |
| | 228 | + config.y |
| | 229 | + ); |
| | 230 | + |
| | 231 | + Ok(()) |
| | 232 | + } |
| | 233 | + |
| | 234 | + /// Disable an output. |
| | 235 | + pub fn disable_output(&self, name: &str) -> Result<()> { |
| | 236 | + let resources = self.get_resources()?; |
| | 237 | + let output = self.find_output_by_name(name)?; |
| | 238 | + |
| | 239 | + let output_info = self |
| | 240 | + .conn |
| | 241 | + .inner() |
| | 242 | + .randr_get_output_info(output, resources.config_timestamp)? |
| | 243 | + .reply()?; |
| | 244 | + |
| | 245 | + if output_info.crtc == 0 { |
| | 246 | + // Already disabled |
| | 247 | + return Ok(()); |
| | 248 | + } |
| | 249 | + |
| | 250 | + // Disable by setting mode to 0 |
| | 251 | + self.conn |
| | 252 | + .inner() |
| | 253 | + .randr_set_crtc_config( |
| | 254 | + output_info.crtc, |
| | 255 | + resources.timestamp, |
| | 256 | + resources.config_timestamp, |
| | 257 | + 0, |
| | 258 | + 0, |
| | 259 | + 0, // mode 0 = disable |
| | 260 | + randr::Rotation::ROTATE0, |
| | 261 | + &[], |
| | 262 | + )? |
| | 263 | + .reply()?; |
| | 264 | + |
| | 265 | + tracing::info!("disabled output {}", name); |
| | 266 | + Ok(()) |
| | 267 | + } |
| | 268 | + |
| | 269 | + /// Find a matching mode ID for the given config. |
| | 270 | + fn find_mode_id( |
| | 271 | + &self, |
| | 272 | + resources: &randr::GetScreenResourcesCurrentReply, |
| | 273 | + output_modes: &[randr::Mode], |
| | 274 | + config: &MonitorConfig, |
| | 275 | + ) -> Result<randr::Mode> { |
| | 276 | + for &mode_id in output_modes { |
| | 277 | + if let Some(mode) = self.get_mode_info(resources, mode_id) { |
| | 278 | + if mode.width as u32 == config.width |
| | 279 | + && mode.height as u32 == config.height |
| | 280 | + && (mode.refresh - config.refresh).abs() < 1.0 |
| | 281 | + { |
| | 282 | + return Ok(mode_id); |
| | 283 | + } |
| | 284 | + } |
| | 285 | + } |
| | 286 | + |
| | 287 | + // Fallback: find mode with matching resolution (any refresh) |
| | 288 | + for &mode_id in output_modes { |
| | 289 | + if let Some(mode) = self.get_mode_info(resources, mode_id) { |
| | 290 | + if mode.width as u32 == config.width && mode.height as u32 == config.height { |
| | 291 | + tracing::warn!( |
| | 292 | + "exact refresh rate not found, using {}x{}@{:.1}Hz", |
| | 293 | + mode.width, |
| | 294 | + mode.height, |
| | 295 | + mode.refresh |
| | 296 | + ); |
| | 297 | + return Ok(mode_id); |
| | 298 | + } |
| | 299 | + } |
| | 300 | + } |
| | 301 | + |
| | 302 | + Err(RandrError::ModeNotFound { |
| | 303 | + width: config.width, |
| | 304 | + height: config.height, |
| | 305 | + refresh: config.refresh, |
| | 306 | + }) |
| | 307 | + } |
| | 308 | + |
| | 309 | + /// Find an available CRTC for the output. |
| | 310 | + fn find_available_crtc( |
| | 311 | + &self, |
| | 312 | + resources: &randr::GetScreenResourcesCurrentReply, |
| | 313 | + output_info: &randr::GetOutputInfoReply, |
| | 314 | + output: randr::Output, |
| | 315 | + ) -> Result<randr::Crtc> { |
| | 316 | + // First, check if output already has a CRTC |
| | 317 | + if output_info.crtc != 0 { |
| | 318 | + return Ok(output_info.crtc); |
| | 319 | + } |
| | 320 | + |
| | 321 | + // Find a free CRTC that the output can use |
| | 322 | + for &crtc in &output_info.crtcs { |
| | 323 | + let crtc_info = self |
| | 324 | + .conn |
| | 325 | + .inner() |
| | 326 | + .randr_get_crtc_info(crtc, resources.config_timestamp)? |
| | 327 | + .reply()?; |
| | 328 | + |
| | 329 | + // CRTC is free if it has no outputs |
| | 330 | + if crtc_info.outputs.is_empty() { |
| | 331 | + return Ok(crtc); |
| | 332 | + } |
| | 333 | + } |
| | 334 | + |
| | 335 | + // Try to find any CRTC we can use |
| | 336 | + for &crtc in &resources.crtcs { |
| | 337 | + let crtc_info = self |
| | 338 | + .conn |
| | 339 | + .inner() |
| | 340 | + .randr_get_crtc_info(crtc, resources.config_timestamp)? |
| | 341 | + .reply()?; |
| | 342 | + |
| | 343 | + if crtc_info.outputs.is_empty() && crtc_info.possible.contains(&output) { |
| | 344 | + return Ok(crtc); |
| | 345 | + } |
| | 346 | + } |
| | 347 | + |
| | 348 | + let name = String::from_utf8_lossy(&output_info.name).to_string(); |
| | 349 | + Err(RandrError::NoCrtcAvailable(name)) |
| | 350 | + } |
| | 351 | + |
| | 352 | + /// Flush pending X11 requests. |
| | 353 | + pub fn flush(&self) -> Result<()> { |
| | 354 | + self.conn.inner().flush()?; |
| | 355 | + Ok(()) |
| | 356 | + } |
| | 357 | +} |