Massive refactor (Zed wouldn't stop complaining about formatting) and
beginning of proper GNOME implementation.
This commit is contained in:
@@ -1,16 +1,20 @@
|
||||
from .abstracts import AbstractDisplay
|
||||
from .enums import DisplayManagerDriver, Error
|
||||
|
||||
def get_display(requested_driver: DisplayManagerDriver = DisplayManagerDriver.INFER) -> AbstractDisplay:
|
||||
|
||||
def get_display(
|
||||
requested_driver: DisplayManagerDriver = DisplayManagerDriver.INFER,
|
||||
) -> AbstractDisplay:
|
||||
match requested_driver:
|
||||
case DisplayManagerDriver.INFER:
|
||||
for display in DisplayManagerDriver.get_display_list():
|
||||
if display.meta.detect(): return display.meta()
|
||||
|
||||
if display.meta.detect():
|
||||
return display.meta()
|
||||
|
||||
raise RuntimeError(Error.DISPLAY_INFER_TYPE_FAILURE)
|
||||
|
||||
|
||||
case _ if requested_driver in DisplayManagerDriver.get_display_list():
|
||||
return requested_driver.meta()
|
||||
|
||||
|
||||
case _:
|
||||
raise ValueError(Error.DISPLAY_TYPE_UNKNOWN)
|
||||
raise ValueError(Error.DISPLAY_TYPE_UNKNOWN)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from .structs import MonitorInfo, Modeline
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
|
||||
from .structs import Modeline, MonitorInfo
|
||||
|
||||
|
||||
class AbstractDisplay(ABC):
|
||||
@abstractmethod
|
||||
@@ -12,15 +14,14 @@ class AbstractDisplay(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def save_current_mode(self, location: str) -> None:
|
||||
def save_current_mode(self, location: str | Path) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def read_saved_mode(self, location: str) -> Modeline:
|
||||
def read_saved_mode(self, location: str | Path) -> Modeline:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def detect(self) -> bool:
|
||||
def detect() -> bool:
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from .abstracts import AbstractDisplay
|
||||
|
||||
from . import gnome, plasma, wlroots
|
||||
|
||||
from enum import Enum, StrEnum
|
||||
from typing import Optional, Type
|
||||
|
||||
from . import gnome, plasma, wlroots
|
||||
from .abstracts import AbstractDisplay
|
||||
|
||||
|
||||
class MetaMixin(str):
|
||||
def __new__(cls, text: str, meta: AbstractDisplay):
|
||||
obj = str.__new__(cls, text)
|
||||
@@ -12,6 +12,7 @@ class MetaMixin(str):
|
||||
obj.meta = meta
|
||||
return obj
|
||||
|
||||
|
||||
class DisplayManagerDriver(MetaMixin, Enum):
|
||||
GNOME = ("GNOME (Mutter)", gnome.Display)
|
||||
PLASMA = ("KDE Plasma 5/6 (KWin)", plasma.Display)
|
||||
@@ -22,7 +23,10 @@ class DisplayManagerDriver(MetaMixin, Enum):
|
||||
def get_display_list(cls):
|
||||
return [display for display in cls if display.meta is not None]
|
||||
|
||||
|
||||
class Error(StrEnum):
|
||||
DISPLAY_TYPE_NOT_IMPL = "Requested manager type has not been implemented!"
|
||||
DISPLAY_TYPE_UNKNOWN = "Requested manager type does not exist."
|
||||
DISPLAY_INFER_TYPE_FAILURE = "Could not infer a compatible display manager from supported list."
|
||||
DISPLAY_INFER_TYPE_FAILURE = (
|
||||
"Could not infer a compatible display manager from supported list."
|
||||
)
|
||||
|
||||
@@ -1,45 +1,67 @@
|
||||
from ..structs import MonitorInfo, Modeline
|
||||
from ..abstracts import AbstractDisplay
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
### Dirty Implementation ###
|
||||
# Needs to be replaced with a proper dbus solution. Preferably by taking reference from gdctl. `cat /usr/bin/gdctl` #
|
||||
# Issues:
|
||||
# - Only works with one monitor (will lead to strange results if multiple are present).
|
||||
# - Possible has 1 quadrillion issues that aren't known.
|
||||
# - Sucks dog eggs and unironically uses the unholy trinity `os`, `re`, and `subprocess`.
|
||||
from ..abstracts import AbstractDisplay
|
||||
from ..structs import Modeline, MonitorInfo
|
||||
from ._gnome_types import GNOMEDisplayConfig
|
||||
|
||||
MODE_REGEX_PATTERN = re.compile(r'(\d+)x(\d+)@(\d+\.\d+)')
|
||||
# Half replaced implementation. More refactoring needs to be done in regards to setting the display.
|
||||
# Everything will be replaced with dorkbus Mutter stuff.
|
||||
|
||||
MODE_REGEX_PATTERN = re.compile(r"(\d+)x(\d+)@(\d+\.\d+)")
|
||||
|
||||
class Display(AbstractDisplay):
|
||||
def get_modes(self):
|
||||
gdctl_output = subprocess.run(["gdctl", "show", "--modes"], capture_output=True, text=True, check=True)
|
||||
modes = MODE_REGEX_PATTERN.findall(gdctl_output.stdout)
|
||||
mutter_display_state = GNOMEDisplayConfig()
|
||||
mutter_display_state.get_dbus_info()
|
||||
|
||||
# Hard coding my beloved
|
||||
return [
|
||||
MonitorInfo(
|
||||
connector = "HDMI-2",
|
||||
primary = True,
|
||||
modes = [
|
||||
Modeline(
|
||||
width = int(w),
|
||||
height = int(h),
|
||||
refresh_rate = float(r)
|
||||
)
|
||||
for _, (w, h, r) in enumerate(modes)
|
||||
]
|
||||
primary_connectors = {
|
||||
m[0]
|
||||
for lm in mutter_display_state.logical_monitors
|
||||
if lm.is_primary
|
||||
for m in lm.monitors
|
||||
}
|
||||
|
||||
result = []
|
||||
for monitor in mutter_display_state.monitors:
|
||||
modes = [
|
||||
Modeline(
|
||||
mode.width,
|
||||
mode.height,
|
||||
mode.refresh_rate,
|
||||
)
|
||||
for mode in monitor.modes
|
||||
]
|
||||
|
||||
result.append(
|
||||
MonitorInfo(
|
||||
monitor.connector,
|
||||
monitor.connector in primary_connectors,
|
||||
modes
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
def set_mode(self, modeline, requested_connector = None) -> None:
|
||||
subprocess.run(["gdctl", "set", "--logical-monitor", "--primary", "--monitor", requested_connector, "--mode", str(modeline)])
|
||||
|
||||
return result
|
||||
|
||||
def set_mode(self, modeline, requested_connector=None) -> None:
|
||||
subprocess.run(
|
||||
[
|
||||
"gdctl",
|
||||
"set",
|
||||
"--logical-monitor",
|
||||
"--primary",
|
||||
"--monitor",
|
||||
requested_connector,
|
||||
"--mode",
|
||||
str(modeline),
|
||||
]
|
||||
)
|
||||
|
||||
def save_current_mode(self, location):
|
||||
gdctl_output = subprocess.run(["gdctl", "show"], capture_output=True, text=True, check=True)
|
||||
gdctl_output = subprocess.run(
|
||||
["gdctl", "show"], capture_output=True, text=True, check=True
|
||||
)
|
||||
match = MODE_REGEX_PATTERN.search(gdctl_output.stdout)
|
||||
w, h, r = match.groups()
|
||||
with open(location, "w") as file:
|
||||
@@ -49,16 +71,11 @@ class Display(AbstractDisplay):
|
||||
with open(location, "r") as file:
|
||||
config_file_content = file.read()
|
||||
|
||||
|
||||
match = MODE_REGEX_PATTERN.search(config_file_content)
|
||||
|
||||
w, h, r = match.groups()
|
||||
return Modeline(
|
||||
width = int(w),
|
||||
height = int(h),
|
||||
refresh_rate = float(r)
|
||||
)
|
||||
return Modeline(width=int(w), height=int(h), refresh_rate=float(r))
|
||||
|
||||
@staticmethod
|
||||
def detect():
|
||||
return "gnome" in os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
|
||||
return "gnome" in os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
# This is going to need some blast zone.
|
||||
|
||||
@dataclass
|
||||
class _GNOMEDisplayMode:
|
||||
id: str
|
||||
width: int
|
||||
height: int
|
||||
refresh_rate: float
|
||||
preferred_scale: float
|
||||
supported_scales: list[float]
|
||||
|
||||
@dataclass
|
||||
class _GNOMEMonitor:
|
||||
connector: str
|
||||
vendor: str
|
||||
product: str
|
||||
serial: str
|
||||
modes: list[_GNOMEDisplayMode]
|
||||
properties: dict[str, Any]
|
||||
|
||||
@dataclass
|
||||
class _GNOMELogicalMonitor:
|
||||
x: int
|
||||
y: int
|
||||
scale: float
|
||||
transform: int
|
||||
is_primary: bool
|
||||
monitors: list[tuple[str, str, str, str]]
|
||||
properties: dict[str, Any]
|
||||
|
||||
@dataclass
|
||||
class GNOMEDisplayConfig:
|
||||
serial: int = 0
|
||||
monitors: list[_GNOMEMonitor] = field(default_factory=list)
|
||||
logical_monitors: list[_GNOMELogicalMonitor] = field(default_factory=list)
|
||||
global_properties: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def get_dbus_info(self):
|
||||
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||
|
||||
# Some beautiful dorkbus
|
||||
res = bus.call_sync(
|
||||
"org.gnome.Mutter.DisplayConfig",
|
||||
"/org/gnome/Mutter/DisplayConfig",
|
||||
"org.gnome.Mutter.DisplayConfig",
|
||||
"GetCurrentState",
|
||||
None,
|
||||
None,
|
||||
Gio.DBusCallFlags.NONE,
|
||||
-1,
|
||||
None
|
||||
)
|
||||
|
||||
# Serial, Monitors, Logical Monitors, Properties
|
||||
dbus_serial, monitors, logical_monitors, global_props = res.unpack()
|
||||
|
||||
self.serial = dbus_serial
|
||||
self.global_properties = global_props
|
||||
|
||||
self.logical_monitors = [
|
||||
_GNOMELogicalMonitor(
|
||||
x=lm[0],
|
||||
y=lm[1],
|
||||
scale=lm[2],
|
||||
transform=lm[3],
|
||||
is_primary=lm[4],
|
||||
monitors=lm[5],
|
||||
properties=lm[6],
|
||||
)
|
||||
for lm in logical_monitors
|
||||
]
|
||||
|
||||
self.monitors = []
|
||||
for info, modes_data, monitor_props in monitors:
|
||||
connector, vendor, product, monitor_serial = info
|
||||
|
||||
modes = [
|
||||
_GNOMEDisplayMode(
|
||||
id=m[0],
|
||||
width=m[1],
|
||||
height=m[2],
|
||||
refresh_rate=m[3],
|
||||
preferred_scale=m[4],
|
||||
supported_scales=m[5],
|
||||
)
|
||||
for m in modes_data
|
||||
]
|
||||
|
||||
monitor = _GNOMEMonitor(
|
||||
connector=connector,
|
||||
vendor=vendor,
|
||||
product=product,
|
||||
serial=monitor_serial,
|
||||
modes=modes,
|
||||
properties=monitor_props,
|
||||
)
|
||||
self.monitors.append(monitor)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
|
||||
from ..abstracts import AbstractDisplay
|
||||
|
||||
import os
|
||||
|
||||
class Display(AbstractDisplay):
|
||||
def get_modes(self):
|
||||
@@ -11,4 +12,4 @@ class Display(AbstractDisplay):
|
||||
|
||||
@staticmethod
|
||||
def detect():
|
||||
return "plasma" in os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
|
||||
return "plasma" in os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Modeline:
|
||||
width: int
|
||||
height: int
|
||||
refresh_rate: float
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.width}x{self.height}@{self.refresh_rate:.3f}"
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
|
||||
@dataclass
|
||||
class MonitorInfo:
|
||||
connector: str
|
||||
primary: bool
|
||||
modes: list[Modeline]
|
||||
|
||||
|
||||
@@ -1,46 +1,58 @@
|
||||
from ..structs import MonitorInfo, Modeline
|
||||
from ..abstracts import AbstractDisplay
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
from ..abstracts import AbstractDisplay
|
||||
from ..structs import Modeline, MonitorInfo
|
||||
|
||||
# Matches "1920x1080 @ 60.000 Hz" or "1920x1080@60.000"
|
||||
MODE_REGEX = re.compile(r'(\d+)x(\d+)@(\d+\.\d+)')
|
||||
MODE_REGEX = re.compile(r"(\d+)x(\d+)@(\d+\.\d+)")
|
||||
|
||||
|
||||
class Display(AbstractDisplay):
|
||||
def get_modes(self):
|
||||
# wlr-randr --json is the 'civilized' way to do this on Trixie
|
||||
result = subprocess.run(["wlr-randr", "--json"], capture_output=True, text=True, check=True)
|
||||
result = subprocess.run(
|
||||
["wlr-randr", "--json"], capture_output=True, text=True, check=True
|
||||
)
|
||||
data = json.loads(result.stdout)
|
||||
|
||||
|
||||
monitors = []
|
||||
for output in data:
|
||||
modes = []
|
||||
for m in output.get("modes", []):
|
||||
modes.append(Modeline(
|
||||
width=int(m["width"]),
|
||||
height=int(m["height"]),
|
||||
refresh_rate=float(m["refresh"])
|
||||
))
|
||||
|
||||
monitors.append(MonitorInfo(
|
||||
connector=output["name"], # Usually 'HEADLESS-1' in Cage
|
||||
primary=output.get("focused", False),
|
||||
modes=modes
|
||||
))
|
||||
modes.append(
|
||||
Modeline(
|
||||
width=int(m["width"]),
|
||||
height=int(m["height"]),
|
||||
refresh_rate=float(m["refresh"]),
|
||||
)
|
||||
)
|
||||
|
||||
monitors.append(
|
||||
MonitorInfo(
|
||||
connector=output["name"], # Usually 'HEADLESS-1' in Cage
|
||||
primary=output.get("focused", False),
|
||||
modes=modes,
|
||||
)
|
||||
)
|
||||
return monitors
|
||||
|
||||
|
||||
def set_mode(self, modeline, requested_connector="HEADLESS-1") -> None:
|
||||
# Format: 1920x1080@60Hz
|
||||
mode_str = f"{modeline.width}x{modeline.height}@{modeline.refresh_rate}Hz"
|
||||
subprocess.run(["wlr-randr", "--output", requested_connector, "--mode", mode_str], check=True)
|
||||
subprocess.run(
|
||||
["wlr-randr", "--output", requested_connector, "--mode", mode_str],
|
||||
check=True,
|
||||
)
|
||||
|
||||
def save_current_mode(self, location):
|
||||
# Implementation similar to your GNOME one but using wlr-randr
|
||||
result = subprocess.run(["wlr-randr", "--json"], capture_output=True, text=True, check=True)
|
||||
data = json.loads(result.stdout)[0] # Grab first monitor
|
||||
result = subprocess.run(
|
||||
["wlr-randr", "--json"], capture_output=True, text=True, check=True
|
||||
)
|
||||
data = json.loads(result.stdout)[0] # Grab first monitor
|
||||
curr = data["current_mode"]
|
||||
with open(location, "w") as f:
|
||||
f.write(f"{curr['width']}x{curr['height']}@{curr['refresh']}")
|
||||
@@ -53,4 +65,7 @@ class Display(AbstractDisplay):
|
||||
@staticmethod
|
||||
def detect():
|
||||
# Cage doesn't always set XDG_CURRENT_DESKTOP, but it's a wlroots compositor
|
||||
return os.environ.get("WLR_BACKENDS") == "headless" or "cage" in os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
|
||||
return (
|
||||
os.environ.get("WLR_BACKENDS") == "headless"
|
||||
or "cage" in os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user