#!/usr/bin/env python3 import argparse import enum import subprocess import sys 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', } class Source(enum.Enum): DBUS = 1 COMMAND_LINE = 2 FILE = 3 class MonitorConfig: CONFIG_VARIANT_TYPE = GLib.VariantType.new( '(ua((ssss)a(siiddada{sv})a{sv})a(iiduba(ssss)a{sv})a{sv})') def get_current_state(self) -> GLib.Variant: raise NotImplementedError() def parse_data(self): """TODO: add data parser so that can be used for reconfiguring""" def print_data(self, *, level, is_last, lines, data): 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(self, *, level, lines, properties): property_list = list(properties) self.print_data(level=level, is_last=True, lines=lines, data=f'Properties: ({len(property_list)})') for property in property_list: is_last = property == property_list[-1] self.print_data(level=level + 1, is_last=is_last, lines=lines, data=f'{property} ⇒ {properties[property]}') def print_current_state(self, short): variant = self.get_current_state() print('Serial: {}'.format(variant[0])) print() print('Monitors:') monitors = variant[1] lines = [] for monitor in monitors: is_last = monitor == monitors[-1] spec = monitor[0] modes = monitor[1] properties = monitor[2] self.print_data(level=0, is_last=is_last, lines=lines, data='Monitor {}'.format(spec[0])) self.print_data(level=1, is_last=False, lines=lines, data=f'EDID: vendor: {spec[1]}, product: {spec[2]}, serial: {spec[3]}') mode_count = len(modes) if short: modes = [mode for mode in modes if len(mode[6]) > 0] self.print_data(level=1, is_last=False, lines=lines, data=f'Modes ({len(modes)}, {mode_count - len(modes)} omitted)') else: self.print_data(level=1, is_last=False, lines=lines, data=f'Modes ({len(modes)})') for mode in modes: is_last = mode == modes[-1] self.print_data(level=2, is_last=is_last, lines=lines, data=f'{mode[0]}') self.print_data(level=3, is_last=False, lines=lines, data=f'Dimension: {mode[1]}x{mode[2]}') self.print_data(level=3, is_last=False, lines=lines, data=f'Refresh rate: {mode[3]:.3f}') self.print_data(level=3, is_last=False, lines=lines, data=f'Preferred scale: {mode[4]}') self.print_data(level=3, is_last=False, lines=lines, data=f'Supported scales: {mode[5]}') mode_properties = mode[6] self.print_properties(level=3, lines=lines, properties=mode_properties) self.print_properties(level=1, lines=lines, properties=properties) print() print('Logical monitors:') logical_monitors = variant[2] index = 1 for logical_monitor in logical_monitors: is_last = logical_monitor == logical_monitors[-1] properties = logical_monitor[2] self.print_data(level=0, is_last=is_last, lines=lines, data=f'Logical monitor #{index}') self.print_data(level=1, is_last=False, lines=lines, data=f'Position: ({logical_monitor[0]}, {logical_monitor[1]})') self.print_data(level=1, is_last=False, lines=lines, data=f'Scale: {logical_monitor[2]}') self.print_data(level=1, is_last=False, lines=lines, data=f'Transform: {TRANSFORM_STRINGS.get(logical_monitor[3])}') self.print_data(level=1, is_last=False, lines=lines, data=f'Primary: {logical_monitor[4]}') monitors = logical_monitor[5] self.print_data(level=1, is_last=False, lines=lines, data=f'Monitors: ({len(monitors)})') for monitor in monitors: is_last = monitor == monitors[-1] self.print_data(level=2, is_last=is_last, lines=lines, data=f'{monitor[0]} ({monitor[1]}, {monitor[2]}, {monitor[3]})') properties = logical_monitor[6] self.print_properties(level=1, lines=lines, properties=properties) index += 1 properties = variant[3] print() self.print_properties(level=-1, lines=lines, properties=properties) class MonitorConfigDBus(MonitorConfig): 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.CONFIG_VARIANT_TYPE) return variant class MonitorConfigCommandLine(MonitorConfig): def get_current_state(self) -> GLib.Variant: command = ('gdbus call -e ' f'-d {NAME} ' f'-o {OBJECT_PATH} ' f'-m {INTERFACE}.GetCurrentState') result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True) return GLib.variant_parse(self.CONFIG_VARIANT_TYPE, result.stdout) class MonitorConfigFile(MonitorConfig): def __init__(self, file_path): if file_path == '-': self._data = sys.stdin.read() else: with open(file_path) as file: self._data = file.read() def get_current_state(self) -> GLib.Variant: return GLib.variant_parse(self.CONFIG_VARIANT_TYPE, self._data) if __name__ == '__main__': parser = argparse.ArgumentParser(description='Get display state') parser.add_argument('--file', metavar='FILE', type=str, nargs='?', help='Read the output from gdbus call instead of calling D-Bus') parser.add_argument('--gdbus', action='store_true') parser.add_argument('--short', action='store_true') args = parser.parse_args() if args.file and args.gdbus: raise argparse.ArgumentTypeError('Incompatible arguments') if args.file: monitor_config = MonitorConfigFile(args.file) elif args.gdbus: monitor_config = MonitorConfigCommandLine() else: monitor_config = MonitorConfigDBus() monitor_config.print_current_state(short=args.short)