1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
|
# -*- coding: utf-8 -*-
import os
import re
import subprocess
import sys
from typing import Dict, List, Union
from DisplayCAL import argyll
from DisplayCAL import localization as lang
from DisplayCAL.util_dbus import BUSTYPE_SESSION, DBusException, DBusObject
from DisplayCAL.util_x import get_display as _get_x_display
_displays = None
def GetXRandROutputXID(display_no=0):
"""Return the XRandR output X11 ID of a given display.
Args:
display_no (int): Display number.
Returns:
dict:
"""
display = get_display(display_no)
if display:
return display.get("output", 0)
return 0
def RealDisplaySizeMM(display_no=0):
"""Return the size (in mm) of a given display.
Args:
display_no (int): Display number.
Returns:
(int, int): The display size in mm.
"""
if display := get_display(display_no):
return display.get("size_mm", (0, 0))
return 0, 0
class Display:
"""Store information about display."""
def __init__(self):
self.name = None
"""Display name."""
self.description = None # USED
"""Description of display or URL."""
self.xrandr_name = None # Generated from self.description
self.pos = (0, 0) # USED
"""Displays offset in pixel."""
# self.sx = None
# """Displays offset in pixels (X)."""
# self.sy = None
# """Displays offset in pixels (Y)."""
self.size = (0, 0) # USED
"""Displays width and height in pixels."""
# WINDOWS / NT
self.monid = None
"""Monitor ID."""
self.prim = None
""" NZ if primary display monitor."""
# APPLE
self.ddid = None
# UNIX
self.screen = None
"""X11 (possibly virtual) Screen."""
self.uscreen = None
"""Underlying Xinerama/XRandr screen."""
self.rscreen = None
"""Underlying RAMDAC screen (user override)."""
self.icc_atom = None
"""ICC profile root/output atom for this display."""
self.edid = None
"""128, 256 or 384 bytes of monitor EDID, NULL if none."""
self.edid_len = None
"""128, 256 or 384."""
# Xrandr stuff - output is connected 1:1 to a display
self.crtc = None
"""Associated crtc."""
self.output = None
"""Associated output."""
self.icc_out_atom = None
"""ICC profile atom for this output."""
def from_dispwin_data(self, display_info_line):
"""Parse from dispwin display list data.
Args:
display_info_line (str): The dispwin data line.
"""
display_info_line = display_info_line.strip()
description_data = re.findall(
rb"[\s\d]+= '(?P<description>.*)'", display_info_line
)
dispwin_error_message = lang.getstr(
"error.generic",
(-1, "dispwin returns no usable data while enumerating displays."),
)
if not description_data:
raise ValueError(dispwin_error_message)
self.description = description_data[0]
match = re.match(
rb"[\s]*(?P<id>\d) = '(?P<name>.*) at (?P<x>[-\d]+), (?P<y>[-\d]+), "
rb"width (?P<width>\d+), height (?P<height>\d+).*'",
display_info_line,
)
if not match:
raise ValueError(dispwin_error_message)
groups_dict = match.groupdict()
self.monid = int(groups_dict["id"])
self.name = groups_dict["name"]
# fix the name ending with "," for ArgyllCMS<3.3.0
if self.name.endswith(b","):
self.name = self.name[:-1]
x = int(groups_dict["x"])
y = int(groups_dict["y"])
self.pos = (x, y)
width = int(groups_dict["width"])
height = int(groups_dict["height"])
self.size = (width, height)
def to_dict(self):
"""Return a dictionary.
Returns:
dict: The display data as dictionary, matching the previous implementation.
"""
display_dict = {}
if self.monid is not None:
display_dict["monid"] = self.monid
if self.description is not None:
display_dict["description"] = self.description
if self.name is not None:
display_dict["name"] = self.name
if self.pos is not None:
display_dict["pos"] = self.pos
if self.size is not None:
display_dict["size"] = self.size
return display_dict
def get_dispwin_output() -> bytes:
"""Return Argyll dispwin output.
Returns:
bytes: The dispwin output.
"""
dispwin_path = argyll.get_argyll_util("dispwin")
if dispwin_path is None:
return b""
if sys.platform == "win32":
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
else:
startupinfo = None
# the invalid value of "-d0" is intentional,
# we just want to get the information we want and close the dispwin,
# if we don't supply "-d0" it will start showing color patches.
p = subprocess.Popen(
[dispwin_path, "-v", "-d0"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
startupinfo=startupinfo,
)
output, _ = p.communicate()
return output
def _enumerate_displays() -> List[dict]:
"""Generate display information data from ArgyllCMS's dispwin.
Returns:
List[dict]: A list of dictionary containing display data.
"""
displays = []
has_display = False
dispwin_output = get_dispwin_output()
for line in dispwin_output.split(b"\n"):
if has_display and b"-dweb[:port]" in line:
break
if has_display and b"=" in line:
display = Display()
display.from_dispwin_data(line)
displays.append(display.to_dict())
if not has_display and b"-d n" in line:
has_display = True
return displays
def enumerate_displays():
"""Enumerate and return a list of displays."""
global _displays
_displays = _enumerate_displays()
if _displays is None:
_displays = []
for display in _displays:
desc = display.get("description")
if not desc:
continue
match = re.findall(
rb"(.+?),? at (-?\d+), (-?\d+), width (\d+), height (\d+)", desc
)
if not len(match):
continue
# update xrandr_name from description
if sys.platform not in ("darwin", "win32"):
if (
os.getenv("XDG_SESSION_TYPE") == "wayland"
and "pos" in display
and "size" in display
):
x, y, w, h = display["pos"] + display["size"]
wayland_display = get_wayland_display(x, y, w, h)
if wayland_display:
display.update(wayland_display)
else:
xrandr_name = re.search(rb", Output (.+)", match[0][0])
if xrandr_name:
display["xrandr_name"] = xrandr_name.group(1)
desc = b"%s @ %s, %s, %sx%s" % match[0]
display["description"] = desc
return _displays
def get_display(display_no: int = 0) -> Union[None, Dict]:
"""Return display data for a given display number.
Args:
display_no (int): Display number.
Returns:
Dict: The display data.
"""
if _displays is None:
enumerate_displays()
# Ensure _displays is not None after calling enumerate_displays
if _displays is None:
return
# Translate from Argyll display index to enumerated display index using the
# coordinates and dimensions
from DisplayCAL.config import getcfg, is_virtual_display
if is_virtual_display(display_no):
return
getcfg_displays = getcfg("displays")
if len(getcfg_displays) < display_no:
return
argyll_display = getcfg_displays[display_no]
if argyll_display.endswith(" [PRIMARY]"):
argyll_display = " ".join(argyll_display.split(" ")[:-1])
for display in _displays:
desc = display.get("description")
if not desc:
continue
geometry = b"".join(desc.split(b"@ ")[-1:])
if argyll_display.endswith((b"@ " + geometry).decode("utf-8")):
return display
def get_wayland_display(x, y, w, h):
"""Find matching Wayland display.
Given x, y, width and height of display geometry, find matching Wayland display.
"""
# Note that we apparently CANNOT use width and height because the reported
# values from Argyll code and Mutter can be slightly different,
# e.g. 3660x1941 from Mutter vs 3656x1941 from Argyll when HiDPI is enabled.
# The xrandr output is also interesting in that case:
# $ xrandr
# Screen 0: minimum 320 x 200, current 3660 x 1941, maximum 8192 x 8192
# XWAYLAND0 connected 3656x1941+0+0 (normal left inverted right x axis y axis) 0mm x 0mm,B950
# 3656x1941 59.96*+
# Note the apparent mismatch between first and 2nd/3rd line.
# Look for active display at x, y instead.
# Currently, only support for GNOME 3 / Mutter
try:
iface = DBusObject(
BUSTYPE_SESSION,
"org.gnome.Mutter.DisplayConfig",
"/org/gnome/Mutter/DisplayConfig",
)
res = iface.get_resources()
except DBusException:
return
if not res or len(res) < 2:
return
# See
# https://github.com/GNOME/mutter/blob/master/src/org.gnome.Mutter.DisplayConfig.xml
output_storage = None
found = False
crtcs = res[1]
# Look for matching CRTC
for crtc in crtcs:
if len(crtc) < 7 or crtc[2:4] != (x, y) or crtc[6] == -1:
continue
# Found our CRTC
crtc_id = crtc[0]
# Look for matching output
outputs = res[2]
for output in outputs:
if len(output) < 2:
continue
if output[2] == crtc_id:
# Found our output
found = True
output_storage = output
break
if found:
break
if found and output_storage is not None and len(output_storage) > 7:
properties = output_storage[7]
wayland_display = {"xrandr_name": output_storage[4]}
raw_edid = properties.get("edid", ())
edid = b"".join(v.to_bytes(1, "big") for v in raw_edid)
if edid:
wayland_display["edid"] = edid
w_mm = properties.get("width-mm")
h_mm = properties.get("height-mm")
if w_mm and h_mm:
wayland_display["size_mm"] = (w_mm, h_mm)
return wayland_display
def get_x_display(display_no=0):
if display := get_display(display_no):
if name := display.get("name"):
return _get_x_display(name)
def get_x_icc_profile_atom_id(display_no=0):
if display := get_display(display_no):
return display.get("icc_profile_atom_id")
def get_x_icc_profile_output_atom_id(display_no=0):
if display := get_display(display_no):
return display.get("icc_profile_output_atom_id")
|