1
0
mirror of https://github.com/kellyjonbrazil/jc.git synced 2025-06-17 00:07:37 +02:00

New parser: xrandr

Tests passing, working as intended in plugin directory
This commit is contained in:
Kevin Lyter
2022-02-02 18:25:55 -08:00
parent 021e296d45
commit 520082d963
9 changed files with 968 additions and 0 deletions

353
jc/parsers/xrandr.py Normal file
View File

@ -0,0 +1,353 @@
"""jc - JSON CLI output utility `xrandr` command output parser
Options supported:
Usage (module):
import jc.parsers.xrandr
result = jc.parsers.xrandr.parse(xrandr_command_output)
Schema:
{
"screens": [
{
"screen_number": 0,
"minimum_width": 8,
"minimum_height": 8,
"current_width": 1920,
"current_height": 1080,
"maximum_width": 32767,
"maximum_height": 32767,
"associated_device": {
"associated_modes": [
{
"resolution_width": 1920,
"resolution_height": 1080,
"is_high_resolution": false,
"frequencies": [
{
"frequency": 60.03,
"is_current": true,
"is_preferred": true
},
{
"frequency": 59.93,
"is_current": false,
"is_preferred": false
}
]
},
{
"resolution_width": 1680,
"resolution_height": 1050,
"is_high_resolution": false,
"frequencies": [
{
"frequency": 59.88,
"is_current": false,
"is_preferred": false
}
]
}
],
"is_connected": true,
"is_primary": true,
"device_name": "eDP1",
"resolution_width": 1920,
"resolution_height": 1080,
"offset_width": 0,
"offset_height": 0,
"dimension_width": 310,
"dimension_height": 170
}
}
],
"unassociated_devices": []
}
Translated from:
Screen 0: minimum 8 x 8, current 1920 x 1080, maximum 32767 x 32767
eDP1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 310mm x 170mm
1920x1080 60.03*+ 59.93
1680x1050 59.88
Examples:
$ xrandr | jc --xrandr
"""
import re
from typing import Dict, List, Iterator, Optional, Union
import jc.utils
class info:
"""Provides parser metadata (version, author, etc.)"""
version = "1.9"
description = "`xrandr` command parser"
author = "Kevin Lyter"
author_email = "lyter_git at sent.com"
# compatible options: linux, darwin, cygwin, win32, aix, freebsd
compatible = ["linux", "darwin", "cygwin", "aix", "freebsd"]
magic_commands = ["xrandr"]
__version__ = info.version
# ScreenLine = Dict[str, int]
# DeviceLine = Dict[str, int | bool | str]
# ModeLine = Dict[str, int]
try:
from typing import TypedDict
Frequency = TypedDict(
"Frequency",
{
"frequency": float,
"is_current": bool,
"is_preferred": bool,
},
)
Mode = TypedDict(
"Mode",
{
"resolution_width": int,
"resolution_height": int,
"is_high_resolution": bool,
"frequencies": List[Frequency],
},
)
Device = TypedDict(
"Device",
{
"device_name": str,
"is_connected": bool,
"is_primary": bool,
"resolution_width": int,
"resolution_height": int,
"offset_width": int,
"offset_height": int,
"dimension_width": int,
"dimension_height": int,
"associated_modes": List[Mode],
},
)
Screen = TypedDict(
"Screen",
{
"screen_number": int,
"minimum_width": int,
"minimum_height": int,
"current_width": int,
"current_height": int,
"maximum_width": int,
"maximum_height": int,
"associated_device": Device,
},
)
Response = TypedDict(
"Response",
{
"screens": List[Screen],
"unassociated_devices": List[Device],
},
)
except ImportError:
Screen = Dict[str, int | str]
Device = Dict[str, str | int | bool]
Frequency = Dict[str, float | bool]
Mode = Dict[str, int | bool | List[Frequency]]
Response = Dict[str, Device | Mode | Screen]
def _process(proc_data):
"""
Final processing to conform to the schema.
Parameters:
proc_data: (List of Dictionaries) raw structured data to process
Returns:
List of Dictionaries. Structured data to conform to the schema.
"""
for entry in proc_data:
int_list = ["links", "size"]
for key in entry:
if key in int_list:
entry[key] = jc.utils.convert_to_int(entry[key])
if "date" in entry:
# to speed up processing only try to convert the date if it's not the default format
if not re.match(
r"[a-zA-Z]{3}\s{1,2}\d{1,2}\s{1,2}[0-9:]{4,5}", entry["date"]
):
ts = jc.utils.timestamp(entry["date"])
entry["epoch"] = ts.naive
entry["epoch_utc"] = ts.utc
return proc_data
_screen_pattern = (
r"Screen (?P<screen_number>\d+): "
+ "minimum (?P<minimum_width>\d+) x (?P<minimum_height>\d+), "
+ "current (?P<current_width>\d+) x (?P<current_height>\d+), "
+ "maximum (?P<maximum_width>\d+) x (?P<maximum_height>\d+)"
)
def _parse_screen(next_lines: List[str]) -> Optional[Screen]:
next_line = next_lines.pop()
result = re.match(_screen_pattern, next_line)
if not result:
next_lines.append(next_line)
return None
raw_matches = result.groupdict()
screen: Screen = {}
for k, v in raw_matches.items():
screen[k] = int(v)
if next_lines:
device: Optional[Device] = _parse_device(next_lines)
if device:
screen["associated_device"] = device
return screen
# eDP1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis)
# 310mm x 170mm
# regex101 demo link
# https://regex101.com/r/5ZQEDC/1
_device_pattern = (
r"(?P<device_name>.+) "
+ "(?P<is_connected>(connected|disconnected)) ?"
+ "(?P<is_primary> primary)? ?"
+ "((?P<resolution_width>\d+)x(?P<resolution_height>\d+)"
+ "\+(?P<offset_width>\d+)\+(?P<offset_height>\d+))? "
+ "\(normal left inverted right x axis y axis\)"
+ "( ((?P<dimension_width>\d+)mm x (?P<dimension_height>\d+)mm)?)?"
)
def _parse_device(next_lines: List[str]) -> Optional[Device]:
if not next_lines:
return None
next_line = next_lines.pop()
result = re.match(_device_pattern, next_line)
if not result:
next_lines.append(next_line)
return None
matches = result.groupdict()
device: Device = {
"associated_modes": [],
"is_connected": matches["is_connected"] == "connected",
"is_primary": matches["is_primary"] is not None
and len(matches["is_primary"]) > 0,
"device_name": matches["device_name"],
}
for k, v in matches.items():
if k not in {"is_connected", "is_primary", "device_name"}:
try:
if v:
device[k] = int(v)
except ValueError:
print(f"Error: {next_line} : {k} - {v} is not int-able")
while next_lines:
next_line = next_lines.pop()
next_mode: Optional[Mode] = _parse_mode(next_line)
if next_mode:
device["associated_modes"].append(next_mode)
else:
next_lines.append(next_line)
break
return device
# 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] = []
if not result:
return None
d = result.groupdict()
resolution_width = int(d["resolution_width"])
resolution_height = int(d["resolution_height"])
is_high_resolution = d["is_high_resolution"] is not None
mode: Mode = {
"resolution_width": resolution_width,
"resolution_height": resolution_height,
"is_high_resolution": is_high_resolution,
"frequencies": frequencies,
}
result = re.finditer(_frequencies_pattern, d["rest"])
if not result:
return mode
for match in result:
d = match.groupdict()
frequency = float(d["frequency"])
is_current = len(d["star"]) > 0
is_preferred = len(d["plus"]) > 0
f: Frequency = {
"frequency": frequency,
"is_current": is_current,
"is_preferred": is_preferred,
}
mode["frequencies"].append(f)
return mode
def parse(data: str, raw=False, quiet=False):
"""
Main text parsing function
Parameters:
data: (string) text data to parse
raw: (boolean) output preprocessed JSON if True
quiet: (boolean) suppress warning messages if True
Returns:
List of Dictionaries. Raw or processed structured data.
"""
if not quiet:
jc.utils.compatibility(__name__, info.compatible)
warned = False
parent = ""
next_is_parent = False
new_section = False
linedata = data.splitlines()
linedata.reverse() # For popping
result: Response = {"screens": [], "unassociated_devices": []}
if jc.utils.has_data(data):
result: Response = {"screens": [], "unassociated_devices": []}
while linedata:
screen = _parse_screen(linedata)
if screen:
result["screens"].append(screen)
else:
device = _parse_device(linedata)
if device:
result["unassociated_devices"].append(device)
return result

View File

@ -0,0 +1,38 @@
Screen 0: minimum 8 x 8, current 1920 x 1080, maximum 32767 x 32767
eDP1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 310mm x 170mm
1920x1080 60.03*+ 59.93
1680x1050 59.88
1400x1050 59.98
1600x900 60.00 59.95 59.82
1280x1024 60.02
1400x900 59.96 59.88
1280x960 60.00
1368x768 60.00 59.88 59.85
1280x800 59.81 59.91
1280x720 59.86 60.00 59.74
1024x768 60.00
1024x576 60.00 59.90 59.82
960x540 60.00 59.63 59.82
800x600 60.32 56.25
864x486 60.00 59.92 59.57
640x480 59.94
720x405 59.51 60.00 58.99
640x360 59.84 59.32 60.00
DP1 disconnected (normal left inverted right x axis y axis)
DP2 disconnected (normal left inverted right x axis y axis)
HDMI1 connected (normal left inverted right x axis y axis)
1920x1080 60.00 + 50.00 59.94
1920x1080i 60.00 50.00 59.94
1680x1050 59.88
1280x1024 75.02 60.02
1440x900 59.90
1280x960 60.00
1280x720 60.00 50.00 59.94
1024x768 75.03 70.07 60.00
832x624 74.55
800x600 72.19 75.00 60.32 56.25
720x576 50.00
720x480 60.00 59.94
640x480 75.00 72.81 66.67 60.00 59.94
720x400 70.08
VIRTUAL1 disconnected (normal left inverted right x axis y axis)

View File

@ -0,0 +1,249 @@
Screen 0: minimum 8 x 8, current 1920 x 1080, maximum 32767 x 32767
eDP1 connected primary 1920x1080+0+0 (0x49) normal (normal left inverted right x axis y axis) 310mm x 170mm
Identifier: 0x43
Timestamp: 910112952
Subpixel: unknown
Gamma: 1.0:1.0:1.0
Brightness: 1.0
Clones:
CRTC: 0
CRTCs: 0 1 2
Transform: 1.000000 0.000000 0.000000
0.000000 1.000000 0.000000
0.000000 0.000000 1.000000
filter:
EDID:
00ffffffffffff0006af3d5700000000
001c0104a51f1178022285a5544d9a27
0e505400000001010101010101010101
010101010101b43780a070383e401010
350035ae100000180000000f00000000
00000000000000000020000000fe0041
554f0a202020202020202020000000fe
004231343048414e30352e37200a0070
BACKLIGHT: 19394
range: (0, 24242)
Backlight: 19394
range: (0, 24242)
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
non-desktop: 0
range: (0, 1)
1920x1080 (0x49) 142.600MHz -HSync -VSync *current +preferred
h: width 1920 start 1936 end 1952 total 2080 skew 0 clock 68.56KHz
v: height 1080 start 1083 end 1088 total 1142 clock 60.03Hz
1920x1080 (0x1b5) 138.500MHz +HSync -VSync
h: width 1920 start 1968 end 2000 total 2080 skew 0 clock 66.59KHz
v: height 1080 start 1083 end 1088 total 1111 clock 59.93Hz
1680x1050 (0x1b6) 119.000MHz +HSync -VSync
h: width 1680 start 1728 end 1760 total 1840 skew 0 clock 64.67KHz
v: height 1050 start 1053 end 1059 total 1080 clock 59.88Hz
1400x1050 (0x1b7) 122.000MHz +HSync +VSync
h: width 1400 start 1488 end 1640 total 1880 skew 0 clock 64.89KHz
v: height 1050 start 1052 end 1064 total 1082 clock 59.98Hz
1600x900 (0x1b8) 118.997MHz -HSync +VSync
h: width 1600 start 1696 end 1864 total 2128 skew 0 clock 55.92KHz
v: height 900 start 901 end 904 total 932 clock 60.00Hz
1600x900 (0x1b9) 118.250MHz -HSync +VSync
h: width 1600 start 1696 end 1856 total 2112 skew 0 clock 55.99KHz
v: height 900 start 903 end 908 total 934 clock 59.95Hz
1600x900 (0x1ba) 97.500MHz +HSync -VSync
h: width 1600 start 1648 end 1680 total 1760 skew 0 clock 55.40KHz
v: height 900 start 903 end 908 total 926 clock 59.82Hz
1280x1024 (0x1bb) 108.000MHz +HSync +VSync
h: width 1280 start 1328 end 1440 total 1688 skew 0 clock 63.98KHz
v: height 1024 start 1025 end 1028 total 1066 clock 60.02Hz
1400x900 (0x1bc) 103.500MHz -HSync +VSync
h: width 1400 start 1480 end 1624 total 1848 skew 0 clock 56.01KHz
v: height 900 start 903 end 913 total 934 clock 59.96Hz
1400x900 (0x1bd) 86.500MHz +HSync -VSync
h: width 1400 start 1448 end 1480 total 1560 skew 0 clock 55.45KHz
v: height 900 start 903 end 913 total 926 clock 59.88Hz
1280x960 (0x1be) 108.000MHz +HSync +VSync
h: width 1280 start 1376 end 1488 total 1800 skew 0 clock 60.00KHz
v: height 960 start 961 end 964 total 1000 clock 60.00Hz
1368x768 (0x1bf) 85.860MHz -HSync +VSync
h: width 1368 start 1440 end 1584 total 1800 skew 0 clock 47.70KHz
v: height 768 start 769 end 772 total 795 clock 60.00Hz
1368x768 (0x1c0) 85.250MHz -HSync +VSync
h: width 1368 start 1440 end 1576 total 1784 skew 0 clock 47.79KHz
v: height 768 start 771 end 781 total 798 clock 59.88Hz
1368x768 (0x1c1) 72.250MHz +HSync -VSync
h: width 1368 start 1416 end 1448 total 1528 skew 0 clock 47.28KHz
v: height 768 start 771 end 781 total 790 clock 59.85Hz
1280x800 (0x1c2) 83.500MHz -HSync +VSync
h: width 1280 start 1352 end 1480 total 1680 skew 0 clock 49.70KHz
v: height 800 start 803 end 809 total 831 clock 59.81Hz
1280x800 (0x1c3) 71.000MHz +HSync -VSync
h: width 1280 start 1328 end 1360 total 1440 skew 0 clock 49.31KHz
v: height 800 start 803 end 809 total 823 clock 59.91Hz
1280x720 (0x1c4) 74.500MHz -HSync +VSync
h: width 1280 start 1344 end 1472 total 1664 skew 0 clock 44.77KHz
v: height 720 start 723 end 728 total 748 clock 59.86Hz
1280x720 (0x1c5) 74.480MHz -HSync +VSync
h: width 1280 start 1336 end 1472 total 1664 skew 0 clock 44.76KHz
v: height 720 start 721 end 724 total 746 clock 60.00Hz
1280x720 (0x1c6) 63.750MHz +HSync -VSync
h: width 1280 start 1328 end 1360 total 1440 skew 0 clock 44.27KHz
v: height 720 start 723 end 728 total 741 clock 59.74Hz
1024x768 (0x1c7) 65.000MHz -HSync -VSync
h: width 1024 start 1048 end 1184 total 1344 skew 0 clock 48.36KHz
v: height 768 start 771 end 777 total 806 clock 60.00Hz
1024x576 (0x1c8) 46.995MHz -HSync +VSync
h: width 1024 start 1064 end 1168 total 1312 skew 0 clock 35.82KHz
v: height 576 start 577 end 580 total 597 clock 60.00Hz
1024x576 (0x1c9) 46.500MHz -HSync +VSync
h: width 1024 start 1064 end 1160 total 1296 skew 0 clock 35.88KHz
v: height 576 start 579 end 584 total 599 clock 59.90Hz
1024x576 (0x1ca) 42.000MHz +HSync -VSync
h: width 1024 start 1072 end 1104 total 1184 skew 0 clock 35.47KHz
v: height 576 start 579 end 584 total 593 clock 59.82Hz
960x540 (0x1cb) 40.784MHz -HSync +VSync
h: width 960 start 992 end 1088 total 1216 skew 0 clock 33.54KHz
v: height 540 start 541 end 544 total 559 clock 60.00Hz
960x540 (0x1cc) 40.750MHz -HSync +VSync
h: width 960 start 992 end 1088 total 1216 skew 0 clock 33.51KHz
v: height 540 start 543 end 548 total 562 clock 59.63Hz
960x540 (0x1cd) 37.250MHz +HSync -VSync
h: width 960 start 1008 end 1040 total 1120 skew 0 clock 33.26KHz
v: height 540 start 543 end 548 total 556 clock 59.82Hz
800x600 (0x1ce) 40.000MHz +HSync +VSync
h: width 800 start 840 end 968 total 1056 skew 0 clock 37.88KHz
v: height 600 start 601 end 605 total 628 clock 60.32Hz
800x600 (0x1cf) 36.000MHz +HSync +VSync
h: width 800 start 824 end 896 total 1024 skew 0 clock 35.16KHz
v: height 600 start 601 end 603 total 625 clock 56.25Hz
864x486 (0x1d0) 32.901MHz -HSync +VSync
h: width 864 start 888 end 976 total 1088 skew 0 clock 30.24KHz
v: height 486 start 487 end 490 total 504 clock 60.00Hz
864x486 (0x1d1) 32.500MHz -HSync +VSync
h: width 864 start 888 end 968 total 1072 skew 0 clock 30.32KHz
v: height 486 start 489 end 494 total 506 clock 59.92Hz
864x486 (0x1d2) 30.500MHz +HSync -VSync
h: width 864 start 912 end 944 total 1024 skew 0 clock 29.79KHz
v: height 486 start 489 end 494 total 500 clock 59.57Hz
640x480 (0x1d3) 25.175MHz -HSync -VSync
h: width 640 start 656 end 752 total 800 skew 0 clock 31.47KHz
v: height 480 start 490 end 492 total 525 clock 59.94Hz
720x405 (0x1d4) 22.500MHz -HSync +VSync
h: width 720 start 744 end 808 total 896 skew 0 clock 25.11KHz
v: height 405 start 408 end 413 total 422 clock 59.51Hz
720x405 (0x1d5) 22.176MHz -HSync +VSync
h: width 720 start 728 end 800 total 880 skew 0 clock 25.20KHz
v: height 405 start 406 end 409 total 420 clock 60.00Hz
720x405 (0x1d6) 21.750MHz +HSync -VSync
h: width 720 start 768 end 800 total 880 skew 0 clock 24.72KHz
v: height 405 start 408 end 413 total 419 clock 58.99Hz
640x360 (0x1d7) 18.000MHz -HSync +VSync
h: width 640 start 664 end 720 total 800 skew 0 clock 22.50KHz
v: height 360 start 363 end 368 total 376 clock 59.84Hz
640x360 (0x1d8) 17.750MHz +HSync -VSync
h: width 640 start 688 end 720 total 800 skew 0 clock 22.19KHz
v: height 360 start 363 end 368 total 374 clock 59.32Hz
640x360 (0x1d9) 17.187MHz -HSync +VSync
h: width 640 start 640 end 704 total 768 skew 0 clock 22.38KHz
v: height 360 start 361 end 364 total 373 clock 60.00Hz
DP1 disconnected (normal left inverted right x axis y axis)
Identifier: 0x44
Timestamp: 910112952
Subpixel: unknown
Clones: HDMI1
CRTCs: 0 1 2
Transform: 1.000000 0.000000 0.000000
0.000000 1.000000 0.000000
0.000000 0.000000 1.000000
filter:
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
link-status: Good
supported: Good, Bad
non-desktop: 0
range: (0, 1)
DP2 disconnected (normal left inverted right x axis y axis)
Identifier: 0x45
Timestamp: 910112952
Subpixel: unknown
Clones:
CRTCs: 0 1 2
Transform: 1.000000 0.000000 0.000000
0.000000 1.000000 0.000000
0.000000 0.000000 1.000000
filter:
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
link-status: Good
supported: Good, Bad
non-desktop: 0
range: (0, 1)
HDMI1 disconnected (normal left inverted right x axis y axis)
Identifier: 0x46
Timestamp: 910112952
Subpixel: unknown
Clones: DP1
CRTCs: 0 1 2
Transform: 1.000000 0.000000 0.000000
0.000000 1.000000 0.000000
0.000000 0.000000 1.000000
filter:
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
non-desktop: 0
range: (0, 1)
VIRTUAL1 disconnected (normal left inverted right x axis y axis)
Identifier: 0x47
Timestamp: 910112952
Subpixel: no subpixels
Clones:
CRTCs: 3
Transform: 1.000000 0.000000 0.000000
0.000000 1.000000 0.000000
0.000000 0.000000 1.000000
filter:
non-desktop: 0
supported: 0, 1

View File

@ -0,0 +1,56 @@
{
"screens": [
{
"screen_number": 0,
"minimum_width": 8,
"minimum_height": 8,
"current_width": 1920,
"current_height": 1080,
"maximum_width": 32767,
"maximum_height": 32767,
"associated_device": {
"associated_modes": [
{
"resolution_width": 1920,
"resolution_height": 1080,
"is_high_resolution": false,
"frequencies": [
{
"frequency": 60.03,
"is_current": true,
"is_preferred": true
},
{
"frequency": 59.93,
"is_current": false,
"is_preferred": false
}
]
},
{
"resolution_width": 1680,
"resolution_height": 1050,
"is_high_resolution": false,
"frequencies": [
{
"frequency": 59.88,
"is_current": false,
"is_preferred": false
}
]
}
],
"is_connected": true,
"is_primary": true,
"device_name": "eDP1",
"resolution_width": 1920,
"resolution_height": 1080,
"offset_width": 0,
"offset_height": 0,
"dimension_width": 310,
"dimension_height": 170
}
}
],
"unassociated_devices": []
}

View File

@ -0,0 +1,4 @@
Screen 0: minimum 8 x 8, current 1920 x 1080, maximum 32767 x 32767
eDP1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 310mm x 170mm
1920x1080 60.03*+ 59.93
1680x1050 59.88

38
tests/fixtures/generic/xrandr.out vendored Normal file
View File

@ -0,0 +1,38 @@
Screen 0: minimum 8 x 8, current 1920 x 1080, maximum 32767 x 32767
eDP1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 310mm x 170mm
1920x1080 60.03*+ 59.93
1680x1050 59.88
1400x1050 59.98
1600x900 60.00 59.95 59.82
1280x1024 60.02
1400x900 59.96 59.88
1280x960 60.00
1368x768 60.00 59.88 59.85
1280x800 59.81 59.91
1280x720 59.86 60.00 59.74
1024x768 60.00
1024x576 60.00 59.90 59.82
960x540 60.00 59.63 59.82
800x600 60.32 56.25
864x486 60.00 59.92 59.57
640x480 59.94
720x405 59.51 60.00 58.99
640x360 59.84 59.32 60.00
DP1 disconnected (normal left inverted right x axis y axis)
DP2 disconnected (normal left inverted right x axis y axis)
HDMI1 connected (normal left inverted right x axis y axis)
1920x1080 60.00 + 50.00 59.94
1920x1080i 60.00 50.00 59.94
1680x1050 59.88
1280x1024 75.02 60.02
1440x900 59.90
1280x960 60.00
1280x720 60.00 50.00 59.94
1024x768 75.03 70.07 60.00
832x624 74.55
800x600 72.19 75.00 60.32 56.25
720x576 50.00
720x480 60.00 59.94
640x480 75.00 72.81 66.67 60.00 59.94
720x400 70.08
VIRTUAL1 disconnected (normal left inverted right x axis y axis)

43
tests/fixtures/generic/xrandr_2.out vendored Normal file
View File

@ -0,0 +1,43 @@
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
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)
HDMI-1 disconnected (normal left inverted right x axis y axis)
DP-2 disconnected (normal left inverted right x axis y axis)

View File

@ -0,0 +1,15 @@
HDMI1 connected (normal left inverted right x axis y axis)
1920x1080 60.00 + 50.00 59.94
1920x1080i 60.00 50.00 59.94
1680x1050 59.88
1280x1024 75.02 60.02
1440x900 59.90
1280x960 60.00
1280x720 60.00 50.00 59.94
1024x768 75.03 70.07 60.00
832x624 74.55
800x600 72.19 75.00 60.32 56.25
720x576 50.00
720x480 60.00 59.94
640x480 75.00 72.81 66.67 60.00 59.94
720x400 70.08

172
tests/test_xrandr.py Normal file
View File

@ -0,0 +1,172 @@
import json
import re
import unittest
from typing import Optional
from jc.parsers.xrandr import (
_parse_screen,
_parse_device,
_parse_mode,
_device_pattern,
_screen_pattern,
_mode_pattern,
_frequencies_pattern,
parse,
Mode,
Device,
Screen,
)
class XrandrTests(unittest.TestCase):
def test_regexes(self):
devices = [
"HDMI1 connected (normal left inverted right x axis y axis)",
"VIRTUAL1 disconnected (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",
"eDP-1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 309mm x 174mm"
]
for device in devices:
self.assertIsNotNone(re.match(_device_pattern, device))
screens = [
"Screen 0: minimum 8 x 8, current 1920 x 1080, maximum 32767 x 32767",
"Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 16384 x 16384"
]
for screen in screens:
self.assertIsNotNone(re.match(_screen_pattern, screen))
modes = [
"1920x1080 60.03*+ 59.93",
"1680x1050 59.88",
"1400x1050 59.98",
"1600x900 60.00 59.95 59.82",
"1280x1024 60.02",
"1400x900 59.96 59.88",
]
for mode in modes:
match = re.match(_mode_pattern, mode)
self.assertIsNotNone(match)
if match:
rest = match.groupdict()["rest"]
self.assertIsNotNone(re.match(_frequencies_pattern, rest))
def test_screens(self):
sample = "Screen 0: minimum 8 x 8, current 1920 x 1080, maximum 32767 x 32767"
actual: Optional[Screen] = _parse_screen([sample])
self.assertIsNotNone(actual)
expected = {
"screen_number": 0,
"minimum_width": 8,
"minimum_height": 8,
"current_width": 1920,
"current_height": 1080,
"maximum_width": 32767,
"maximum_height": 32767,
}
if actual:
for k, v in expected.items():
self.assertEqual(v, actual[k], f"screens regex failed on {k}")
sample = "Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 16384 x 16384"
actual = _parse_screen([sample])
if actual:
self.assertEqual(320, actual["minimum_width"])
else:
raise AssertionError("Screen should not be None")
def test_device(self):
# regex101 sample link for tests/edits https://regex101.com/r/3cHMv3/1
sample = "eDP1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 310mm x 170mm"
actual: Optional[Device] = _parse_device([sample])
expected = {
"device_name": "eDP1",
"is_connected": True,
"is_primary": True,
"resolution_width": 1920,
"resolution_height": 1080,
"offset_width": 0,
"offset_height": 0,
"dimension_width": 310,
"dimension_height": 170,
}
self.assertIsNotNone(actual)
if actual:
for k, v in expected.items():
self.assertEqual(v, actual[k], f"Devices regex failed on {k}")
with open("tests/fixtures/generic/xrandr_device.out", "r") as f:
extended_sample = f.read().splitlines()
extended_sample.reverse()
device = _parse_device(extended_sample)
if device:
self.assertEqual(
59.94, device["associated_modes"][12]["frequencies"][4]["frequency"]
)
def test_mode(self):
sample_1 = "1920x1080 60.03*+ 59.93"
expected = {
"frequencies": [
{"frequency": 60.03, "is_current": True, "is_preferred": True},
{"frequency": 59.93, "is_current": False, "is_preferred": False},
],
"resolution_width": 1920,
"resolution_height": 1080,
"is_high_resolution": False,
}
actual: Optional[Mode] = _parse_mode(sample_1)
self.assertIsNotNone(actual)
if actual:
for k, v in expected.items():
self.assertEqual(v, actual[k], f"mode regex failed on {k}")
sample_2 = " 1920x1080i 60.00 50.00 59.94"
actual: Optional[Mode] = _parse_mode(sample_2)
self.assertIsNotNone(actual)
if actual:
self.assertEqual(True, actual["is_high_resolution"])
self.assertEqual(50.0, actual["frequencies"][1]["frequency"])
def test_complete(self):
self.maxDiff = None
with open("tests/fixtures/generic/xrandr.out", "r") as f:
txt = f.read()
actual = parse(txt)
self.assertEqual(1, len(actual["screens"]))
self.assertEqual(4, len(actual["unassociated_devices"]))
self.assertEqual(
18, len(actual["screens"][0]["associated_device"]["associated_modes"])
)
with open("tests/fixtures/generic/xrandr_2.out", "r") as f:
txt = f.read()
actual = parse(txt)
self.assertEqual(1, len(actual["screens"]))
self.assertEqual(3, len(actual["unassociated_devices"]))
self.assertEqual(
38, len(actual["screens"][0]["associated_device"]["associated_modes"])
)
with open("tests/fixtures/generic/simple_xrandr.out", "r") as f:
txt = f.read()
actual = parse(txt)
with open("tests/fixtures/generic/simple_xrandr.json", "w") as f:
json.dump(actual, f, indent=True)
self.assertEqual(1, len(actual["screens"]))
self.assertEqual(0, len(actual["unassociated_devices"]))
self.assertEqual(
2, len(actual["screens"][0]["associated_device"]["associated_modes"])
)