mutter/tools/gdctl
Jonas Ådahl cc11b0682b gdtl: Add Monitor class
This makes it possible to avoid dealing directly with the variant when
operating on a monitor.

Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4190>
2025-01-30 11:29:38 +00:00

433 lines
12 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import sys
from dataclasses import dataclass
from gi.repository import GLib, Gio
NAME = "org.gnome.Mutter.DisplayConfig"
INTERFACE = "org.gnome.Mutter.DisplayConfig"
OBJECT_PATH = "/org/gnome/Mutter/DisplayConfig"
TRANSFORM_STRINGS = {
0: "normal",
1: "90",
2: "180",
3: "270",
4: "flipped",
5: "flipped-90",
6: "flipped-180",
7: "flipped-270",
}
LAYOUT_MODE = {
1: "logical",
2: "physical",
}
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 maybe_describe(property_name, value):
enum_properties = {
"layout-mode": LAYOUT_MODE,
}
if property_name in enum_properties:
if isinstance(value, list):
return [
enum_properties[property_name].get(entry) for entry in value
]
else:
return enum_properties[property_name].get(value)
else:
return value
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_string = maybe_describe(key, properties[key])
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
@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=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 = variant[2]
class MonitorsState:
STATE_VARIANT_TYPE = GLib.VariantType.new(
"(ua((ssss)a(siiddada{sv})a{sv})a(iiduba(ssss)a{sv})a{sv})"
)
def __init__(self):
self.current_state = self.get_current_state()
self.init_monitors()
def get_current_state(self) -> GLib.Variant:
raise NotImplementedError()
def init_monitors(self):
self.monitors = {}
for monitor_variant in self.get_monitors_variant():
monitor = Monitor(monitor_variant)
self.monitors[monitor.connector] = monitor
def get_monitors_variant(self):
return self.current_state[1]
def get_logical_monitors_variant(self):
return self.current_state[2]
def get_properties_variant(self):
return self.current_state[3]
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
print_data(
level=0,
is_last=is_last,
lines=lines,
data=f"Monitor {monitor.connector}",
)
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:")
logical_monitors = self.get_logical_monitors_variant()
index = 1
for logical_monitor in logical_monitors:
is_last = logical_monitor == logical_monitors[-1]
properties = logical_monitor[2]
print_data(
level=0,
is_last=is_last,
lines=lines,
data=f"Logical monitor #{index}",
)
print_data(
level=1,
is_last=False,
lines=lines,
data=f"Position: ({logical_monitor[0]}, {logical_monitor[1]})",
)
print_data(
level=1,
is_last=False,
lines=lines,
data=f"Scale: {logical_monitor[2]}",
)
print_data(
level=1,
is_last=False,
lines=lines,
data=f"Transform: {TRANSFORM_STRINGS.get(logical_monitor[3])}",
)
print_data(
level=1,
is_last=False,
lines=lines,
data=f"Primary: {logical_monitor[4]}",
)
monitors = logical_monitor[5]
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]
print_data(
level=2,
is_last=is_last,
lines=lines,
data=f"{monitor[0]} ({monitor[1]}, {monitor[2]}, {monitor[3]})",
)
if show_properties:
properties = logical_monitor[6]
print_properties(level=1, lines=lines, properties=properties)
index += 1
if show_properties:
properties = self.get_properties_variant()
print()
print_properties(level=-1, lines=lines, properties=properties)
class MonitorsStateDBus(MonitorsState):
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,
)
super().__init__()
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
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:
monitors_state = MonitorsStateDBus()
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,
)