mirror of
https://github.com/kellyjonbrazil/jc.git
synced 2025-06-17 00:07:37 +02:00
[xrandr] Allow props command (#540)
* [xrandr] Allow props command Responding to issue #525 Somewhat substantial rewriting here to make the parser more resilient - Change parser to not mutate the incoming data list, instead index - Create `Line` class and `categorize` classmethod - Every line is categorized and regexed, so it gets dispatched to the right level of responsibility * Bump version --------- Co-authored-by: Kelly Brazil <kellyjonbrazil@gmail.com>
This commit is contained in:
@ -8,6 +8,16 @@ from typing import ByteString
|
|||||||
|
|
||||||
__all__ = ["Edid"]
|
__all__ = ["Edid"]
|
||||||
|
|
||||||
|
# EDID:
|
||||||
|
# 00ffffffffffff004ca3523100000000
|
||||||
|
# 0014010380221378eac8959e57549226
|
||||||
|
# 0f505400000001010101010101010101
|
||||||
|
# 010101010101381d56d4500016303020
|
||||||
|
# 250058c2100000190000000f00000000
|
||||||
|
# 000000000025d9066a00000000fe0053
|
||||||
|
# 414d53554e470a204ca34154000000fe
|
||||||
|
# 004c544e313536415432343430310018
|
||||||
|
|
||||||
|
|
||||||
class Edid:
|
class Edid:
|
||||||
"""Edid class
|
"""Edid class
|
||||||
@ -64,36 +74,39 @@ class Edid:
|
|||||||
|
|
||||||
_ASPECT_RATIOS = {
|
_ASPECT_RATIOS = {
|
||||||
0b00: (16, 10),
|
0b00: (16, 10),
|
||||||
0b01: ( 4, 3),
|
0b01: (4, 3),
|
||||||
0b10: ( 5, 4),
|
0b10: (5, 4),
|
||||||
0b11: (16, 9),
|
0b11: (16, 9),
|
||||||
}
|
}
|
||||||
|
|
||||||
_RawEdid = namedtuple("RawEdid",
|
_RawEdid = namedtuple(
|
||||||
("header",
|
"RawEdid",
|
||||||
"manu_id",
|
(
|
||||||
"prod_id",
|
"header",
|
||||||
"serial_no",
|
"manu_id",
|
||||||
"manu_week",
|
"prod_id",
|
||||||
"manu_year",
|
"serial_no",
|
||||||
"edid_version",
|
"manu_week",
|
||||||
"edid_revision",
|
"manu_year",
|
||||||
"input_type",
|
"edid_version",
|
||||||
"width",
|
"edid_revision",
|
||||||
"height",
|
"input_type",
|
||||||
"gamma",
|
"width",
|
||||||
"features",
|
"height",
|
||||||
"color",
|
"gamma",
|
||||||
"timings_supported",
|
"features",
|
||||||
"timings_reserved",
|
"color",
|
||||||
"timings_edid",
|
"timings_supported",
|
||||||
"timing_1",
|
"timings_reserved",
|
||||||
"timing_2",
|
"timings_edid",
|
||||||
"timing_3",
|
"timing_1",
|
||||||
"timing_4",
|
"timing_2",
|
||||||
"extension",
|
"timing_3",
|
||||||
"checksum")
|
"timing_4",
|
||||||
)
|
"extension",
|
||||||
|
"checksum",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, edid: ByteString):
|
def __init__(self, edid: ByteString):
|
||||||
self._parse_edid(edid)
|
self._parse_edid(edid)
|
||||||
@ -109,18 +122,20 @@ class Edid:
|
|||||||
unpacked = struct.unpack(self._STRUCT_FORMAT, edid)
|
unpacked = struct.unpack(self._STRUCT_FORMAT, edid)
|
||||||
raw_edid = self._RawEdid(*unpacked)
|
raw_edid = self._RawEdid(*unpacked)
|
||||||
|
|
||||||
if raw_edid.header != b'\x00\xff\xff\xff\xff\xff\xff\x00':
|
if raw_edid.header != b"\x00\xff\xff\xff\xff\xff\xff\x00":
|
||||||
raise ValueError("Invalid header.")
|
raise ValueError("Invalid header.")
|
||||||
|
|
||||||
self.raw = edid
|
self.raw = edid
|
||||||
self.manufacturer_id = raw_edid.manu_id
|
self.manufacturer_id = raw_edid.manu_id
|
||||||
self.product = raw_edid.prod_id
|
self.product = raw_edid.prod_id
|
||||||
self.year = raw_edid.manu_year + 1990
|
self.year = raw_edid.manu_year + 1990
|
||||||
self.edid_version = "{:d}.{:d}".format(raw_edid.edid_version, raw_edid.edid_revision)
|
self.edid_version = "{:d}.{:d}".format(
|
||||||
|
raw_edid.edid_version, raw_edid.edid_revision
|
||||||
|
)
|
||||||
self.type = "digital" if (raw_edid.input_type & 0xFF) else "analog"
|
self.type = "digital" if (raw_edid.input_type & 0xFF) else "analog"
|
||||||
self.width = float(raw_edid.width)
|
self.width = float(raw_edid.width)
|
||||||
self.height = float(raw_edid.height)
|
self.height = float(raw_edid.height)
|
||||||
self.gamma = (raw_edid.gamma+100)/100
|
self.gamma = (raw_edid.gamma + 100) / 100
|
||||||
self.dpms_standby = bool(raw_edid.features & 0xFF)
|
self.dpms_standby = bool(raw_edid.features & 0xFF)
|
||||||
self.dpms_suspend = bool(raw_edid.features & 0x7F)
|
self.dpms_suspend = bool(raw_edid.features & 0x7F)
|
||||||
self.dpms_activeoff = bool(raw_edid.features & 0x3F)
|
self.dpms_activeoff = bool(raw_edid.features & 0x3F)
|
||||||
@ -132,22 +147,27 @@ class Edid:
|
|||||||
self.resolutions.append(self._TIMINGS[i])
|
self.resolutions.append(self._TIMINGS[i])
|
||||||
|
|
||||||
for i in range(8):
|
for i in range(8):
|
||||||
bytes_data = raw_edid.timings_edid[2*i:2*i+2]
|
bytes_data = raw_edid.timings_edid[2 * i : 2 * i + 2]
|
||||||
if bytes_data == b'\x01\x01':
|
if bytes_data == b"\x01\x01":
|
||||||
continue
|
continue
|
||||||
byte1, byte2 = bytes_data
|
byte1, byte2 = bytes_data
|
||||||
x_res = 8*(int(byte1)+31)
|
x_res = 8 * (int(byte1) + 31)
|
||||||
aspect_ratio = self._ASPECT_RATIOS[(byte2>>6) & 0b11]
|
aspect_ratio = self._ASPECT_RATIOS[(byte2 >> 6) & 0b11]
|
||||||
y_res = int(x_res * aspect_ratio[1]/aspect_ratio[0])
|
y_res = int(x_res * aspect_ratio[1] / aspect_ratio[0])
|
||||||
rate = (int(byte2) & 0b00111111) + 60.0
|
rate = (int(byte2) & 0b00111111) + 60.0
|
||||||
self.resolutions.append((x_res, y_res, rate))
|
self.resolutions.append((x_res, y_res, rate))
|
||||||
|
|
||||||
self.name = None
|
self.name = None
|
||||||
self.serial = None
|
self.serial = None
|
||||||
|
|
||||||
for timing_bytes in (raw_edid.timing_1, raw_edid.timing_2, raw_edid.timing_3, raw_edid.timing_4):
|
for timing_bytes in (
|
||||||
|
raw_edid.timing_1,
|
||||||
|
raw_edid.timing_2,
|
||||||
|
raw_edid.timing_3,
|
||||||
|
raw_edid.timing_4,
|
||||||
|
):
|
||||||
# "other" descriptor
|
# "other" descriptor
|
||||||
if timing_bytes[0:2] == b'\x00\x00':
|
if timing_bytes[0:2] == b"\x00\x00":
|
||||||
timing_type = timing_bytes[3]
|
timing_type = timing_bytes[3]
|
||||||
if timing_type in (0xFF, 0xFE, 0xFC):
|
if timing_type in (0xFF, 0xFE, 0xFC):
|
||||||
buffer = timing_bytes[5:]
|
buffer = timing_bytes[5:]
|
||||||
|
@ -28,7 +28,7 @@ Schema:
|
|||||||
"maximum_height": integer,
|
"maximum_height": integer,
|
||||||
"devices": [
|
"devices": [
|
||||||
{
|
{
|
||||||
"modes": [
|
"resolution_modes": [
|
||||||
{
|
{
|
||||||
"resolution_width": integer,
|
"resolution_width": integer,
|
||||||
"resolution_height": integer,
|
"resolution_height": integer,
|
||||||
@ -77,7 +77,7 @@ Examples:
|
|||||||
"maximum_height": 32767,
|
"maximum_height": 32767,
|
||||||
"devices": [
|
"devices": [
|
||||||
{
|
{
|
||||||
"modes": [
|
"resolution_modes": [
|
||||||
{
|
{
|
||||||
"resolution_width": 1920,
|
"resolution_width": 1920,
|
||||||
"resolution_height": 1080,
|
"resolution_height": 1080,
|
||||||
@ -138,7 +138,7 @@ Examples:
|
|||||||
"maximum_height": 32767,
|
"maximum_height": 32767,
|
||||||
"devices": [
|
"devices": [
|
||||||
{
|
{
|
||||||
"modes": [
|
"resolution_modes": [
|
||||||
{
|
{
|
||||||
"resolution_width": 1920,
|
"resolution_width": 1920,
|
||||||
"resolution_height": 1080,
|
"resolution_height": 1080,
|
||||||
@ -189,16 +189,27 @@ Examples:
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
from collections import defaultdict
|
||||||
|
from enum import Enum
|
||||||
import re
|
import re
|
||||||
from typing import Dict, List, Optional, Union
|
from typing import Dict, List, Tuple, Union
|
||||||
|
|
||||||
import jc.utils
|
import jc.utils
|
||||||
from jc.parsers.pyedid.edid import Edid
|
from jc.parsers.pyedid.edid import Edid
|
||||||
from jc.parsers.pyedid.helpers.edid_helper import EdidHelper
|
from jc.parsers.pyedid.helpers.edid_helper import EdidHelper
|
||||||
|
|
||||||
|
Match = None
|
||||||
|
try:
|
||||||
|
# Added Python 3.7
|
||||||
|
Match = re.Match
|
||||||
|
except AttributeError:
|
||||||
|
Match = type(re.match("", ""))
|
||||||
|
|
||||||
|
|
||||||
class info:
|
class info:
|
||||||
"""Provides parser metadata (version, author, etc.)"""
|
"""Provides parser metadata (version, author, etc.)"""
|
||||||
version = "1.4"
|
|
||||||
|
version = "2.0"
|
||||||
description = "`xrandr` command parser"
|
description = "`xrandr` command parser"
|
||||||
author = "Kevin Lyter"
|
author = "Kevin Lyter"
|
||||||
author_email = "code (at) lyterk.com"
|
author_email = "code (at) lyterk.com"
|
||||||
@ -210,36 +221,10 @@ class info:
|
|||||||
|
|
||||||
__version__ = info.version
|
__version__ = info.version
|
||||||
|
|
||||||
# keep parsing state so we know which parsers have already tried the line
|
# NOTE: When developing, comment out the try statement and catch block to get
|
||||||
# Structure is:
|
# TypedDict type hints and valid type errors.
|
||||||
# {
|
|
||||||
# <line_string>: [
|
|
||||||
# <parser_string>
|
|
||||||
# ]
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# Where <line_string> is the xrandr output line to be checked and <parser_string>
|
|
||||||
# can contain "screen", "device", or "model"
|
|
||||||
parse_state: Dict[str, List] = {}
|
|
||||||
|
|
||||||
|
|
||||||
def _was_parsed(line: str, parser: str) -> bool:
|
|
||||||
"""
|
|
||||||
Check if entered parser has already parsed. If so return True.
|
|
||||||
If not, return false and add the parser to the list for the line entry.
|
|
||||||
"""
|
|
||||||
if line in parse_state:
|
|
||||||
if parser in parse_state[line]:
|
|
||||||
return True
|
|
||||||
|
|
||||||
parse_state[line].append(parser)
|
|
||||||
return False
|
|
||||||
|
|
||||||
parse_state[line] = [parser]
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Added in Python 3.8
|
||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
|
|
||||||
Frequency = TypedDict(
|
Frequency = TypedDict(
|
||||||
@ -250,8 +235,8 @@ try:
|
|||||||
"is_preferred": bool,
|
"is_preferred": bool,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
Mode = TypedDict(
|
ResolutionMode = TypedDict(
|
||||||
"Mode",
|
"ResolutionMode",
|
||||||
{
|
{
|
||||||
"resolution_width": int,
|
"resolution_width": int,
|
||||||
"resolution_height": int,
|
"resolution_height": int,
|
||||||
@ -259,14 +244,15 @@ try:
|
|||||||
"frequencies": List[Frequency],
|
"frequencies": List[Frequency],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
Model = TypedDict(
|
EdidModel = TypedDict(
|
||||||
"Model",
|
"EdidModel",
|
||||||
{
|
{
|
||||||
"name": str,
|
"name": str,
|
||||||
"product_id": str,
|
"product_id": str,
|
||||||
"serial_number": str,
|
"serial_number": str,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
Props = Dict[str, Union[List[str], EdidModel]]
|
||||||
Device = TypedDict(
|
Device = TypedDict(
|
||||||
"Device",
|
"Device",
|
||||||
{
|
{
|
||||||
@ -282,7 +268,8 @@ try:
|
|||||||
"offset_height": int,
|
"offset_height": int,
|
||||||
"dimension_width": int,
|
"dimension_width": int,
|
||||||
"dimension_height": int,
|
"dimension_height": int,
|
||||||
"modes": List[Mode],
|
"props": Props,
|
||||||
|
"resolution_modes": List[ResolutionMode],
|
||||||
"rotation": str,
|
"rotation": str,
|
||||||
"reflection": str,
|
"reflection": str,
|
||||||
},
|
},
|
||||||
@ -307,12 +294,13 @@ try:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
Screen = Dict[str, Union[int, str]]
|
EdidModel = Dict[str, str]
|
||||||
Device = Dict[str, Union[str, int, bool]]
|
Props = Dict[str, Union[List[str], EdidModel]]
|
||||||
Frequency = Dict[str, Union[float, bool]]
|
Frequency = Dict[str, Union[float, bool]]
|
||||||
Mode = Dict[str, Union[int, bool, List[Frequency]]]
|
ResolutionMode = Dict[str, Union[int, bool, List[Frequency]]]
|
||||||
Model = Dict[str, str]
|
Device = Dict[str, Union[str, int, bool, List[ResolutionMode]]]
|
||||||
Response = Dict[str, Union[Device, Mode, Screen]]
|
Screen = Dict[str, Union[int, List[Device]]]
|
||||||
|
Response = Dict[str, Screen]
|
||||||
|
|
||||||
|
|
||||||
_screen_pattern = (
|
_screen_pattern = (
|
||||||
@ -323,33 +311,6 @@ _screen_pattern = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _parse_screen(next_lines: List[str]) -> Optional[Screen]:
|
|
||||||
next_line = next_lines.pop()
|
|
||||||
|
|
||||||
if _was_parsed(next_line, 'screen'):
|
|
||||||
return None
|
|
||||||
|
|
||||||
result = re.match(_screen_pattern, next_line)
|
|
||||||
if not result:
|
|
||||||
next_lines.append(next_line)
|
|
||||||
return None
|
|
||||||
|
|
||||||
raw_matches = result.groupdict()
|
|
||||||
|
|
||||||
screen: Screen = {"devices": []}
|
|
||||||
for k, v in raw_matches.items():
|
|
||||||
screen[k] = int(v)
|
|
||||||
|
|
||||||
while next_lines:
|
|
||||||
device: Optional[Device] = _parse_device(next_lines)
|
|
||||||
if not device:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
screen["devices"].append(device)
|
|
||||||
|
|
||||||
return screen
|
|
||||||
|
|
||||||
|
|
||||||
# eDP1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis)
|
# eDP1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis)
|
||||||
# 310mm x 170mm
|
# 310mm x 170mm
|
||||||
# regex101 demo link
|
# regex101 demo link
|
||||||
@ -365,25 +326,106 @@ _device_pattern = (
|
|||||||
+ r"( ?((?P<dimension_width>\d+)mm x (?P<dimension_height>\d+)mm)?)?"
|
+ r"( ?((?P<dimension_width>\d+)mm x (?P<dimension_height>\d+)mm)?)?"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 1920x1080i 60.03*+ 59.93
|
||||||
|
# 1920x1080 60.00 + 50.00 59.94
|
||||||
|
_resolution_mode_pattern = r"\s*(?P<resolution_width>\d+)x(?P<resolution_height>\d+)(?P<is_high_resolution>i)?\s+(?P<rest>.*)"
|
||||||
|
_frequencies_pattern = r"(((?P<frequency>\d+\.\d+)(?P<star>\*| |)(?P<plus>\+?)?)+)"
|
||||||
|
|
||||||
def _parse_device(next_lines: List[str], quiet: bool = False) -> Optional[Device]:
|
|
||||||
if not next_lines:
|
|
||||||
return None
|
|
||||||
|
|
||||||
next_line = next_lines.pop()
|
# Values sometimes appear on the same lines as the keys (CscMatrix), sometimes on the line
|
||||||
|
# below (as with EDIDs), and sometimes both (CTM).
|
||||||
|
# Capture the key line that way.
|
||||||
|
#
|
||||||
|
# CTM: 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0
|
||||||
|
# 0 1
|
||||||
|
# CscMatrix: 65536 0 0 0 0 65536 0 0 0 0 65536 0
|
||||||
|
# EDID:
|
||||||
|
# 00ffffffffffff0010ac33424c303541
|
||||||
|
# 0f210104b53c22783eee95a3544c9926
|
||||||
|
_prop_key_pattern = r"\s+(?P<key>[\w| |\-|_]+):\s?(?P<maybe_value>.*)"
|
||||||
|
|
||||||
if _was_parsed(next_line, 'device'):
|
|
||||||
return None
|
|
||||||
|
|
||||||
result = re.match(_device_pattern, next_line)
|
class LineType(Enum):
|
||||||
if not result:
|
Screen = 1
|
||||||
next_lines.append(next_line)
|
Device = 2
|
||||||
return None
|
ResolutionMode = 3
|
||||||
|
PropKey = 4
|
||||||
|
PropValue = 5
|
||||||
|
Invalid = 6
|
||||||
|
|
||||||
matches = result.groupdict()
|
|
||||||
|
class Line:
|
||||||
|
"""Provide metadata about line to make handling it more simple across fn boundaries"""
|
||||||
|
|
||||||
|
def __init__(self, s: str, t: LineType, m: Match):
|
||||||
|
self.s = s
|
||||||
|
self.t = t
|
||||||
|
self.m = m
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def categorize(cls, line: str) -> "Line":
|
||||||
|
"""Iterate through line char by char to see what type of line it is. Apply regexes for more distinctness. Save the regexes and return them for later processing."""
|
||||||
|
i = 0
|
||||||
|
tab_count = 0
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
c = line[i]
|
||||||
|
except:
|
||||||
|
# Really shouldn't be getting to the end of the line
|
||||||
|
raise Exception(f"Reached end of line unexpectedly: '{line}'")
|
||||||
|
|
||||||
|
if not c.isspace():
|
||||||
|
if tab_count == 0:
|
||||||
|
screen_match = re.match(_screen_pattern, line)
|
||||||
|
if screen_match:
|
||||||
|
return cls(line, LineType.Screen, screen_match)
|
||||||
|
|
||||||
|
device_match = re.match(_device_pattern, line)
|
||||||
|
if device_match:
|
||||||
|
return cls(line, LineType.Device, device_match)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
elif tab_count == 1:
|
||||||
|
match = re.match(_prop_key_pattern, line)
|
||||||
|
if match:
|
||||||
|
return cls(line, LineType.PropKey, match)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
match = re.match(r"\s+(.*)\s+", line)
|
||||||
|
if match:
|
||||||
|
return cls(line, LineType.PropValue, match)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if c == " ":
|
||||||
|
match = re.match(_resolution_mode_pattern, line)
|
||||||
|
if match:
|
||||||
|
return cls(line, LineType.ResolutionMode, match)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
elif c == "\t":
|
||||||
|
tab_count += 1
|
||||||
|
i += 1
|
||||||
|
raise Exception(f"Line could not be categorized: '{line}'")
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_screen(line: Line) -> Screen:
|
||||||
|
d = line.m.groupdict()
|
||||||
|
|
||||||
|
screen: Screen = {"devices": []} # type: ignore # Will be populated, but not immediately.
|
||||||
|
for k, v in d.items():
|
||||||
|
screen[k] = int(v)
|
||||||
|
|
||||||
|
return screen
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_device(line: Line) -> Device:
|
||||||
|
matches = line.m.groupdict()
|
||||||
|
|
||||||
device: Device = {
|
device: Device = {
|
||||||
"modes": [],
|
"props": defaultdict(list),
|
||||||
|
"resolution_modes": [],
|
||||||
"is_connected": matches["is_connected"] == "connected",
|
"is_connected": matches["is_connected"] == "connected",
|
||||||
"is_primary": matches["is_primary"] is not None
|
"is_primary": matches["is_primary"] is not None
|
||||||
and len(matches["is_primary"]) > 0,
|
and len(matches["is_primary"]) > 0,
|
||||||
@ -403,97 +445,20 @@ def _parse_device(next_lines: List[str], quiet: bool = False) -> Optional[Device
|
|||||||
if v:
|
if v:
|
||||||
device[k] = int(v)
|
device[k] = int(v)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
if not quiet:
|
raise Exception([f"{line.s} : {k} - {v} is not int-able"])
|
||||||
jc.utils.warning_message(
|
|
||||||
[f"{next_line} : {k} - {v} is not int-able"]
|
|
||||||
)
|
|
||||||
|
|
||||||
model: Optional[Model] = _parse_model(next_lines, quiet)
|
|
||||||
if model:
|
|
||||||
device["model_name"] = model["name"]
|
|
||||||
device["product_id"] = model["product_id"]
|
|
||||||
device["serial_number"] = model["serial_number"]
|
|
||||||
|
|
||||||
while next_lines:
|
|
||||||
next_line = next_lines.pop()
|
|
||||||
next_mode: Optional[Mode] = _parse_mode(next_line)
|
|
||||||
if next_mode:
|
|
||||||
device["modes"].append(next_mode)
|
|
||||||
else:
|
|
||||||
if re.match(_device_pattern, next_line):
|
|
||||||
next_lines.append(next_line)
|
|
||||||
break
|
|
||||||
return device
|
return device
|
||||||
|
|
||||||
|
|
||||||
# EDID:
|
def _parse_resolution_mode(line: Line) -> ResolutionMode:
|
||||||
# 00ffffffffffff004ca3523100000000
|
|
||||||
# 0014010380221378eac8959e57549226
|
|
||||||
# 0f505400000001010101010101010101
|
|
||||||
# 010101010101381d56d4500016303020
|
|
||||||
# 250058c2100000190000000f00000000
|
|
||||||
# 000000000025d9066a00000000fe0053
|
|
||||||
# 414d53554e470a204ca34154000000fe
|
|
||||||
# 004c544e313536415432343430310018
|
|
||||||
_edid_head_pattern = r"\s*EDID:\s*"
|
|
||||||
_edid_line_pattern = r"\s*(?P<edid_line>[0-9a-fA-F]{32})\s*"
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_model(next_lines: List[str], quiet: bool = False) -> Optional[Model]:
|
|
||||||
if not next_lines:
|
|
||||||
return None
|
|
||||||
|
|
||||||
next_line = next_lines.pop()
|
|
||||||
|
|
||||||
if _was_parsed(next_line, 'model'):
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not re.match(_edid_head_pattern, next_line):
|
|
||||||
next_lines.append(next_line)
|
|
||||||
return None
|
|
||||||
|
|
||||||
edid_hex_value = ""
|
|
||||||
|
|
||||||
while next_lines:
|
|
||||||
next_line = next_lines.pop()
|
|
||||||
result = re.match(_edid_line_pattern, next_line)
|
|
||||||
|
|
||||||
if not result:
|
|
||||||
next_lines.append(next_line)
|
|
||||||
break
|
|
||||||
|
|
||||||
matches = result.groupdict()
|
|
||||||
edid_hex_value += matches["edid_line"]
|
|
||||||
|
|
||||||
edid = Edid(EdidHelper.hex2bytes(edid_hex_value))
|
|
||||||
|
|
||||||
model: Model = {
|
|
||||||
"name": edid.name or "Generic",
|
|
||||||
"product_id": str(edid.product),
|
|
||||||
"serial_number": str(edid.serial),
|
|
||||||
}
|
|
||||||
return model
|
|
||||||
|
|
||||||
|
|
||||||
# 1920x1080i 60.03*+ 59.93
|
|
||||||
# 1920x1080 60.00 + 50.00 59.94
|
|
||||||
_mode_pattern = r"\s*(?P<resolution_width>\d+)x(?P<resolution_height>\d+)(?P<is_high_resolution>i)?\s+(?P<rest>.*)"
|
|
||||||
_frequencies_pattern = r"(((?P<frequency>\d+\.\d+)(?P<star>\*| |)(?P<plus>\+?)?)+)"
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_mode(line: str) -> Optional[Mode]:
|
|
||||||
result = re.match(_mode_pattern, line)
|
|
||||||
frequencies: List[Frequency] = []
|
frequencies: List[Frequency] = []
|
||||||
|
|
||||||
if not result:
|
d = line.m.groupdict()
|
||||||
return None
|
|
||||||
|
|
||||||
d = result.groupdict()
|
|
||||||
resolution_width = int(d["resolution_width"])
|
resolution_width = int(d["resolution_width"])
|
||||||
resolution_height = int(d["resolution_height"])
|
resolution_height = int(d["resolution_height"])
|
||||||
is_high_resolution = d["is_high_resolution"] is not None
|
is_high_resolution = d["is_high_resolution"] is not None
|
||||||
|
|
||||||
mode: Mode = {
|
mode: ResolutionMode = {
|
||||||
"resolution_width": resolution_width,
|
"resolution_width": resolution_width,
|
||||||
"resolution_height": resolution_height,
|
"resolution_height": resolution_height,
|
||||||
"is_high_resolution": is_high_resolution,
|
"is_high_resolution": is_high_resolution,
|
||||||
@ -518,7 +483,45 @@ def _parse_mode(line: str) -> Optional[Mode]:
|
|||||||
return mode
|
return mode
|
||||||
|
|
||||||
|
|
||||||
def parse(data: str, raw: bool = False, quiet: bool = False) -> Dict:
|
def _parse_props(index: int, line: Line, lines: List[str]) -> Tuple[int, Props]:
|
||||||
|
tmp_props: Dict[str, List[str]] = {}
|
||||||
|
key = ""
|
||||||
|
while index <= len(lines):
|
||||||
|
if line.t == LineType.PropKey:
|
||||||
|
d = line.m.groupdict()
|
||||||
|
# See _prop_key_pattern
|
||||||
|
key = d["key"]
|
||||||
|
maybe_value = d["maybe_value"]
|
||||||
|
if not maybe_value:
|
||||||
|
tmp_props[key] = []
|
||||||
|
else:
|
||||||
|
tmp_props[key] = [maybe_value]
|
||||||
|
elif line.t == LineType.PropValue:
|
||||||
|
tmp_props[key].append(line.s.strip())
|
||||||
|
else:
|
||||||
|
# We've gone past our props and need to ascend
|
||||||
|
index = index - 1
|
||||||
|
break
|
||||||
|
index += 1
|
||||||
|
try:
|
||||||
|
line = Line.categorize(lines[index])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
props: Props = {}
|
||||||
|
if "EDID" in tmp_props:
|
||||||
|
edid = Edid(EdidHelper.hex2bytes("".join(tmp_props["EDID"])))
|
||||||
|
model: EdidModel = {
|
||||||
|
"name": edid.name or "Generic",
|
||||||
|
"product_id": str(edid.product),
|
||||||
|
"serial_number": str(edid.serial),
|
||||||
|
}
|
||||||
|
props["EdidModel"] = model
|
||||||
|
|
||||||
|
return index, {**tmp_props, **props}
|
||||||
|
|
||||||
|
|
||||||
|
def parse(data: str, raw: bool = False, quiet: bool = False) -> Response:
|
||||||
"""
|
"""
|
||||||
Main text parsing function
|
Main text parsing function
|
||||||
|
|
||||||
@ -535,15 +538,34 @@ def parse(data: str, raw: bool = False, quiet: bool = False) -> Dict:
|
|||||||
jc.utils.compatibility(__name__, info.compatible, quiet)
|
jc.utils.compatibility(__name__, info.compatible, quiet)
|
||||||
jc.utils.input_type_check(data)
|
jc.utils.input_type_check(data)
|
||||||
|
|
||||||
linedata = data.splitlines()
|
index = 0
|
||||||
linedata.reverse() # For popping
|
lines = data.splitlines()
|
||||||
result: Dict = {}
|
screen, device = None, None
|
||||||
|
|
||||||
|
result: Response = {"screens": []}
|
||||||
if jc.utils.has_data(data):
|
if jc.utils.has_data(data):
|
||||||
result = {"screens": []}
|
while index < len(lines):
|
||||||
while linedata:
|
line = Line.categorize(lines[index])
|
||||||
screen = _parse_screen(linedata)
|
if line.t == LineType.Screen:
|
||||||
if screen:
|
screen = _parse_screen(line)
|
||||||
result["screens"].append(screen)
|
result["screens"].append(screen)
|
||||||
|
elif line.t == LineType.Device:
|
||||||
|
device = _parse_device(line)
|
||||||
|
if not screen:
|
||||||
|
raise Exception("There should be an identifiable screen")
|
||||||
|
screen["devices"].append(device)
|
||||||
|
elif line.t == LineType.ResolutionMode:
|
||||||
|
resolution_mode = _parse_resolution_mode(line)
|
||||||
|
if not device:
|
||||||
|
raise Exception("Undefined device")
|
||||||
|
device["resolution_modes"].append(resolution_mode)
|
||||||
|
elif line.t == LineType.PropKey:
|
||||||
|
# Props needs to be state aware, it owns the index.
|
||||||
|
ix, props = _parse_props(index, line, lines)
|
||||||
|
index = ix
|
||||||
|
if not device:
|
||||||
|
raise Exception("Undefined device")
|
||||||
|
device["props"] = props
|
||||||
|
index += 1
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
138
tests/fixtures/generic/xrandr_issue_525.out
vendored
Normal file
138
tests/fixtures/generic/xrandr_issue_525.out
vendored
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 16384 x 16384
|
||||||
|
eDP-1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 309mm x 174mm
|
||||||
|
EDID:
|
||||||
|
00ffffffffffff0006af3d5700000000
|
||||||
|
001c0104a51f1178022285a5544d9a27
|
||||||
|
0e505400000001010101010101010101
|
||||||
|
010101010101b43780a070383e401010
|
||||||
|
350035ae100000180000000f00000000
|
||||||
|
00000000000000000020000000fe0041
|
||||||
|
554f0a202020202020202020000000fe
|
||||||
|
004231343048414e30352e37200a0070
|
||||||
|
scaling mode: Full aspect
|
||||||
|
supported: Full, Center, Full aspect
|
||||||
|
Colorspace: Default
|
||||||
|
supported: Default, RGB_Wide_Gamut_Fixed_Point, RGB_Wide_Gamut_Floating_Point, opRGB, DCI-P3_RGB_D65, BT2020_RGB, BT601_YCC, BT709_YCC, XVYCC_601, XVYCC_709, SYCC_601, opYCC_601, BT2020_CYCC, BT2020_YCC
|
||||||
|
max bpc: 12
|
||||||
|
range: (6, 12)
|
||||||
|
Broadcast RGB: Automatic
|
||||||
|
supported: Automatic, Full, Limited 16:235
|
||||||
|
panel orientation: Normal
|
||||||
|
supported: Normal, Upside Down, Left Side Up, Right Side Up
|
||||||
|
link-status: Good
|
||||||
|
supported: Good, Bad
|
||||||
|
CTM: 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0
|
||||||
|
0 1
|
||||||
|
CONNECTOR_ID: 95
|
||||||
|
supported: 95
|
||||||
|
non-desktop: 0
|
||||||
|
range: (0, 1)
|
||||||
|
1920x1080 60.03*+ 60.01 59.97 59.96 59.93
|
||||||
|
1680x1050 59.95 59.88
|
||||||
|
1400x1050 59.98
|
||||||
|
1600x900 59.99 59.94 59.95 59.82
|
||||||
|
1280x1024 60.02
|
||||||
|
1400x900 59.96 59.88
|
||||||
|
1280x960 60.00
|
||||||
|
1440x810 60.00 59.97
|
||||||
|
1368x768 59.88 59.85
|
||||||
|
1280x800 59.99 59.97 59.81 59.91
|
||||||
|
1280x720 60.00 59.99 59.86 59.74
|
||||||
|
1024x768 60.04 60.00
|
||||||
|
960x720 60.00
|
||||||
|
928x696 60.05
|
||||||
|
896x672 60.01
|
||||||
|
1024x576 59.95 59.96 59.90 59.82
|
||||||
|
960x600 59.93 60.00
|
||||||
|
960x540 59.96 59.99 59.63 59.82
|
||||||
|
800x600 60.00 60.32 56.25
|
||||||
|
840x525 60.01 59.88
|
||||||
|
864x486 59.92 59.57
|
||||||
|
700x525 59.98
|
||||||
|
800x450 59.95 59.82
|
||||||
|
640x512 60.02
|
||||||
|
700x450 59.96 59.88
|
||||||
|
640x480 60.00 59.94
|
||||||
|
720x405 59.51 58.99
|
||||||
|
684x384 59.88 59.85
|
||||||
|
640x400 59.88 59.98
|
||||||
|
640x360 59.86 59.83 59.84 59.32
|
||||||
|
512x384 60.00
|
||||||
|
512x288 60.00 59.92
|
||||||
|
480x270 59.63 59.82
|
||||||
|
400x300 60.32 56.34
|
||||||
|
432x243 59.92 59.57
|
||||||
|
320x240 60.05
|
||||||
|
360x202 59.51 59.13
|
||||||
|
320x180 59.84 59.32
|
||||||
|
DP-1 disconnected (normal left inverted right x axis y axis)
|
||||||
|
HDCP Content Type: HDCP Type0
|
||||||
|
supported: HDCP Type0, HDCP Type1
|
||||||
|
Content Protection: Undesired
|
||||||
|
supported: Undesired, Desired, Enabled
|
||||||
|
Colorspace: Default
|
||||||
|
supported: Default, RGB_Wide_Gamut_Fixed_Point, RGB_Wide_Gamut_Floating_Point, opRGB, DCI-P3_RGB_D65, BT2020_RGB, BT601_YCC, BT709_YCC, XVYCC_601, XVYCC_709, SYCC_601, opYCC_601, BT2020_CYCC, BT2020_YCC
|
||||||
|
max bpc: 12
|
||||||
|
range: (6, 12)
|
||||||
|
Broadcast RGB: Automatic
|
||||||
|
supported: Automatic, Full, Limited 16:235
|
||||||
|
audio: auto
|
||||||
|
supported: force-dvi, off, auto, on
|
||||||
|
subconnector: Unknown
|
||||||
|
supported: Unknown, VGA, DVI-D, HDMI, DP, Wireless, Native
|
||||||
|
link-status: Good
|
||||||
|
supported: Good, Bad
|
||||||
|
CTM: 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0
|
||||||
|
0 1
|
||||||
|
CONNECTOR_ID: 103
|
||||||
|
supported: 103
|
||||||
|
non-desktop: 0
|
||||||
|
range: (0, 1)
|
||||||
|
HDMI-1 disconnected (normal left inverted right x axis y axis)
|
||||||
|
HDCP Content Type: HDCP Type0
|
||||||
|
supported: HDCP Type0, HDCP Type1
|
||||||
|
Content Protection: Undesired
|
||||||
|
supported: Undesired, Desired, Enabled
|
||||||
|
max bpc: 12
|
||||||
|
range: (8, 12)
|
||||||
|
content type: No Data
|
||||||
|
supported: No Data, Graphics, Photo, Cinema, Game
|
||||||
|
Colorspace: Default
|
||||||
|
supported: Default, SMPTE_170M_YCC, BT709_YCC, XVYCC_601, XVYCC_709, SYCC_601, opYCC_601, opRGB, BT2020_CYCC, BT2020_RGB, BT2020_YCC, DCI-P3_RGB_D65, DCI-P3_RGB_Theater
|
||||||
|
aspect ratio: Automatic
|
||||||
|
supported: Automatic, 4:3, 16:9
|
||||||
|
Broadcast RGB: Automatic
|
||||||
|
supported: Automatic, Full, Limited 16:235
|
||||||
|
audio: auto
|
||||||
|
supported: force-dvi, off, auto, on
|
||||||
|
link-status: Good
|
||||||
|
supported: Good, Bad
|
||||||
|
CTM: 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0
|
||||||
|
0 1
|
||||||
|
CONNECTOR_ID: 113
|
||||||
|
supported: 113
|
||||||
|
non-desktop: 0
|
||||||
|
range: (0, 1)
|
||||||
|
DP-2 disconnected (normal left inverted right x axis y axis)
|
||||||
|
HDCP Content Type: HDCP Type0
|
||||||
|
supported: HDCP Type0, HDCP Type1
|
||||||
|
Content Protection: Undesired
|
||||||
|
supported: Undesired, Desired, Enabled
|
||||||
|
Colorspace: Default
|
||||||
|
supported: Default, RGB_Wide_Gamut_Fixed_Point, RGB_Wide_Gamut_Floating_Point, opRGB, DCI-P3_RGB_D65, BT2020_RGB, BT601_YCC, BT709_YCC, XVYCC_601, XVYCC_709, SYCC_601, opYCC_601, BT2020_CYCC, BT2020_YCC
|
||||||
|
max bpc: 12
|
||||||
|
range: (6, 12)
|
||||||
|
Broadcast RGB: Automatic
|
||||||
|
supported: Automatic, Full, Limited 16:235
|
||||||
|
audio: auto
|
||||||
|
supported: force-dvi, off, auto, on
|
||||||
|
subconnector: Unknown
|
||||||
|
supported: Unknown, VGA, DVI-D, HDMI, DP, Wireless, Native
|
||||||
|
link-status: Good
|
||||||
|
supported: Good, Bad
|
||||||
|
CTM: 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0
|
||||||
|
0 1
|
||||||
|
CONNECTOR_ID: 119
|
||||||
|
supported: 119
|
||||||
|
non-desktop: 0
|
||||||
|
range: (0, 1)
|
138
tests/fixtures/generic/xrandr_properties_1.out
vendored
Normal file
138
tests/fixtures/generic/xrandr_properties_1.out
vendored
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 16384 x 16384
|
||||||
|
eDP-1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 309mm x 174mm
|
||||||
|
EDID:
|
||||||
|
00ffffffffffff0006af3d5700000000
|
||||||
|
001c0104a51f1178022285a5544d9a27
|
||||||
|
0e505400000001010101010101010101
|
||||||
|
010101010101b43780a070383e401010
|
||||||
|
350035ae100000180000000f00000000
|
||||||
|
00000000000000000020000000fe0041
|
||||||
|
554f0a202020202020202020000000fe
|
||||||
|
004231343048414e30352e37200a0070
|
||||||
|
scaling mode: Full aspect
|
||||||
|
supported: Full, Center, Full aspect
|
||||||
|
Colorspace: Default
|
||||||
|
supported: Default, RGB_Wide_Gamut_Fixed_Point, RGB_Wide_Gamut_Floating_Point, opRGB, DCI-P3_RGB_D65, BT2020_RGB, BT601_YCC, BT709_YCC, XVYCC_601, XVYCC_709, SYCC_601, opYCC_601, BT2020_CYCC, BT2020_YCC
|
||||||
|
max bpc: 12
|
||||||
|
range: (6, 12)
|
||||||
|
Broadcast RGB: Automatic
|
||||||
|
supported: Automatic, Full, Limited 16:235
|
||||||
|
panel orientation: Normal
|
||||||
|
supported: Normal, Upside Down, Left Side Up, Right Side Up
|
||||||
|
link-status: Good
|
||||||
|
supported: Good, Bad
|
||||||
|
CTM: 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0
|
||||||
|
0 1
|
||||||
|
CONNECTOR_ID: 95
|
||||||
|
supported: 95
|
||||||
|
non-desktop: 0
|
||||||
|
range: (0, 1)
|
||||||
|
1920x1080 60.03*+ 60.01 59.97 59.96 59.93
|
||||||
|
1680x1050 59.95 59.88
|
||||||
|
1400x1050 59.98
|
||||||
|
1600x900 59.99 59.94 59.95 59.82
|
||||||
|
1280x1024 60.02
|
||||||
|
1400x900 59.96 59.88
|
||||||
|
1280x960 60.00
|
||||||
|
1440x810 60.00 59.97
|
||||||
|
1368x768 59.88 59.85
|
||||||
|
1280x800 59.99 59.97 59.81 59.91
|
||||||
|
1280x720 60.00 59.99 59.86 59.74
|
||||||
|
1024x768 60.04 60.00
|
||||||
|
960x720 60.00
|
||||||
|
928x696 60.05
|
||||||
|
896x672 60.01
|
||||||
|
1024x576 59.95 59.96 59.90 59.82
|
||||||
|
960x600 59.93 60.00
|
||||||
|
960x540 59.96 59.99 59.63 59.82
|
||||||
|
800x600 60.00 60.32 56.25
|
||||||
|
840x525 60.01 59.88
|
||||||
|
864x486 59.92 59.57
|
||||||
|
700x525 59.98
|
||||||
|
800x450 59.95 59.82
|
||||||
|
640x512 60.02
|
||||||
|
700x450 59.96 59.88
|
||||||
|
640x480 60.00 59.94
|
||||||
|
720x405 59.51 58.99
|
||||||
|
684x384 59.88 59.85
|
||||||
|
640x400 59.88 59.98
|
||||||
|
640x360 59.86 59.83 59.84 59.32
|
||||||
|
512x384 60.00
|
||||||
|
512x288 60.00 59.92
|
||||||
|
480x270 59.63 59.82
|
||||||
|
400x300 60.32 56.34
|
||||||
|
432x243 59.92 59.57
|
||||||
|
320x240 60.05
|
||||||
|
360x202 59.51 59.13
|
||||||
|
320x180 59.84 59.32
|
||||||
|
DP-1 disconnected (normal left inverted right x axis y axis)
|
||||||
|
HDCP Content Type: HDCP Type0
|
||||||
|
supported: HDCP Type0, HDCP Type1
|
||||||
|
Content Protection: Undesired
|
||||||
|
supported: Undesired, Desired, Enabled
|
||||||
|
Colorspace: Default
|
||||||
|
supported: Default, RGB_Wide_Gamut_Fixed_Point, RGB_Wide_Gamut_Floating_Point, opRGB, DCI-P3_RGB_D65, BT2020_RGB, BT601_YCC, BT709_YCC, XVYCC_601, XVYCC_709, SYCC_601, opYCC_601, BT2020_CYCC, BT2020_YCC
|
||||||
|
max bpc: 12
|
||||||
|
range: (6, 12)
|
||||||
|
Broadcast RGB: Automatic
|
||||||
|
supported: Automatic, Full, Limited 16:235
|
||||||
|
audio: auto
|
||||||
|
supported: force-dvi, off, auto, on
|
||||||
|
subconnector: Unknown
|
||||||
|
supported: Unknown, VGA, DVI-D, HDMI, DP, Wireless, Native
|
||||||
|
link-status: Good
|
||||||
|
supported: Good, Bad
|
||||||
|
CTM: 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0
|
||||||
|
0 1
|
||||||
|
CONNECTOR_ID: 103
|
||||||
|
supported: 103
|
||||||
|
non-desktop: 0
|
||||||
|
range: (0, 1)
|
||||||
|
HDMI-1 disconnected (normal left inverted right x axis y axis)
|
||||||
|
HDCP Content Type: HDCP Type0
|
||||||
|
supported: HDCP Type0, HDCP Type1
|
||||||
|
Content Protection: Undesired
|
||||||
|
supported: Undesired, Desired, Enabled
|
||||||
|
max bpc: 12
|
||||||
|
range: (8, 12)
|
||||||
|
content type: No Data
|
||||||
|
supported: No Data, Graphics, Photo, Cinema, Game
|
||||||
|
Colorspace: Default
|
||||||
|
supported: Default, SMPTE_170M_YCC, BT709_YCC, XVYCC_601, XVYCC_709, SYCC_601, opYCC_601, opRGB, BT2020_CYCC, BT2020_RGB, BT2020_YCC, DCI-P3_RGB_D65, DCI-P3_RGB_Theater
|
||||||
|
aspect ratio: Automatic
|
||||||
|
supported: Automatic, 4:3, 16:9
|
||||||
|
Broadcast RGB: Automatic
|
||||||
|
supported: Automatic, Full, Limited 16:235
|
||||||
|
audio: auto
|
||||||
|
supported: force-dvi, off, auto, on
|
||||||
|
link-status: Good
|
||||||
|
supported: Good, Bad
|
||||||
|
CTM: 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0
|
||||||
|
0 1
|
||||||
|
CONNECTOR_ID: 113
|
||||||
|
supported: 113
|
||||||
|
non-desktop: 0
|
||||||
|
range: (0, 1)
|
||||||
|
DP-2 disconnected (normal left inverted right x axis y axis)
|
||||||
|
HDCP Content Type: HDCP Type0
|
||||||
|
supported: HDCP Type0, HDCP Type1
|
||||||
|
Content Protection: Undesired
|
||||||
|
supported: Undesired, Desired, Enabled
|
||||||
|
Colorspace: Default
|
||||||
|
supported: Default, RGB_Wide_Gamut_Fixed_Point, RGB_Wide_Gamut_Floating_Point, opRGB, DCI-P3_RGB_D65, BT2020_RGB, BT601_YCC, BT709_YCC, XVYCC_601, XVYCC_709, SYCC_601, opYCC_601, BT2020_CYCC, BT2020_YCC
|
||||||
|
max bpc: 12
|
||||||
|
range: (6, 12)
|
||||||
|
Broadcast RGB: Automatic
|
||||||
|
supported: Automatic, Full, Limited 16:235
|
||||||
|
audio: auto
|
||||||
|
supported: force-dvi, off, auto, on
|
||||||
|
subconnector: Unknown
|
||||||
|
supported: Unknown, VGA, DVI-D, HDMI, DP, Wireless, Native
|
||||||
|
link-status: Good
|
||||||
|
supported: Good, Bad
|
||||||
|
CTM: 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0
|
||||||
|
0 1
|
||||||
|
CONNECTOR_ID: 119
|
||||||
|
supported: 119
|
||||||
|
non-desktop: 0
|
||||||
|
range: (0, 1)
|
@ -1,36 +1,33 @@
|
|||||||
|
import pprint
|
||||||
import re
|
import re
|
||||||
import unittest
|
import unittest
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from jc.parsers.xrandr import (
|
from jc.parsers.xrandr import (
|
||||||
_parse_screen,
|
|
||||||
_parse_device,
|
|
||||||
_parse_mode,
|
|
||||||
_parse_model,
|
|
||||||
_device_pattern,
|
|
||||||
_screen_pattern,
|
|
||||||
_mode_pattern,
|
|
||||||
_frequencies_pattern,
|
|
||||||
_edid_head_pattern,
|
|
||||||
_edid_line_pattern,
|
|
||||||
parse,
|
|
||||||
Mode,
|
|
||||||
Model,
|
|
||||||
Device,
|
Device,
|
||||||
Screen
|
Edid,
|
||||||
|
Line,
|
||||||
|
LineType,
|
||||||
|
ResolutionMode,
|
||||||
|
Response,
|
||||||
|
Screen,
|
||||||
|
_device_pattern,
|
||||||
|
_frequencies_pattern,
|
||||||
|
_parse_device,
|
||||||
|
_parse_resolution_mode,
|
||||||
|
_parse_screen,
|
||||||
|
_resolution_mode_pattern,
|
||||||
|
_screen_pattern,
|
||||||
|
parse,
|
||||||
)
|
)
|
||||||
import jc.parsers.xrandr
|
|
||||||
|
|
||||||
|
|
||||||
class XrandrTests(unittest.TestCase):
|
class XrandrTests(unittest.TestCase):
|
||||||
def setUp(self):
|
|
||||||
jc.parsers.xrandr.parse_state = {}
|
|
||||||
|
|
||||||
def test_xrandr_nodata(self):
|
def test_xrandr_nodata(self):
|
||||||
"""
|
"""
|
||||||
Test 'xrandr' with no data
|
Test 'xrandr' with no data
|
||||||
"""
|
"""
|
||||||
self.assertEqual(parse("", quiet=True), {})
|
self.assertEqual(parse("", quiet=True), {"screens": []})
|
||||||
|
|
||||||
def test_regexes(self):
|
def test_regexes(self):
|
||||||
devices = [
|
devices = [
|
||||||
@ -61,37 +58,30 @@ class XrandrTests(unittest.TestCase):
|
|||||||
"1400x900 59.96 59.88",
|
"1400x900 59.96 59.88",
|
||||||
]
|
]
|
||||||
for mode in modes:
|
for mode in modes:
|
||||||
match = re.match(_mode_pattern, mode)
|
match = re.match(_resolution_mode_pattern, mode)
|
||||||
self.assertIsNotNone(match)
|
self.assertIsNotNone(match)
|
||||||
if match:
|
if match:
|
||||||
rest = match.groupdict()["rest"]
|
rest = match.groupdict()["rest"]
|
||||||
self.assertIsNotNone(re.match(_frequencies_pattern, rest))
|
self.assertIsNotNone(re.match(_frequencies_pattern, rest))
|
||||||
|
|
||||||
edid_lines = [
|
def test_line_categorize(self):
|
||||||
" EDID: ",
|
base = "eDP-1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 309mm x 174mm"
|
||||||
" 00ffffffffffff000469d41901010101 ",
|
resolution_mode = " 320x240 60.05"
|
||||||
" 2011010308291a78ea8585a6574a9c26 ",
|
prop_key = " EDID:"
|
||||||
" 125054bfef80714f8100810f81408180 ",
|
prop_value = " 00ffffffffffff0006af3d5700000000"
|
||||||
" 9500950f01019a29a0d0518422305098 ",
|
invalid = ""
|
||||||
" 360098ff1000001c000000fd00374b1e ",
|
|
||||||
" 530f000a202020202020000000fc0041 ",
|
|
||||||
" 535553205657313933530a20000000ff ",
|
|
||||||
" 0037384c383032313130370a20200077 ",
|
|
||||||
]
|
|
||||||
|
|
||||||
for i in range(len(edid_lines)):
|
self.assertEqual(LineType.Device, Line.categorize(base).t)
|
||||||
line = edid_lines[i]
|
self.assertEqual(LineType.ResolutionMode, Line.categorize(resolution_mode).t)
|
||||||
if i == 0:
|
self.assertEqual(LineType.PropKey, Line.categorize(prop_key).t)
|
||||||
match = re.match(_edid_head_pattern, line)
|
self.assertEqual(LineType.PropValue, Line.categorize(prop_value).t)
|
||||||
else:
|
with self.assertRaises(Exception):
|
||||||
match = re.match(_edid_line_pattern, line)
|
Line.categorize(invalid)
|
||||||
|
|
||||||
self.assertIsNotNone(match)
|
|
||||||
|
|
||||||
def test_screens(self):
|
def test_screens(self):
|
||||||
sample = "Screen 0: minimum 8 x 8, current 1920 x 1080, maximum 32767 x 32767"
|
sample = "Screen 0: minimum 8 x 8, current 1920 x 1080, maximum 32767 x 32767"
|
||||||
|
line = Line.categorize(sample)
|
||||||
actual: Optional[Screen] = _parse_screen([sample])
|
actual: Optional[Screen] = _parse_screen(line)
|
||||||
self.assertIsNotNone(actual)
|
self.assertIsNotNone(actual)
|
||||||
|
|
||||||
expected = {
|
expected = {
|
||||||
@ -110,7 +100,8 @@ class XrandrTests(unittest.TestCase):
|
|||||||
sample = (
|
sample = (
|
||||||
"Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 16384 x 16384"
|
"Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 16384 x 16384"
|
||||||
)
|
)
|
||||||
actual = _parse_screen([sample])
|
line = Line.categorize(sample)
|
||||||
|
actual = _parse_screen(line)
|
||||||
if actual:
|
if actual:
|
||||||
self.assertEqual(320, actual["minimum_width"])
|
self.assertEqual(320, actual["minimum_width"])
|
||||||
else:
|
else:
|
||||||
@ -119,7 +110,8 @@ class XrandrTests(unittest.TestCase):
|
|||||||
def test_device(self):
|
def test_device(self):
|
||||||
# regex101 sample link for tests/edits https://regex101.com/r/3cHMv3/1
|
# regex101 sample link for tests/edits https://regex101.com/r/3cHMv3/1
|
||||||
sample = "eDP1 connected primary 1920x1080+0+0 left (normal left inverted right x axis y axis) 310mm x 170mm"
|
sample = "eDP1 connected primary 1920x1080+0+0 left (normal left inverted right x axis y axis) 310mm x 170mm"
|
||||||
actual: Optional[Device] = _parse_device([sample])
|
line = Line.categorize(sample)
|
||||||
|
actual: Optional[Device] = _parse_device(line)
|
||||||
|
|
||||||
expected = {
|
expected = {
|
||||||
"device_name": "eDP1",
|
"device_name": "eDP1",
|
||||||
@ -140,17 +132,19 @@ class XrandrTests(unittest.TestCase):
|
|||||||
for k, v in expected.items():
|
for k, v in expected.items():
|
||||||
self.assertEqual(v, actual[k], f"Devices regex failed on {k}")
|
self.assertEqual(v, actual[k], f"Devices regex failed on {k}")
|
||||||
|
|
||||||
with open("tests/fixtures/generic/xrandr_device.out", "r") as f:
|
# with open("tests/fixtures/generic/xrandr_device.out", "r") as f:
|
||||||
extended_sample = f.read().splitlines()
|
# extended_sample = f.read().splitlines()
|
||||||
extended_sample.reverse()
|
|
||||||
|
|
||||||
device = _parse_device(extended_sample)
|
# device = _parse_device(extended_sample)
|
||||||
if device:
|
# if device:
|
||||||
self.assertEqual(59.94, device["modes"][12]["frequencies"][4]["frequency"])
|
# self.assertEqual(
|
||||||
|
# 59.94, device["resolution_modes"][12]["frequencies"][4]["frequency"]
|
||||||
|
# )
|
||||||
|
|
||||||
def test_device_with_reflect(self):
|
def test_device_with_reflect(self):
|
||||||
sample = "VGA-1 connected primary 1920x1080+0+0 left X and Y axis (normal left inverted right x axis y axis) 310mm x 170mm"
|
sample = "VGA-1 connected primary 1920x1080+0+0 left X and Y axis (normal left inverted right x axis y axis) 310mm x 170mm"
|
||||||
actual: Optional[Device] = _parse_device([sample])
|
line = Line.categorize(sample)
|
||||||
|
actual: Optional[Device] = _parse_device(line)
|
||||||
|
|
||||||
expected = {
|
expected = {
|
||||||
"device_name": "VGA-1",
|
"device_name": "VGA-1",
|
||||||
@ -173,7 +167,7 @@ class XrandrTests(unittest.TestCase):
|
|||||||
self.assertEqual(v, actual[k], f"Devices regex failed on {k}")
|
self.assertEqual(v, actual[k], f"Devices regex failed on {k}")
|
||||||
|
|
||||||
def test_mode(self):
|
def test_mode(self):
|
||||||
sample_1 = "1920x1080 60.03*+ 59.93"
|
sample_1 = " 1920x1080 60.03*+ 59.93"
|
||||||
expected = {
|
expected = {
|
||||||
"frequencies": [
|
"frequencies": [
|
||||||
{"frequency": 60.03, "is_current": True, "is_preferred": True},
|
{"frequency": 60.03, "is_current": True, "is_preferred": True},
|
||||||
@ -183,7 +177,8 @@ class XrandrTests(unittest.TestCase):
|
|||||||
"resolution_height": 1080,
|
"resolution_height": 1080,
|
||||||
"is_high_resolution": False,
|
"is_high_resolution": False,
|
||||||
}
|
}
|
||||||
actual: Optional[Mode] = _parse_mode(sample_1)
|
line = Line.categorize(sample_1)
|
||||||
|
actual: Optional[ResolutionMode] = _parse_resolution_mode(line)
|
||||||
|
|
||||||
self.assertIsNotNone(actual)
|
self.assertIsNotNone(actual)
|
||||||
|
|
||||||
@ -191,8 +186,9 @@ class XrandrTests(unittest.TestCase):
|
|||||||
for k, v in expected.items():
|
for k, v in expected.items():
|
||||||
self.assertEqual(v, actual[k], f"mode regex failed on {k}")
|
self.assertEqual(v, actual[k], f"mode regex failed on {k}")
|
||||||
|
|
||||||
sample_2 = " 1920x1080i 60.00 50.00 59.94"
|
sample_2 = " 1920x1080i 60.00 50.00 59.94"
|
||||||
actual: Optional[Mode] = _parse_mode(sample_2)
|
line = Line.categorize(sample_2)
|
||||||
|
actual: Optional[ResolutionMode] = _parse_resolution_mode(line)
|
||||||
self.assertIsNotNone(actual)
|
self.assertIsNotNone(actual)
|
||||||
if actual:
|
if actual:
|
||||||
self.assertEqual(True, actual["is_high_resolution"])
|
self.assertEqual(True, actual["is_high_resolution"])
|
||||||
@ -205,7 +201,9 @@ class XrandrTests(unittest.TestCase):
|
|||||||
actual = parse(txt, quiet=True)
|
actual = parse(txt, quiet=True)
|
||||||
|
|
||||||
self.assertEqual(1, len(actual["screens"]))
|
self.assertEqual(1, len(actual["screens"]))
|
||||||
self.assertEqual(18, len(actual["screens"][0]["devices"][0]["modes"]))
|
self.assertEqual(
|
||||||
|
18, len(actual["screens"][0]["devices"][0]["resolution_modes"])
|
||||||
|
)
|
||||||
|
|
||||||
def test_complete_2(self):
|
def test_complete_2(self):
|
||||||
with open("tests/fixtures/generic/xrandr_2.out", "r") as f:
|
with open("tests/fixtures/generic/xrandr_2.out", "r") as f:
|
||||||
@ -213,7 +211,9 @@ class XrandrTests(unittest.TestCase):
|
|||||||
actual = parse(txt, quiet=True)
|
actual = parse(txt, quiet=True)
|
||||||
|
|
||||||
self.assertEqual(1, len(actual["screens"]))
|
self.assertEqual(1, len(actual["screens"]))
|
||||||
self.assertEqual(38, len(actual["screens"][0]["devices"][0]["modes"]))
|
self.assertEqual(
|
||||||
|
38, len(actual["screens"][0]["devices"][0]["resolution_modes"])
|
||||||
|
)
|
||||||
|
|
||||||
def test_complete_3(self):
|
def test_complete_3(self):
|
||||||
with open("tests/fixtures/generic/xrandr_3.out", "r") as f:
|
with open("tests/fixtures/generic/xrandr_3.out", "r") as f:
|
||||||
@ -232,84 +232,119 @@ class XrandrTests(unittest.TestCase):
|
|||||||
actual = parse(txt, quiet=True)
|
actual = parse(txt, quiet=True)
|
||||||
|
|
||||||
self.assertEqual(1, len(actual["screens"]))
|
self.assertEqual(1, len(actual["screens"]))
|
||||||
self.assertEqual(2, len(actual["screens"][0]["devices"][0]["modes"]))
|
self.assertEqual(2, len(actual["screens"][0]["devices"][0]["resolution_modes"]))
|
||||||
|
|
||||||
def test_complete_5(self):
|
def test_complete_5(self):
|
||||||
with open("tests/fixtures/generic/xrandr_properties.out", "r") as f:
|
with open("tests/fixtures/generic/xrandr_properties_1.out", "r") as f:
|
||||||
txt = f.read()
|
txt = f.read()
|
||||||
actual = parse(txt, quiet=True)
|
actual = parse(txt, quiet=True)
|
||||||
|
|
||||||
self.assertEqual(1, len(actual["screens"]))
|
self.assertEqual(1, len(actual["screens"]))
|
||||||
self.assertEqual(29, len(actual["screens"][0]["devices"][0]["modes"]))
|
self.assertEqual(
|
||||||
|
38, len(actual["screens"][0]["devices"][0]["resolution_modes"])
|
||||||
|
)
|
||||||
|
|
||||||
def test_model(self):
|
# def test_model(self):
|
||||||
asus_edid = [
|
# asus_edid = [
|
||||||
" EDID: ",
|
# " EDID: ",
|
||||||
" 00ffffffffffff000469d41901010101",
|
# " 00ffffffffffff000469d41901010101",
|
||||||
" 2011010308291a78ea8585a6574a9c26",
|
# " 2011010308291a78ea8585a6574a9c26",
|
||||||
" 125054bfef80714f8100810f81408180",
|
# " 125054bfef80714f8100810f81408180",
|
||||||
" 9500950f01019a29a0d0518422305098",
|
# " 9500950f01019a29a0d0518422305098",
|
||||||
" 360098ff1000001c000000fd00374b1e",
|
# " 360098ff1000001c000000fd00374b1e",
|
||||||
" 530f000a202020202020000000fc0041",
|
# " 530f000a202020202020000000fc0041",
|
||||||
" 535553205657313933530a20000000ff",
|
# " 535553205657313933530a20000000ff",
|
||||||
" 0037384c383032313130370a20200077",
|
# " 0037384c383032313130370a20200077",
|
||||||
]
|
# ]
|
||||||
asus_edid.reverse()
|
# asus_edid.reverse()
|
||||||
|
|
||||||
expected = {
|
# expected = {
|
||||||
"name": "ASUS VW193S",
|
# "name": "ASUS VW193S",
|
||||||
"product_id": "6612",
|
# "product_id": "6612",
|
||||||
"serial_number": "78L8021107",
|
# "serial_number": "78L8021107",
|
||||||
}
|
# }
|
||||||
|
|
||||||
actual: Optional[Model] = _parse_model(asus_edid)
|
# actual: Optional[EdidModel] = _parse_model(asus_edid)
|
||||||
self.assertIsNotNone(actual)
|
# self.assertIsNotNone(actual)
|
||||||
|
|
||||||
if actual:
|
# if actual:
|
||||||
for k, v in expected.items():
|
# for k, v in expected.items():
|
||||||
self.assertEqual(v, actual[k], f"mode regex failed on {k}")
|
# self.assertEqual(v, actual[k], f"mode regex failed on {k}")
|
||||||
|
|
||||||
generic_edid = [
|
# generic_edid = [
|
||||||
" EDID: ",
|
# " EDID: ",
|
||||||
" 00ffffffffffff004ca3523100000000",
|
# " 00ffffffffffff004ca3523100000000",
|
||||||
" 0014010380221378eac8959e57549226",
|
# " 0014010380221378eac8959e57549226",
|
||||||
" 0f505400000001010101010101010101",
|
# " 0f505400000001010101010101010101",
|
||||||
" 010101010101381d56d4500016303020",
|
# " 010101010101381d56d4500016303020",
|
||||||
" 250058c2100000190000000f00000000",
|
# " 250058c2100000190000000f00000000",
|
||||||
" 000000000025d9066a00000000fe0053",
|
# " 000000000025d9066a00000000fe0053",
|
||||||
" 414d53554e470a204ca34154000000fe",
|
# " 414d53554e470a204ca34154000000fe",
|
||||||
" 004c544e313536415432343430310018",
|
# " 004c544e313536415432343430310018",
|
||||||
]
|
# ]
|
||||||
generic_edid.reverse()
|
# generic_edid.reverse()
|
||||||
|
|
||||||
expected = {
|
# expected = {
|
||||||
"name": "Generic",
|
# "name": "Generic",
|
||||||
"product_id": "12626",
|
# "product_id": "12626",
|
||||||
"serial_number": "0",
|
# "serial_number": "0",
|
||||||
}
|
# }
|
||||||
|
|
||||||
jc.parsers.xrandr.parse_state = {}
|
# jc.parsers.xrandr.parse_state = {}
|
||||||
actual: Optional[Model] = _parse_model(generic_edid)
|
# actual: Optional[EdidModel] = _parse_model(generic_edid)
|
||||||
self.assertIsNotNone(actual)
|
# self.assertIsNotNone(actual)
|
||||||
|
|
||||||
if actual:
|
# if actual:
|
||||||
for k, v in expected.items():
|
# for k, v in expected.items():
|
||||||
self.assertEqual(v, actual[k], f"mode regex failed on {k}")
|
# self.assertEqual(v, actual[k], f"mode regex failed on {k}")
|
||||||
|
|
||||||
empty_edid = [""]
|
|
||||||
actual: Optional[Model] = _parse_model(empty_edid)
|
|
||||||
self.assertIsNone(actual)
|
|
||||||
|
|
||||||
|
# empty_edid = [""]
|
||||||
|
# actual: Optional[EdidModel] = _parse_model(empty_edid)
|
||||||
|
# self.assertIsNone(actual)
|
||||||
|
|
||||||
def test_issue_490(self):
|
def test_issue_490(self):
|
||||||
"""test for issue 490: https://github.com/kellyjonbrazil/jc/issues/490"""
|
"""test for issue 490: https://github.com/kellyjonbrazil/jc/issues/490"""
|
||||||
data_in = '''\
|
data_in = """\
|
||||||
Screen 0: minimum 1024 x 600, current 1024 x 600, maximum 1024 x 600
|
Screen 0: minimum 1024 x 600, current 1024 x 600, maximum 1024 x 600
|
||||||
default connected 1024x600+0+0 0mm x 0mm
|
default connected 1024x600+0+0 0mm x 0mm
|
||||||
1024x600 0.00*
|
1024x600 0.00*
|
||||||
'''
|
"""
|
||||||
expected = {"screens":[{"devices":[{"modes":[{"resolution_width":1024,"resolution_height":600,"is_high_resolution":False,"frequencies":[{"frequency":0.0,"is_current":True,"is_preferred":False}]}],"is_connected":True,"is_primary":False,"device_name":"default","rotation":"normal","reflection":"normal","resolution_width":1024,"resolution_height":600,"offset_width":0,"offset_height":0,"dimension_width":0,"dimension_height":0}],"screen_number":0,"minimum_width":1024,"minimum_height":600,"current_width":1024,"current_height":600,"maximum_width":1024,"maximum_height":600}]}
|
actual: Response = parse(data_in)
|
||||||
self.assertEqual(jc.parsers.xrandr.parse(data_in), expected)
|
self.maxDiff = None
|
||||||
|
self.assertEqual(1024, actual["screens"][0]["devices"][0]["resolution_width"])
|
||||||
|
|
||||||
|
def test_issue_525(self):
|
||||||
|
self.maxDiff = None
|
||||||
|
with open("tests/fixtures/generic/xrandr_issue_525.out", "r") as f:
|
||||||
|
txt = f.read()
|
||||||
|
actual = parse(txt, quiet=True)
|
||||||
|
dp4 = actual["screens"][0]["devices"][0]["props"]["Broadcast RGB"][1] # type: ignore
|
||||||
|
# pprint.pprint(actual)
|
||||||
|
self.assertEqual("supported: Automatic, Full, Limited 16:235", dp4)
|
||||||
|
edp1_expected_keys = {
|
||||||
|
"EDID",
|
||||||
|
"EdidModel",
|
||||||
|
"scaling mode",
|
||||||
|
"Colorspace",
|
||||||
|
"max bpc",
|
||||||
|
"Broadcast RGB",
|
||||||
|
"panel orientation",
|
||||||
|
"link-status",
|
||||||
|
"CTM",
|
||||||
|
"CONNECTOR_ID",
|
||||||
|
"non-desktop",
|
||||||
|
}
|
||||||
|
actual_keys = set(actual["screens"][0]["devices"][0]["props"].keys())
|
||||||
|
self.assertSetEqual(edp1_expected_keys, actual_keys)
|
||||||
|
expected_edid_model = {
|
||||||
|
"name": "Generic",
|
||||||
|
"product_id": "22333",
|
||||||
|
"serial_number": "0",
|
||||||
|
}
|
||||||
|
self.assertDictEqual(
|
||||||
|
expected_edid_model,
|
||||||
|
actual["screens"][0]["devices"][0]["props"]["EdidModel"], # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
Reference in New Issue
Block a user