mutter/gdctl-patches.patch
Jonas Ådahl 193f8a96eb Backport gdctl utility
Resolves: RHEL-108048
2025-08-15 15:55:55 +02:00

4941 lines
152 KiB
Diff

From eb67a630584af8231a717c4030364a29e9e1fcaf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Fri, 13 Dec 2024 10:55:34 +0100
Subject: [PATCH 01/28] Introduce GNOME Display Control (gdctl) utility
It's based on `get-state.py`, but with the intention to expand its
functionality into not only listing information, but setting and
changing monitor configurations. It's meant to complement monitor
configurations from Settings with something that has more level of
control.
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4190>
(cherry picked from commit a3cfd7bd91c6d98d0af30668fd1c39a73c790ac9)
---
tools/gdctl | 355 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 355 insertions(+)
create mode 100755 tools/gdctl
diff --git a/tools/gdctl b/tools/gdctl
new file mode 100755
index 0000000000..37b3a6b708
--- /dev/null
+++ b/tools/gdctl
@@ -0,0 +1,355 @@
+#!/usr/bin/env python3
+
+import argparse
+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",
+}
+
+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
+
+
+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()
+
+ def get_current_state(self) -> GLib.Variant:
+ raise NotImplementedError()
+
+ def print_mode(self, mode, is_last, show_properties, lines):
+ print_data(level=2, is_last=is_last, lines=lines, data=f"{mode[0]}")
+
+ if not show_properties:
+ return
+
+ print_data(
+ level=3,
+ is_last=False,
+ lines=lines,
+ data=f"Dimension: {mode[1]}x{mode[2]}",
+ )
+ print_data(
+ level=3,
+ is_last=False,
+ lines=lines,
+ data=f"Refresh rate: {mode[3]:.3f}",
+ )
+ print_data(
+ level=3,
+ is_last=False,
+ lines=lines,
+ data=f"Preferred scale: {mode[4]}",
+ )
+ print_data(
+ level=3,
+ is_last=False,
+ lines=lines,
+ data=f"Supported scales: {mode[5]}",
+ )
+
+ if show_properties:
+ mode_properties = mode[6]
+ print_properties(level=3, lines=lines, properties=mode_properties)
+
+ def print_current_state(self, show_modes=False, show_properties=False):
+ variant = self.current_state
+
+ print("Monitors:")
+ monitors = variant[1]
+ lines = []
+ for monitor in monitors:
+ is_last = monitor == monitors[-1]
+ spec = monitor[0]
+ modes = monitor[1]
+ properties = monitor[2]
+ print_data(
+ level=0,
+ is_last=is_last,
+ lines=lines,
+ data="Monitor {}".format(spec[0]),
+ )
+ print_data(
+ level=1,
+ is_last=False,
+ lines=lines,
+ data=f"EDID: vendor: {spec[1]}, product: {spec[2]}, serial: {spec[3]}",
+ )
+
+ 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[6]), None
+ )
+ if mode:
+ mode_type = "Current"
+ else:
+ mode = next(
+ (mode for mode in modes if "is-preferred" in mode[6]),
+ 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 = variant[2]
+ 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 = variant[3]
+ 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,
+ )
--
2.49.0
From 9fe33ced8e25428c4f1bf626cdc9245cb87d875a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Fri, 13 Dec 2024 11:35:44 +0100
Subject: [PATCH 02/28] gdctl: Add helpers to get relevant state variants
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4190>
(cherry picked from commit 3469530deca21f8354250c0e865449f370135e8c)
---
tools/gdctl | 17 ++++++++++++-----
1 file changed, 12 insertions(+), 5 deletions(-)
diff --git a/tools/gdctl b/tools/gdctl
index 37b3a6b708..fb3d429c8f 100755
--- a/tools/gdctl
+++ b/tools/gdctl
@@ -111,6 +111,15 @@ class MonitorsState:
def get_current_state(self) -> GLib.Variant:
raise NotImplementedError()
+ 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[0]}")
@@ -147,10 +156,8 @@ class MonitorsState:
print_properties(level=3, lines=lines, properties=mode_properties)
def print_current_state(self, show_modes=False, show_properties=False):
- variant = self.current_state
-
print("Monitors:")
- monitors = variant[1]
+ monitors = self.get_monitors_variant()
lines = []
for monitor in monitors:
is_last = monitor == monitors[-1]
@@ -208,7 +215,7 @@ class MonitorsState:
print()
print("Logical monitors:")
- logical_monitors = variant[2]
+ logical_monitors = self.get_logical_monitors_variant()
index = 1
for logical_monitor in logical_monitors:
is_last = logical_monitor == logical_monitors[-1]
@@ -266,7 +273,7 @@ class MonitorsState:
index += 1
if show_properties:
- properties = variant[3]
+ properties = self.get_properties_variant()
print()
print_properties(level=-1, lines=lines, properties=properties)
--
2.49.0
From f5b876f8b13423bbec62e7fd3d29daba21d0b40a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Fri, 13 Dec 2024 11:56:53 +0100
Subject: [PATCH 03/28] 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>
(cherry picked from commit cc11b0682bad096351ca56472e8ec7edbf4a9e92)
---
tools/gdctl | 108 +++++++++++++++++++++++++++++++++++++++++++---------
1 file changed, 89 insertions(+), 19 deletions(-)
diff --git a/tools/gdctl b/tools/gdctl
index fb3d429c8f..7373243159 100755
--- a/tools/gdctl
+++ b/tools/gdctl
@@ -3,6 +3,7 @@
import argparse
import sys
+from dataclasses import dataclass
from gi.repository import GLib, Gio
NAME = "org.gnome.Mutter.DisplayConfig"
@@ -100,6 +101,45 @@ def strip_dbus_error_prefix(message):
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})"
@@ -108,9 +148,17 @@ class MonitorsState:
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]
@@ -121,62 +169,79 @@ class MonitorsState:
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[0]}")
+ 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: {mode[1]}x{mode[2]}",
+ data=f"Dimension: {width}x{height}",
)
print_data(
level=3,
is_last=False,
lines=lines,
- data=f"Refresh rate: {mode[3]:.3f}",
+ data=f"Refresh rate: {mode.refresh_rate:.3f}",
)
print_data(
level=3,
is_last=False,
lines=lines,
- data=f"Preferred scale: {mode[4]}",
+ data=f"Preferred scale: {mode.preferred_scale}",
)
print_data(
level=3,
is_last=False,
lines=lines,
- data=f"Supported scales: {mode[5]}",
+ data=f"Supported scales: {mode.supported_scales}",
)
if show_properties:
- mode_properties = mode[6]
+ 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:")
- monitors = self.get_monitors_variant()
lines = []
+ monitors = list(self.monitors.values())
for monitor in monitors:
is_last = monitor == monitors[-1]
- spec = monitor[0]
- modes = monitor[1]
- properties = monitor[2]
+ modes = monitor.modes
+ properties = monitor.properties
+
print_data(
level=0,
is_last=is_last,
lines=lines,
- data="Monitor {}".format(spec[0]),
- )
- print_data(
- level=1,
- is_last=False,
- lines=lines,
- data=f"EDID: vendor: {spec[1]}, product: {spec[2]}, serial: {spec[3]}",
+ 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,
@@ -189,13 +254,18 @@ class MonitorsState:
self.print_mode(mode, is_last, show_properties, lines)
else:
mode = next(
- (mode for mode in modes if "is-current" in mode[6]), None
+ (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[6]),
+ (
+ mode
+ for mode in modes
+ if "is-preferred" in mode.properties
+ ),
None,
)
if mode:
--
2.49.0
From beb9cc9fc1a6330085692fbe416f4d27bbd3df66 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Fri, 13 Dec 2024 12:10:18 +0100
Subject: [PATCH 04/28] gdctl: Always display monitor display name if available
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4190>
(cherry picked from commit 00d5a6a0cd0c207a2b9831504ce5938ad2264052)
---
tools/gdctl | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/tools/gdctl b/tools/gdctl
index 7373243159..d8f1349d36 100755
--- a/tools/gdctl
+++ b/tools/gdctl
@@ -139,6 +139,8 @@ class Monitor:
]
self.properties = variant[2]
+ self.display_name = self.properties.get("display-name", None)
+
class MonitorsState:
STATE_VARIANT_TYPE = GLib.VariantType.new(
@@ -213,11 +215,18 @@ class MonitorsState:
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=f"Monitor {monitor.connector}",
+ data=monitor_title,
)
if monitor.vendor:
--
2.49.0
From 778fbb85685b6a4ddeac3c43e6764db8178550ef Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Fri, 13 Dec 2024 12:51:57 +0100
Subject: [PATCH 05/28] gdctl: Add LogicalMonitor class
This abstracts what makes a logical monitor. Will make it easy doing
manipulations, and makes the print function a bit more readable.
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4190>
(cherry picked from commit 34195d905a4f07edfb56d4278896c1ad37154f8b)
---
tools/gdctl | 115 ++++++++++++++++++++++++++++++++++++++++++----------
1 file changed, 94 insertions(+), 21 deletions(-)
diff --git a/tools/gdctl b/tools/gdctl
index d8f1349d36..e67dc4b240 100755
--- a/tools/gdctl
+++ b/tools/gdctl
@@ -5,22 +5,42 @@ 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"
-TRANSFORM_STRINGS = {
- 0: "normal",
- 1: "90",
- 2: "180",
- 3: "270",
- 4: "flipped",
- 5: "flipped-90",
- 6: "flipped-180",
- 7: "flipped-270",
-}
+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"
+
LAYOUT_MODE = {
1: "logical",
@@ -142,6 +162,45 @@ class Monitor:
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 = variant[6]
+
+ return cls(
+ monitors=monitors,
+ position=position,
+ scale=scale,
+ transform=transform,
+ is_primary=is_primary,
+ properties=properties,
+ )
+
+
class MonitorsState:
STATE_VARIANT_TYPE = GLib.VariantType.new(
"(ua((ssss)a(siiddada{sv})a{sv})a(iiduba(ssss)a{sv})a{sv})"
@@ -151,6 +210,7 @@ class MonitorsState:
self.current_state = self.get_current_state()
self.init_monitors()
+ self.init_logical_monitors()
def get_current_state(self) -> GLib.Variant:
raise NotImplementedError()
@@ -161,6 +221,12 @@ class MonitorsState:
monitor = Monitor(monitor_variant)
self.monitors[monitor.connector] = monitor
+ def init_logical_monitors(self):
+ self.logical_monitors = []
+ for variant in self.get_logical_monitors_variant():
+ logical_monitor = LogicalMonitor.new_from_variant(self, variant)
+ self.logical_monitors.append(logical_monitor)
+
def get_monitors_variant(self):
return self.current_state[1]
@@ -294,42 +360,41 @@ class MonitorsState:
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]
+ 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: ({logical_monitor[0]}, {logical_monitor[1]})",
+ data=f"Position: ({x}, {y})",
)
print_data(
level=1,
is_last=False,
lines=lines,
- data=f"Scale: {logical_monitor[2]}",
+ data=f"Scale: {logical_monitor.scale}",
)
print_data(
level=1,
is_last=False,
lines=lines,
- data=f"Transform: {TRANSFORM_STRINGS.get(logical_monitor[3])}",
+ data=f"Transform: {logical_monitor.transform}",
)
print_data(
level=1,
is_last=False,
lines=lines,
- data=f"Primary: {logical_monitor[4]}",
+ data=f"Primary: {'yes' if logical_monitor.is_primary else 'no'}",
)
- monitors = logical_monitor[5]
+ monitors = logical_monitor.monitors
print_data(
level=1,
is_last=not show_properties,
@@ -338,15 +403,23 @@ class MonitorsState:
)
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=f"{monitor[0]} ({monitor[1]}, {monitor[2]}, {monitor[3]})",
+ data=monitor_title,
)
if show_properties:
- properties = logical_monitor[6]
+ properties = logical_monitor.properties
print_properties(level=1, lines=lines, properties=properties)
index += 1
--
2.49.0
From d435bea71f742537b79299ec935dfcd57d0d22b4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Fri, 13 Dec 2024 17:16:37 +0100
Subject: [PATCH 06/28] gdctl: Store property enum values as real enums
This means declaring an enum class inheriting from enum.Enum.
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4190>
(cherry picked from commit 481f046cd52deac332b3777123bac7efeff51955)
---
tools/gdctl | 69 ++++++++++++++++++++++++++++++++++-------------------
1 file changed, 44 insertions(+), 25 deletions(-)
diff --git a/tools/gdctl b/tools/gdctl
index e67dc4b240..4b0ca244f0 100755
--- a/tools/gdctl
+++ b/tools/gdctl
@@ -42,10 +42,36 @@ class Transform(Enum):
return "flipped-270"
-LAYOUT_MODE = {
- 1: "logical",
- 2: "physical",
-}
+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):
@@ -78,22 +104,6 @@ def print_data(*, level: int, is_last: bool, lines: list[int], data: str):
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())
@@ -105,7 +115,14 @@ def print_properties(*, level, lines, properties):
)
for key in property_keys:
is_last = key == property_keys[-1]
- value_string = maybe_describe(key, properties[key])
+
+ value = properties[key]
+ if isinstance(value, list):
+ elements_string = ", ".join([str(element) for element in value])
+ value_string = f"[{elements_string}]"
+ else:
+ value_string = str(value)
+
print_data(
level=level + 1,
is_last=is_last,
@@ -139,7 +156,7 @@ class MonitorMode:
refresh_rate=variant[3],
preferred_scale=variant[4],
supported_scales=variant[5],
- properties=variant[6],
+ properties=translate_properties(variant[6]),
)
@@ -157,7 +174,7 @@ class Monitor:
MonitorMode.from_variant(mode_variant)
for mode_variant in variant[1]
]
- self.properties = variant[2]
+ self.properties = translate_properties(variant[2])
self.display_name = self.properties.get("display-name", None)
@@ -189,7 +206,7 @@ class LogicalMonitor:
monitors = [
monitors_state.monitors[connector] for connector in connectors
]
- properties = variant[6]
+ properties = translate_properties(variant[6])
return cls(
monitors=monitors,
@@ -209,6 +226,8 @@ class MonitorsState:
def __init__(self):
self.current_state = self.get_current_state()
+ self.properties = translate_properties(self.current_state[3])
+
self.init_monitors()
self.init_logical_monitors()
@@ -425,7 +444,7 @@ class MonitorsState:
index += 1
if show_properties:
- properties = self.get_properties_variant()
+ properties = self.properties
print()
print_properties(level=-1, lines=lines, properties=properties)
--
2.49.0
From 924c440c17e71281e748f6384db2441abd259247 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Thu, 19 Dec 2024 21:41:14 +0100
Subject: [PATCH 07/28] gdctl: Display booleans as yes / no
Slightly more human readible and less programmer speak.
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4190>
(cherry picked from commit e326aed70ef811f77b2ec21a8f4ec315f2028403)
---
tools/gdctl | 2 ++
1 file changed, 2 insertions(+)
diff --git a/tools/gdctl b/tools/gdctl
index 4b0ca244f0..c8211edea8 100755
--- a/tools/gdctl
+++ b/tools/gdctl
@@ -120,6 +120,8 @@ def print_properties(*, level, lines, properties):
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)
--
2.49.0
From 279d99fc893d11523698ae2fefc4f4876cc38c2b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Fri, 13 Dec 2024 21:09:51 +0100
Subject: [PATCH 08/28] gdctl: Hook up to o.g.M.DisplayConfig via dedicated
class
The "current state" one will use DisplayConfig to query, which
eventually will also do configuration method calls.
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4190>
(cherry picked from commit 7ddaf23130141a34a29be143c350824109f88ace)
---
tools/gdctl | 67 +++++++++++++++++++++++++----------------------------
1 file changed, 32 insertions(+), 35 deletions(-)
diff --git a/tools/gdctl b/tools/gdctl
index c8211edea8..00b437648f 100755
--- a/tools/gdctl
+++ b/tools/gdctl
@@ -140,6 +140,34 @@ def strip_dbus_error_prefix(message):
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
@@ -221,21 +249,14 @@ class LogicalMonitor:
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()
+ def __init__(self, display_config):
+ self.current_state = display_config.get_current_state()
self.properties = translate_properties(self.current_state[3])
self.init_monitors()
self.init_logical_monitors()
- def get_current_state(self) -> GLib.Variant:
- raise NotImplementedError()
-
def init_monitors(self):
self.monitors = {}
for monitor_variant in self.get_monitors_variant():
@@ -451,31 +472,6 @@ class MonitorsState:
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")
@@ -512,7 +508,8 @@ if __name__ == "__main__":
match args.command:
case "show":
try:
- monitors_state = MonitorsStateDBus()
+ 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)
--
2.49.0
From 1e90c7fb084a670d3ffb4a972e83a402324e4d35 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Fri, 13 Dec 2024 21:18:43 +0100
Subject: [PATCH 09/28] gdctl/monitors-state: Don't keep current state variant
around
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>
(cherry picked from commit 2ee918a9491c1bc23db6df7531e93f8b4a224613)
---
tools/gdctl | 25 ++++++++-----------------
1 file changed, 8 insertions(+), 17 deletions(-)
diff --git a/tools/gdctl b/tools/gdctl
index 00b437648f..3b47a1c842 100755
--- a/tools/gdctl
+++ b/tools/gdctl
@@ -250,34 +250,25 @@ class LogicalMonitor:
class MonitorsState:
def __init__(self, display_config):
- self.current_state = display_config.get_current_state()
+ current_state = display_config.get_current_state()
- self.properties = translate_properties(self.current_state[3])
+ self.properties = translate_properties(current_state[3])
- self.init_monitors()
- self.init_logical_monitors()
+ self.init_monitors(current_state)
+ self.init_logical_monitors(current_state)
- def init_monitors(self):
+ def init_monitors(self, current_state):
self.monitors = {}
- for monitor_variant in self.get_monitors_variant():
+ for monitor_variant in current_state[1]:
monitor = Monitor(monitor_variant)
self.monitors[monitor.connector] = monitor
- def init_logical_monitors(self):
+ def init_logical_monitors(self, current_state):
self.logical_monitors = []
- for variant in self.get_logical_monitors_variant():
+ for variant in current_state[2]:
logical_monitor = LogicalMonitor.new_from_variant(self, variant)
self.logical_monitors.append(logical_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}")
--
2.49.0
From 964f83f8871829551bad3367975d6b7dc520ed59 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Tue, 17 Dec 2024 16:26:19 +0100
Subject: [PATCH 10/28] gdctl: Introduce and use new named enum
The named enum (NamedEnum) is used to describe a mapping for enum values
to strings. Enums using this define a function that defines the mapping,
and the named enum handles converting from to strings. This replaces
existing manually coded translations.
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4190>
(cherry picked from commit 6750136fd326479f4e4b9b2c84c4d250dfc18826)
---
tools/gdctl | 65 ++++++++++++++++++++++++++++++-----------------------
1 file changed, 37 insertions(+), 28 deletions(-)
diff --git a/tools/gdctl b/tools/gdctl
index 3b47a1c842..1e8b355675 100755
--- a/tools/gdctl
+++ b/tools/gdctl
@@ -12,7 +12,22 @@ INTERFACE = "org.gnome.Mutter.DisplayConfig"
OBJECT_PATH = "/org/gnome/Mutter/DisplayConfig"
-class Transform(Enum):
+class NamedEnum(Enum):
+ def __str__(self):
+ return next(
+ string for enum, string in type(self).enum_names() if enum == self
+ )
+
+ @classmethod
+ def from_string(cls, string):
+ return next(
+ enum
+ for enum, enum_string in cls.enum_names()
+ if string == enum_string
+ )
+
+
+class Transform(NamedEnum):
NORMAL = 0
ROTATE_90 = 1
ROTATE_180 = 2
@@ -22,36 +37,30 @@ class Transform(Enum):
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):
+ @classmethod
+ def enum_names(cls):
+ return [
+ (Transform.NORMAL, "normal"),
+ (Transform.ROTATE_90, "90"),
+ (Transform.ROTATE_180, "180"),
+ (Transform.ROTATE_270, "270"),
+ (Transform.FLIPPED, "flipped"),
+ (Transform.ROTATE_90_FLIPPED, "flipped-90"),
+ (Transform.ROTATE_180_FLIPPED, "flipped-180"),
+ (Transform.ROTATE_270_FLIPPED, "flipped-270"),
+ ]
+
+
+class LayoutMode(NamedEnum):
LOGICAL = 1
PHYSICAL = 2
- def __str__(self):
- match self:
- case self.LOGICAL:
- return "logical"
- case self.PHYSICAL:
- return "physical"
+ @classmethod
+ def enum_names(cls):
+ return [
+ (LayoutMode.LOGICAL, "logical"),
+ (LayoutMode.PHYSICAL, "physical"),
+ ]
def translate_property(name, value):
--
2.49.0
From 71dfeb57ba01459738c64d92302ba29ba6236e56 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Tue, 17 Dec 2024 16:53:39 +0100
Subject: [PATCH 11/28] gdctl: Add support for applying configuration
Support defining and applying full configurations, meaning one describes
the whole configuration with one command, fully replacing the active
configuration.
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4190>
(cherry picked from commit c80134d1ba9fb844c85e7024c3de29f6f50f62d8)
---
tools/gdctl | 833 +++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 831 insertions(+), 2 deletions(-)
diff --git a/tools/gdctl b/tools/gdctl
index 1e8b355675..005e0b0d40 100755
--- a/tools/gdctl
+++ b/tools/gdctl
@@ -5,7 +5,7 @@ import sys
from dataclasses import dataclass
from gi.repository import GLib, Gio
-from enum import Enum
+from enum import Enum, Flag
NAME = "org.gnome.Mutter.DisplayConfig"
INTERFACE = "org.gnome.Mutter.DisplayConfig"
@@ -63,6 +63,12 @@ class LayoutMode(NamedEnum):
]
+class ConfigMethod(Enum):
+ VERIFY = 0
+ TEMPORARY = 1
+ PERSISTENT = 2
+
+
def translate_property(name, value):
enum_properties = {
"layout-mode": LayoutMode,
@@ -149,6 +155,32 @@ def strip_dbus_error_prefix(message):
return message
+def transform_size(size, transform) -> tuple[int, int]:
+ match transform:
+ case (
+ Transform.NORMAL
+ | Transform.ROTATE_180
+ | Transform.FLIPPED
+ | Transform.ROTATE_180_FLIPPED
+ ):
+ return size
+ case (
+ Transform.ROTATE_90
+ | Transform.ROTATE_270
+ | Transform.ROTATE_90_FLIPPED
+ | Transform.ROTATE_270_FLIPPED
+ ):
+ width, height = size
+ return (height, width)
+ case _:
+ raise NotImplementedError
+
+
+def scale_size(size, scale) -> tuple[int, int]:
+ width, height = size
+ return (round(width / scale), round(height / scale))
+
+
class DisplayConfig:
STATE_VARIANT_TYPE = GLib.VariantType.new(
"(ua((ssss)a(siiddada{sv})a{sv})a(iiduba(ssss)a{sv})a{sv})"
@@ -176,6 +208,33 @@ class DisplayConfig:
assert variant.get_type().equal(self.STATE_VARIANT_TYPE)
return variant
+ def apply_monitors_config(self, config, config_method):
+ serial = config.monitors_state.server_serial
+ logical_monitors = config.generate_logical_monitor_tuples()
+ properties = {}
+
+ if monitors_state.supports_changing_layout_mode:
+ properties["layout-mode"] = GLib.Variant(
+ "u", config.layout_mode.value
+ )
+
+ parameters = GLib.Variant(
+ "(uua(iiduba(ssa{sv}))a{sv})",
+ (
+ serial,
+ config_method.value,
+ logical_monitors,
+ properties,
+ ),
+ )
+ self._proxy.call_sync(
+ method_name="ApplyMonitorsConfig",
+ parameters=parameters,
+ flags=Gio.DBusCallFlags.NO_AUTO_START,
+ timeout_msec=-1,
+ cancellable=None,
+ )
+
@dataclass
class MonitorMode:
@@ -215,6 +274,15 @@ class Monitor:
]
self.properties = translate_properties(variant[2])
+ self.current_mode = next(
+ (mode for mode in self.modes if "is-current" in mode.properties),
+ None,
+ )
+ self.preferred_mode = next(
+ (mode for mode in self.modes if "is-preferred" in mode.properties),
+ None,
+ )
+
self.display_name = self.properties.get("display-name", None)
@@ -227,6 +295,7 @@ class LogicalMonitor:
transform=Transform.NORMAL,
is_primary=False,
properties={},
+ args=None,
):
self.position = position
self.scale = scale
@@ -234,6 +303,7 @@ class LogicalMonitor:
self.is_primary = is_primary
self.monitors = monitors
self.properties = properties
+ self.args = args
@classmethod
def new_from_variant(cls, monitors_state, variant):
@@ -256,12 +326,498 @@ class LogicalMonitor:
properties=properties,
)
+ def calculate_size(self, layout_mode):
+ mode = next(monitor.mode for monitor in self.monitors)
+ size = transform_size(mode.resolution, self.transform)
+ match layout_mode:
+ case LayoutMode.LOGICAL:
+ return scale_size(size, self.scale)
+ case LayoutMode.PHYSICAL:
+ return size
+
+ def calculate_right_edge(self, layout_mode):
+ x, _ = self.position
+ width, _ = self.calculate_size(layout_mode)
+ return x + width
+
+ def calculate_bottom_edge(self, layout_mode):
+ _, y = self.position
+ _, height = self.calculate_size(layout_mode)
+ return y + height
+
+
+def find_closest_scale(mode, scale) -> float:
+ @dataclass
+ class Scale:
+ scale: float
+ distance: float
+
+ best: Scale | None = None
+ for supported_scale in mode.supported_scales:
+ scale_distance = abs(scale - supported_scale)
+
+ if scale_distance > 0.1:
+ continue
+
+ if not best or scale_distance < best.distance:
+ best = Scale(supported_scale, scale_distance)
+
+ if not best:
+ raise ValueError(f"Scale {scale} not supported by mode")
+
+ return best.scale
+
+
+def count_keys(dictionary, keys):
+ in_both = set(keys) & set(dictionary)
+ return len(in_both)
+
+
+def place_right_of(
+ logical_monitor: LogicalMonitor,
+ monitor_mappings: dict,
+ layout_mode: LayoutMode,
+ connector: str,
+ set_y_position: bool,
+):
+ connector_logical_monitor = monitor_mappings[connector]
+ if not connector_logical_monitor.position:
+ raise ValueError(
+ f"Logical monitor position configured before {connector} "
+ )
+
+ x = connector_logical_monitor.calculate_right_edge(layout_mode)
+ if set_y_position:
+ _, y = connector_logical_monitor.position
+ else:
+ y = None
+
+ logical_monitor.position = (x, y)
+
+
+def place_left_of(
+ logical_monitor: LogicalMonitor,
+ monitor_mappings: dict,
+ layout_mode: LayoutMode,
+ connector: str,
+ set_y_position: bool,
+):
+ connector_logical_monitor = monitor_mappings[connector]
+ if not connector_logical_monitor.position:
+ raise ValueError(
+ f"Logical monitor position configured before {connector} "
+ )
+
+ width, _ = logical_monitor.calculate_size(layout_mode)
+ left_edge, _ = connector_logical_monitor.position
+ x = left_edge - width
+
+ if set_y_position:
+ _, y = connector_logical_monitor.position
+ else:
+ y = None
+
+ logical_monitor.position = (x, y)
+
+
+def place_below(
+ logical_monitor: LogicalMonitor,
+ monitor_mappings: dict,
+ layout_mode: LayoutMode,
+ connector: str,
+ set_x_position: bool,
+):
+ connector_logical_monitor = monitor_mappings[connector]
+ if not connector_logical_monitor.position:
+ raise ValueError(
+ f"Logical monitor position configured before {connector} "
+ )
+
+ y = connector_logical_monitor.calculate_bottom_edge(layout_mode)
+ if set_x_position:
+ x, _ = connector_logical_monitor.position
+ else:
+ x = logical_monitor.position[0]
+
+ logical_monitor.position = (x, y)
+
+
+def place_above(
+ logical_monitor: LogicalMonitor,
+ monitor_mappings: dict,
+ layout_mode: LayoutMode,
+ connector: str,
+ set_x_position: bool,
+):
+ connector_logical_monitor = monitor_mappings[connector]
+ if not connector_logical_monitor.position:
+ raise ValueError(
+ f"Logical monitor position configured before {connector} "
+ )
+
+ _, height = logical_monitor.calculate_size(layout_mode)
+ _, top_edge = connector_logical_monitor.position
+ y = top_edge - height
+
+ if set_x_position:
+ x, _ = connector_logical_monitor.position
+ else:
+ x = logical_monitor.position[0]
+
+ logical_monitor.position = (x, y)
+
+
+class PositionType(Flag):
+ NONE = 0
+ ABSOLUTE_X = 1 << 0
+ RELATIVE_X = 1 << 1
+ ABSOLUTE_Y = 1 << 2
+ RELATIVE_Y = 1 << 3
+
+
+def calculate_position(
+ logical_monitor: LogicalMonitor,
+ layout_mode: LayoutMode,
+ monitor_mappings: dict,
+):
+ horizontal_args = count_keys(
+ logical_monitor.args, ["right_of", "left_of", "x"]
+ )
+ vertical_args = count_keys(logical_monitor.args, ["above", "below", "y"])
+
+ if horizontal_args > 1:
+ raise ValueError("Multiple horizontal placement instructions used")
+ if vertical_args > 1:
+ raise ValueError("Multiple vertical placement instructions used")
+
+ position_types = PositionType.NONE
+
+ set_y_position = vertical_args == 0
+
+ if "x" in logical_monitor.args:
+ x = int(logical_monitor.args["x"])
+ if set_y_position:
+ y = 0
+ else:
+ y = None
+ logical_monitor.position = (x, y)
+ position_types |= PositionType.ABSOLUTE_X
+ elif "right_of" in logical_monitor.args:
+ connector = logical_monitor.args["right_of"]
+ if connector not in monitor_mappings:
+ raise ValueError(
+ f"Invalid connector {connector} passed to --right-of"
+ )
+ place_right_of(
+ logical_monitor,
+ monitor_mappings,
+ layout_mode,
+ connector,
+ set_y_position,
+ )
+ position_types |= PositionType.RELATIVE_X
+ elif "left_of" in logical_monitor.args:
+ connector = logical_monitor.args["left_of"]
+ if connector not in monitor_mappings:
+ raise ValueError(
+ f"Invalid connector {connector} passed to --left-of"
+ )
+ place_left_of(
+ logical_monitor,
+ monitor_mappings,
+ layout_mode,
+ connector,
+ set_y_position,
+ )
+ position_types |= PositionType.RELATIVE_X
+ else:
+ logical_monitor.position = (0, 0)
+
+ set_x_position = horizontal_args == 0
+
+ if "y" in logical_monitor.args:
+ y = int(logical_monitor.args["y"])
+ if set_x_position:
+ x = 0
+ else:
+ x = logical_monitor.position[0]
+ logical_monitor.position = (x, y)
+ position_types |= PositionType.ABSOLUTE_Y
+ elif "below" in logical_monitor.args:
+ connector = logical_monitor.args["below"]
+ if connector not in monitor_mappings:
+ raise ValueError(f"Invalid connector {connector} passed to --below")
+ place_below(
+ logical_monitor,
+ monitor_mappings,
+ layout_mode,
+ connector,
+ set_x_position,
+ )
+ position_types |= PositionType.RELATIVE_Y
+ elif "above" in logical_monitor.args:
+ connector = logical_monitor.args["above"]
+ if connector not in monitor_mappings:
+ raise ValueError(f"Invalid connector {connector} passed to --above")
+ place_above(
+ logical_monitor,
+ monitor_mappings,
+ layout_mode,
+ connector,
+ set_x_position,
+ )
+ position_types |= PositionType.RELATIVE_Y
+ else:
+ x, y = logical_monitor.position
+ if not y:
+ y = 0
+ logical_monitor.position = (x, y)
+
+ assert logical_monitor.position[0] is not None
+ assert logical_monitor.position[1] is not None
+
+ return position_types
+
+
+def align_horizontally(logical_monitors: list[LogicalMonitor]):
+ min_x = min(
+ logical_monitor.position[0] for logical_monitor in logical_monitors
+ )
+
+ dx = min_x
+ if dx == 0:
+ return
+
+ for logical_monitor in logical_monitors:
+ x, y = logical_monitor.position
+ logical_monitor.position = (x - dx, y)
+
+
+def align_vertically(logical_monitors: list[LogicalMonitor]):
+ min_y = min(
+ logical_monitor.position[1] for logical_monitor in logical_monitors
+ )
+
+ dy = min_y
+ if dy == 0:
+ return
+
+ for logical_monitor in logical_monitors:
+ x, y = logical_monitor.position
+ logical_monitor.position = (x, y - dy)
+
+
+def calculate_positions(
+ logical_monitors: list[LogicalMonitor],
+ layout_mode: LayoutMode,
+ monitor_mappings: dict,
+):
+ position_types = PositionType.NONE
+ for logical_monitor in logical_monitors:
+ position_types |= calculate_position(
+ logical_monitor, layout_mode, monitor_mappings
+ )
+
+ if not position_types & PositionType.ABSOLUTE_X:
+ align_horizontally(logical_monitors)
+ if not position_types & PositionType.ABSOLUTE_Y:
+ align_vertically(logical_monitors)
+
+
+def create_logical_monitor(monitors_state, layout_mode, logical_monitor_args):
+ if "monitors" not in logical_monitor_args:
+ raise ValueError("Logical monitor empty")
+ monitors_arg = logical_monitor_args["monitors"]
+
+ scale = logical_monitor_args.get("scale", None)
+ is_primary = logical_monitor_args.get("primary", False)
+ transform = Transform.from_string(
+ logical_monitor_args.get("transform", "normal")
+ )
+
+ monitors = []
+
+ common_mode_resolution = None
+
+ for monitor_args in monitors_arg:
+ (connector,) = monitor_args["key"]
+ if connector not in monitors_state.monitors:
+ raise ValueError(f"Monitor {connector} not found")
+ monitor = monitors_state.monitors[connector]
+
+ mode_name = monitor_args.get("mode", None)
+ if mode_name:
+ mode = next(
+ (mode for mode in monitor.modes if mode.name == mode_name), None
+ )
+ if not mode:
+ raise ValueError(
+ f"No mode {mode_name} available for {connector}"
+ )
+ else:
+ mode = monitor.preferred_mode
+
+ if not common_mode_resolution:
+ common_mode_resolution = mode.resolution
+
+ if not scale:
+ scale = mode.preferred_scale
+ else:
+ scale = find_closest_scale(mode, scale)
+ else:
+ mode_width, mode_height = mode.resolution
+ common_mode_width, common_mode_height = common_mode_resolution
+ if (
+ mode_width != common_mode_width
+ or mode_height != common_mode_height
+ ):
+ raise ValueError(
+ "Different monitor resolutions within the same logical monitor"
+ )
+
+ monitor.mode = mode
+
+ monitors.append(monitor)
+
+ return LogicalMonitor(
+ monitors_state,
+ monitors,
+ scale,
+ is_primary=is_primary,
+ transform=transform,
+ position=None,
+ args=logical_monitor_args,
+ )
+
+
+def generate_configuration(monitors_state, args):
+ layout_mode_str = args.layout_mode
+ if not layout_mode_str:
+ layout_mode = monitors_state.layout_mode
+ else:
+ if not monitors_state.supports_changing_layout_mode:
+ raise ValueError(
+ "Configuring layout mode not supported by the server"
+ )
+ layout_mode = LayoutMode.from_string(layout_mode_str)
+
+ logical_monitors = []
+ monitor_mappings = {}
+ for logical_monitor_args in args.logical_monitors:
+ logical_monitor = create_logical_monitor(
+ monitors_state, layout_mode, logical_monitor_args
+ )
+ logical_monitors.append(logical_monitor)
+ for monitor in logical_monitor.monitors:
+ monitor_mappings[monitor.connector] = logical_monitor
+
+ calculate_positions(logical_monitors, layout_mode, monitor_mappings)
+
+ return Config(monitors_state, logical_monitors, layout_mode)
+
+
+def derive_config_method(args):
+ if args.persistent and args.verify:
+ raise ValueError(
+ "Configuration can't be both persistent and verify-only"
+ )
+ if args.persistent:
+ return ConfigMethod.PERSISTENT
+ elif args.verify:
+ return ConfigMethod.VERIFY
+ else:
+ return ConfigMethod.TEMPORARY
+
+
+def print_config(config):
+ print("Configuration:")
+ lines = []
+
+ print_data(
+ level=0,
+ is_last=False,
+ lines=lines,
+ data=f"Layout mode: {config.layout_mode}",
+ )
+
+ print_data(
+ level=0,
+ is_last=True,
+ lines=lines,
+ data=f"Logical monitors ({len(config.logical_monitors)})",
+ )
+
+ index = 1
+ for logical_monitor in config.logical_monitors:
+ is_last = logical_monitor == config.logical_monitors[-1]
+ print_data(
+ level=1,
+ is_last=is_last,
+ lines=lines,
+ data=f"Logical monitor #{index}",
+ )
+
+ print_data(
+ level=2,
+ is_last=False,
+ lines=lines,
+ data=f"Position: {logical_monitor.position}",
+ )
+ print_data(
+ level=2,
+ is_last=False,
+ lines=lines,
+ data=f"Scale: {logical_monitor.scale}",
+ )
+ print_data(
+ level=2,
+ is_last=False,
+ lines=lines,
+ data=f"Transform: {logical_monitor.transform}",
+ )
+ print_data(
+ level=2,
+ is_last=False,
+ lines=lines,
+ data=f"Primary: {'yes' if logical_monitor.is_primary else 'no'}",
+ )
+
+ print_data(
+ level=2,
+ is_last=True,
+ lines=lines,
+ data=f"Monitors: ({len(logical_monitor.monitors)})",
+ )
+ for monitor in logical_monitor.monitors:
+ is_last = monitor == logical_monitor.monitors[-1]
+ print_data(
+ level=3,
+ is_last=is_last,
+ lines=lines,
+ data=f"Monitor {monitor.connector} ({monitor.display_name})",
+ )
+ print_data(
+ level=4,
+ is_last=True,
+ lines=lines,
+ data=f"Mode: {monitor.mode.name}",
+ )
+
+ index += 1
+
class MonitorsState:
def __init__(self, display_config):
current_state = display_config.get_current_state()
+ self.server_serial = current_state[0]
self.properties = translate_properties(current_state[3])
+ self.supports_changing_layout_mode = self.properties.get(
+ "supports-changing-layout-mode", False
+ )
+ self.layout_mode = (
+ self.properties.get("layout-mode") or LayoutMode.LOGICAL
+ )
self.init_monitors(current_state)
self.init_logical_monitors(current_state)
@@ -278,6 +834,9 @@ class MonitorsState:
logical_monitor = LogicalMonitor.new_from_variant(self, variant)
self.logical_monitors.append(logical_monitor)
+ def create_current_config(self):
+ return Config.create_current(self)
+
def print_mode(self, mode, is_last, show_properties, lines):
print_data(level=2, is_last=is_last, lines=lines, data=f"{mode.name}")
@@ -472,8 +1031,109 @@ class MonitorsState:
print_properties(level=-1, lines=lines, properties=properties)
+@dataclass
+class Config:
+ monitors_state: MonitorsState
+ logical_monitors: list[LogicalMonitor]
+ layout_mode: LayoutMode
+
+ def generate_monitor_tuples(self, monitors):
+ return [
+ # Variant type: (ssa{sv})
+ (
+ monitor.connector,
+ monitor.mode.name,
+ {},
+ )
+ for monitor in monitors
+ ]
+
+ def generate_logical_monitor_tuples(self):
+ tuples = []
+ for logical_monitor in self.logical_monitors:
+ x, y = logical_monitor.position
+ scale = logical_monitor.scale
+ transform = logical_monitor.transform.value
+ is_primary = logical_monitor.is_primary
+
+ monitors = self.generate_monitor_tuples(logical_monitor.monitors)
+
+ # Variant type: (iiduba(ssa{sv}))
+ tuples.append(
+ (
+ x,
+ y,
+ scale,
+ transform,
+ is_primary,
+ monitors,
+ )
+ )
+ return tuples
+
+
+class GroupAction(argparse.Action):
+ def __call__(self, parser, namespace, values, option_string=None):
+ namespace._current_group = {}
+ groups = namespace.__dict__.setdefault(self.dest, [])
+ groups.append(namespace._current_group)
+
+
+class SubGroupAction(argparse.Action):
+ def __call__(self, parser, namespace, values, option_string=None):
+ if not hasattr(namespace, "_current_group"):
+ raise argparse.ArgumentError(
+ self, "No current group to add sub-group to"
+ )
+ if self.dest not in namespace._current_group:
+ namespace._current_group[self.dest] = []
+ sub_group = {
+ "key": values,
+ }
+ namespace._current_group[self.dest].append(sub_group)
+ namespace._current_sub_group = sub_group
+
+
+class AppendToGlobal(argparse.Action):
+ def __call__(self, parser, namespace, values, option_string=None):
+ if getattr(namespace, "_current_group", None) is not None:
+ raise argparse.ArgumentError(self, "Must pass during global scope")
+ setattr(namespace, self.dest, self.const or values)
+
+
+class AppendToGroup(argparse.Action):
+ def __call__(self, parser, namespace, values, option_string=None):
+ if getattr(namespace, "_current_group", None) is None:
+ raise argparse.ArgumentError(self, "No current group to add to")
+ namespace._current_group[self.dest] = self.const or values
+
+
+class AppendToSubGroup(argparse.Action):
+ def __call__(self, parser, namespace, values, option_string=None):
+ if getattr(namespace, "_current_group", None) is None:
+ raise argparse.ArgumentError(self, "No current group")
+ if getattr(namespace, "_current_sub_group", None) is None:
+ raise argparse.ArgumentError(self, "No current sub-group")
+ namespace._current_sub_group[self.dest] = self.const or values
+
+
+def clearattr(namespace, attr):
+ if hasattr(namespace, attr):
+ delattr(namespace, attr)
+
+
+class GdctlParser(argparse.ArgumentParser):
+ def parse_args(self):
+ namespace = super().parse_args()
+ clearattr(namespace, "_current_group")
+ clearattr(namespace, "_current_sub_group")
+ return namespace
+
+
if __name__ == "__main__":
- parser = argparse.ArgumentParser(description="Display control utility")
+ parser = GdctlParser(
+ description="Display control utility",
+ )
subparser = parser.add_subparsers(
dest="command",
@@ -502,6 +1162,140 @@ if __name__ == "__main__":
action="store_true",
help="Display all available information",
)
+ set_parser = subparser.add_parser(
+ "set",
+ help="Set display configuration",
+ )
+ set_parser.add_argument(
+ "-P",
+ "--persistent",
+ action=AppendToGlobal,
+ const=True,
+ nargs=0,
+ default=False,
+ )
+ set_parser.add_argument(
+ "-v",
+ "--verbose",
+ action=AppendToGlobal,
+ const=True,
+ nargs=0,
+ default=False,
+ )
+ set_parser.add_argument(
+ "-V",
+ "--verify",
+ action=AppendToGlobal,
+ const=True,
+ nargs=0,
+ default=False,
+ )
+ set_parser.add_argument(
+ "-l",
+ "--layout-mode",
+ choices=[str(layout_mode) for layout_mode in list(LayoutMode)],
+ type=str,
+ action=AppendToGlobal,
+ )
+ set_parser.add_argument(
+ "-L",
+ "--logical-monitor",
+ dest="logical_monitors",
+ action=GroupAction,
+ nargs=0,
+ default=[],
+ )
+ logical_monitor_parser = set_parser.add_argument_group(
+ "logical_monitor",
+ "Logical monitor options (pass after --logical-monitor)",
+ argument_default=argparse.SUPPRESS,
+ )
+ logical_monitor_parser.add_argument(
+ "-M",
+ "--monitor",
+ dest="monitors",
+ metavar="CONNECTOR",
+ action=SubGroupAction,
+ help="Configure monitor",
+ )
+ monitor_parser = set_parser.add_argument_group(
+ "monitor",
+ "Monitor options (pass after --monitor)",
+ argument_default=argparse.SUPPRESS,
+ )
+ monitor_parser.add_argument(
+ "--mode",
+ "-m",
+ action=AppendToSubGroup,
+ help="Monitor mode",
+ type=str,
+ )
+ logical_monitor_parser.add_argument(
+ "--primary",
+ "-p",
+ action=AppendToGroup,
+ help="Mark as primary",
+ type=bool,
+ const=True,
+ nargs=0,
+ )
+ logical_monitor_parser.add_argument(
+ "--scale",
+ "-s",
+ action=AppendToGroup,
+ help="Logical monitor scale",
+ type=float,
+ )
+ logical_monitor_parser.add_argument(
+ "--transform",
+ "-t",
+ action=AppendToGroup,
+ help="Apply viewport transform",
+ choices=[str(transform) for transform in list(Transform)],
+ type=str,
+ )
+ logical_monitor_parser.add_argument(
+ "--x",
+ "-x",
+ action=AppendToGroup,
+ help="X position",
+ type=int,
+ )
+ logical_monitor_parser.add_argument(
+ "--y",
+ "-y",
+ action=AppendToGroup,
+ help="Y position",
+ type=int,
+ )
+ logical_monitor_parser.add_argument(
+ "--right-of",
+ action=AppendToGroup,
+ metavar="CONNECTOR",
+ help="Place right of other monitor",
+ type=str,
+ )
+ logical_monitor_parser.add_argument(
+ "--left-of",
+ action=AppendToGroup,
+ metavar="CONNECTOR",
+ help="Place left of other monitor",
+ type=str,
+ )
+ logical_monitor_parser.add_argument(
+ "--above",
+ action=AppendToGroup,
+ metavar="CONNECTOR",
+ help="Place above other monitor",
+ type=str,
+ )
+ logical_monitor_parser.add_argument(
+ "--below",
+ action=AppendToGroup,
+ metavar="CONNECTOR",
+ help="Place below other monitor",
+ type=str,
+ )
args = parser.parse_args()
@@ -530,3 +1324,38 @@ if __name__ == "__main__":
show_modes=show_modes,
show_properties=show_properties,
)
+ case "set":
+ 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)
+
+ try:
+ config = generate_configuration(monitors_state, args)
+ config_method = derive_config_method(args)
+ if args.verbose:
+ print_config(config)
+ display_config.apply_monitors_config(config, config_method)
+ except ValueError as e:
+ print(f"Failed to create configuration: {e}", file=sys.stderr)
+ sys.exit(1)
+ 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 to apply configuration: {error_message}",
+ file=sys.stderr,
+ )
+ else:
+ print(
+ f"Failed to apply configuration: {e.message}",
+ file=sys.stderr,
+ )
+ sys.exit(1)
--
2.49.0
From 63ccfe3d8492c01a8a047372f78e094208869641 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Wed, 18 Dec 2024 00:09:17 +0100
Subject: [PATCH 12/28] tests/build: Allow passing commandline arguments to
tests
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4190>
(cherry picked from commit 7a39f05b6983be2cd1635ad50341d0131952e7eb)
---
src/tests/generic.test.in | 2 +-
src/tests/meson.build | 4 ++++
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/tests/generic.test.in b/src/tests/generic.test.in
index 1913544638..623e51f24d 100644
--- a/src/tests/generic.test.in
+++ b/src/tests/generic.test.in
@@ -1,5 +1,5 @@
[Test]
Description=Mutter test: @testname@
-Exec=sh -ec 'env G_TEST_SRCDIR=@pkgdatadir@ G_TEST_BUILDDIR=@libexecdir@/installed-tests/mutter-@apiversion@ MUTTER_REF_TEST_RESULT_DIR=@reftestresultdir@ @libexecdir@/installed-tests/mutter-@apiversion@/mutter-installed-dbus-session.py @runnerargs@ -- @libexecdir@/installed-tests/mutter-@apiversion@/@testexecutable@'
+Exec=sh -ec 'env G_TEST_SRCDIR=@pkgdatadir@ G_TEST_BUILDDIR=@libexecdir@/installed-tests/mutter-@apiversion@ MUTTER_REF_TEST_RESULT_DIR=@reftestresultdir@ @libexecdir@/installed-tests/mutter-@apiversion@/mutter-installed-dbus-session.py @runnerargs@ -- @libexecdir@/installed-tests/mutter-@apiversion@/@testexecutable@ @testargs@'
Type=session
Output=TAP
diff --git a/src/tests/meson.build b/src/tests/meson.build
index 79e7d48a31..9f1fc1157b 100644
--- a/src/tests/meson.build
+++ b/src/tests/meson.build
@@ -792,8 +792,11 @@ foreach test_case: test_cases
test_depends = [ default_plugin ] + test_case.get('depends', [])
+ args = test_case.get('args', [])
+
test(test_case['name'], test_executable,
suite: ['core', 'mutter/' + test_case['suite']],
+ args: args,
env: test_env,
depends: test_depends,
is_parallel: false,
@@ -1043,6 +1046,7 @@ if have_installed_tests
installed_tests_cdata.set('reftestresultdir', '/tmp/mutter-ref-test-results')
installed_tests_cdata.set('testname', test_case['name'])
installed_tests_cdata.set('testexecutable', 'mutter-' + test_case['name'])
+ installed_tests_cdata.set('testargs', ' '.join(test_case.get('args', [])))
installed_tests_cdata.set('runnerargs', ' '.join(runner_args))
configure_file(
--
2.49.0
From ccf5cd267e2990f400567f23c4b06295434fc68c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Wed, 18 Dec 2024 00:15:27 +0100
Subject: [PATCH 13/28] gdctl: Install to bin/
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4190>
(cherry picked from commit a9d8aaa6fd4ae6f9a7371eb304197118d10888f6)
---
tools/meson.build | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/tools/meson.build b/tools/meson.build
index 83e98721cb..2603ed3d87 100644
--- a/tools/meson.build
+++ b/tools/meson.build
@@ -1 +1,6 @@
+install_data(
+ 'gdctl',
+ install_dir: bindir,
+)
+
get_state_tool = find_program('get-state.py')
--
2.49.0
From 3510d6afc7698845cbb4fd5ed13e4e7f901abd75 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Wed, 18 Dec 2024 00:16:43 +0100
Subject: [PATCH 14/28] tests: Add get state D-Bus test using gdctl
Test is ref test like, with gdctl outputs being compared.
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4190>
(cherry picked from commit 1cbfc07df0c90d68a3e629afe10056e277c8e6f4)
---
src/tests/gdctl/show | 29 ++++
src/tests/gdctl/show-modes | 35 ++++
src/tests/gdctl/show-properties | 57 ++++++
src/tests/gdctl/show-verbose | 93 ++++++++++
src/tests/generic.test.in | 2 +-
src/tests/meson.build | 12 ++
src/tests/monitor-dbus-tests.c | 298 ++++++++++++++++++++++++++++++++
tools/meson.build | 1 +
8 files changed, 526 insertions(+), 1 deletion(-)
create mode 100644 src/tests/gdctl/show
create mode 100644 src/tests/gdctl/show-modes
create mode 100644 src/tests/gdctl/show-properties
create mode 100644 src/tests/gdctl/show-verbose
create mode 100644 src/tests/monitor-dbus-tests.c
diff --git a/src/tests/gdctl/show b/src/tests/gdctl/show
new file mode 100644
index 0000000000..62f9c0ba5c
--- /dev/null
+++ b/src/tests/gdctl/show
@@ -0,0 +1,29 @@
+Monitors:
+├──Monitor DP-1 (MetaProduct's Inc. 14")
+│ ├──Vendor: MetaProduct's Inc.
+│ ├──Product: MetaMonitor
+│ ├──Serial: 0x1234560
+│ └──Current mode
+│ └──3840x2160@60.000
+└──Monitor DP-2 (MetaProduct's Inc. 13")
+ ├──Vendor: MetaProduct's Inc.
+ ├──Product: MetaMonitor
+ ├──Serial: 0x1234561
+ └──Current mode
+ └──2560x1440@60.000
+
+Logical monitors:
+├──Logical monitor #1
+│ ├──Position: (0, 0)
+│ ├──Scale: 2.2018349170684814
+│ ├──Transform: normal
+│ ├──Primary: yes
+│ └──Monitors: (1)
+│ └──DP-1 (MetaProduct's Inc. 14")
+└──Logical monitor #2
+ ├──Position: (1744, 0)
+ ├──Scale: 1.7582417726516724
+ ├──Transform: normal
+ ├──Primary: no
+ └──Monitors: (1)
+ └──DP-2 (MetaProduct's Inc. 13")
diff --git a/src/tests/gdctl/show-modes b/src/tests/gdctl/show-modes
new file mode 100644
index 0000000000..b175e1fed9
--- /dev/null
+++ b/src/tests/gdctl/show-modes
@@ -0,0 +1,35 @@
+Monitors:
+├──Monitor DP-1 (MetaProduct's Inc. 14")
+│ ├──Vendor: MetaProduct's Inc.
+│ ├──Product: MetaMonitor
+│ ├──Serial: 0x1234560
+│ └──Modes (4)
+│ ├──3840x2160@60.000
+│ ├──3840x2160@30.000
+│ ├──2560x1440@60.000
+│ └──1440x900@60.000
+└──Monitor DP-2 (MetaProduct's Inc. 13")
+ ├──Vendor: MetaProduct's Inc.
+ ├──Product: MetaMonitor
+ ├──Serial: 0x1234561
+ └──Modes (4)
+ ├──2560x1440@60.000
+ ├──1440x900@60.000
+ ├──1366x768@60.000
+ └──800x600@60.000
+
+Logical monitors:
+├──Logical monitor #1
+│ ├──Position: (0, 0)
+│ ├──Scale: 2.2018349170684814
+│ ├──Transform: normal
+│ ├──Primary: yes
+│ └──Monitors: (1)
+│ └──DP-1 (MetaProduct's Inc. 14")
+└──Logical monitor #2
+ ├──Position: (1744, 0)
+ ├──Scale: 1.7582417726516724
+ ├──Transform: normal
+ ├──Primary: no
+ └──Monitors: (1)
+ └──DP-2 (MetaProduct's Inc. 13")
diff --git a/src/tests/gdctl/show-properties b/src/tests/gdctl/show-properties
new file mode 100644
index 0000000000..d8c7aa03de
--- /dev/null
+++ b/src/tests/gdctl/show-properties
@@ -0,0 +1,57 @@
+Monitors:
+├──Monitor DP-1 (MetaProduct's Inc. 14")
+│ ├──Vendor: MetaProduct's Inc.
+│ ├──Product: MetaMonitor
+│ ├──Serial: 0x1234560
+│ ├──Current mode
+│ │ └──3840x2160@60.000
+│ │ ├──Dimension: 3840x2160
+│ │ ├──Refresh rate: 60.000
+│ │ ├──Preferred scale: 2.2018349170684814
+│ │ ├──Supported scales: [1.0, 1.25, 1.5, 1.7518248558044434, 2.0, 2.2018349170684814, 2.5, 2.7586207389831543, 3.0, 3.2432432174682617, 3.4782607555389404, 3.75, 4.0]
+│ │ └──Properties: (2)
+│ │ ├──is-current ⇒ yes
+│ │ └──is-preferred ⇒ yes
+│ └──Properties: (3)
+│ ├──is-builtin ⇒ no
+│ ├──display-name ⇒ MetaProduct's Inc. 14"
+│ └──is-for-lease ⇒ no
+└──Monitor DP-2 (MetaProduct's Inc. 13")
+ ├──Vendor: MetaProduct's Inc.
+ ├──Product: MetaMonitor
+ ├──Serial: 0x1234561
+ ├──Current mode
+ │ └──2560x1440@60.000
+ │ ├──Dimension: 2560x1440
+ │ ├──Refresh rate: 60.000
+ │ ├──Preferred scale: 1.7582417726516724
+ │ ├──Supported scales: [1.0, 1.25, 1.495327115058899, 1.7582417726516724, 2.0, 2.253521203994751, 2.5, 2.7586207389831543, 3.0188679695129395]
+ │ └──Properties: (2)
+ │ ├──is-current ⇒ yes
+ │ └──is-preferred ⇒ yes
+ └──Properties: (3)
+ ├──is-builtin ⇒ no
+ ├──display-name ⇒ MetaProduct's Inc. 13"
+ └──is-for-lease ⇒ no
+
+Logical monitors:
+├──Logical monitor #1
+│ ├──Position: (0, 0)
+│ ├──Scale: 2.2018349170684814
+│ ├──Transform: normal
+│ ├──Primary: yes
+│ ├──Monitors: (1)
+│ │ └──DP-1 (MetaProduct's Inc. 14")
+│ └──Properties: (0)
+└──Logical monitor #2
+ ├──Position: (1744, 0)
+ ├──Scale: 1.7582417726516724
+ ├──Transform: normal
+ ├──Primary: no
+ ├──Monitors: (1)
+ │ └──DP-2 (MetaProduct's Inc. 13")
+ └──Properties: (0)
+
+Properties: (2)
+├──layout-mode ⇒ logical
+└──supports-changing-layout-mode ⇒ yes
diff --git a/src/tests/gdctl/show-verbose b/src/tests/gdctl/show-verbose
new file mode 100644
index 0000000000..c88a0a8e35
--- /dev/null
+++ b/src/tests/gdctl/show-verbose
@@ -0,0 +1,93 @@
+Monitors:
+├──Monitor DP-1 (MetaProduct's Inc. 14")
+│ ├──Vendor: MetaProduct's Inc.
+│ ├──Product: MetaMonitor
+│ ├──Serial: 0x1234560
+│ ├──Modes (4)
+│ │ ├──3840x2160@60.000
+│ │ │ ├──Dimension: 3840x2160
+│ │ │ ├──Refresh rate: 60.000
+│ │ │ ├──Preferred scale: 2.2018349170684814
+│ │ │ ├──Supported scales: [1.0, 1.25, 1.5, 1.7518248558044434, 2.0, 2.2018349170684814, 2.5, 2.7586207389831543, 3.0, 3.2432432174682617, 3.4782607555389404, 3.75, 4.0]
+│ │ │ └──Properties: (2)
+│ │ │ ├──is-current ⇒ yes
+│ │ │ └──is-preferred ⇒ yes
+│ │ ├──3840x2160@30.000
+│ │ │ ├──Dimension: 3840x2160
+│ │ │ ├──Refresh rate: 30.000
+│ │ │ ├──Preferred scale: 2.2018349170684814
+│ │ │ ├──Supported scales: [1.0, 1.25, 1.5, 1.7518248558044434, 2.0, 2.2018349170684814, 2.5, 2.7586207389831543, 3.0, 3.2432432174682617, 3.4782607555389404, 3.75, 4.0]
+│ │ │ └──Properties: (0)
+│ │ ├──2560x1440@60.000
+│ │ │ ├──Dimension: 2560x1440
+│ │ │ ├──Refresh rate: 60.000
+│ │ │ ├──Preferred scale: 1.495327115058899
+│ │ │ ├──Supported scales: [1.0, 1.25, 1.495327115058899, 1.7582417726516724, 2.0, 2.253521203994751, 2.5, 2.7586207389831543, 3.0188679695129395]
+│ │ │ └──Properties: (0)
+│ │ └──1440x900@60.000
+│ │ ├──Dimension: 1440x900
+│ │ ├──Refresh rate: 60.000
+│ │ ├──Preferred scale: 1.0
+│ │ ├──Supported scales: [1.0, 1.25, 1.5, 1.7475727796554565]
+│ │ └──Properties: (0)
+│ └──Properties: (3)
+│ ├──is-builtin ⇒ no
+│ ├──display-name ⇒ MetaProduct's Inc. 14"
+│ └──is-for-lease ⇒ no
+└──Monitor DP-2 (MetaProduct's Inc. 13")
+ ├──Vendor: MetaProduct's Inc.
+ ├──Product: MetaMonitor
+ ├──Serial: 0x1234561
+ ├──Modes (4)
+ │ ├──2560x1440@60.000
+ │ │ ├──Dimension: 2560x1440
+ │ │ ├──Refresh rate: 60.000
+ │ │ ├──Preferred scale: 1.7582417726516724
+ │ │ ├──Supported scales: [1.0, 1.25, 1.495327115058899, 1.7582417726516724, 2.0, 2.253521203994751, 2.5, 2.7586207389831543, 3.0188679695129395]
+ │ │ └──Properties: (2)
+ │ │ ├──is-current ⇒ yes
+ │ │ └──is-preferred ⇒ yes
+ │ ├──1440x900@60.000
+ │ │ ├──Dimension: 1440x900
+ │ │ ├──Refresh rate: 60.000
+ │ │ ├──Preferred scale: 1.0
+ │ │ ├──Supported scales: [1.0, 1.25, 1.5, 1.7475727796554565]
+ │ │ └──Properties: (0)
+ │ ├──1366x768@60.000
+ │ │ ├──Dimension: 1366x768
+ │ │ ├──Refresh rate: 60.000
+ │ │ ├──Preferred scale: 1.0
+ │ │ ├──Supported scales: [1.0]
+ │ │ └──Properties: (0)
+ │ └──800x600@60.000
+ │ ├──Dimension: 800x600
+ │ ├──Refresh rate: 60.000
+ │ ├──Preferred scale: 1.0
+ │ ├──Supported scales: [1.0]
+ │ └──Properties: (0)
+ └──Properties: (3)
+ ├──is-builtin ⇒ no
+ ├──display-name ⇒ MetaProduct's Inc. 13"
+ └──is-for-lease ⇒ no
+
+Logical monitors:
+├──Logical monitor #1
+│ ├──Position: (0, 0)
+│ ├──Scale: 2.2018349170684814
+│ ├──Transform: normal
+│ ├──Primary: yes
+│ ├──Monitors: (1)
+│ │ └──DP-1 (MetaProduct's Inc. 14")
+│ └──Properties: (0)
+└──Logical monitor #2
+ ├──Position: (1744, 0)
+ ├──Scale: 1.7582417726516724
+ ├──Transform: normal
+ ├──Primary: no
+ ├──Monitors: (1)
+ │ └──DP-2 (MetaProduct's Inc. 13")
+ └──Properties: (0)
+
+Properties: (2)
+├──layout-mode ⇒ logical
+└──supports-changing-layout-mode ⇒ yes
diff --git a/src/tests/generic.test.in b/src/tests/generic.test.in
index 623e51f24d..d9b8ed3c8a 100644
--- a/src/tests/generic.test.in
+++ b/src/tests/generic.test.in
@@ -1,5 +1,5 @@
[Test]
Description=Mutter test: @testname@
-Exec=sh -ec 'env G_TEST_SRCDIR=@pkgdatadir@ G_TEST_BUILDDIR=@libexecdir@/installed-tests/mutter-@apiversion@ MUTTER_REF_TEST_RESULT_DIR=@reftestresultdir@ @libexecdir@/installed-tests/mutter-@apiversion@/mutter-installed-dbus-session.py @runnerargs@ -- @libexecdir@/installed-tests/mutter-@apiversion@/@testexecutable@ @testargs@'
+Exec=sh -ec 'env G_TEST_SRCDIR=@pkgdatadir@ G_TEST_BUILDDIR=@libexecdir@/installed-tests/mutter-@apiversion@ MUTTER_REF_TEST_RESULT_DIR=@reftestresultdir@ MUTTER_GDCTL_TEST_RESULT_DIR=@gdctltestresultdir@ @libexecdir@/installed-tests/mutter-@apiversion@/mutter-installed-dbus-session.py @runnerargs@ -- @libexecdir@/installed-tests/mutter-@apiversion@/@testexecutable@ @testargs@'
Type=session
Output=TAP
diff --git a/src/tests/meson.build b/src/tests/meson.build
index 9f1fc1157b..a91dfb9893 100644
--- a/src/tests/meson.build
+++ b/src/tests/meson.build
@@ -149,6 +149,7 @@ test_env_variables = {
'XDG_CONFIG_HOME': mutter_builddir / '.config',
'MUTTER_TEST_PLUGIN_PATH': '@0@'.format(default_plugin.full_path()),
'MUTTER_REF_TEST_RESULT_DIR': mutter_builddir / 'meson-logs' / 'tests' / 'ref-tests',
+ 'MUTTER_GDCTL_TEST_RESULT_DIR': mutter_builddir / 'meson-logs' / 'tests' / 'gdctl',
'GSETTINGS_SCHEMA_DIR': ':'.join([mutter_builddir / 'src' / 'tests',
locally_compiled_schemas_dir]),
}
@@ -208,6 +209,9 @@ install_data(
install_subdir('dbusmock-templates',
install_dir: tests_datadir,
)
+install_subdir('gdctl',
+ install_dir: tests_datadir,
+)
if have_installed_tests
configure_file(
@@ -279,6 +283,12 @@ test_cases += [
'suite': 'backend',
'sources': [ 'monitor-backlight-tests.c', ]
},
+ {
+ 'name': 'monitor-dbus',
+ 'suite': 'backend',
+ 'sources': [ 'monitor-dbus-tests.c', ],
+ 'args': gdctl.full_path(),
+ },
{
'name': 'stage-views',
'suite': 'compositor',
@@ -794,6 +804,7 @@ foreach test_case: test_cases
args = test_case.get('args', [])
+ message('test @0@ args: @1@'.format(test_case['name'], args))
test(test_case['name'], test_executable,
suite: ['core', 'mutter/' + test_case['suite']],
args: args,
@@ -1044,6 +1055,7 @@ if have_installed_tests
installed_tests_cdata.set('libexecdir', libexecdir)
installed_tests_cdata.set('pkgdatadir', pkgdatadir)
installed_tests_cdata.set('reftestresultdir', '/tmp/mutter-ref-test-results')
+ installed_tests_cdata.set('gdctltestresultdir', '/tmp/mutter-gdctl-test-results')
installed_tests_cdata.set('testname', test_case['name'])
installed_tests_cdata.set('testexecutable', 'mutter-' + test_case['name'])
installed_tests_cdata.set('testargs', ' '.join(test_case.get('args', [])))
diff --git a/src/tests/monitor-dbus-tests.c b/src/tests/monitor-dbus-tests.c
new file mode 100644
index 0000000000..f8f56e1cd4
--- /dev/null
+++ b/src/tests/monitor-dbus-tests.c
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2024 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "config.h"
+
+#include <stdarg.h>
+
+#include "tests/meta-monitor-test-utils.h"
+#include "tests/meta-test/meta-context-test.h"
+
+static MetaContext *test_context;
+static char *gdctl_path;
+
+static MonitorTestCaseSetup test_case_setup = {
+ .modes = {
+ {
+ .width = 3840,
+ .height = 2160,
+ .refresh_rate = 60.0
+ },
+ {
+ .width = 3840,
+ .height = 2160,
+ .refresh_rate = 30.0
+ },
+ {
+ .width = 2560,
+ .height = 1440,
+ .refresh_rate = 60.0
+ },
+ {
+ .width = 1440,
+ .height = 900,
+ .refresh_rate = 60.0
+ },
+ {
+ .width = 1366,
+ .height = 768,
+ .refresh_rate = 60.0
+ },
+ {
+ .width = 800,
+ .height = 600,
+ .refresh_rate = 60.0
+ },
+ },
+ .n_modes = 6,
+ .outputs = {
+ {
+ .crtc = 0,
+ .modes = { 0, 1, 2, 3 },
+ .n_modes = 4,
+ .preferred_mode = 0,
+ .possible_crtcs = { 0 },
+ .n_possible_crtcs = 1,
+ .width_mm = 300,
+ .height_mm = 190,
+ .dynamic_scale = TRUE,
+ },
+ {
+ .crtc = 1,
+ .modes = { 2, 3, 4, 5 },
+ .n_modes = 4,
+ .preferred_mode = 2,
+ .possible_crtcs = { 1 },
+ .n_possible_crtcs = 1,
+ .width_mm = 290,
+ .height_mm = 180,
+ .dynamic_scale = TRUE,
+ },
+ },
+ .n_outputs = 2,
+ .n_crtcs = 2
+};
+
+static void
+read_all_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ gboolean *done = user_data;
+ GError *error = NULL;
+
+ g_input_stream_read_all_finish (G_INPUT_STREAM (source_object),
+ res,
+ NULL,
+ &error);
+ g_assert_no_error (error);
+
+ *done = TRUE;
+}
+
+static void
+wait_check_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ gboolean *done = user_data;
+ GError *error = NULL;
+
+ g_subprocess_wait_check_finish (G_SUBPROCESS (source_object), res, &error);
+ g_assert_no_error (error);
+
+ *done = TRUE;
+}
+
+static char *
+save_output (const char *output,
+ const char *expected_output_file)
+{
+ const char *gdctl_result_dir;
+ char *output_path;
+ GError *error = NULL;
+
+ gdctl_result_dir = g_getenv ("MUTTER_GDCTL_TEST_RESULT_DIR");
+ g_assert_no_errno (g_mkdir_with_parents (gdctl_result_dir, 0755));
+
+ output_path = g_strdup_printf ("%s/%s",
+ gdctl_result_dir,
+ expected_output_file);
+
+ g_file_set_contents (output_path, output, -1, &error);
+ g_assert_no_error (error);
+
+ return output_path;
+}
+
+static void
+run_diff (const char *output_path,
+ const char *expected_output_path)
+{
+ g_autoptr (GSubprocessLauncher) launcher = NULL;
+ g_autoptr (GSubprocess) subprocess = NULL;
+ GError *error = NULL;
+
+ launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_NONE);
+ subprocess = g_subprocess_launcher_spawn (launcher,
+ &error,
+ "diff",
+ "-u",
+ expected_output_path,
+ output_path,
+ NULL);
+ g_subprocess_wait (subprocess, NULL, &error);
+ g_assert_no_error (error);
+}
+
+static void
+check_gdctl_output (const char *expected_output_file,
+ ...)
+{
+ g_autoptr (GPtrArray) args = NULL;
+ va_list va_args;
+ char *arg;
+ g_autoptr (GSubprocessLauncher) launcher = NULL;
+ g_autoptr (GSubprocess) subprocess = NULL;
+ GInputStream *stdout_pipe;
+ size_t max_output_size;
+ g_autofree char *output = NULL;
+ gboolean read_done = FALSE;
+ gboolean process_done = FALSE;
+ GError *error = NULL;
+ g_autofree char *expected_output_path = NULL;
+ g_autofree char *expected_output = NULL;
+
+ args = g_ptr_array_new ();
+ g_ptr_array_add (args, gdctl_path);
+ va_start (va_args, expected_output_file);
+ while ((arg = va_arg (va_args, char *)))
+ g_ptr_array_add (args, arg);
+ va_end (va_args);
+ g_ptr_array_add (args, NULL);
+
+ launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE |
+ G_SUBPROCESS_FLAGS_STDERR_PIPE);
+
+ subprocess = g_subprocess_launcher_spawnv (launcher,
+ (const char * const*) args->pdata,
+ &error);
+ stdout_pipe = g_subprocess_get_stdout_pipe (subprocess);
+ max_output_size = 1024 * 1024;
+ output = g_malloc0 (max_output_size);
+ g_input_stream_read_all_async (stdout_pipe,
+ output,
+ max_output_size - 1,
+ G_PRIORITY_DEFAULT,
+ NULL,
+ read_all_cb,
+ &read_done);
+
+ g_subprocess_wait_check_async (subprocess, NULL,
+ wait_check_cb, &process_done);
+
+ while (!read_done || !process_done)
+ g_main_context_iteration (NULL, TRUE);
+
+ expected_output_path = g_test_build_filename (G_TEST_DIST,
+ "tests",
+ "gdctl",
+ expected_output_file,
+ NULL);
+ g_file_get_contents (expected_output_path,
+ &expected_output,
+ NULL,
+ &error);
+ g_assert_no_error (error);
+
+ if (g_strcmp0 (expected_output, output) != 0)
+ {
+ g_autofree char *output_path = NULL;
+
+ output_path = save_output (output, expected_output_file);
+ run_diff (output_path, expected_output_path);
+ g_error ("Incorrect gdctl output");
+ }
+}
+
+static void
+meta_test_monitor_dbus_get_state (void)
+{
+ MetaBackend *backend = meta_context_get_backend (test_context);
+ MetaMonitorManager *monitor_manager =
+ meta_backend_get_monitor_manager (backend);
+ MetaMonitorManagerTest *monitor_manager_test =
+ META_MONITOR_MANAGER_TEST (monitor_manager);
+ MetaMonitorTestSetup *test_setup;
+
+ test_setup = meta_create_monitor_test_setup (backend,
+ &test_case_setup,
+ MONITOR_TEST_FLAG_NO_STORED);
+ meta_monitor_manager_test_emulate_hotplug (monitor_manager_test, test_setup);
+
+ check_gdctl_output ("show",
+ "show", NULL);
+ check_gdctl_output ("show-properties",
+ "show", "--properties", NULL);
+ check_gdctl_output ("show-modes",
+ "show", "--modes", NULL);
+ check_gdctl_output ("show-verbose",
+ "show", "--verbose", NULL);
+}
+
+static void
+init_tests (void)
+{
+ g_test_add_func ("/backends/native/monitor/dbus/get-state",
+ meta_test_monitor_dbus_get_state);
+}
+
+int
+main (int argc,
+ char **argv)
+{
+ g_autoptr (MetaContext) context = NULL;
+ g_autoptr (GFile) gdctl_file = NULL;
+ char **argv_ignored = NULL;
+ GOptionEntry options[] = {
+ {
+ G_OPTION_REMAINING,
+ .arg = G_OPTION_ARG_STRING_ARRAY,
+ &argv_ignored,
+ .arg_description = "GDCTL-PATH"
+ },
+ { NULL }
+ };
+
+ context = meta_create_test_context (META_CONTEXT_TEST_TYPE_TEST,
+ META_CONTEXT_TEST_FLAG_NO_X11);
+ meta_context_add_option_entries (context, options, NULL);
+ g_assert_true (meta_context_configure (context, &argc, &argv, NULL));
+
+ g_assert_nonnull (argv_ignored);
+ g_assert_nonnull (argv_ignored[0]);
+ g_assert_null (argv_ignored[1]);
+ gdctl_path = argv_ignored[0];
+
+ test_context = context;
+
+ init_tests ();
+
+ return meta_context_test_run_tests (META_CONTEXT_TEST (context),
+ META_TEST_RUN_FLAG_NONE);
+}
diff --git a/tools/meson.build b/tools/meson.build
index 2603ed3d87..763507c2e9 100644
--- a/tools/meson.build
+++ b/tools/meson.build
@@ -3,4 +3,5 @@ install_data(
install_dir: bindir,
)
+gdctl = find_program('gdctl')
get_state_tool = find_program('get-state.py')
--
2.49.0
From ca85c10b64c43a8c261b70244ec1d017ba22cd2e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Wed, 18 Dec 2024 16:49:08 +0100
Subject: [PATCH 15/28] tests/monitor-test-utils: Add debug log for logical
monitor checking
This was helpful when figuring out what logical monitor test data was
wrong.
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4190>
(cherry picked from commit fe79188b58cec406585c42d7bb6197c92bb05f4a)
---
src/tests/meta-monitor-test-utils.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/src/tests/meta-monitor-test-utils.c b/src/tests/meta-monitor-test-utils.c
index c022afdea8..4a7a46a527 100644
--- a/src/tests/meta-monitor-test-utils.c
+++ b/src/tests/meta-monitor-test-utils.c
@@ -238,6 +238,12 @@ check_logical_monitor (MetaMonitorManager *monitor_manager,
GList *l;
int i;
+ g_debug ("Checking logical monitor with layout %dx%d+%d+%d",
+ test_logical_monitor->layout.width,
+ test_logical_monitor->layout.height,
+ test_logical_monitor->layout.x,
+ test_logical_monitor->layout.y);
+
logical_monitor = logical_monitor_from_layout (monitor_manager,
&test_logical_monitor->layout);
g_assert_nonnull (logical_monitor);
--
2.49.0
From 07b3e24e2b8f8c5b75f4dffe083107b2eb119e56 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Wed, 18 Dec 2024 16:52:41 +0100
Subject: [PATCH 16/28] tests/monitor-dbus: Test changing configuration
Uses some various combinations of the gdctl commands that configures the
available monitors differently.
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4190>
(cherry picked from commit 839f67f1de1d3f84bebe04b12f4ef0e91d0ccc54)
---
src/tests/monitor-dbus-tests.c | 412 +++++++++++++++++++++++++++++++++
1 file changed, 412 insertions(+)
diff --git a/src/tests/monitor-dbus-tests.c b/src/tests/monitor-dbus-tests.c
index f8f56e1cd4..82e3178c6a 100644
--- a/src/tests/monitor-dbus-tests.c
+++ b/src/tests/monitor-dbus-tests.c
@@ -20,6 +20,7 @@
#include <stdarg.h>
+#include "backends/meta-monitor-config-manager.h"
#include "tests/meta-monitor-test-utils.h"
#include "tests/meta-test/meta-context-test.h"
@@ -88,6 +89,149 @@ static MonitorTestCaseSetup test_case_setup = {
.n_crtcs = 2
};
+static MonitorTestCaseExpect test_case_expect = {
+ .monitors = {
+ {
+ .outputs = { 0 },
+ .n_outputs = 1,
+ .modes = {
+ {
+ .width = 3840,
+ .height = 2160,
+ .refresh_rate = 60.0,
+ .crtc_modes = {
+ {
+ .output = 0,
+ .crtc_mode = 0,
+ },
+ },
+ },
+ {
+ .width = 3840,
+ .height = 2160,
+ .refresh_rate = 30.0,
+ .crtc_modes = {
+ {
+ .output = 0,
+ .crtc_mode = 1,
+ },
+ },
+ },
+ {
+ .width = 2560,
+ .height = 1440,
+ .refresh_rate = 60.0,
+ .crtc_modes = {
+ {
+ .output = 0,
+ .crtc_mode = 2,
+ },
+ },
+ },
+ {
+ .width = 1440,
+ .height = 900,
+ .refresh_rate = 60.0,
+ .crtc_modes = {
+ {
+ .output = 0,
+ .crtc_mode = 3,
+ },
+ },
+ },
+ },
+ .n_modes = 4,
+ .current_mode = 0,
+ .width_mm = 300,
+ .height_mm = 190,
+ },
+ {
+ .outputs = { 1 },
+ .n_outputs = 1,
+ .modes = {
+ {
+ .width = 2560,
+ .height = 1440,
+ .refresh_rate = 60.0,
+ .crtc_modes = {
+ {
+ .output = 1,
+ .crtc_mode = 2,
+ },
+ },
+ },
+ {
+ .width = 1440,
+ .height = 900,
+ .refresh_rate = 60.0,
+ .crtc_modes = {
+ {
+ .output = 1,
+ .crtc_mode = 3,
+ },
+ },
+ },
+ {
+ .width = 1366,
+ .height = 768,
+ .refresh_rate = 60.0,
+ .crtc_modes = {
+ {
+ .output = 1,
+ .crtc_mode = 4,
+ },
+ },
+ },
+ {
+ .width = 800,
+ .height = 600,
+ .refresh_rate = 60.0,
+ .crtc_modes = {
+ {
+ .output = 1,
+ .crtc_mode = 5,
+ },
+ },
+ },
+ },
+ .n_modes = 4,
+ .current_mode = 0,
+ .width_mm = 290,
+ .height_mm = 180,
+ },
+ },
+ .n_monitors = 2,
+ .logical_monitors = {
+ {
+ .monitors = { 0 },
+ .n_monitors = 1,
+ .layout = { .x = 0, .y = 0, .width = 1744, .height = 981 },
+ .scale = 2.2018349170684814,
+ },
+ {
+ .monitors = { 1 },
+ .n_monitors = 1,
+ .layout = { .x = 1744, .y = 0, .width = 1456, .height = 819 },
+ .scale = 1.7582417726516724,
+ },
+ },
+ .n_logical_monitors = 2,
+ .primary_logical_monitor = 0,
+ .n_outputs = 2,
+ .crtcs = {
+ {
+ .current_mode = 0,
+ },
+ {
+ .current_mode = 2,
+ .x = 1744,
+ }
+ },
+ .n_crtcs = 2,
+ .screen_width = 3200,
+ .screen_height = 981,
+};
+
static void
read_all_cb (GObject *source_object,
GAsyncResult *res,
@@ -160,6 +304,39 @@ run_diff (const char *output_path,
g_assert_no_error (error);
}
+static void
+check_gdctl_result (const char *first_argument,
+ ...)
+{
+ g_autoptr (GPtrArray) args = NULL;
+ va_list va_args;
+ char *arg;
+ g_autoptr (GSubprocessLauncher) launcher = NULL;
+ g_autoptr (GSubprocess) subprocess = NULL;
+ gboolean process_done = FALSE;
+ GError *error = NULL;
+
+ args = g_ptr_array_new ();
+ g_ptr_array_add (args, gdctl_path);
+ g_ptr_array_add (args, (char *) first_argument);
+ va_start (va_args, first_argument);
+ while ((arg = va_arg (va_args, char *)))
+ g_ptr_array_add (args, arg);
+ va_end (va_args);
+ g_ptr_array_add (args, NULL);
+
+ launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_NONE);
+
+ subprocess = g_subprocess_launcher_spawnv (launcher,
+ (const char * const*) args->pdata,
+ &error);
+ g_subprocess_wait_check_async (subprocess, NULL,
+ wait_check_cb, &process_done);
+
+ while (!process_done)
+ g_main_context_iteration (NULL, TRUE);
+}
+
static void
check_gdctl_output (const char *expected_output_file,
...)
@@ -255,11 +432,246 @@ meta_test_monitor_dbus_get_state (void)
"show", "--verbose", NULL);
}
+static void
+meta_test_monitor_dbus_apply_verify (void)
+{
+ MetaBackend *backend = meta_context_get_backend (test_context);
+ MetaMonitorManager *monitor_manager =
+ meta_backend_get_monitor_manager (backend);
+ MetaMonitorConfigManager *config_manager =
+ meta_monitor_manager_get_config_manager (monitor_manager);
+ MetaMonitorManagerTest *monitor_manager_test =
+ META_MONITOR_MANAGER_TEST (monitor_manager);
+ MetaMonitorTestSetup *test_setup;
+ MetaMonitorsConfig *config;
+
+ test_setup = meta_create_monitor_test_setup (backend,
+ &test_case_setup,
+ MONITOR_TEST_FLAG_NO_STORED);
+ meta_monitor_manager_test_emulate_hotplug (monitor_manager_test, test_setup);
+
+ config = meta_monitor_config_manager_get_current (config_manager);
+
+ check_gdctl_result ("set",
+ "--verbose",
+ "--verify",
+ "--layout-mode", "logical",
+ "--logical-monitor",
+ "--primary",
+ "--monitor", "DP-1",
+ "--logical-monitor",
+ "--monitor", "DP-2",
+ "--right-of", "DP-1",
+ NULL);
+ g_assert_true (config ==
+ meta_monitor_config_manager_get_current (config_manager));
+}
+
+static void
+setup_apply_configuration_test (void)
+{
+ MetaBackend *backend = meta_context_get_backend (test_context);
+ MetaMonitorManager *monitor_manager =
+ meta_backend_get_monitor_manager (backend);
+ MetaMonitorManagerTest *monitor_manager_test =
+ META_MONITOR_MANAGER_TEST (monitor_manager);
+ MetaMonitorTestSetup *test_setup;
+
+ test_setup = meta_create_monitor_test_setup (backend,
+ &test_case_setup,
+ MONITOR_TEST_FLAG_NO_STORED);
+ meta_monitor_manager_test_emulate_hotplug (monitor_manager_test, test_setup);
+
+ META_TEST_LOG_CALL ("Checking monitor configuration",
+ meta_check_monitor_configuration (test_context,
+ &test_case_expect));
+}
+
+static void
+meta_test_monitor_dbus_apply_left_of (void)
+{
+ MonitorTestCaseExpect expect;
+
+ setup_apply_configuration_test ();
+
+ check_gdctl_result ("set",
+ "--verbose",
+ "--layout-mode", "logical",
+ "--logical-monitor",
+ "--primary",
+ "--monitor", "DP-1",
+ "--logical-monitor",
+ "--monitor", "DP-2",
+ "--left-of", "DP-1",
+ NULL);
+
+ expect = test_case_expect;
+ expect.logical_monitors[0].layout.x = 1456;
+ expect.logical_monitors[1].layout.x = 0;
+ expect.crtcs[0].x = 1456;
+ expect.crtcs[1].x = 0;
+ META_TEST_LOG_CALL ("Checking monitor configuration",
+ meta_check_monitor_configuration (test_context,
+ &expect));
+}
+
+static void
+meta_test_monitor_dbus_apply_right_of_transform (void)
+{
+ MonitorTestCaseExpect expect;
+
+ setup_apply_configuration_test ();
+
+ check_gdctl_result ("set",
+ "--verbose",
+ "--layout-mode", "logical",
+ "--logical-monitor",
+ "--primary",
+ "--monitor", "DP-2",
+ "--transform", "270",
+ "--logical-monitor",
+ "--monitor", "DP-1",
+ "--right-of", "DP-2",
+ "--y", "400",
+ NULL);
+
+ expect = test_case_expect;
+ expect.logical_monitors[0].layout.x = 0;
+ expect.logical_monitors[0].layout.y = 0;
+ expect.logical_monitors[0].layout.width = 819;
+ expect.logical_monitors[0].layout.height = 1456;
+ expect.logical_monitors[0].scale = 1.7582417726516724;
+ expect.logical_monitors[0].transform = MTK_MONITOR_TRANSFORM_270;
+ expect.logical_monitors[0].monitors[0] = 1;
+
+ expect.logical_monitors[1].layout.x = 819;
+ expect.logical_monitors[1].layout.y = 400;
+ expect.logical_monitors[1].layout.width = 1744;
+ expect.logical_monitors[1].layout.height = 981;
+ expect.logical_monitors[1].scale = 2.2018349170684814;
+ expect.logical_monitors[1].monitors[0] = 0;
+
+ expect.crtcs[1].x = 0;
+ expect.crtcs[1].y = 0;
+ expect.crtcs[1].transform = MTK_MONITOR_TRANSFORM_270;
+ expect.crtcs[0].x = 819;
+ expect.crtcs[0].y = 400;
+ expect.screen_width = 2563;
+ expect.screen_height = 1456;
+ META_TEST_LOG_CALL ("Checking monitor configuration",
+ meta_check_monitor_configuration (test_context,
+ &expect));
+}
+
+static void
+meta_test_monitor_dbus_apply_mode_scale_below_transform (void)
+{
+ MonitorTestCaseExpect expect;
+
+ setup_apply_configuration_test ();
+
+ check_gdctl_result ("set",
+ "--verbose",
+ "--layout-mode", "logical",
+ "--logical-monitor",
+ "--primary",
+ "--monitor", "DP-2",
+ "--transform", "270",
+ "--logical-monitor",
+ "--monitor", "DP-1",
+ "--below", "DP-2",
+ "--transform", "90",
+ "--x", "100",
+ "--mode", "1440x900@60.000",
+ "--scale", "1.5",
+ NULL);
+
+ expect = test_case_expect;
+ expect.monitors[0].current_mode = 3;
+ expect.logical_monitors[0].layout.x = 0;
+ expect.logical_monitors[0].layout.y = 0;
+ expect.logical_monitors[0].layout.width = 819;
+ expect.logical_monitors[0].layout.height = 1456;
+ expect.logical_monitors[0].scale = 1.7582417726516724;
+ expect.logical_monitors[0].transform = MTK_MONITOR_TRANSFORM_270;
+ expect.logical_monitors[0].monitors[0] = 1;
+ expect.logical_monitors[1].layout.x = 100;
+ expect.logical_monitors[1].layout.y = 1456;
+ expect.logical_monitors[1].layout.width = 600;
+ expect.logical_monitors[1].layout.height = 960;
+ expect.logical_monitors[1].scale = 1.5;
+ expect.logical_monitors[1].transform = MTK_MONITOR_TRANSFORM_90;
+ expect.logical_monitors[1].monitors[0] = 0;
+ expect.crtcs[0].x = 100;
+ expect.crtcs[0].y = 1456;
+ expect.crtcs[0].current_mode = 3;
+ expect.crtcs[0].transform = MTK_MONITOR_TRANSFORM_90;
+ expect.crtcs[1].x = 0;
+ expect.crtcs[1].y = 0;
+ expect.crtcs[1].transform = MTK_MONITOR_TRANSFORM_270;
+ expect.screen_width = 819;
+ expect.screen_height = 2416;
+
+ META_TEST_LOG_CALL ("Checking monitor configuration",
+ meta_check_monitor_configuration (test_context,
+ &expect));
+}
+
+static void
+meta_test_monitor_dbus_apply_mirror (void)
+{
+ MonitorTestCaseExpect expect;
+
+ setup_apply_configuration_test ();
+
+ check_gdctl_result ("set",
+ "--verbose",
+ "--layout-mode", "logical",
+ "--logical-monitor",
+ "--primary",
+ "--monitor", "DP-1",
+ "--mode", "2560x1440@60.000",
+ "--monitor", "DP-2",
+ "--scale", "1.7582417726516724",
+ NULL);
+
+ expect = test_case_expect;
+ expect.monitors[0].current_mode = 2;
+ expect.logical_monitors[0].layout.width = 1456;
+ expect.logical_monitors[0].layout.height = 819;
+ expect.logical_monitors[0].scale = 1.7582417726516724;
+ expect.logical_monitors[0].monitors[0] = 0;
+ expect.logical_monitors[0].monitors[1] = 1;
+ expect.logical_monitors[0].n_monitors = 2;
+ expect.n_logical_monitors = 1;
+ expect.screen_width = 1456;
+ expect.screen_height = 819;
+ expect.crtcs[0].x = 0;
+ expect.crtcs[0].y = 0;
+ expect.crtcs[0].current_mode = 2;
+ expect.crtcs[1].x = 0;
+ expect.crtcs[1].y = 0;
+
+ META_TEST_LOG_CALL ("Checking monitor configuration",
+ meta_check_monitor_configuration (test_context,
+ &expect));
+}
+
static void
init_tests (void)
{
g_test_add_func ("/backends/native/monitor/dbus/get-state",
meta_test_monitor_dbus_get_state);
+ g_test_add_func ("/backends/native/monitor/dbus/apply/verify",
+ meta_test_monitor_dbus_apply_verify);
+ g_test_add_func ("/backends/native/monitor/dbus/apply/left-of",
+ meta_test_monitor_dbus_apply_left_of);
+ g_test_add_func ("/backends/native/monitor/dbus/apply/right-of-transform",
+ meta_test_monitor_dbus_apply_right_of_transform);
+ g_test_add_func ("/backends/native/monitor/dbus/apply/mode-scale-below-transform",
+ meta_test_monitor_dbus_apply_mode_scale_below_transform);
+ g_test_add_func ("/backends/native/monitor/dbus/apply/mirror",
+ meta_test_monitor_dbus_apply_mirror);
}
int
--
2.49.0
From 16eca2a30b83376197cdb8e8f0baf57f8306f2ee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Wed, 18 Dec 2024 22:53:09 +0100
Subject: [PATCH 17/28] Add gdctl man page using rst2man
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4190>
(cherry picked from commit 3cc5d201a249ffb60b5b84b39e7ea0a536feec39)
---
doc/man/gdctl.rst | 164 ++++++++++++++++++++++++++++++++++++++++++++
doc/man/meson.build | 11 +++
meson.build | 2 +
3 files changed, 177 insertions(+)
create mode 100644 doc/man/gdctl.rst
diff --git a/doc/man/gdctl.rst b/doc/man/gdctl.rst
new file mode 100644
index 0000000000..d62013fe12
--- /dev/null
+++ b/doc/man/gdctl.rst
@@ -0,0 +1,164 @@
+=====
+gdctl
+=====
+
+------------------------
+GNOME Display Controller
+------------------------
+
+:Manual section: 1
+:Manual group: User Commands
+
+SYNOPSIS
+--------
+**gdctl** [-h] COMMAND ...
+
+DESCRIPTION
+-----------
+gdctl provides means to show the active monitor configuration, and set new
+monitor configuration using command line arguments.
+
+It requires a compositor that supports the ``org.gnome.Mutter.DisplayConfig``
+D-Bus API, such as GNOME Shell.
+
+COMMANDS
+--------
+``show``
+
+ Show the current display configuration
+
+``set``
+
+ Set a new display configuration
+
+SHOW OPTIONS
+------------
+``--help``, ``-h``
+
+ Show a help message and exit.
+
+``--modes``, ``-m``
+
+ List available monitor modes.
+
+``--properties``, ``-p``
+
+ List properties.
+
+``--verbose``, ``-v``
+
+ Display all available information (equivalent to ``--modes --properties``).
+
+SET OPTIONS
+-----------
+
+``--help``, ``-h``
+
+ Show a help message and exit.
+
+``--persistent``, ``-P``
+
+ Store applied configuration on persistant storage and restore when applicable.
+
+``--verbose``, ``-v``
+
+ Print configuration to standard out before applying it.
+
+``--verify``, ``-V``
+
+ Only verify, without applying, the configuration.
+
+``--layout-mode LAYOUT-MODE``, ``-l``
+
+ Specify the layout mode the configuration should use. Either ``logical``, or
+ ``physical``.
+
+``--logical-monitor``, ``-L``
+
+ Add and configure a logical monitor. See LOGICAL MONITOR OPTIONS.
+
+LOGICAL MONITOR OPTIONS
+-----------------------
+
+``--monitor CONNECTOR``, ``-M CONNECTOR``
+
+ Add a monitor to the currently configured logical monitor. All monitors
+ within the same logical monitor must have the same monitor resolution.
+
+``--primary``, ``-p``
+
+ Mark currently configured logical monitor as primary.
+
+``--scale SCALE``, ``-s SCALE``
+
+ Scale monitors within the currently configured logical monitor with
+ ``SCALE``. Must be a scale supported by all monitors and their configured
+ modes.
+
+``--transform TRANSFORM``, ``-t TRANSFORM``
+
+ Transform monitors within the currently configured logical monitor using
+ ``TRANSFORM``. Possible transforms are ``normal``, ``90``, ``180``, ``270``,
+ ``flipped``, ``flipped-90``, ``flipped-270`` and ``flipped-180``.
+
+``--x X``, ``-x X``
+
+ Set the X position of the currently configured logical monitor.
+
+``--y``, ``-y Y`` Y position
+
+ Set the Y position of the currently configured logical monitor.
+
+``--right-of CONNECTOR``
+
+ Place the logical monitor to the right of the logical monitor ``CONNECTOR``
+ belongs to.
+
+``--left-of CONNECTOR`` Place left of other monitor
+
+ Place the logical monitor to the left of the logical monitor ``CONNECTOR``
+ belongs to.
+
+``--above CONNECTOR``
+
+ Place the logical monitor above the logical monitor ``CONNECTOR`` belongs to.
+
+``--below CONNECTOR``
+
+ Place the logical monitor below the logical monitor ``CONNECTOR`` belongs to.
+
+MONITOR OPTIONS
+---------------
+
+``--mode``, ``-M``
+
+ Set the mode of the monitor.
+
+EXAMPLES
+--------
+
+Mirror DP-1 and eDP-1, and place DP-2, transformed by 270 degrees, to the right
+of the two mirrored monitors.
+
+::
+
+ gdctl set --logical-monitor
+ --primary
+ --monitor DP-1
+ --monitor eDP-1
+ --logical-monitor
+ --monitor DP-2
+ --right-of DP-1
+
+BUGS
+----
+The bug tracker can be reached by visiting the website
+https://gitlab.gnome.org/GNOME/mutter/-/issues.
+Before sending a bug report, please verify that you have the latest version
+of gnome-shell. Many bugs (major and minor) are fixed at each release, and
+if yours is out of date, the problem may already have been solved.
+
+ADDITIONAL INFORMATION
+----------------------
+For further information, visit the website
+https://gitlab.gnome.org/GNOME/mutter/-/blob/main/README.md.
diff --git a/doc/man/meson.build b/doc/man/meson.build
index dd543269da..38649eeb92 100644
--- a/doc/man/meson.build
+++ b/doc/man/meson.build
@@ -1 +1,12 @@
install_man('mutter.1')
+
+rst2man = find_program('rst2man')
+
+custom_target('gdctl.1',
+ input: 'gdctl.rst',
+ output: 'gdctl.1',
+ command: [rst2man, '--syntax-highlight=none', '@INPUT@'],
+ capture: true,
+ install_dir: mandir + '/man1',
+ install: true
+)
diff --git a/meson.build b/meson.build
index c630ec2369..be5ec11eb1 100644
--- a/meson.build
+++ b/meson.build
@@ -88,6 +88,7 @@ libdir = prefix / get_option('libdir')
libexecdir = prefix / get_option('libexecdir')
includedir = prefix / get_option('includedir')
sysconfdir = get_option('sysconfdir')
+mandir = prefix / get_option('mandir')
pkgname = '@0@-@1@'.format(meson.project_name(), libmutter_api_version)
@@ -713,6 +714,7 @@ meson.add_dist_script('meson/check-version.py', meson.project_version(), 'NEWS')
summary('prefix', prefix, section: 'Directories')
summary('libexecdir', libexecdir, section: 'Directories')
summary('pkgdatadir', pkgdatadir, section: 'Directories')
+summary('mandir', mandir, section: 'Directories')
summary('buildtype', get_option('buildtype'), section: 'Build Configuration')
summary('debug', get_option('debug'), section: 'Build Configuration')
--
2.49.0
From fa91583f581bd8b3699858a29de38a222f1223b4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Thu, 19 Dec 2024 22:07:46 +0100
Subject: [PATCH 18/28] ci: Run ruff check/format on gdctl
Use a CI job using ruff that enforces the coding style to follow
`ruff format --line-length 80`, and that no new linting issues appear
using `ruff check`.
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4190>
(cherry picked from commit a0c5c09e9b5b22f73e50a599d62ddabaa5c11022)
---
.gitlab-ci.yml | 22 ++++++++++++++++++++--
1 file changed, 20 insertions(+), 2 deletions(-)
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index fc88aeffc5..7061ef9126 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -105,7 +105,7 @@ variables:
- .skip-git-clone
variables:
FDO_DISTRIBUTION_VERSION: 41
- BASE_TAG: '2024-09-20.2'
+ BASE_TAG: '2025-01-29.0'
MUTTER_USER: 'meta-user'
FDO_DISTRIBUTION_PACKAGES:
clang
@@ -124,6 +124,7 @@ variables:
zenity
python3-dbusmock
gnome-desktop-testing
+ ruff
FDO_DISTRIBUTION_EXEC: |
set -e
@@ -343,7 +344,7 @@ build-fedora-container@aarch64:
- !reference [.pipeline-guard, rules]
when: manual
-check-code-style:
+check-c-code-style:
extends:
- .mutter.distribution-image
- .mutter.fedora@x86_64
@@ -363,6 +364,23 @@ check-code-style:
rules:
- !reference [.only-merge-requests, rules]
+check-python-code-style:
+ extends:
+ - .mutter.distribution-image
+ - .mutter.fedora@x86_64
+ variables:
+ PYTHON_FILES:
+ tools/gdctl
+ stage: code-review
+ needs:
+ - job: build-fedora-container@x86_64
+ artifacts: false
+ script:
+ - ruff format --line-length 80 --check $PYTHON_FILES
+ - ruff check $PYTHON_FILES
+ rules:
+ - !reference [.only-merge-requests, rules]
+
.build-mutter-base:
variables:
BASE_MESON_OPTIONS:
--
2.49.0
From 432a33b413322eb0c19d6de77d1e3da87dc791c5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Fri, 20 Dec 2024 10:15:47 +0100
Subject: [PATCH 19/28] gdctl: Add bash completion integration
This auto-completes things such as available connectors, modes, scales,
transforms, etc.
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4190>
(cherry picked from commit f9bb7aa2e68589d6fd5b10a8d54b211f4dc862e6)
---
.gitlab-ci.yml | 4 +-
meson.build | 3 ++
meson_options.txt | 6 +++
tools/gdctl | 95 ++++++++++++++++++++++++++++++++++++++++++-----
tools/meson.build | 25 +++++++++++++
5 files changed, 122 insertions(+), 11 deletions(-)
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 7061ef9126..5bafb06ee3 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -11,6 +11,7 @@ include:
meson-options:
-Dxwayland_initfd=enabled
-Dprofiler=true
+ -Dbash_completion=false
build-sysext:
before_script:
@@ -124,6 +125,7 @@ variables:
zenity
python3-dbusmock
gnome-desktop-testing
+ python3-argcomplete
ruff
FDO_DISTRIBUTION_EXEC: |
@@ -190,8 +192,6 @@ variables:
mkdir -p /opt/mutter
cp build/src/tests/kvm/bzImage /opt/mutter/bzImage
- dnf install -y python3-argcomplete
-
git clone https://github.com/arighi/virtme-ng.git
cd virtme-ng
git fetch --tags
diff --git a/meson.build b/meson.build
index be5ec11eb1..6948f478a3 100644
--- a/meson.build
+++ b/meson.build
@@ -358,6 +358,7 @@ have_kvm_tests = false
have_tty_tests = false
have_installed_tests = false
have_x11_tests = false
+have_bash_completion = get_option('bash_completion')
if have_tests
gtk3_dep = dependency('gtk+-3.0', version: gtk3_req)
@@ -719,6 +720,8 @@ summary('mandir', mandir, section: 'Directories')
summary('buildtype', get_option('buildtype'), section: 'Build Configuration')
summary('debug', get_option('debug'), section: 'Build Configuration')
+summary('Bash completion', have_bash_completion, section: 'Shell integration')
+
summary('OpenGL', have_gl, section: 'Rendering APIs')
summary('GLES2', have_gles2, section: 'Rendering APIs')
summary('EGL', have_egl, section: 'Rendering APIs')
diff --git a/meson_options.txt b/meson_options.txt
index ec7acc1372..790df935c9 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -225,3 +225,9 @@ option('libdisplay_info',
deprecated: {'true': 'enabled', 'false': 'disabled'},
description: 'Build with or without libdisplay-info'
)
+
+option('bash_completion',
+ type: 'boolean',
+ value: true,
+ description: 'Integrate bash completion for gdctl'
+)
diff --git a/tools/gdctl b/tools/gdctl
index 005e0b0d40..caa670eb9c 100755
--- a/tools/gdctl
+++ b/tools/gdctl
@@ -1,11 +1,13 @@
#!/usr/bin/env python3
import argparse
+import argcomplete
import sys
from dataclasses import dataclass
from gi.repository import GLib, Gio
from enum import Enum, Flag
+from argcomplete.completers import BaseCompleter, SuppressCompleter
NAME = "org.gnome.Mutter.DisplayConfig"
INTERFACE = "org.gnome.Mutter.DisplayConfig"
@@ -1130,6 +1132,70 @@ class GdctlParser(argparse.ArgumentParser):
return namespace
+class MonitorCompleter(BaseCompleter):
+ def __call__(self, **kwargs):
+ try:
+ display_config = DisplayConfig()
+ monitors_state = MonitorsState(display_config)
+ return tuple(monitors_state.monitors)
+ except Exception:
+ return ()
+
+
+class MonitorModeCompleter(BaseCompleter):
+ def __call__(self, parsed_args=None, **kwargs):
+ try:
+ (connector,) = parsed_args._current_sub_group["key"]
+
+ display_config = DisplayConfig()
+ monitors_state = MonitorsState(display_config)
+
+ monitor = monitors_state.monitors[connector]
+ return (mode.name for mode in monitor.modes)
+ except Exception:
+ return ()
+
+
+class ScaleCompleter(BaseCompleter):
+ def __call__(self, parsed_args=None, **kwargs):
+ try:
+ (connector,) = parsed_args._current_sub_group["key"]
+
+ display_config = DisplayConfig()
+ monitors_state = MonitorsState(display_config)
+
+ monitor = monitors_state.monitors[connector]
+
+ mode = parsed_args._current_sub_group.get("mode", None)
+ if not mode:
+ mode = monitor.preferred_mode
+
+ scales = mode.supported_scales
+ scales.sort(key=lambda scale: abs(scale - mode.preferred_scale))
+
+ return (repr(scale) for scale in scales)
+ except Exception:
+ return ()
+
+
+class NamedEnumCompleter(BaseCompleter):
+ def __init__(self, enum_type):
+ self.enum_type = enum_type
+
+ def __call__(self, **kwargs):
+ return (str(enum_value) for enum_value in self.enum_type)
+
+
+class LayoutModeCompleter(NamedEnumCompleter):
+ def __init__(self):
+ super().__init__(LayoutMode)
+
+
+class TransformCompleter(NamedEnumCompleter):
+ def __init__(self):
+ super().__init__(Transform)
+
+
if __name__ == "__main__":
parser = GdctlParser(
description="Display control utility",
@@ -1196,7 +1262,7 @@ if __name__ == "__main__":
choices=[str(layout_mode) for layout_mode in list(LayoutMode)],
type=str,
action=AppendToGlobal,
- )
+ ).completer = LayoutModeCompleter()
set_parser.add_argument(
"-L",
"--logical-monitor",
@@ -1216,8 +1282,9 @@ if __name__ == "__main__":
dest="monitors",
metavar="CONNECTOR",
action=SubGroupAction,
+ nargs=1,
help="Configure monitor",
- )
+ ).completer = MonitorCompleter()
monitor_parser = set_parser.add_argument_group(
"monitor",
"Monitor options (pass after --monitor)",
@@ -1229,7 +1296,7 @@ if __name__ == "__main__":
action=AppendToSubGroup,
help="Monitor mode",
type=str,
- )
+ ).completer = MonitorModeCompleter()
logical_monitor_parser.add_argument(
"--primary",
"-p",
@@ -1245,7 +1312,7 @@ if __name__ == "__main__":
action=AppendToGroup,
help="Logical monitor scale",
type=float,
- )
+ ).completer = ScaleCompleter()
logical_monitor_parser.add_argument(
"--transform",
"-t",
@@ -1253,7 +1320,7 @@ if __name__ == "__main__":
help="Apply viewport transform",
choices=[str(transform) for transform in list(Transform)],
type=str,
- )
+ ).completer = TransformCompleter()
logical_monitor_parser.add_argument(
"--x",
"-x",
@@ -1274,28 +1341,38 @@ if __name__ == "__main__":
metavar="CONNECTOR",
help="Place right of other monitor",
type=str,
- )
+ ).completer = MonitorCompleter()
logical_monitor_parser.add_argument(
"--left-of",
action=AppendToGroup,
metavar="CONNECTOR",
help="Place left of other monitor",
type=str,
- )
+ ).completer = MonitorCompleter()
logical_monitor_parser.add_argument(
"--above",
action=AppendToGroup,
metavar="CONNECTOR",
help="Place above other monitor",
type=str,
- )
+ ).completer = MonitorCompleter()
logical_monitor_parser.add_argument(
"--below",
action=AppendToGroup,
metavar="CONNECTOR",
help="Place below other monitor",
type=str,
- )
+ ).completer = MonitorCompleter()
+
+ for action in [
+ GroupAction,
+ SubGroupAction,
+ AppendToGroup,
+ AppendToSubGroup,
+ AppendToGlobal,
+ ]:
+ argcomplete.safe_actions.add(action)
+ argcomplete.autocomplete(parser, default_completer=SuppressCompleter)
args = parser.parse_args()
diff --git a/tools/meson.build b/tools/meson.build
index 763507c2e9..3aebe47643 100644
--- a/tools/meson.build
+++ b/tools/meson.build
@@ -3,5 +3,30 @@ install_data(
install_dir: bindir,
)
+if have_bash_completion
+ bash_completion = dependency('bash-completion', required: false)
+ if bash_completion.found()
+ bash_completion_dir = bash_completion.get_variable(pkgconfig: 'completionsdir')
+ else
+ bash_completion_dir = get_option('sysconfdir') / 'bash_completion.d'
+ endif
+
+ register_python_argcomplete = find_program('register-python-argcomplete')
+
+ custom_target(
+ 'gdctl-bash-completion',
+ output: 'gdctl',
+ command: [
+ register_python_argcomplete,
+ 'gdctl',
+ '--complete-arguments',
+ '-o nosort',
+ ],
+ capture: true,
+ install_dir: bash_completion_dir,
+ install: true,
+ )
+endif
+
gdctl = find_program('gdctl')
get_state_tool = find_program('get-state.py')
--
2.49.0
From 1e790c31b865cf8fb8c252bad5d32f1a5cdd8f37 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Wed, 29 Jan 2025 21:50:45 +0800
Subject: [PATCH 20/28] gdctl: Make LogicalMonitor a dataclass
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4190>
(cherry picked from commit 4837d1ce64ce0b1a67b269dacc8d12d755f5067f)
---
tools/gdctl | 36 +++++++++++++-----------------------
1 file changed, 13 insertions(+), 23 deletions(-)
diff --git a/tools/gdctl b/tools/gdctl
index caa670eb9c..db59299988 100755
--- a/tools/gdctl
+++ b/tools/gdctl
@@ -4,7 +4,7 @@ import argparse
import argcomplete
import sys
-from dataclasses import dataclass
+from dataclasses import dataclass, field
from gi.repository import GLib, Gio
from enum import Enum, Flag
from argcomplete.completers import BaseCompleter, SuppressCompleter
@@ -288,27 +288,18 @@ class Monitor:
self.display_name = self.properties.get("display-name", None)
+@dataclass
class LogicalMonitor:
- def __init__(
- self,
- monitors,
- scale,
- position=(0, 0),
- transform=Transform.NORMAL,
- is_primary=False,
- properties={},
- args=None,
- ):
- self.position = position
- self.scale = scale
- self.transform = transform
- self.is_primary = is_primary
- self.monitors = monitors
- self.properties = properties
- self.args = args
+ monitors: list[Monitor]
+ scale: float
+ position: tuple[int, int] | None = (0, 0)
+ transform: Transform = Transform.NORMAL
+ is_primary: bool = False
+ properties: dict = field(default_factory=dict)
+ args: dict | None = None
@classmethod
- def new_from_variant(cls, monitors_state, variant):
+ def from_variant(cls, monitors_state, variant):
position = (variant[0], variant[1])
scale = variant[2]
transform = Transform(variant[3])
@@ -682,9 +673,8 @@ def create_logical_monitor(monitors_state, layout_mode, logical_monitor_args):
monitors.append(monitor)
return LogicalMonitor(
- monitors_state,
- monitors,
- scale,
+ monitors=monitors,
+ scale=scale,
is_primary=is_primary,
transform=transform,
position=None,
@@ -833,7 +823,7 @@ class MonitorsState:
def init_logical_monitors(self, current_state):
self.logical_monitors = []
for variant in current_state[2]:
- logical_monitor = LogicalMonitor.new_from_variant(self, variant)
+ logical_monitor = LogicalMonitor.from_variant(self, variant)
self.logical_monitors.append(logical_monitor)
def create_current_config(self):
--
2.49.0
From a5362e425df02fd16a822097196edeff1b9f3faa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Wed, 29 Jan 2025 21:58:00 +0800
Subject: [PATCH 21/28] gdctl: Make Monitor a dataclass
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4190>
(cherry picked from commit 4f218537cd69d71ab4d0cc5017db4bee02261dc8)
---
tools/gdctl | 51 ++++++++++++++++++++++++++++++++++++---------------
1 file changed, 36 insertions(+), 15 deletions(-)
diff --git a/tools/gdctl b/tools/gdctl
index db59299988..bd8df818e4 100755
--- a/tools/gdctl
+++ b/tools/gdctl
@@ -260,32 +260,53 @@ class MonitorMode:
)
+@dataclass
class Monitor:
- def __init__(self, variant):
- self.init_from_variant(variant)
+ connector: str
+ vendor: str
+ product: str
+ display_name: str
+ serial: str
+ modes: list[MonitorMode]
+ properties: dict
+ current_mode: MonitorMode | None
+ preferred_mode: MonitorMode | None
- def init_from_variant(self, variant):
+ @classmethod
+ def from_variant(cls, 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 = [
+ connector = spec[0]
+ vendor = spec[1] if spec[1] != "" else None
+ product = spec[2] if spec[2] != "" else None
+ serial = spec[3] if spec[3] != "" else None
+ modes = [
MonitorMode.from_variant(mode_variant)
for mode_variant in variant[1]
]
- self.properties = translate_properties(variant[2])
+ properties = translate_properties(variant[2])
- self.current_mode = next(
- (mode for mode in self.modes if "is-current" in mode.properties),
+ current_mode = next(
+ (mode for mode in modes if "is-current" in mode.properties),
None,
)
- self.preferred_mode = next(
- (mode for mode in self.modes if "is-preferred" in mode.properties),
+ preferred_mode = next(
+ (mode for mode in modes if "is-preferred" in mode.properties),
None,
)
- self.display_name = self.properties.get("display-name", None)
+ display_name = properties.get("display-name", None)
+
+ return cls(
+ connector=connector,
+ vendor=vendor,
+ product=product,
+ serial=serial,
+ modes=modes,
+ properties=properties,
+ current_mode=current_mode,
+ preferred_mode=preferred_mode,
+ display_name=display_name,
+ )
@dataclass
@@ -817,7 +838,7 @@ class MonitorsState:
def init_monitors(self, current_state):
self.monitors = {}
for monitor_variant in current_state[1]:
- monitor = Monitor(monitor_variant)
+ monitor = Monitor.from_variant(monitor_variant)
self.monitors[monitor.connector] = monitor
def init_logical_monitors(self, current_state):
--
2.49.0
From d44e477deca0cb86213a71e877aa2063ecb47e03 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Thu, 19 Dec 2024 00:44:40 +0100
Subject: [PATCH 22/28] gdctl: Add None friendly named enum helper to create
from string
If the string is None, don't create a named enum instance.
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4192>
(cherry picked from commit 23ab763be03ff28fbbdae9fda79c24821385f93e)
---
tools/gdctl | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/tools/gdctl b/tools/gdctl
index bd8df818e4..5bd5b32360 100755
--- a/tools/gdctl
+++ b/tools/gdctl
@@ -28,6 +28,17 @@ class NamedEnum(Enum):
if string == enum_string
)
+ @classmethod
+ def maybe_from_string(cls, string):
+ if string:
+ return next(
+ enum
+ for enum, enum_string in cls.enum_names()
+ if string == enum_string
+ )
+ else:
+ return None
+
class Transform(NamedEnum):
NORMAL = 0
--
2.49.0
From 578f10d981bd2a61b8028e9059d6ea03027a911e Mon Sep 17 00:00:00 2001
From: Sebastian Wick <sebastian.wick@redhat.com>
Date: Wed, 12 Feb 2025 15:13:44 +0100
Subject: [PATCH 23/28] gdctl: Fix typing and resulting handling of int|None
variables
Introduces two new NamedTuples to deal with dimensions and positions.
The position is special in that x and y can be None. This was previously
wrongly declared to be only int. This commit fixes instances mypy found
where None positions were not handled.
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4267>
(cherry picked from commit 674ebecd00f4ebfcc5262ed9e07dc14a4bf75c24)
---
tools/gdctl | 111 +++++++++++++++++++++++++++++-----------------------
1 file changed, 62 insertions(+), 49 deletions(-)
diff --git a/tools/gdctl b/tools/gdctl
index 5bd5b32360..87bec9e8b4 100755
--- a/tools/gdctl
+++ b/tools/gdctl
@@ -5,7 +5,8 @@ import argcomplete
import sys
from dataclasses import dataclass, field
-from gi.repository import GLib, Gio
+from typing import NamedTuple, Any
+from gi.repository import GLib, Gio # type: ignore
from enum import Enum, Flag
from argcomplete.completers import BaseCompleter, SuppressCompleter
@@ -14,6 +15,16 @@ INTERFACE = "org.gnome.Mutter.DisplayConfig"
OBJECT_PATH = "/org/gnome/Mutter/DisplayConfig"
+class Dimension(NamedTuple):
+ width: int
+ height: int
+
+
+class Position(NamedTuple):
+ x: int | None
+ y: int | None
+
+
class NamedEnum(Enum):
def __str__(self):
return next(
@@ -111,8 +122,7 @@ def print_data(*, level: int, is_last: bool, lines: list[int], data: str):
if level >= 0:
indent = level
- buffer = f"{link:{padding}>{indent * 4}}──{data}"
- buffer = list(buffer)
+ buffer = list(f"{link:{padding}>{indent * 4}}──{data}")
for line in lines:
if line == level:
continue
@@ -120,11 +130,10 @@ def print_data(*, level: int, is_last: bool, lines: list[int], data: str):
if line > 0:
index -= 1
buffer[index] = "│"
- buffer = "".join(buffer)
else:
- buffer = data
+ buffer = list(data)
- print(buffer)
+ print("".join(buffer))
if is_last and level in lines:
lines.remove(level)
@@ -168,7 +177,7 @@ def strip_dbus_error_prefix(message):
return message
-def transform_size(size, transform) -> tuple[int, int]:
+def transform_size(size: Dimension, transform) -> Dimension:
match transform:
case (
Transform.NORMAL
@@ -184,14 +193,14 @@ def transform_size(size, transform) -> tuple[int, int]:
| Transform.ROTATE_270_FLIPPED
):
width, height = size
- return (height, width)
+ return Dimension(height, width)
case _:
raise NotImplementedError
-def scale_size(size, scale) -> tuple[int, int]:
+def scale_size(size: Dimension, scale) -> Dimension:
width, height = size
- return (round(width / scale), round(height / scale))
+ return Dimension(round(width / scale), round(height / scale))
class DisplayConfig:
@@ -252,11 +261,10 @@ class DisplayConfig:
@dataclass
class MonitorMode:
name: str
- resolution: tuple[int, int]
+ resolution: Dimension
refresh_rate: float
preferred_scale: float
supported_scales: list[float]
- refresh_rate: float
properties: dict
@classmethod
@@ -324,11 +332,11 @@ class Monitor:
class LogicalMonitor:
monitors: list[Monitor]
scale: float
- position: tuple[int, int] | None = (0, 0)
+ position: Position = Position(0, 0)
transform: Transform = Transform.NORMAL
is_primary: bool = False
- properties: dict = field(default_factory=dict)
- args: dict | None = None
+ properties: dict[str, Any] = field(default_factory=dict)
+ args: dict[str, Any] = field(default_factory=dict)
@classmethod
def from_variant(cls, monitors_state, variant):
@@ -417,7 +425,7 @@ def place_right_of(
else:
y = None
- logical_monitor.position = (x, y)
+ logical_monitor.position = Position(x, y)
def place_left_of(
@@ -442,7 +450,7 @@ def place_left_of(
else:
y = None
- logical_monitor.position = (x, y)
+ logical_monitor.position = Position(x, y)
def place_below(
@@ -462,9 +470,9 @@ def place_below(
if set_x_position:
x, _ = connector_logical_monitor.position
else:
- x = logical_monitor.position[0]
+ x = logical_monitor.position.x
- logical_monitor.position = (x, y)
+ logical_monitor.position = Position(x, y)
def place_above(
@@ -487,9 +495,9 @@ def place_above(
if set_x_position:
x, _ = connector_logical_monitor.position
else:
- x = logical_monitor.position[0]
+ x = logical_monitor.position.x
- logical_monitor.position = (x, y)
+ logical_monitor.position = Position(x, y)
class PositionType(Flag):
@@ -519,13 +527,13 @@ def calculate_position(
set_y_position = vertical_args == 0
+ x = None
+ y = None
+
if "x" in logical_monitor.args:
x = int(logical_monitor.args["x"])
- if set_y_position:
- y = 0
- else:
- y = None
- logical_monitor.position = (x, y)
+ y = 0 if set_y_position else None
+ logical_monitor.position = Position(x, y)
position_types |= PositionType.ABSOLUTE_X
elif "right_of" in logical_monitor.args:
connector = logical_monitor.args["right_of"]
@@ -556,17 +564,14 @@ def calculate_position(
)
position_types |= PositionType.RELATIVE_X
else:
- logical_monitor.position = (0, 0)
+ logical_monitor.position = Position(0, 0)
set_x_position = horizontal_args == 0
if "y" in logical_monitor.args:
y = int(logical_monitor.args["y"])
- if set_x_position:
- x = 0
- else:
- x = logical_monitor.position[0]
- logical_monitor.position = (x, y)
+ x = 0 if set_x_position else logical_monitor.position.x
+ logical_monitor.position = Position(x, y)
position_types |= PositionType.ABSOLUTE_Y
elif "below" in logical_monitor.args:
connector = logical_monitor.args["below"]
@@ -596,17 +601,19 @@ def calculate_position(
x, y = logical_monitor.position
if not y:
y = 0
- logical_monitor.position = (x, y)
+ logical_monitor.position = Position(x, y)
- assert logical_monitor.position[0] is not None
- assert logical_monitor.position[1] is not None
+ assert logical_monitor.position.x is not None
+ assert logical_monitor.position.y is not None
return position_types
def align_horizontally(logical_monitors: list[LogicalMonitor]):
min_x = min(
- logical_monitor.position[0] for logical_monitor in logical_monitors
+ logical_monitor.position.x
+ for logical_monitor in logical_monitors
+ if logical_monitor.position.x is not None
)
dx = min_x
@@ -615,12 +622,16 @@ def align_horizontally(logical_monitors: list[LogicalMonitor]):
for logical_monitor in logical_monitors:
x, y = logical_monitor.position
- logical_monitor.position = (x - dx, y)
+ logical_monitor.position = Position(
+ x - dx if x is not None else None, y
+ )
def align_vertically(logical_monitors: list[LogicalMonitor]):
min_y = min(
- logical_monitor.position[1] for logical_monitor in logical_monitors
+ logical_monitor.position.y
+ for logical_monitor in logical_monitors
+ if logical_monitor.position.y is not None
)
dy = min_y
@@ -629,7 +640,9 @@ def align_vertically(logical_monitors: list[LogicalMonitor]):
for logical_monitor in logical_monitors:
x, y = logical_monitor.position
- logical_monitor.position = (x, y - dy)
+ logical_monitor.position = Position(
+ x, y - dy if y is not None else None
+ )
def calculate_positions(
@@ -1284,7 +1297,7 @@ if __name__ == "__main__":
choices=[str(layout_mode) for layout_mode in list(LayoutMode)],
type=str,
action=AppendToGlobal,
- ).completer = LayoutModeCompleter()
+ ).completer = LayoutModeCompleter() # type: ignore[attr-defined]
set_parser.add_argument(
"-L",
"--logical-monitor",
@@ -1306,7 +1319,7 @@ if __name__ == "__main__":
action=SubGroupAction,
nargs=1,
help="Configure monitor",
- ).completer = MonitorCompleter()
+ ).completer = MonitorCompleter() # type: ignore[attr-defined]
monitor_parser = set_parser.add_argument_group(
"monitor",
"Monitor options (pass after --monitor)",
@@ -1318,7 +1331,7 @@ if __name__ == "__main__":
action=AppendToSubGroup,
help="Monitor mode",
type=str,
- ).completer = MonitorModeCompleter()
+ ).completer = MonitorModeCompleter() # type: ignore[attr-defined]
logical_monitor_parser.add_argument(
"--primary",
"-p",
@@ -1334,7 +1347,7 @@ if __name__ == "__main__":
action=AppendToGroup,
help="Logical monitor scale",
type=float,
- ).completer = ScaleCompleter()
+ ).completer = ScaleCompleter() # type: ignore[attr-defined]
logical_monitor_parser.add_argument(
"--transform",
"-t",
@@ -1342,7 +1355,7 @@ if __name__ == "__main__":
help="Apply viewport transform",
choices=[str(transform) for transform in list(Transform)],
type=str,
- ).completer = TransformCompleter()
+ ).completer = TransformCompleter() # type: ignore[attr-defined]
logical_monitor_parser.add_argument(
"--x",
"-x",
@@ -1363,28 +1376,28 @@ if __name__ == "__main__":
metavar="CONNECTOR",
help="Place right of other monitor",
type=str,
- ).completer = MonitorCompleter()
+ ).completer = MonitorCompleter() # type: ignore[attr-defined]
logical_monitor_parser.add_argument(
"--left-of",
action=AppendToGroup,
metavar="CONNECTOR",
help="Place left of other monitor",
type=str,
- ).completer = MonitorCompleter()
+ ).completer = MonitorCompleter() # type: ignore[attr-defined]
logical_monitor_parser.add_argument(
"--above",
action=AppendToGroup,
metavar="CONNECTOR",
help="Place above other monitor",
type=str,
- ).completer = MonitorCompleter()
+ ).completer = MonitorCompleter() # type: ignore[attr-defined]
logical_monitor_parser.add_argument(
"--below",
action=AppendToGroup,
metavar="CONNECTOR",
help="Place below other monitor",
type=str,
- ).completer = MonitorCompleter()
+ ).completer = MonitorCompleter() # type: ignore[attr-defined]
for action in [
GroupAction,
@@ -1394,7 +1407,7 @@ if __name__ == "__main__":
AppendToGlobal,
]:
argcomplete.safe_actions.add(action)
- argcomplete.autocomplete(parser, default_completer=SuppressCompleter)
+ argcomplete.autocomplete(parser, default_completer=SuppressCompleter) # type: ignore[arg-type]
args = parser.parse_args()
--
2.49.0
From 50213e90a5ccf409593e61592c25ce7c05558a0f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= <mail@3v1n0.net>
Date: Sat, 15 Feb 2025 16:08:25 +0100
Subject: [PATCH 24/28] tools/gdctl: Make argcomplete optional
While having automatic completions is a very nice feature of gdctl (I
had suggested to use it too :)), it's not something that distros may
have by default and in particular it's not a package in main in ubuntu.
So, make the code less restrictive on completions, since completions is
not a core functionality of the tool and it can definitely work without
them.
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4279>
(cherry picked from commit cfdc63df2ddff1bc5479757189bfc2772251cb40)
---
tools/gdctl | 35 +++++++++++++++++++++++------------
1 file changed, 23 insertions(+), 12 deletions(-)
diff --git a/tools/gdctl b/tools/gdctl
index 87bec9e8b4..ee81970bf3 100755
--- a/tools/gdctl
+++ b/tools/gdctl
@@ -1,14 +1,21 @@
#!/usr/bin/env python3
import argparse
-import argcomplete
import sys
from dataclasses import dataclass, field
-from typing import NamedTuple, Any
+from typing import NamedTuple, Any, Optional
+from types import ModuleType
from gi.repository import GLib, Gio # type: ignore
from enum import Enum, Flag
-from argcomplete.completers import BaseCompleter, SuppressCompleter
+
+argcomplete: Optional[ModuleType] = None
+BaseCompleter: Any
+try:
+ import argcomplete
+ from argcomplete.completers import BaseCompleter
+except ModuleNotFoundError:
+ BaseCompleter = object
NAME = "org.gnome.Mutter.DisplayConfig"
INTERFACE = "org.gnome.Mutter.DisplayConfig"
@@ -1399,15 +1406,19 @@ if __name__ == "__main__":
type=str,
).completer = MonitorCompleter() # type: ignore[attr-defined]
- for action in [
- GroupAction,
- SubGroupAction,
- AppendToGroup,
- AppendToSubGroup,
- AppendToGlobal,
- ]:
- argcomplete.safe_actions.add(action)
- argcomplete.autocomplete(parser, default_completer=SuppressCompleter) # type: ignore[arg-type]
+ if argcomplete:
+ for action in [
+ GroupAction,
+ SubGroupAction,
+ AppendToGroup,
+ AppendToSubGroup,
+ AppendToGlobal,
+ ]:
+ argcomplete.safe_actions.add(action)
+
+ argcomplete.autocomplete(
+ parser, default_completer=argcomplete.SuppressCompleter
+ ) # type: ignore[arg-type]
args = parser.parse_args()
--
2.49.0
From 08a54bd8108b7e4a602845c5669e1b278ac80b7c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Tue, 25 Feb 2025 18:43:35 +0800
Subject: [PATCH 25/28] gdctl: Fix printing Position
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4302>
(cherry picked from commit 414156d0a186febe6fba713cce88cdb353b6d16c)
---
tools/gdctl | 3 +++
1 file changed, 3 insertions(+)
diff --git a/tools/gdctl b/tools/gdctl
index ee81970bf3..42952a29fe 100755
--- a/tools/gdctl
+++ b/tools/gdctl
@@ -31,6 +31,9 @@ class Position(NamedTuple):
x: int | None
y: int | None
+ def __str__(self):
+ return f"({self.x}, {self.y})"
+
class NamedEnum(Enum):
def __str__(self):
--
2.49.0
From 40ff8aa1e0e1fc65cf3d2589e9d12e1f059ff41c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Tue, 25 Feb 2025 18:45:40 +0800
Subject: [PATCH 26/28] gdctl: Add __str__(self) for Dimension too
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4302>
(cherry picked from commit c9c77684184061111a3db2bde822dece8c4f36f1)
---
tools/gdctl | 3 +++
1 file changed, 3 insertions(+)
diff --git a/tools/gdctl b/tools/gdctl
index 42952a29fe..34cd1f6750 100755
--- a/tools/gdctl
+++ b/tools/gdctl
@@ -26,6 +26,9 @@ class Dimension(NamedTuple):
width: int
height: int
+ def __str__(self):
+ return f"{self.width}x{self.height}"
+
class Position(NamedTuple):
x: int | None
--
2.49.0
From fbe4cb34b129ab95eb3175330668aa9bf7b94054 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Tue, 25 Feb 2025 18:46:02 +0800
Subject: [PATCH 27/28] gdctl: Make MonitorMode.dimension an actual Dimension
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4302>
(cherry picked from commit 078ba5ea88d80381c82b423068ad52229da45d6f)
---
tools/gdctl | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tools/gdctl b/tools/gdctl
index 34cd1f6750..ac070842cb 100755
--- a/tools/gdctl
+++ b/tools/gdctl
@@ -284,7 +284,7 @@ class MonitorMode:
def from_variant(cls, variant):
return cls(
name=variant[0],
- resolution=(variant[1], variant[2]),
+ resolution=Dimension(variant[1], variant[2]),
refresh_rate=variant[3],
preferred_scale=variant[4],
supported_scales=variant[5],
--
2.49.0
From 8e41ba56351dc7abcf9ed9cb951491d81117d7fc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Date: Tue, 25 Feb 2025 18:46:24 +0800
Subject: [PATCH 28/28] gdctl: Use Dimenison.__str__() to print mode dimension
Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4302>
(cherry picked from commit 1d88f82d0d9b2b6f4ca0f3710ab1f2c1479830d6)
---
tools/gdctl | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/tools/gdctl b/tools/gdctl
index ac070842cb..855cbc8eda 100755
--- a/tools/gdctl
+++ b/tools/gdctl
@@ -893,12 +893,11 @@ class MonitorsState:
if not show_properties:
return
- width, height = mode.resolution
print_data(
level=3,
is_last=False,
lines=lines,
- data=f"Dimension: {width}x{height}",
+ data=f"Dimension: {mode.resolution}",
)
print_data(
level=3,
--
2.49.0