
It's not used by anything, all data is accessed by data structures derived from the variant. Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4190>
524 lines
15 KiB
Python
Executable File
524 lines
15 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import sys
|
|
|
|
from dataclasses import dataclass
|
|
from gi.repository import GLib, Gio
|
|
from enum import Enum
|
|
|
|
NAME = "org.gnome.Mutter.DisplayConfig"
|
|
INTERFACE = "org.gnome.Mutter.DisplayConfig"
|
|
OBJECT_PATH = "/org/gnome/Mutter/DisplayConfig"
|
|
|
|
|
|
class Transform(Enum):
|
|
NORMAL = 0
|
|
ROTATE_90 = 1
|
|
ROTATE_180 = 2
|
|
ROTATE_270 = 3
|
|
FLIPPED = 4
|
|
ROTATE_90_FLIPPED = 5
|
|
ROTATE_270_FLIPPED = 6
|
|
ROTATE_180_FLIPPED = 7
|
|
|
|
def __str__(self):
|
|
match self:
|
|
case self.NORMAL:
|
|
return "normal"
|
|
case self.ROTATE_90:
|
|
return "90"
|
|
case self.ROTATE_180:
|
|
return "180"
|
|
case self.ROTATE_270:
|
|
return "270"
|
|
case self.FLIPPED:
|
|
return "flipped"
|
|
case self.ROTATE_90_FLIPPED:
|
|
return "flipped-90"
|
|
case self.ROTATE_180_FLIPPED:
|
|
return "flipped-180"
|
|
case self.ROTATE_270_FLIPPED:
|
|
return "flipped-270"
|
|
|
|
|
|
class LayoutMode(Enum):
|
|
LOGICAL = 1
|
|
PHYSICAL = 2
|
|
|
|
def __str__(self):
|
|
match self:
|
|
case self.LOGICAL:
|
|
return "logical"
|
|
case self.PHYSICAL:
|
|
return "physical"
|
|
|
|
|
|
def translate_property(name, value):
|
|
enum_properties = {
|
|
"layout-mode": LayoutMode,
|
|
}
|
|
|
|
if name in enum_properties:
|
|
if isinstance(value, list):
|
|
return [enum_properties[name](element) for element in value]
|
|
else:
|
|
return enum_properties[name](value)
|
|
else:
|
|
return value
|
|
|
|
|
|
def translate_properties(variant):
|
|
return {
|
|
key: translate_property(key, value) for key, value in variant.items()
|
|
}
|
|
|
|
|
|
def print_data(*, level: int, is_last: bool, lines: list[int], data: str):
|
|
if is_last:
|
|
link = "└"
|
|
else:
|
|
link = "├"
|
|
padding = " "
|
|
|
|
if level >= 0:
|
|
indent = level
|
|
buffer = f"{link:{padding}>{indent * 4}}──{data}"
|
|
buffer = list(buffer)
|
|
for line in lines:
|
|
if line == level:
|
|
continue
|
|
index = line * 4
|
|
if line > 0:
|
|
index -= 1
|
|
buffer[index] = "│"
|
|
buffer = "".join(buffer)
|
|
else:
|
|
buffer = data
|
|
|
|
print(buffer)
|
|
|
|
if is_last and level in lines:
|
|
lines.remove(level)
|
|
elif not is_last and level not in lines:
|
|
lines.append(level)
|
|
|
|
|
|
def print_properties(*, level, lines, properties):
|
|
property_keys = list(properties.keys())
|
|
|
|
print_data(
|
|
level=level,
|
|
is_last=True,
|
|
lines=lines,
|
|
data=f"Properties: ({len(property_keys)})",
|
|
)
|
|
for key in property_keys:
|
|
is_last = key == property_keys[-1]
|
|
|
|
value = properties[key]
|
|
if isinstance(value, list):
|
|
elements_string = ", ".join([str(element) for element in value])
|
|
value_string = f"[{elements_string}]"
|
|
elif isinstance(value, bool):
|
|
value_string = "yes" if value else "no"
|
|
else:
|
|
value_string = str(value)
|
|
|
|
print_data(
|
|
level=level + 1,
|
|
is_last=is_last,
|
|
lines=lines,
|
|
data=f"{key} ⇒ {value_string}",
|
|
)
|
|
|
|
|
|
def strip_dbus_error_prefix(message):
|
|
if message.startswith("GDBus.Error"):
|
|
return message.partition(" ")[2]
|
|
else:
|
|
return message
|
|
|
|
|
|
class DisplayConfig:
|
|
STATE_VARIANT_TYPE = GLib.VariantType.new(
|
|
"(ua((ssss)a(siiddada{sv})a{sv})a(iiduba(ssss)a{sv})a{sv})"
|
|
)
|
|
|
|
def __init__(self):
|
|
self._proxy = Gio.DBusProxy.new_for_bus_sync(
|
|
bus_type=Gio.BusType.SESSION,
|
|
flags=Gio.DBusProxyFlags.NONE,
|
|
info=None,
|
|
name=NAME,
|
|
object_path=OBJECT_PATH,
|
|
interface_name=INTERFACE,
|
|
cancellable=None,
|
|
)
|
|
|
|
def get_current_state(self) -> GLib.Variant:
|
|
variant = self._proxy.call_sync(
|
|
method_name="GetCurrentState",
|
|
parameters=None,
|
|
flags=Gio.DBusCallFlags.NO_AUTO_START,
|
|
timeout_msec=-1,
|
|
cancellable=None,
|
|
)
|
|
assert variant.get_type().equal(self.STATE_VARIANT_TYPE)
|
|
return variant
|
|
|
|
|
|
@dataclass
|
|
class MonitorMode:
|
|
name: str
|
|
resolution: tuple[int, int]
|
|
refresh_rate: float
|
|
preferred_scale: float
|
|
supported_scales: list[float]
|
|
refresh_rate: float
|
|
properties: dict
|
|
|
|
@classmethod
|
|
def from_variant(cls, variant):
|
|
return cls(
|
|
name=variant[0],
|
|
resolution=(variant[1], variant[2]),
|
|
refresh_rate=variant[3],
|
|
preferred_scale=variant[4],
|
|
supported_scales=variant[5],
|
|
properties=translate_properties(variant[6]),
|
|
)
|
|
|
|
|
|
class Monitor:
|
|
def __init__(self, variant):
|
|
self.init_from_variant(variant)
|
|
|
|
def init_from_variant(self, variant):
|
|
spec = variant[0]
|
|
self.connector = spec[0]
|
|
self.vendor = spec[1] if spec[1] != "" else None
|
|
self.product = spec[2] if spec[2] != "" else None
|
|
self.serial = spec[3] if spec[3] != "" else None
|
|
self.modes = [
|
|
MonitorMode.from_variant(mode_variant)
|
|
for mode_variant in variant[1]
|
|
]
|
|
self.properties = translate_properties(variant[2])
|
|
|
|
self.display_name = self.properties.get("display-name", None)
|
|
|
|
|
|
class LogicalMonitor:
|
|
def __init__(
|
|
self,
|
|
monitors,
|
|
scale,
|
|
position=(0, 0),
|
|
transform=Transform.NORMAL,
|
|
is_primary=False,
|
|
properties={},
|
|
):
|
|
self.position = position
|
|
self.scale = scale
|
|
self.transform = transform
|
|
self.is_primary = is_primary
|
|
self.monitors = monitors
|
|
self.properties = properties
|
|
|
|
@classmethod
|
|
def new_from_variant(cls, monitors_state, variant):
|
|
position = (variant[0], variant[1])
|
|
scale = variant[2]
|
|
transform = Transform(variant[3])
|
|
is_primary = variant[4]
|
|
connectors = [connector for connector, _, _, _ in variant[5]]
|
|
monitors = [
|
|
monitors_state.monitors[connector] for connector in connectors
|
|
]
|
|
properties = translate_properties(variant[6])
|
|
|
|
return cls(
|
|
monitors=monitors,
|
|
position=position,
|
|
scale=scale,
|
|
transform=transform,
|
|
is_primary=is_primary,
|
|
properties=properties,
|
|
)
|
|
|
|
|
|
class MonitorsState:
|
|
def __init__(self, display_config):
|
|
current_state = display_config.get_current_state()
|
|
|
|
self.properties = translate_properties(current_state[3])
|
|
|
|
self.init_monitors(current_state)
|
|
self.init_logical_monitors(current_state)
|
|
|
|
def init_monitors(self, current_state):
|
|
self.monitors = {}
|
|
for monitor_variant in current_state[1]:
|
|
monitor = Monitor(monitor_variant)
|
|
self.monitors[monitor.connector] = monitor
|
|
|
|
def init_logical_monitors(self, current_state):
|
|
self.logical_monitors = []
|
|
for variant in current_state[2]:
|
|
logical_monitor = LogicalMonitor.new_from_variant(self, variant)
|
|
self.logical_monitors.append(logical_monitor)
|
|
|
|
def print_mode(self, mode, is_last, show_properties, lines):
|
|
print_data(level=2, is_last=is_last, lines=lines, data=f"{mode.name}")
|
|
|
|
if not show_properties:
|
|
return
|
|
|
|
width, height = mode.resolution
|
|
print_data(
|
|
level=3,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"Dimension: {width}x{height}",
|
|
)
|
|
print_data(
|
|
level=3,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"Refresh rate: {mode.refresh_rate:.3f}",
|
|
)
|
|
print_data(
|
|
level=3,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"Preferred scale: {mode.preferred_scale}",
|
|
)
|
|
print_data(
|
|
level=3,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"Supported scales: {mode.supported_scales}",
|
|
)
|
|
|
|
if show_properties:
|
|
mode_properties = mode.properties
|
|
print_properties(level=3, lines=lines, properties=mode_properties)
|
|
|
|
def print_current_state(self, show_modes=False, show_properties=False):
|
|
print("Monitors:")
|
|
lines = []
|
|
monitors = list(self.monitors.values())
|
|
for monitor in monitors:
|
|
is_last = monitor == monitors[-1]
|
|
modes = monitor.modes
|
|
properties = monitor.properties
|
|
|
|
if monitor.display_name:
|
|
monitor_title = (
|
|
f"Monitor {monitor.connector} ({monitor.display_name})"
|
|
)
|
|
else:
|
|
monitor_title = f"Monitor {monitor.connector}"
|
|
|
|
print_data(
|
|
level=0,
|
|
is_last=is_last,
|
|
lines=lines,
|
|
data=monitor_title,
|
|
)
|
|
|
|
if monitor.vendor:
|
|
print_data(
|
|
level=1,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"Vendor: {monitor.vendor}",
|
|
)
|
|
if monitor.product:
|
|
print_data(
|
|
level=1,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"Product: {monitor.product}",
|
|
)
|
|
if monitor.serial:
|
|
print_data(
|
|
level=1,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"Serial: {monitor.serial}",
|
|
)
|
|
|
|
if show_modes:
|
|
print_data(
|
|
level=1,
|
|
is_last=not show_properties,
|
|
lines=lines,
|
|
data=f"Modes ({len(modes)})",
|
|
)
|
|
for mode in modes:
|
|
is_last = mode == modes[-1]
|
|
self.print_mode(mode, is_last, show_properties, lines)
|
|
else:
|
|
mode = next(
|
|
(mode for mode in modes if "is-current" in mode.properties),
|
|
None,
|
|
)
|
|
if mode:
|
|
mode_type = "Current"
|
|
else:
|
|
mode = next(
|
|
(
|
|
mode
|
|
for mode in modes
|
|
if "is-preferred" in mode.properties
|
|
),
|
|
None,
|
|
)
|
|
if mode:
|
|
mode_type = "Preferred"
|
|
|
|
if mode:
|
|
print_data(
|
|
level=1,
|
|
is_last=not show_properties,
|
|
lines=lines,
|
|
data=f"{mode_type} mode",
|
|
)
|
|
self.print_mode(mode, True, show_properties, lines)
|
|
|
|
if show_properties:
|
|
print_properties(level=1, lines=lines, properties=properties)
|
|
|
|
print()
|
|
print("Logical monitors:")
|
|
index = 1
|
|
for logical_monitor in self.logical_monitors:
|
|
is_last = logical_monitor == self.logical_monitors[-1]
|
|
print_data(
|
|
level=0,
|
|
is_last=is_last,
|
|
lines=lines,
|
|
data=f"Logical monitor #{index}",
|
|
)
|
|
(x, y) = logical_monitor.position
|
|
print_data(
|
|
level=1,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"Position: ({x}, {y})",
|
|
)
|
|
print_data(
|
|
level=1,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"Scale: {logical_monitor.scale}",
|
|
)
|
|
print_data(
|
|
level=1,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"Transform: {logical_monitor.transform}",
|
|
)
|
|
print_data(
|
|
level=1,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"Primary: {'yes' if logical_monitor.is_primary else 'no'}",
|
|
)
|
|
monitors = logical_monitor.monitors
|
|
print_data(
|
|
level=1,
|
|
is_last=not show_properties,
|
|
lines=lines,
|
|
data=f"Monitors: ({len(monitors)})",
|
|
)
|
|
for monitor in monitors:
|
|
is_last = monitor == monitors[-1]
|
|
|
|
if monitor.display_name:
|
|
monitor_title = (
|
|
f"{monitor.connector} ({monitor.display_name})"
|
|
)
|
|
else:
|
|
monitor_title = f"{monitor.connector}"
|
|
|
|
print_data(
|
|
level=2,
|
|
is_last=is_last,
|
|
lines=lines,
|
|
data=monitor_title,
|
|
)
|
|
|
|
if show_properties:
|
|
properties = logical_monitor.properties
|
|
print_properties(level=1, lines=lines, properties=properties)
|
|
|
|
index += 1
|
|
|
|
if show_properties:
|
|
properties = self.properties
|
|
print()
|
|
print_properties(level=-1, lines=lines, properties=properties)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description="Display control utility")
|
|
|
|
subparser = parser.add_subparsers(
|
|
dest="command",
|
|
title="The following commands are available",
|
|
metavar="COMMAND",
|
|
required=True,
|
|
)
|
|
show_parser = subparser.add_parser(
|
|
"show", help="Show display configuration"
|
|
)
|
|
show_parser.add_argument(
|
|
"-m",
|
|
"--modes",
|
|
action="store_true",
|
|
help="List available monitor modes",
|
|
)
|
|
show_parser.add_argument(
|
|
"-p",
|
|
"--properties",
|
|
action="store_true",
|
|
help="List properties",
|
|
)
|
|
show_parser.add_argument(
|
|
"-v",
|
|
"--verbose",
|
|
action="store_true",
|
|
help="Display all available information",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
match args.command:
|
|
case "show":
|
|
try:
|
|
display_config = DisplayConfig()
|
|
monitors_state = MonitorsState(display_config)
|
|
except GLib.Error as e:
|
|
if e.domain == GLib.quark_to_string(Gio.DBusError.quark()):
|
|
error_message = strip_dbus_error_prefix(e.message)
|
|
print(
|
|
f"Failed retrieve current state: {error_message}",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
|
|
if args.verbose:
|
|
show_modes = True
|
|
show_properties = True
|
|
else:
|
|
show_modes = args.modes
|
|
show_properties = args.properties
|
|
|
|
monitors_state.print_current_state(
|
|
show_modes=show_modes,
|
|
show_properties=show_properties,
|
|
)
|