Skip to content

Commit 861926a

Browse files
committed
Add details about monitors on Linux
See BoboTiG/python-mss PR BoboTiG#469 and issue BoboTiG#153. There are no plans to add similar code to the legacy Xlib backend.
1 parent 403c0e7 commit 861926a

4 files changed

Lines changed: 392 additions & 40 deletions

File tree

src/mss/linux/base.py

Lines changed: 180 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
from __future__ import annotations
22

33
from typing import TYPE_CHECKING, Any
4+
from urllib.parse import urlencode
45

56
from mss.base import MSSBase
67
from mss.exception import ScreenShotError
8+
from mss.tools import parse_edid
79

810
from . import xcb
911
from .xcb import LIB
1012

1113
if 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,168 @@ 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+
if edid_params:
275+
rv["unique_id"] = urlencode(edid_params)
276+
277+
return rv
278+
279+
@staticmethod
280+
def _choose_randr_output(outputs: Array[xcb.RandrOutput], primary_output: xcb.RandrOutput, /) -> xcb.RandrOutput:
281+
if len(outputs) == 0:
282+
msg = "No RandR outputs available"
283+
raise ScreenShotError(msg)
284+
if any(o == primary_output for o in outputs):
285+
return primary_output
286+
return outputs[0]
287+
288+
def _monitors_from_randr_monitors(self, primary_output: xcb.RandrOutput, edid_atom: xcb.Atom | None, /) -> None:
289+
if self.conn is None:
290+
msg = "Cannot identify monitors while the connection is closed"
291+
raise ScreenShotError(msg)
292+
293+
monitors_reply = xcb.randr_get_monitors(self.conn, self.drawable, 1)
294+
timestamp = monitors_reply.timestamp
295+
for randr_monitor in xcb.randr_get_monitors_monitors(monitors_reply):
296+
monitor = {
297+
"left": randr_monitor.x,
298+
"top": randr_monitor.y,
299+
"width": randr_monitor.width,
300+
"height": randr_monitor.height,
301+
}
302+
if randr_monitor.primary:
303+
monitor["is_primary"] = True
304+
305+
if randr_monitor.nOutput > 0:
306+
outputs = xcb.randr_monitor_info_outputs(randr_monitor)
307+
chosen_output = self._choose_randr_output(outputs, primary_output)
308+
monitor |= self._randr_output_ids(chosen_output, timestamp, edid_atom)
309+
310+
self._monitors.append(monitor)
311+
312+
def _monitors_from_randr_crtcs(
313+
self,
314+
randr_version: tuple[int, int],
315+
primary_output: xcb.RandrOutput,
316+
edid_atom: xcb.Atom | None,
317+
/,
318+
) -> None:
319+
if self.conn is None:
320+
msg = "Cannot identify monitors while the connection is closed"
321+
raise ScreenShotError(msg)
172322

173323
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.
176324
if hasattr(LIB.randr, "xcb_randr_get_screen_resources_current") and randr_version >= (1, 3):
177-
screen_resources = xcb.randr_get_screen_resources_current(self.conn, self.drawable.value)
325+
screen_resources = xcb.randr_get_screen_resources_current(self.conn, self.drawable)
178326
crtcs = xcb.randr_get_screen_resources_current_crtcs(screen_resources)
179327
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.
182328
screen_resources = xcb.randr_get_screen_resources(self.conn, self.drawable)
183329
crtcs = xcb.randr_get_screen_resources_crtcs(screen_resources)
330+
timestamp = screen_resources.config_timestamp
184331

185332
for crtc in crtcs:
186-
crtc_info = xcb.randr_get_crtc_info(self.conn, crtc, screen_resources.config_timestamp)
333+
crtc_info = xcb.randr_get_crtc_info(self.conn, crtc, timestamp)
187334
if crtc_info.num_outputs == 0:
188335
continue
189-
self._monitors.append(
190-
{"left": crtc_info.x, "top": crtc_info.y, "width": crtc_info.width, "height": crtc_info.height}
191-
)
336+
monitor = {
337+
"left": crtc_info.x,
338+
"top": crtc_info.y,
339+
"width": crtc_info.width,
340+
"height": crtc_info.height,
341+
}
342+
343+
outputs = xcb.randr_get_crtc_info_outputs(crtc_info)
344+
chosen_output = self._choose_randr_output(outputs, primary_output)
345+
monitor |= self._randr_output_ids(chosen_output, timestamp, edid_atom)
346+
if chosen_output == primary_output:
347+
monitor["is_primary"] = True
192348

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.
349+
self._monitors.append(monitor)
196350

197351
def _cursor_impl_check_xfixes(self) -> bool:
198352
"""Check XFixes availability and version.

src/mss/linux/xcb.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,19 @@
3131
ImageOrder,
3232
Keycode,
3333
Pixmap,
34+
RandrConnection,
3435
RandrCrtc,
3536
RandrGetCrtcInfoReply,
37+
RandrGetMonitorsReply,
38+
RandrGetOutputInfoReply,
39+
RandrGetOutputPrimaryReply,
40+
RandrGetOutputPropertyReply,
3641
RandrGetScreenResourcesCurrentReply,
3742
RandrGetScreenResourcesReply,
3843
RandrMode,
3944
RandrModeInfo,
45+
RandrMonitorInfo,
46+
RandrMonitorInfoIterator,
4047
RandrOutput,
4148
RandrQueryVersionReply,
4249
RandrSetConfig,
@@ -77,6 +84,16 @@
7784
randr_get_crtc_info,
7885
randr_get_crtc_info_outputs,
7986
randr_get_crtc_info_possible,
87+
randr_get_monitors,
88+
randr_get_monitors_monitors,
89+
randr_get_output_info,
90+
randr_get_output_info_clones,
91+
randr_get_output_info_crtcs,
92+
randr_get_output_info_modes,
93+
randr_get_output_info_name,
94+
randr_get_output_primary,
95+
randr_get_output_property,
96+
randr_get_output_property_data,
8097
randr_get_screen_resources,
8198
randr_get_screen_resources_crtcs,
8299
randr_get_screen_resources_current,
@@ -87,6 +104,7 @@
87104
randr_get_screen_resources_modes,
88105
randr_get_screen_resources_names,
89106
randr_get_screen_resources_outputs,
107+
randr_monitor_info_outputs,
90108
randr_query_version,
91109
render_pictdepth_visuals,
92110
render_pictscreen_depths,

0 commit comments

Comments
 (0)