1762 lines
51 KiB
Python
Executable File
1762 lines
51 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import sys
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import NamedTuple, Any, Optional
|
|
from types import ModuleType
|
|
from gi.repository import GLib, Gio # type: ignore
|
|
from enum import Enum, Flag
|
|
|
|
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"
|
|
OBJECT_PATH = "/org/gnome/Mutter/DisplayConfig"
|
|
|
|
|
|
class Dimension(NamedTuple):
|
|
width: int
|
|
height: int
|
|
|
|
def __str__(self):
|
|
return f"{self.width}x{self.height}"
|
|
|
|
|
|
class Position(NamedTuple):
|
|
x: int | None
|
|
y: int | None
|
|
|
|
def __str__(self):
|
|
return f"({self.x}, {self.y})"
|
|
|
|
|
|
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
|
|
)
|
|
|
|
@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
|
|
ROTATE_90 = 1
|
|
ROTATE_180 = 2
|
|
ROTATE_270 = 3
|
|
FLIPPED = 4
|
|
ROTATE_90_FLIPPED = 5
|
|
ROTATE_270_FLIPPED = 6
|
|
ROTATE_180_FLIPPED = 7
|
|
|
|
@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
|
|
|
|
@classmethod
|
|
def enum_names(cls):
|
|
return [
|
|
(LayoutMode.LOGICAL, "logical"),
|
|
(LayoutMode.PHYSICAL, "physical"),
|
|
]
|
|
|
|
|
|
class ColorMode(NamedEnum):
|
|
DEFAULT = 0
|
|
BT2100 = 1
|
|
|
|
@classmethod
|
|
def enum_names(cls):
|
|
return [
|
|
(ColorMode.DEFAULT, "default"),
|
|
(ColorMode.BT2100, "bt2100"),
|
|
]
|
|
|
|
|
|
class ConfigMethod(Enum):
|
|
VERIFY = 0
|
|
TEMPORARY = 1
|
|
PERSISTENT = 2
|
|
|
|
|
|
def translate_property(name, value):
|
|
enum_properties = {
|
|
"layout-mode": LayoutMode,
|
|
"color-mode": ColorMode,
|
|
"supported-color-modes": ColorMode,
|
|
}
|
|
|
|
if name in enum_properties:
|
|
if isinstance(value, list):
|
|
return [enum_properties[name](element) for element in value]
|
|
else:
|
|
return enum_properties[name](value)
|
|
else:
|
|
return value
|
|
|
|
|
|
def translate_properties(variant):
|
|
return {
|
|
key: translate_property(key, value) for key, value in variant.items()
|
|
}
|
|
|
|
|
|
def print_data(*, level: int, is_last: bool, lines: list[int], data: str):
|
|
if is_last:
|
|
link = "└"
|
|
else:
|
|
link = "├"
|
|
padding = " "
|
|
|
|
if level >= 0:
|
|
indent = level
|
|
buffer = list(f"{link:{padding}>{indent * 4}}──{data}")
|
|
for line in lines:
|
|
if line == level:
|
|
continue
|
|
index = line * 4
|
|
if line > 0:
|
|
index -= 1
|
|
buffer[index] = "│"
|
|
else:
|
|
buffer = list(data)
|
|
|
|
print("".join(buffer))
|
|
|
|
if is_last and level in lines:
|
|
lines.remove(level)
|
|
elif not is_last and level not in lines:
|
|
lines.append(level)
|
|
|
|
|
|
def print_properties(*, level, lines, properties):
|
|
property_keys = list(properties.keys())
|
|
|
|
print_data(
|
|
level=level,
|
|
is_last=True,
|
|
lines=lines,
|
|
data=f"Properties: ({len(property_keys)})",
|
|
)
|
|
for key in property_keys:
|
|
is_last = key == property_keys[-1]
|
|
|
|
value = properties[key]
|
|
if isinstance(value, list):
|
|
elements_string = ", ".join([str(element) for element in value])
|
|
value_string = f"[{elements_string}]"
|
|
elif isinstance(value, bool):
|
|
value_string = "yes" if value else "no"
|
|
else:
|
|
value_string = str(value)
|
|
|
|
print_data(
|
|
level=level + 1,
|
|
is_last=is_last,
|
|
lines=lines,
|
|
data=f"{key} ⇒ {value_string}",
|
|
)
|
|
|
|
|
|
def print_monitor_prefs(
|
|
display_config, monitor, level: int, lines: list[int], is_last: bool
|
|
):
|
|
print_data(
|
|
level=level,
|
|
is_last=is_last,
|
|
lines=lines,
|
|
data="Preferences:",
|
|
)
|
|
|
|
print_data(
|
|
level=level + 1,
|
|
is_last=True,
|
|
lines=lines,
|
|
data="Luminances:",
|
|
)
|
|
|
|
for color_mode in monitor.supported_color_modes:
|
|
(output_luminance, is_unset) = display_config.get_luminance(
|
|
monitor, color_mode
|
|
)
|
|
is_last = color_mode == monitor.supported_color_modes[-1]
|
|
|
|
is_default_string = " (default)" if is_unset else ""
|
|
is_current_string = (
|
|
" (current)" if monitor.color_mode == color_mode else ""
|
|
)
|
|
print_data(
|
|
level=level + 2,
|
|
is_last=is_last,
|
|
lines=lines,
|
|
data=f"{color_mode} ⇒ {output_luminance}{is_default_string}{is_current_string}",
|
|
)
|
|
|
|
|
|
def strip_dbus_error_prefix(message):
|
|
if message.startswith("GDBus.Error"):
|
|
return message.partition(" ")[2]
|
|
else:
|
|
return message
|
|
|
|
|
|
def transform_size(size: Dimension, transform) -> Dimension:
|
|
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 Dimension(height, width)
|
|
case _:
|
|
raise NotImplementedError
|
|
|
|
|
|
def scale_size(size: Dimension, scale) -> Dimension:
|
|
width, height = size
|
|
return Dimension(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})"
|
|
)
|
|
|
|
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
|
|
|
|
def apply_monitors_config(self, config, config_method):
|
|
serial = config.monitors_state.server_serial
|
|
logical_monitors = config.generate_logical_monitor_tuples()
|
|
monitors_for_lease = config.generate_monitors_for_lease_tuples()
|
|
properties = {}
|
|
|
|
if monitors_state.supports_changing_layout_mode:
|
|
properties["layout-mode"] = GLib.Variant(
|
|
"u", config.layout_mode.value
|
|
)
|
|
|
|
if monitors_for_lease:
|
|
properties["monitors-for-lease"] = GLib.Variant(
|
|
"a(ssss)", monitors_for_lease
|
|
)
|
|
|
|
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,
|
|
)
|
|
|
|
def get_luminance(self, monitor, color_mode) -> tuple[float, bool]:
|
|
variant = self._proxy.get_cached_property("Luminance")
|
|
|
|
luminance_entry = next(
|
|
entry
|
|
for entry in variant
|
|
if entry["connector"] == monitor.connector
|
|
and ColorMode(entry["color-mode"]) == color_mode
|
|
)
|
|
output_luminance = luminance_entry["luminance"]
|
|
is_unset = luminance_entry["is-unset"]
|
|
|
|
return (output_luminance, is_unset)
|
|
|
|
def set_luminance(self, monitor, color_mode, luminance):
|
|
parameters = GLib.Variant(
|
|
"(sud)",
|
|
(
|
|
monitor.connector,
|
|
color_mode.value,
|
|
luminance,
|
|
),
|
|
)
|
|
self._proxy.call_sync(
|
|
method_name="SetLuminance",
|
|
parameters=parameters,
|
|
flags=Gio.DBusCallFlags.NO_AUTO_START,
|
|
timeout_msec=-1,
|
|
cancellable=None,
|
|
)
|
|
|
|
def reset_luminance(self, monitor, color_mode):
|
|
parameters = GLib.Variant(
|
|
"(su)",
|
|
(monitor.connector, color_mode.value),
|
|
)
|
|
self._proxy.call_sync(
|
|
method_name="ResetLuminance",
|
|
parameters=parameters,
|
|
flags=Gio.DBusCallFlags.NO_AUTO_START,
|
|
timeout_msec=-1,
|
|
cancellable=None,
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class MonitorMode:
|
|
name: str
|
|
resolution: Dimension
|
|
refresh_rate: float
|
|
preferred_scale: float
|
|
supported_scales: list[float]
|
|
properties: dict
|
|
|
|
@classmethod
|
|
def from_variant(cls, variant):
|
|
return cls(
|
|
name=variant[0],
|
|
resolution=Dimension(variant[1], variant[2]),
|
|
refresh_rate=variant[3],
|
|
preferred_scale=variant[4],
|
|
supported_scales=variant[5],
|
|
properties=translate_properties(variant[6]),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class Monitor:
|
|
connector: str
|
|
vendor: str
|
|
product: str
|
|
display_name: str
|
|
serial: str
|
|
modes: list[MonitorMode]
|
|
properties: dict
|
|
current_mode: MonitorMode | None
|
|
preferred_mode: MonitorMode | None
|
|
color_mode: ColorMode | None
|
|
supported_color_modes: list[ColorMode]
|
|
|
|
@classmethod
|
|
def from_variant(cls, variant):
|
|
spec = variant[0]
|
|
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]
|
|
]
|
|
properties = translate_properties(variant[2])
|
|
|
|
current_mode = next(
|
|
(mode for mode in modes if "is-current" in mode.properties),
|
|
None,
|
|
)
|
|
preferred_mode = next(
|
|
(mode for mode in modes if "is-preferred" in mode.properties),
|
|
None,
|
|
)
|
|
|
|
display_name = properties.get("display-name", None)
|
|
color_mode = properties.get("color-mode", None)
|
|
supported_color_modes = properties.get("supported-color-modes")
|
|
|
|
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,
|
|
color_mode=color_mode,
|
|
supported_color_modes=supported_color_modes,
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class LogicalMonitor:
|
|
monitors: list[Monitor]
|
|
scale: float
|
|
position: Position = Position(0, 0)
|
|
transform: Transform = Transform.NORMAL
|
|
is_primary: bool = False
|
|
properties: dict[str, Any] = field(default_factory=dict)
|
|
args: dict[str, Any] = field(default_factory=dict)
|
|
|
|
@classmethod
|
|
def from_variant(cls, monitors_state, variant):
|
|
position = (variant[0], variant[1])
|
|
scale = variant[2]
|
|
transform = Transform(variant[3])
|
|
is_primary = variant[4]
|
|
connectors = [connector for connector, _, _, _ in variant[5]]
|
|
monitors = [
|
|
monitors_state.monitors[connector] for connector in connectors
|
|
]
|
|
properties = translate_properties(variant[6])
|
|
|
|
return cls(
|
|
monitors=monitors,
|
|
position=position,
|
|
scale=scale,
|
|
transform=transform,
|
|
is_primary=is_primary,
|
|
properties=properties,
|
|
)
|
|
|
|
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 = 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 = 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.x
|
|
|
|
logical_monitor.position = 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.x
|
|
|
|
logical_monitor.position = 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
|
|
|
|
x = None
|
|
y = None
|
|
|
|
if "x" in logical_monitor.args:
|
|
x = int(logical_monitor.args["x"])
|
|
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"]
|
|
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 = Position(0, 0)
|
|
|
|
set_x_position = horizontal_args == 0
|
|
|
|
if "y" in logical_monitor.args:
|
|
y = int(logical_monitor.args["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"]
|
|
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 = Position(x, y)
|
|
|
|
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.x
|
|
for logical_monitor in logical_monitors
|
|
if logical_monitor.position.x is not None
|
|
)
|
|
|
|
dx = min_x
|
|
if dx == 0:
|
|
return
|
|
|
|
for logical_monitor in logical_monitors:
|
|
x, y = logical_monitor.position
|
|
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.y
|
|
for logical_monitor in logical_monitors
|
|
if logical_monitor.position.y is not None
|
|
)
|
|
|
|
dy = min_y
|
|
if dy == 0:
|
|
return
|
|
|
|
for logical_monitor in logical_monitors:
|
|
x, y = logical_monitor.position
|
|
logical_monitor.position = Position(
|
|
x, y - dy if y is not None else None
|
|
)
|
|
|
|
|
|
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
|
|
monitor.color_mode = ColorMode.maybe_from_string(
|
|
monitor_args.get("color_mode", None)
|
|
)
|
|
|
|
monitors.append(monitor)
|
|
|
|
return LogicalMonitor(
|
|
monitors=monitors,
|
|
scale=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
|
|
|
|
monitors_for_lease = []
|
|
for connector in args.monitors_for_lease:
|
|
monitors_for_lease.append(monitors_state.monitors[connector])
|
|
|
|
calculate_positions(logical_monitors, layout_mode, monitor_mappings)
|
|
|
|
return Config(
|
|
monitors_state, logical_monitors, layout_mode, monitors_for_lease
|
|
)
|
|
|
|
|
|
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=False,
|
|
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=not monitor.color_mode,
|
|
lines=lines,
|
|
data=f"Mode: {monitor.mode.name}",
|
|
)
|
|
if monitor.color_mode:
|
|
print_data(
|
|
level=4,
|
|
is_last=True,
|
|
lines=lines,
|
|
data=f"Color mode: {monitor.color_mode}",
|
|
)
|
|
|
|
index += 1
|
|
|
|
print_data(
|
|
level=0,
|
|
is_last=True,
|
|
lines=lines,
|
|
data=f"Monitors for lease ({len(config.monitors_for_lease)})",
|
|
)
|
|
|
|
for monitor in config.monitors_for_lease:
|
|
is_last = monitor == config.monitors_for_lease[-1]
|
|
print_data(
|
|
level=1,
|
|
is_last=is_last,
|
|
lines=lines,
|
|
data=f"Monitor {monitor.connector} ({monitor.display_name})",
|
|
)
|
|
|
|
|
|
class MonitorsState:
|
|
def __init__(self, display_config):
|
|
current_state = display_config.get_current_state()
|
|
|
|
self.display_config = display_config
|
|
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)
|
|
|
|
def init_monitors(self, current_state):
|
|
self.monitors = {}
|
|
for monitor_variant in current_state[1]:
|
|
monitor = Monitor.from_variant(monitor_variant)
|
|
self.monitors[monitor.connector] = monitor
|
|
|
|
def init_logical_monitors(self, current_state):
|
|
self.logical_monitors = []
|
|
for variant in current_state[2]:
|
|
logical_monitor = LogicalMonitor.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}")
|
|
|
|
if not show_properties:
|
|
return
|
|
|
|
print_data(
|
|
level=3,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"Dimension: {mode.resolution}",
|
|
)
|
|
print_data(
|
|
level=3,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"Refresh rate: {mode.refresh_rate:.3f}",
|
|
)
|
|
print_data(
|
|
level=3,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"Preferred scale: {mode.preferred_scale}",
|
|
)
|
|
print_data(
|
|
level=3,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"Supported scales: {mode.supported_scales}",
|
|
)
|
|
|
|
if show_properties:
|
|
mode_properties = mode.properties
|
|
print_properties(level=3, lines=lines, properties=mode_properties)
|
|
|
|
def print_current_state(self, show_modes=False, show_properties=False):
|
|
print("Monitors:")
|
|
lines = []
|
|
monitors = list(self.monitors.values())
|
|
for monitor in monitors:
|
|
is_last = monitor == monitors[-1]
|
|
modes = monitor.modes
|
|
properties = monitor.properties
|
|
|
|
if monitor.display_name:
|
|
monitor_title = (
|
|
f"Monitor {monitor.connector} ({monitor.display_name})"
|
|
)
|
|
else:
|
|
monitor_title = f"Monitor {monitor.connector}"
|
|
|
|
print_data(
|
|
level=0,
|
|
is_last=is_last,
|
|
lines=lines,
|
|
data=monitor_title,
|
|
)
|
|
|
|
if monitor.vendor:
|
|
print_data(
|
|
level=1,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"Vendor: {monitor.vendor}",
|
|
)
|
|
if monitor.product:
|
|
print_data(
|
|
level=1,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"Product: {monitor.product}",
|
|
)
|
|
if monitor.serial:
|
|
print_data(
|
|
level=1,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"Serial: {monitor.serial}",
|
|
)
|
|
|
|
if show_modes:
|
|
print_data(
|
|
level=1,
|
|
is_last=not show_properties,
|
|
lines=lines,
|
|
data=f"Modes ({len(modes)})",
|
|
)
|
|
for mode in modes:
|
|
is_last = mode == modes[-1]
|
|
self.print_mode(mode, is_last, show_properties, lines)
|
|
else:
|
|
mode = next(
|
|
(mode for mode in modes if "is-current" in mode.properties),
|
|
None,
|
|
)
|
|
if mode:
|
|
mode_type = "Current"
|
|
else:
|
|
mode = next(
|
|
(
|
|
mode
|
|
for mode in modes
|
|
if "is-preferred" in mode.properties
|
|
),
|
|
None,
|
|
)
|
|
if mode:
|
|
mode_type = "Preferred"
|
|
|
|
if mode:
|
|
print_data(
|
|
level=1,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"{mode_type} mode",
|
|
)
|
|
self.print_mode(mode, True, show_properties, lines)
|
|
|
|
print_monitor_prefs(
|
|
self.display_config,
|
|
monitor,
|
|
level=1,
|
|
lines=lines,
|
|
is_last=not show_properties,
|
|
)
|
|
|
|
if show_properties:
|
|
print_properties(level=1, lines=lines, properties=properties)
|
|
|
|
print()
|
|
print("Logical monitors:")
|
|
index = 1
|
|
for logical_monitor in self.logical_monitors:
|
|
is_last = logical_monitor == self.logical_monitors[-1]
|
|
print_data(
|
|
level=0,
|
|
is_last=is_last,
|
|
lines=lines,
|
|
data=f"Logical monitor #{index}",
|
|
)
|
|
(x, y) = logical_monitor.position
|
|
print_data(
|
|
level=1,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"Position: ({x}, {y})",
|
|
)
|
|
print_data(
|
|
level=1,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"Scale: {logical_monitor.scale}",
|
|
)
|
|
print_data(
|
|
level=1,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"Transform: {logical_monitor.transform}",
|
|
)
|
|
print_data(
|
|
level=1,
|
|
is_last=False,
|
|
lines=lines,
|
|
data=f"Primary: {'yes' if logical_monitor.is_primary else 'no'}",
|
|
)
|
|
monitors = logical_monitor.monitors
|
|
print_data(
|
|
level=1,
|
|
is_last=not show_properties,
|
|
lines=lines,
|
|
data=f"Monitors: ({len(monitors)})",
|
|
)
|
|
for monitor in monitors:
|
|
is_last = monitor == monitors[-1]
|
|
|
|
if monitor.display_name:
|
|
monitor_title = (
|
|
f"{monitor.connector} ({monitor.display_name})"
|
|
)
|
|
else:
|
|
monitor_title = f"{monitor.connector}"
|
|
|
|
print_data(
|
|
level=2,
|
|
is_last=is_last,
|
|
lines=lines,
|
|
data=monitor_title,
|
|
)
|
|
|
|
if show_properties:
|
|
properties = logical_monitor.properties
|
|
print_properties(level=1, lines=lines, properties=properties)
|
|
|
|
index += 1
|
|
|
|
if show_properties:
|
|
properties = self.properties
|
|
print()
|
|
print_properties(level=-1, lines=lines, properties=properties)
|
|
|
|
|
|
@dataclass
|
|
class Config:
|
|
monitors_state: MonitorsState
|
|
logical_monitors: list[LogicalMonitor]
|
|
layout_mode: LayoutMode
|
|
monitors_for_lease: Monitor
|
|
|
|
def generate_monitor_tuples(self, monitors):
|
|
tuples = []
|
|
for monitor in monitors:
|
|
options = {}
|
|
if monitor.color_mode:
|
|
options["color-mode"] = GLib.Variant(
|
|
"u", monitor.color_mode.value
|
|
)
|
|
|
|
# Variant type: (ssa{sv})
|
|
tuples.append(
|
|
(
|
|
monitor.connector,
|
|
monitor.mode.name,
|
|
options,
|
|
)
|
|
)
|
|
return tuples
|
|
|
|
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
|
|
|
|
def generate_monitors_for_lease_tuples(self):
|
|
tuples = []
|
|
for monitor in self.monitors_for_lease:
|
|
tuples.append(
|
|
(
|
|
monitor.connector,
|
|
monitor.vendor,
|
|
monitor.product,
|
|
monitor.serial,
|
|
)
|
|
)
|
|
return tuples
|
|
|
|
|
|
class GroupAction(argparse.Action):
|
|
def __call__(self, parser, namespace, values, option_string=None):
|
|
if len(values) == 1:
|
|
(value,) = values
|
|
namespace._current_group = {
|
|
"key": value,
|
|
}
|
|
else:
|
|
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
|
|
|
|
|
|
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)
|
|
|
|
|
|
class ColorModeCompleter(NamedEnumCompleter):
|
|
def __init__(self):
|
|
super().__init__(ColorMode)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = GdctlParser(
|
|
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",
|
|
)
|
|
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,
|
|
).completer = LayoutModeCompleter() # type: ignore[attr-defined]
|
|
set_parser.add_argument(
|
|
"-L",
|
|
"--logical-monitor",
|
|
dest="logical_monitors",
|
|
action=GroupAction,
|
|
nargs=0,
|
|
default=[],
|
|
)
|
|
set_parser.add_argument(
|
|
"-e",
|
|
"--for-lease-monitor",
|
|
dest="monitors_for_lease",
|
|
action="append",
|
|
type=str,
|
|
default=[],
|
|
).completer = MonitorCompleter() # type: ignore[attr-defined]
|
|
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,
|
|
nargs=1,
|
|
help="Configure monitor",
|
|
).completer = MonitorCompleter() # type: ignore[attr-defined]
|
|
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,
|
|
).completer = MonitorModeCompleter() # type: ignore[attr-defined]
|
|
monitor_parser.add_argument(
|
|
"--color-mode",
|
|
"-c",
|
|
action=AppendToSubGroup,
|
|
help="Color mode",
|
|
choices=[str(color_mode) for color_mode in list(ColorMode)],
|
|
type=str,
|
|
).completer = ColorModeCompleter() # type: ignore[attr-defined]
|
|
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,
|
|
).completer = ScaleCompleter() # type: ignore[attr-defined]
|
|
logical_monitor_parser.add_argument(
|
|
"--transform",
|
|
"-t",
|
|
action=AppendToGroup,
|
|
help="Apply viewport transform",
|
|
choices=[str(transform) for transform in list(Transform)],
|
|
type=str,
|
|
).completer = TransformCompleter() # type: ignore[attr-defined]
|
|
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,
|
|
).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() # type: ignore[attr-defined]
|
|
logical_monitor_parser.add_argument(
|
|
"--above",
|
|
action=AppendToGroup,
|
|
metavar="CONNECTOR",
|
|
help="Place above other monitor",
|
|
type=str,
|
|
).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() # type: ignore[attr-defined]
|
|
|
|
prefs_parser = subparser.add_parser(
|
|
"prefs",
|
|
help="Set display preferences",
|
|
)
|
|
prefs_parser.add_argument(
|
|
"-M",
|
|
"--monitor",
|
|
dest="monitors",
|
|
metavar="CONNECTOR",
|
|
action=GroupAction,
|
|
nargs=1,
|
|
default=[],
|
|
help="Change monitor preferences",
|
|
).completer = MonitorCompleter() # type: ignore[attr-defined]
|
|
monitor_prefs_parser = prefs_parser.add_argument_group(
|
|
"monitor",
|
|
"Monitor preferences (pass after --monitor)",
|
|
argument_default=argparse.SUPPRESS,
|
|
)
|
|
monitor_prefs_parser.add_argument(
|
|
"-l",
|
|
"--luminance",
|
|
action=AppendToGroup,
|
|
type=float,
|
|
nargs=1,
|
|
)
|
|
monitor_prefs_parser.add_argument(
|
|
"--reset-luminance",
|
|
action=AppendToGroup,
|
|
type=bool,
|
|
const=True,
|
|
nargs=0,
|
|
)
|
|
|
|
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()
|
|
|
|
match args.command:
|
|
case "show":
|
|
try:
|
|
display_config = DisplayConfig()
|
|
monitors_state = MonitorsState(display_config)
|
|
except GLib.Error as e:
|
|
if e.domain == GLib.quark_to_string(Gio.DBusError.quark()):
|
|
error_message = strip_dbus_error_prefix(e.message)
|
|
print(
|
|
f"Failed to 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,
|
|
)
|
|
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 to 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)
|
|
case "prefs":
|
|
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 to retrieve current state: {error_message}",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
|
|
for monitor_prefs in args.monitors:
|
|
connector = monitor_prefs["key"]
|
|
|
|
if (
|
|
"luminance" in monitor_prefs
|
|
and "reset_luminance" in monitor_prefs
|
|
):
|
|
print(
|
|
"Cannot both set and reset luminance",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
|
|
if connector not in monitors_state.monitors:
|
|
print(
|
|
f"Monitor with connector {connector} not found",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
|
|
monitor = monitors_state.monitors[connector]
|
|
|
|
if "luminance" in monitor_prefs:
|
|
(luminance,) = monitor_prefs["luminance"]
|
|
display_config.set_luminance(
|
|
monitor, monitor.color_mode, luminance
|
|
)
|
|
elif "reset_luminance" in monitor_prefs:
|
|
display_config.reset_luminance(monitor, monitor.color_mode)
|