1
0
mirror of https://github.com/kellyjonbrazil/jc.git synced 2025-06-17 00:07:37 +02:00
Files
jc/jc/parsers/xrandr.py
2022-04-10 10:31:13 -07:00

381 lines
11 KiB
Python

"""jc - JSON Convert `xrandr` command output parser
Usage (cli):
$ xrandr | jc --xrandr
or
$ jc xrandr
Usage (module):
import jc
result = jc.parse('xrandr', xrandr_command_output)
Schema:
{
"screens": [
{
"screen_number": integer,
"minimum_width": integer,
"minimum_height": integer,
"current_width": integer,
"current_height": integer,
"maximum_width": integer,
"maximum_height": integer,
"associated_device": {
"associated_modes": [
{
"resolution_width": integer,
"resolution_height": integer,
"is_high_resolution": boolean,
"frequencies": [
{
"frequency": float,
"is_current": boolean,
"is_preferred": boolean
}
]
}
]
},
"is_connected": boolean,
"is_primary": boolean,
"device_name": string,
"resolution_width": integer,
"resolution_height": integer,
"offset_width": integer,
"offset_height": integer,
"dimension_width": integer,
"dimension_height": integer,
"rotation": string
}
],
"unassociated_devices": [
{
"associated_modes": [
{
"resolution_width": integer,
"resolution_height": integer,
"is_high_resolution": boolean,
"frequencies": [
{
"frequency": float,
"is_current": boolean,
"is_preferred": boolean
}
]
}
]
}
]
}
Examples:
$ xrandr | jc --xrandr -p
{
"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,
"rotation": "normal"
}
}
],
"unassociated_devices": []
}
"""
import re
from typing import Dict, List, Optional, Union
import jc.utils
class info:
"""Provides parser metadata (version, author, etc.)"""
version = "1.1"
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
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, Union[int, str]]
Device = Dict[str, Union[str, int, bool]]
Frequency = Dict[str, Union[float, bool]]
Mode = Dict[str, Union[int, bool, List[Frequency]]]
Response = Dict[str, Union[Device, Mode, Screen]]
_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
_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+))? "
+ "(?P<rotation>(inverted|left|right))? ?"
+ "\(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], quiet: bool = False) -> 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"],
"rotation": matches["rotation"] or "normal",
}
for k, v in matches.items():
if k not in {"is_connected", "is_primary", "device_name", "rotation"}:
try:
if v:
device[k] = int(v)
except ValueError and not quiet:
jc.utils.warning_message(
[f"{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: bool =False, quiet: bool =False) -> Dict:
"""
Main text parsing function
Parameters:
data: (string) text data to parse
raw: (boolean) unprocessed output if True
quiet: (boolean) suppress warning messages if True
Returns:
Dictionary. Raw or processed structured data.
"""
jc.utils.compatibility(__name__, info.compatible, quiet)
jc.utils.input_type_check(data)
linedata = data.splitlines()
linedata.reverse() # For popping
result: Response = {"screens": [], "unassociated_devices": []}
if jc.utils.has_data(data):
while linedata:
screen = _parse_screen(linedata)
if screen:
result["screens"].append(screen)
else:
device = _parse_device(linedata, quiet)
if device:
result["unassociated_devices"].append(device)
if not result["unassociated_devices"] and not result["screens"]:
return {}
return result