11from __future__ import annotations
22
33from typing import TYPE_CHECKING , Any
4+ from urllib .parse import urlencode
45
56from mss .base import MSSBase
67from mss .exception import ScreenShotError
8+ from mss .tools import parse_edid
79
810from . import xcb
911from .xcb import LIB
1012
1113if TYPE_CHECKING :
14+ from ctypes import Array
15+
1216 from mss .models import Monitor
1317 from mss .screenshot import ScreenShot
1418
@@ -140,8 +144,37 @@ def _monitors_impl(self) -> None:
140144 msg = "Cannot identify monitors while the connection is closed"
141145 raise ScreenShotError (msg )
142146
143- # The first entry is the whole X11 screen that the root is on. That's the one that covers all the
144- # monitors.
147+ self ._append_root_monitor ()
148+
149+ randr_version = self ._randr_get_version ()
150+ if randr_version is None or randr_version < (1 , 2 ):
151+ return
152+
153+ # XRandR terminology (very abridged, but enough for this code):
154+ # - X screen / framebuffer: the overall drawable area for this root.
155+ # - CRTC: a display controller that scans out a rectangular region of the X screen. A CRTC with zero
156+ # outputs is inactive. A CRTC may drive multiple outputs in clone/mirroring mode.
157+ # - Output: a physical connector (e.g. "HDMI-1", "DP-1"). The RandR "connection" state (connected vs
158+ # disconnected) is separate from whether the output is currently driven by a CRTC.
159+ # - Monitor (RandR 1.5+): a logical rectangle presented to clients. Monitors may be client-defined (useful
160+ # for tiled displays) and are the closest match to what MSS wants.
161+ #
162+ # This implementation prefers RandR 1.5+ Monitors when available; otherwise it falls back to enumerating
163+ # active CRTCs.
164+
165+ primary_output = self ._randr_get_primary_output (randr_version )
166+ edid_atom = self ._randr_get_edid_atom ()
167+
168+ if randr_version >= (1 , 5 ):
169+ self ._monitors_from_randr_monitors (primary_output , edid_atom )
170+ else :
171+ self ._monitors_from_randr_crtcs (randr_version , primary_output , edid_atom )
172+
173+ def _append_root_monitor (self ) -> None :
174+ if self .conn is None :
175+ msg = "Cannot identify monitors while the connection is closed"
176+ raise ScreenShotError (msg )
177+
145178 root_geom = xcb .get_geometry (self .conn , self .root )
146179 self ._monitors .append (
147180 {
@@ -152,47 +185,164 @@ def _monitors_impl(self) -> None:
152185 }
153186 )
154187
155- # After that, we have one for each monitor on that X11 screen. For decades, that's been handled by
156- # Xrandr. We don't presently try to work with Xinerama. So, we're going to check the different outputs,
157- # according to Xrandr. If that fails, we'll just leave the one root covering everything.
188+ def _randr_get_version (self ) -> tuple [int , int ] | None :
189+ if self .conn is None :
190+ msg = "Cannot identify monitors while the connection is closed"
191+ raise ScreenShotError (msg )
158192
159- # Make sure we have the Xrandr extension we need. This will query the cache that we started populating in
160- # __init__.
161193 randr_ext_data = xcb .get_extension_data (self .conn , LIB .randr_id )
162194 if not randr_ext_data .present :
163- return
195+ return None
164196
165- # We ask the server to give us anything up to the version we support (i.e., what we expect the reply
166- # structs to look like). If the server only supports 1.2, then that's what it'll give us, and we're ok
167- # with that, but we also use a faster path if the server implements at least 1.3.
168197 randr_version_data = xcb .randr_query_version (self .conn , xcb .RANDR_MAJOR_VERSION , xcb .RANDR_MINOR_VERSION )
169- randr_version = (randr_version_data .major_version , randr_version_data .minor_version )
170- if randr_version < (1 , 2 ):
171- return
198+ return (randr_version_data .major_version , randr_version_data .minor_version )
199+
200+ def _randr_get_primary_output (self , randr_version : tuple [int , int ], / ) -> xcb .RandrOutput :
201+ if self .conn is None :
202+ msg = "Cannot identify monitors while the connection is closed"
203+ raise ScreenShotError (msg )
204+
205+ if randr_version >= (1 , 3 ):
206+ primary_output_data = xcb .randr_get_output_primary (self .conn , self .drawable )
207+ return primary_output_data .output
208+ return xcb .RandrOutput (0 )
209+
210+ def _randr_get_edid_atom (self ) -> xcb .Atom | None :
211+ if self .conn is None :
212+ msg = "Cannot identify monitors while the connection is closed"
213+ raise ScreenShotError (msg )
214+
215+ edid_atom = xcb .intern_atom (self .conn , "EDID" , only_if_exists = True )
216+ if edid_atom is not None :
217+ return edid_atom
218+
219+ # Formerly, "EDID" was known as "EdidData". I don't know when it changed.
220+ return xcb .intern_atom (self .conn , "EdidData" , only_if_exists = True )
221+
222+ def _randr_output_ids (
223+ self ,
224+ output : xcb .RandrOutput ,
225+ timestamp : xcb .Timestamp ,
226+ edid_atom : xcb .Atom | None ,
227+ / ,
228+ ) -> dict [str , Any ]:
229+ if self .conn is None :
230+ msg = "Cannot identify monitors while the connection is closed"
231+ raise ScreenShotError (msg )
232+
233+ output_info = xcb .randr_get_output_info (self .conn , output , timestamp )
234+ if output_info .status != 0 :
235+ msg = "Display configuration changed while detecting monitors."
236+ raise ScreenShotError (msg )
237+
238+ rv : dict [str , Any ] = {}
239+
240+ output_name_arr = xcb .randr_get_output_info_name (output_info )
241+ rv ["output" ] = bytes (output_name_arr ).decode ("utf_8" , errors = "replace" )
242+
243+ if edid_atom is not None :
244+ edid_prop = xcb .randr_get_output_property (
245+ self .conn , # connection
246+ output , # output
247+ edid_atom , # property
248+ xcb .XCB_NONE , # property type: Any
249+ 0 , # long-offset: 0
250+ 1024 , # long-length: in 4-byte units; 4k is plenty for an EDID
251+ 0 , # delete: false
252+ 0 , # pending: false
253+ )
254+ if edid_prop .type_ .value != 0 :
255+ edid_block = bytes (xcb .randr_get_output_property_data (edid_prop ))
256+ edid_data = parse_edid (edid_block )
257+ if "display_name" in edid_data :
258+ rv ["name" ] = edid_data ["display_name" ]
259+
260+ edid_params : dict [str , str ] = {}
261+ if "id_legacy" in edid_data :
262+ edid_params ["model" ] = edid_data ["id_legacy" ]
263+ if "serial_number" in edid_data :
264+ edid_params ["serial" ] = str (edid_data ["serial_number" ])
265+ if "manufacture_year" in edid_data :
266+ if "manufacture_week" in edid_data :
267+ edid_params ["mfr_date" ] = (
268+ f"{ edid_data ['manufacture_year' ]:04d} W{ edid_data ['manufacture_week' ]:02d} "
269+ )
270+ else :
271+ edid_params ["mfr_date" ] = f"{ edid_data ['manufacture_year' ]:04d} "
272+ if "model_year" in edid_data :
273+ edid_params ["model_year" ] = f"{ edid_data ['model_year' ]:04d} "
274+ rv ["unique_id" ] = urlencode (edid_params )
275+
276+ return rv
277+
278+ @staticmethod
279+ def _choose_randr_output (outputs : Array [xcb .RandrOutput ], primary_output : xcb .RandrOutput , / ) -> xcb .RandrOutput :
280+ if any (o .value == primary_output .value for o in outputs ):
281+ return primary_output
282+ return outputs [0 ]
283+
284+ def _monitors_from_randr_monitors (self , primary_output : xcb .RandrOutput , edid_atom : xcb .Atom | None , / ) -> None :
285+ if self .conn is None :
286+ msg = "Cannot identify monitors while the connection is closed"
287+ raise ScreenShotError (msg )
288+
289+ monitors_reply = xcb .randr_get_monitors (self .conn , self .drawable .value , 1 )
290+ timestamp = monitors_reply .timestamp
291+ for randr_monitor in xcb .randr_get_monitors_monitors (monitors_reply ):
292+ monitor = {
293+ "left" : randr_monitor .x ,
294+ "top" : randr_monitor .y ,
295+ "width" : randr_monitor .width ,
296+ "height" : randr_monitor .height ,
297+ }
298+ if randr_monitor .primary :
299+ monitor ["is_primary" ] = True
300+
301+ if randr_monitor .nOutput > 0 :
302+ outputs = xcb .randr_monitor_info_outputs (randr_monitor )
303+ chosen_output = self ._choose_randr_output (outputs , primary_output )
304+ monitor |= self ._randr_output_ids (chosen_output , timestamp , edid_atom )
305+
306+ self ._monitors .append (monitor )
307+
308+ def _monitors_from_randr_crtcs (
309+ self ,
310+ randr_version : tuple [int , int ],
311+ primary_output : xcb .RandrOutput ,
312+ edid_atom : xcb .Atom | None ,
313+ / ,
314+ ) -> None :
315+ if self .conn is None :
316+ msg = "Cannot identify monitors while the connection is closed"
317+ raise ScreenShotError (msg )
172318
173319 screen_resources : xcb .RandrGetScreenResourcesReply | xcb .RandrGetScreenResourcesCurrentReply
174- # Check to see if we have the xcb_randr_get_screen_resources_current function in libxcb-randr, and that
175- # the server supports it.
176320 if hasattr (LIB .randr , "xcb_randr_get_screen_resources_current" ) and randr_version >= (1 , 3 ):
177321 screen_resources = xcb .randr_get_screen_resources_current (self .conn , self .drawable .value )
178322 crtcs = xcb .randr_get_screen_resources_current_crtcs (screen_resources )
179323 else :
180- # Either the client or the server doesn't support the _current form. That's ok; we'll use the old
181- # function, which forces a new query to the physical monitors.
182324 screen_resources = xcb .randr_get_screen_resources (self .conn , self .drawable )
183325 crtcs = xcb .randr_get_screen_resources_crtcs (screen_resources )
326+ timestamp = screen_resources .config_timestamp
184327
185328 for crtc in crtcs :
186- crtc_info = xcb .randr_get_crtc_info (self .conn , crtc , screen_resources . config_timestamp )
329+ crtc_info = xcb .randr_get_crtc_info (self .conn , crtc , timestamp )
187330 if crtc_info .num_outputs == 0 :
188331 continue
189- self ._monitors .append (
190- {"left" : crtc_info .x , "top" : crtc_info .y , "width" : crtc_info .width , "height" : crtc_info .height }
191- )
332+ monitor = {
333+ "left" : crtc_info .x ,
334+ "top" : crtc_info .y ,
335+ "width" : crtc_info .width ,
336+ "height" : crtc_info .height ,
337+ }
338+
339+ outputs = xcb .randr_get_crtc_info_outputs (crtc_info )
340+ chosen_output = self ._choose_randr_output (outputs , primary_output )
341+ monitor |= self ._randr_output_ids (chosen_output , timestamp , edid_atom )
342+ if chosen_output .value == primary_output .value :
343+ monitor ["is_primary" ] = True
192344
193- # Extra credit would be to enumerate the virtual desktops; see
194- # https://specifications.freedesktop.org/wm/latest/ar01s03.html. But I don't know how widely-used that
195- # style is.
345+ self ._monitors .append (monitor )
196346
197347 def _cursor_impl_check_xfixes (self ) -> bool :
198348 """Check XFixes availability and version.
0 commit comments