diff --git a/jc/parsers/xrandr.py b/jc/parsers/xrandr.py new file mode 100644 index 00000000..9d8254bb --- /dev/null +++ b/jc/parsers/xrandr.py @@ -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\d+): " + + "minimum (?P\d+) x (?P\d+), " + + "current (?P\d+) x (?P\d+), " + + "maximum (?P\d+) x (?P\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.+) " + + "(?P(connected|disconnected)) ?" + + "(?P primary)? ?" + + "((?P\d+)x(?P\d+)" + + "\+(?P\d+)\+(?P\d+))? " + + "\(normal left inverted right x axis y axis\)" + + "( ((?P\d+)mm x (?P\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\d+)x(?P\d+)(?Pi)?\s+(?P.*)" +_frequencies_pattern = r"(((?P\d+\.\d+)(?P\*| |)(?P\+?)?)+)" + + +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 diff --git a/templates/xrandr_sample.txt b/templates/xrandr_sample.txt new file mode 100644 index 00000000..93bbf7b5 --- /dev/null +++ b/templates/xrandr_sample.txt @@ -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) diff --git a/templates/xrandr_sample_verbose.txt b/templates/xrandr_sample_verbose.txt new file mode 100644 index 00000000..ad01cb39 --- /dev/null +++ b/templates/xrandr_sample_verbose.txt @@ -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 diff --git a/tests/fixtures/generic/simple_xrandr.json b/tests/fixtures/generic/simple_xrandr.json new file mode 100644 index 00000000..a5aff206 --- /dev/null +++ b/tests/fixtures/generic/simple_xrandr.json @@ -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": [] +} \ No newline at end of file diff --git a/tests/fixtures/generic/simple_xrandr.out b/tests/fixtures/generic/simple_xrandr.out new file mode 100644 index 00000000..2ced152f --- /dev/null +++ b/tests/fixtures/generic/simple_xrandr.out @@ -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 diff --git a/tests/fixtures/generic/xrandr.out b/tests/fixtures/generic/xrandr.out new file mode 100644 index 00000000..93bbf7b5 --- /dev/null +++ b/tests/fixtures/generic/xrandr.out @@ -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) diff --git a/tests/fixtures/generic/xrandr_2.out b/tests/fixtures/generic/xrandr_2.out new file mode 100644 index 00000000..3ed0efd5 --- /dev/null +++ b/tests/fixtures/generic/xrandr_2.out @@ -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) diff --git a/tests/fixtures/generic/xrandr_device.out b/tests/fixtures/generic/xrandr_device.out new file mode 100644 index 00000000..32134538 --- /dev/null +++ b/tests/fixtures/generic/xrandr_device.out @@ -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 diff --git a/tests/test_xrandr.py b/tests/test_xrandr.py new file mode 100644 index 00000000..2b86368a --- /dev/null +++ b/tests/test_xrandr.py @@ -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"]) + )