Rust · 6110 bytes Raw Blame History
1 //! Multi-monitor detection using XRandR
2 //!
3 //! Detects connected monitors and their positions for proper UI placement.
4
5 use anyhow::{Context, Result};
6 use x11rb::connection::Connection;
7 use x11rb::protocol::randr::{self, ConnectionExt as RandrConnectionExt};
8 use x11rb::protocol::xproto::Window;
9 use x11rb::rust_connection::RustConnection;
10
11 /// Information about a connected monitor
12 #[derive(Debug, Clone)]
13 pub struct Monitor {
14 /// X position in virtual screen
15 pub x: i16,
16 /// Y position in virtual screen
17 pub y: i16,
18 /// Width in pixels
19 pub width: u16,
20 /// Height in pixels
21 pub height: u16,
22 /// Whether this is the primary monitor
23 pub primary: bool,
24 /// Monitor name (e.g., "DP-1", "HDMI-A-1")
25 pub name: String,
26 }
27
28 impl Monitor {
29 /// Get the center X coordinate of this monitor
30 pub fn center_x(&self) -> f64 {
31 self.x as f64 + self.width as f64 / 2.0
32 }
33
34 /// Get the center Y coordinate of this monitor
35 pub fn center_y(&self) -> f64 {
36 self.y as f64 + self.height as f64 / 2.0
37 }
38 }
39
40 /// Detected monitor configuration
41 #[derive(Debug, Clone)]
42 pub struct MonitorConfig {
43 /// All connected monitors
44 pub monitors: Vec<Monitor>,
45 /// Total virtual screen width
46 pub total_width: u16,
47 /// Total virtual screen height
48 pub total_height: u16,
49 }
50
51 impl MonitorConfig {
52 /// Detect monitors using RandR
53 pub fn detect(conn: &RustConnection, root: Window) -> Result<Self> {
54 // Get screen resources
55 let resources = conn
56 .randr_get_screen_resources(root)
57 .context("Failed to get screen resources")?
58 .reply()
59 .context("Failed to get screen resources reply")?;
60
61 // Get primary output
62 let primary_output = conn
63 .randr_get_output_primary(root)
64 .context("Failed to get primary output")?
65 .reply()
66 .context("Failed to get primary output reply")?
67 .output;
68
69 let mut monitors = Vec::new();
70
71 for output in &resources.outputs {
72 let output_info = match conn.randr_get_output_info(*output, 0) {
73 Ok(cookie) => match cookie.reply() {
74 Ok(info) => info,
75 Err(_) => continue,
76 },
77 Err(_) => continue,
78 };
79
80 // Skip disconnected outputs
81 if output_info.connection != randr::Connection::CONNECTED {
82 continue;
83 }
84
85 // Skip outputs without a CRTC (not active)
86 let crtc = match output_info.crtc {
87 0 => continue,
88 c => c,
89 };
90
91 let crtc_info = match conn.randr_get_crtc_info(crtc, 0) {
92 Ok(cookie) => match cookie.reply() {
93 Ok(info) => info,
94 Err(_) => continue,
95 },
96 Err(_) => continue,
97 };
98
99 // Skip CRTCs with zero dimensions
100 if crtc_info.width == 0 || crtc_info.height == 0 {
101 continue;
102 }
103
104 let name = String::from_utf8_lossy(&output_info.name).to_string();
105 let is_primary = *output == primary_output;
106
107 monitors.push(Monitor {
108 x: crtc_info.x,
109 y: crtc_info.y,
110 width: crtc_info.width,
111 height: crtc_info.height,
112 primary: is_primary,
113 name,
114 });
115 }
116
117 // Calculate total virtual screen size
118 let (total_width, total_height) = if monitors.is_empty() {
119 // Fallback to root window size
120 let screen = &conn.setup().roots[0];
121 (screen.width_in_pixels, screen.height_in_pixels)
122 } else {
123 let max_x = monitors
124 .iter()
125 .map(|m| m.x as i32 + m.width as i32)
126 .max()
127 .unwrap_or(0);
128 let max_y = monitors
129 .iter()
130 .map(|m| m.y as i32 + m.height as i32)
131 .max()
132 .unwrap_or(0);
133 (max_x as u16, max_y as u16)
134 };
135
136 // If no primary is set, mark the largest monitor as primary
137 if !monitors.iter().any(|m| m.primary) && !monitors.is_empty() {
138 let largest_idx = monitors
139 .iter()
140 .enumerate()
141 .max_by_key(|(_, m)| m.width as u32 * m.height as u32)
142 .map(|(i, _)| i)
143 .unwrap_or(0);
144 monitors[largest_idx].primary = true;
145 }
146
147 tracing::info!(
148 count = monitors.len(),
149 total_width,
150 total_height,
151 "Detected monitors"
152 );
153
154 for monitor in &monitors {
155 tracing::debug!(
156 name = %monitor.name,
157 x = monitor.x,
158 y = monitor.y,
159 width = monitor.width,
160 height = monitor.height,
161 primary = monitor.primary,
162 "Monitor"
163 );
164 }
165
166 Ok(Self {
167 monitors,
168 total_width,
169 total_height,
170 })
171 }
172
173 /// Get the primary monitor
174 pub fn primary(&self) -> Option<&Monitor> {
175 self.monitors.iter().find(|m| m.primary)
176 }
177
178 /// Get the primary monitor, or the first one if no primary
179 pub fn primary_or_first(&self) -> Option<&Monitor> {
180 self.primary().or_else(|| self.monitors.first())
181 }
182
183 /// Check if this is a single-monitor setup
184 pub fn is_single_monitor(&self) -> bool {
185 self.monitors.len() <= 1
186 }
187 }
188
189 /// Fallback monitor config when RandR fails
190 pub fn fallback_config(screen_width: u16, screen_height: u16) -> MonitorConfig {
191 MonitorConfig {
192 monitors: vec![Monitor {
193 x: 0,
194 y: 0,
195 width: screen_width,
196 height: screen_height,
197 primary: true,
198 name: "default".to_string(),
199 }],
200 total_width: screen_width,
201 total_height: screen_height,
202 }
203 }
204