#!/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, )