gardesk/gardisplay / da60768

Browse files

add randr module for display configuration

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
da6076814ba6607b841483bdd6ad01f79792d10f
Parents
59c4cd1
Tree
84b1819

4 changed files

StatusFile+-
A gardisplay/src/randr/error.rs 31 0
A gardisplay/src/randr/manager.rs 357 0
A gardisplay/src/randr/mod.rs 11 0
A gardisplay/src/randr/types.rs 71 0
gardisplay/src/randr/error.rsadded
@@ -0,0 +1,31 @@
1
+//! RandR error types.
2
+
3
+use thiserror::Error;
4
+
5
+/// Errors that can occur during RandR operations.
6
+#[derive(Debug, Error)]
7
+pub enum RandrError {
8
+    #[error("output not found: {0}")]
9
+    OutputNotFound(String),
10
+
11
+    #[error("mode not found: {width}x{height}@{refresh:.1}Hz")]
12
+    ModeNotFound {
13
+        width: u32,
14
+        height: u32,
15
+        refresh: f64,
16
+    },
17
+
18
+    #[error("no available CRTC for output {0}")]
19
+    NoCrtcAvailable(String),
20
+
21
+    #[error("configuration failed: {0}")]
22
+    ConfigFailed(String),
23
+
24
+    #[error("X11 connection error: {0}")]
25
+    Connection(#[from] x11rb::errors::ConnectionError),
26
+
27
+    #[error("X11 reply error: {0}")]
28
+    Reply(#[from] x11rb::errors::ReplyError),
29
+}
30
+
31
+pub type Result<T> = std::result::Result<T, RandrError>;
gardisplay/src/randr/manager.rsadded
@@ -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
+}
gardisplay/src/randr/mod.rsadded
@@ -0,0 +1,11 @@
1
+//! RandR module for querying and applying display configurations.
2
+
3
+mod error;
4
+mod manager;
5
+mod types;
6
+
7
+#[allow(unused_imports)] // These will be used for mode selection UI
8
+pub use error::RandrError;
9
+pub use manager::RandrManager;
10
+#[allow(unused_imports)] // These will be used for mode selection UI
11
+pub use types::{ModeInfo, OutputInfo};
gardisplay/src/randr/types.rsadded
@@ -0,0 +1,71 @@
1
+//! RandR type definitions.
2
+
3
+use x11rb::protocol::randr;
4
+
5
+/// Information about a display mode (resolution + refresh rate).
6
+#[derive(Debug, Clone)]
7
+#[allow(dead_code)] // Used for mode selection UI
8
+pub struct ModeInfo {
9
+    /// X11 mode ID.
10
+    pub id: randr::Mode,
11
+    /// Width in pixels.
12
+    pub width: u16,
13
+    /// Height in pixels.
14
+    pub height: u16,
15
+    /// Refresh rate in Hz.
16
+    pub refresh: f64,
17
+}
18
+
19
+#[allow(dead_code)] // Used for mode selection UI
20
+impl ModeInfo {
21
+    /// Format as a human-readable string (e.g., "1920x1080@60Hz").
22
+    pub fn display_string(&self) -> String {
23
+        format!("{}x{}@{:.0}Hz", self.width, self.height, self.refresh)
24
+    }
25
+}
26
+
27
+/// Information about an output (monitor connector).
28
+#[derive(Debug, Clone)]
29
+#[allow(dead_code)] // Used for mode selection UI
30
+pub struct OutputInfo {
31
+    /// Output name (e.g., "eDP-1", "HDMI-1").
32
+    pub name: String,
33
+    /// X11 output ID.
34
+    pub output: randr::Output,
35
+    /// Currently assigned CRTC (if any).
36
+    pub crtc: Option<randr::Crtc>,
37
+    /// Whether the output is connected.
38
+    pub connected: bool,
39
+    /// Available modes for this output.
40
+    pub modes: Vec<ModeInfo>,
41
+    /// Currently active mode (if any).
42
+    pub current_mode: Option<ModeInfo>,
43
+    /// Current position (if active).
44
+    pub position: Option<(i16, i16)>,
45
+    /// Physical width in mm.
46
+    pub width_mm: u32,
47
+    /// Physical height in mm.
48
+    pub height_mm: u32,
49
+}
50
+
51
+#[allow(dead_code)] // Used for mode selection UI
52
+impl OutputInfo {
53
+    /// Check if the output is currently active (has a mode set).
54
+    pub fn is_active(&self) -> bool {
55
+        self.crtc.is_some() && self.current_mode.is_some()
56
+    }
57
+
58
+    /// Find a mode matching the given resolution and approximate refresh rate.
59
+    pub fn find_mode(&self, width: u32, height: u32, refresh: f64) -> Option<&ModeInfo> {
60
+        self.modes.iter().find(|m| {
61
+            m.width as u32 == width
62
+                && m.height as u32 == height
63
+                && (m.refresh - refresh).abs() < 1.0
64
+        })
65
+    }
66
+
67
+    /// Get the preferred mode (usually the first/native mode).
68
+    pub fn preferred_mode(&self) -> Option<&ModeInfo> {
69
+        self.modes.first()
70
+    }
71
+}