Massive refactor (Zed wouldn't stop complaining about formatting) and
beginning of proper GNOME implementation.
This commit is contained in:
+61
-34
@@ -1,25 +1,32 @@
|
|||||||
from pkg import sunshine
|
|
||||||
from pkg.sunshine.structs import SunshineEnvironmentVariables
|
|
||||||
|
|
||||||
from pkg import wayland_display_manager
|
|
||||||
from pkg.wayland_display_manager.structs import Modeline
|
|
||||||
|
|
||||||
from app.vars import Arguments, Instructions, CONFIG_DIR, GEN_SCRIPT_PATH, PREVIOUS_MODE_PATH
|
|
||||||
from app.errors import Errors
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
import stat
|
import stat
|
||||||
|
import sys
|
||||||
|
|
||||||
def calculate_closest_mode(sunshine_env: SunshineEnvironmentVariables, supported_modes: list[Modeline]) -> Modeline:
|
from app.errors import Errors
|
||||||
|
from app.vars import (
|
||||||
|
CONFIG_DIR,
|
||||||
|
GEN_SCRIPT_PATH,
|
||||||
|
PREVIOUS_MODE_PATH,
|
||||||
|
Arguments,
|
||||||
|
Instructions,
|
||||||
|
)
|
||||||
|
from pkg import sunshine, wayland_display_manager
|
||||||
|
from pkg.sunshine.structs import SunshineEnvironmentVariables
|
||||||
|
from pkg.wayland_display_manager.structs import Modeline
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_closest_mode(
|
||||||
|
sunshine_env: SunshineEnvironmentVariables, supported_modes: list[Modeline]
|
||||||
|
) -> Modeline:
|
||||||
def _distance(mode: Modeline):
|
def _distance(mode: Modeline):
|
||||||
return(
|
return (
|
||||||
abs(mode.width - sunshine_env.specs.width) *2 +
|
abs(mode.width - sunshine_env.specs.width) * 2
|
||||||
abs(mode.height - sunshine_env.specs.height) *2 +
|
+ abs(mode.height - sunshine_env.specs.height) * 2
|
||||||
abs(mode.refresh_rate - sunshine_env.specs.fps)
|
+ abs(mode.refresh_rate - sunshine_env.specs.fps)
|
||||||
)
|
)
|
||||||
|
|
||||||
return min(supported_modes, key = _distance)
|
return min(supported_modes, key=_distance)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
try:
|
try:
|
||||||
@@ -30,23 +37,27 @@ def main():
|
|||||||
sunshine_env = sunshine.load_env_variables()
|
sunshine_env = sunshine.load_env_variables()
|
||||||
display_manager = wayland_display_manager.get_display()
|
display_manager = wayland_display_manager.get_display()
|
||||||
|
|
||||||
if len(sys.argv) -1 < Arguments.CONNECTOR:
|
if len(sys.argv) - 1 < Arguments.CONNECTOR:
|
||||||
raise SyntaxError(Errors.INSUFFICIENT_INSTRUCTIONS)
|
raise SyntaxError(Errors.INSUFFICIENT_INSTRUCTIONS)
|
||||||
|
|
||||||
match sys.argv[Arguments.INSTRUCTION]:
|
match sys.argv[Arguments.INSTRUCTION]:
|
||||||
case Instructions.SET:
|
case Instructions.SET:
|
||||||
display_manager.save_current_mode(PREVIOUS_MODE_PATH)
|
display_manager.save_current_mode(PREVIOUS_MODE_PATH)
|
||||||
|
|
||||||
for i in display_manager.get_modes():
|
display_modes = next(
|
||||||
if i.connector == sys.argv[Arguments.CONNECTOR]:
|
(
|
||||||
display_modes = i.modes
|
i.modes
|
||||||
|
for i in display_manager.get_modes()
|
||||||
|
if i.connector == sys.argv[Arguments.CONNECTOR]
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
if display_modes:
|
if display_modes:
|
||||||
modeline = calculate_closest_mode(sunshine_env, display_modes)
|
modeline = calculate_closest_mode(sunshine_env, display_modes)
|
||||||
|
|
||||||
display_manager.set_mode(
|
display_manager.set_mode(
|
||||||
modeline = modeline,
|
modeline=modeline, requested_connector=sys.argv[Arguments.CONNECTOR]
|
||||||
requested_connector = sys.argv[Arguments.CONNECTOR]
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError(Errors.CONNECTOR_INVALID)
|
raise ValueError(Errors.CONNECTOR_INVALID)
|
||||||
@@ -57,31 +68,47 @@ def main():
|
|||||||
display_manager.set_mode(modeline, sys.argv[Arguments.CONNECTOR])
|
display_manager.set_mode(modeline, sys.argv[Arguments.CONNECTOR])
|
||||||
|
|
||||||
case Instructions.CREATE:
|
case Instructions.CREATE:
|
||||||
if len(sys.argv) -1 < Arguments.TEMPLATE:
|
if len(sys.argv) - 1 < Arguments.TEMPLATE:
|
||||||
raise SyntaxError(Errors.INSUFFICIENT_INSTRUCTIONS)
|
raise SyntaxError(Errors.INSUFFICIENT_INSTRUCTIONS)
|
||||||
|
|
||||||
for i in display_manager.get_modes():
|
display_modes = next(
|
||||||
if i.connector == sys.argv[Arguments.CONNECTOR]:
|
(
|
||||||
display_modes = i.modes
|
i.modes
|
||||||
|
for i in display_manager.get_modes()
|
||||||
|
if i.connector == sys.argv[Arguments.CONNECTOR]
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not display_modes:
|
||||||
|
raise ValueError(Errors.CONNECTOR_INVALID)
|
||||||
|
|
||||||
modeline = calculate_closest_mode(sunshine_env, display_modes)
|
modeline = calculate_closest_mode(sunshine_env, display_modes)
|
||||||
|
|
||||||
with open(GEN_SCRIPT_PATH, "w") as file:
|
with open(GEN_SCRIPT_PATH, "w") as file:
|
||||||
file.write("#!/bin/bash\n" + sys.argv[Arguments.TEMPLATE].format(
|
_ = file.write(
|
||||||
res_x = modeline.width,
|
"#!/bin/bash\n"
|
||||||
res_y = modeline.height,
|
+ sys.argv[Arguments.TEMPLATE].format(
|
||||||
rate = modeline.refresh_rate
|
res_x=modeline.width,
|
||||||
))
|
res_y=modeline.height,
|
||||||
|
rate=modeline.refresh_rate,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
os.chmod(
|
os.chmod(
|
||||||
GEN_SCRIPT_PATH,
|
GEN_SCRIPT_PATH,
|
||||||
stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR |
|
stat.S_IRUSR
|
||||||
stat.S_IRGRP | stat.S_IXGRP |
|
| stat.S_IWUSR
|
||||||
stat.S_IROTH | stat.S_IXOTH
|
| stat.S_IXUSR
|
||||||
|
| stat.S_IRGRP
|
||||||
|
| stat.S_IXGRP
|
||||||
|
| stat.S_IROTH
|
||||||
|
| stat.S_IXOTH,
|
||||||
)
|
)
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
raise ValueError(Errors.CONNECTOR_UNSPECIFIED)
|
raise ValueError(Errors.CONNECTOR_UNSPECIFIED)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
+4
-1
@@ -1,6 +1,9 @@
|
|||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
class Errors(StrEnum):
|
class Errors(StrEnum):
|
||||||
INSUFFICIENT_INSTRUCTIONS = "Not enough supplied instructions! Usage: {instruction} {connector}"
|
INSUFFICIENT_INSTRUCTIONS = (
|
||||||
|
"Not enough supplied instructions! Usage: {instruction} {connector}"
|
||||||
|
)
|
||||||
CONNECTOR_INVALID = "The requested connector is invalid!"
|
CONNECTOR_INVALID = "The requested connector is invalid!"
|
||||||
CONNECTOR_UNSPECIFIED = "No requested connector!"
|
CONNECTOR_UNSPECIFIED = "No requested connector!"
|
||||||
+5
-1
@@ -1,22 +1,26 @@
|
|||||||
from enum import IntEnum, StrEnum
|
|
||||||
import pathlib
|
import pathlib
|
||||||
|
from enum import IntEnum, StrEnum
|
||||||
|
|
||||||
|
|
||||||
class Config(StrEnum):
|
class Config(StrEnum):
|
||||||
CONFIG_DIR_NAME = ".sunshine-snake-glue"
|
CONFIG_DIR_NAME = ".sunshine-snake-glue"
|
||||||
PREVIOUS_MODE_FILE = "previous_mode"
|
PREVIOUS_MODE_FILE = "previous_mode"
|
||||||
GEN_SCRIPT_FILE = "script.sh"
|
GEN_SCRIPT_FILE = "script.sh"
|
||||||
|
|
||||||
|
|
||||||
CONFIG_DIR = pathlib.Path.home() / Config.CONFIG_DIR_NAME
|
CONFIG_DIR = pathlib.Path.home() / Config.CONFIG_DIR_NAME
|
||||||
|
|
||||||
PREVIOUS_MODE_PATH = CONFIG_DIR / Config.PREVIOUS_MODE_FILE
|
PREVIOUS_MODE_PATH = CONFIG_DIR / Config.PREVIOUS_MODE_FILE
|
||||||
GEN_SCRIPT_PATH = CONFIG_DIR / Config.GEN_SCRIPT_FILE
|
GEN_SCRIPT_PATH = CONFIG_DIR / Config.GEN_SCRIPT_FILE
|
||||||
|
|
||||||
|
|
||||||
class Arguments(IntEnum):
|
class Arguments(IntEnum):
|
||||||
SCRIPT_NAME = 0
|
SCRIPT_NAME = 0
|
||||||
INSTRUCTION = 1
|
INSTRUCTION = 1
|
||||||
CONNECTOR = 2
|
CONNECTOR = 2
|
||||||
TEMPLATE = 3
|
TEMPLATE = 3
|
||||||
|
|
||||||
|
|
||||||
class Instructions(StrEnum):
|
class Instructions(StrEnum):
|
||||||
SET = "set"
|
SET = "set"
|
||||||
RESET = "reset"
|
RESET = "reset"
|
||||||
|
|||||||
+29
-18
@@ -1,29 +1,40 @@
|
|||||||
from .env_key import SunshineEnvKey as EnvKey
|
|
||||||
from .enums import SunshineAudioConfig
|
|
||||||
from .structs import SunshineAudioConfig, SunshineEnvironmentVariables, SunshineClientSpecs, SunshineClientInfo, SunshineClientOptions
|
|
||||||
|
|
||||||
from os import environ
|
from os import environ
|
||||||
|
|
||||||
|
from .enums import SunshineAudioConfig
|
||||||
|
from .env_key import SunshineEnvKey as EnvKey
|
||||||
|
from .structs import (
|
||||||
|
SunshineAudioConfig,
|
||||||
|
SunshineClientInfo,
|
||||||
|
SunshineClientOptions,
|
||||||
|
SunshineClientSpecs,
|
||||||
|
SunshineEnvironmentVariables,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_env_variables() -> SunshineEnvironmentVariables:
|
def load_env_variables() -> SunshineEnvironmentVariables:
|
||||||
def ld_str(var: EnvKey): return environ.get(var.requested_key, var.default)
|
def ld_str(var: EnvKey):
|
||||||
def ld_int(var: EnvKey): return int(ld_str(var))
|
return environ.get(var.requested_key, var.default)
|
||||||
def ld_bool(var: EnvKey): return ld_str(var).lower() == "true"
|
|
||||||
|
def ld_int(var: EnvKey):
|
||||||
|
return int(ld_str(var))
|
||||||
|
|
||||||
|
def ld_bool(var: EnvKey):
|
||||||
|
return ld_str(var).lower() == "true"
|
||||||
|
|
||||||
return SunshineEnvironmentVariables(
|
return SunshineEnvironmentVariables(
|
||||||
SunshineClientInfo(
|
SunshineClientInfo(
|
||||||
app_id = ld_str(EnvKey.APP_ID),
|
app_id=ld_str(EnvKey.APP_ID), app_name=ld_str(EnvKey.APP_NAME)
|
||||||
app_name = ld_str(EnvKey.APP_NAME)
|
|
||||||
),
|
),
|
||||||
SunshineClientSpecs(
|
SunshineClientSpecs(
|
||||||
width = ld_int(EnvKey.WIDTH),
|
width=ld_int(EnvKey.WIDTH),
|
||||||
height = ld_int(EnvKey.HEIGHT),
|
height=ld_int(EnvKey.HEIGHT),
|
||||||
fps = ld_int(EnvKey.FPS),
|
fps=ld_int(EnvKey.FPS),
|
||||||
hdr = ld_bool(EnvKey.HDR),
|
hdr=ld_bool(EnvKey.HDR),
|
||||||
gcmap = ld_int(EnvKey.GCMAP)
|
gcmap=ld_int(EnvKey.GCMAP),
|
||||||
),
|
),
|
||||||
SunshineClientOptions(
|
SunshineClientOptions(
|
||||||
host_audio = ld_bool(EnvKey.HOST_AUDIO),
|
host_audio=ld_bool(EnvKey.HOST_AUDIO),
|
||||||
enable_sops = ld_bool(EnvKey.ENABLE_SOPS),
|
enable_sops=ld_bool(EnvKey.ENABLE_SOPS),
|
||||||
audio_config = SunshineAudioConfig(ld_str(EnvKey.AUDIO_CONFIG))
|
audio_config=SunshineAudioConfig(ld_str(EnvKey.AUDIO_CONFIG)),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
class SunshineAudioConfig(StrEnum):
|
class SunshineAudioConfig(StrEnum):
|
||||||
STEREO = "2.0"
|
STEREO = "2.0"
|
||||||
SURROUND_5_1 = "5.1"
|
SURROUND_5_1 = "5.1"
|
||||||
SURROUND_7_1 = "7.1"
|
SURROUND_7_1 = "7.1"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
class KeyDefaultMixin(str):
|
class KeyDefaultMixin(str):
|
||||||
def __new__(cls, requested_key: str, default: str):
|
def __new__(cls, requested_key: str, default: str):
|
||||||
obj = str.__new__(cls, requested_key)
|
obj = str.__new__(cls, requested_key)
|
||||||
@@ -7,6 +8,7 @@ class KeyDefaultMixin(str):
|
|||||||
obj.default = default
|
obj.default = default
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class SunshineEnvKey(KeyDefaultMixin, Enum):
|
class SunshineEnvKey(KeyDefaultMixin, Enum):
|
||||||
APP_ID = ("SUNSHINE_APP_ID", "Unknown ID")
|
APP_ID = ("SUNSHINE_APP_ID", "Unknown ID")
|
||||||
APP_NAME = ("SUNSHINE_APP_NAME", "Unknown App Name")
|
APP_NAME = ("SUNSHINE_APP_NAME", "Unknown App Name")
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ from dataclasses import dataclass
|
|||||||
|
|
||||||
from .enums import SunshineAudioConfig
|
from .enums import SunshineAudioConfig
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SunshineClientInfo:
|
class SunshineClientInfo:
|
||||||
app_id: str
|
app_id: str
|
||||||
app_name: str
|
app_name: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SunshineClientSpecs:
|
class SunshineClientSpecs:
|
||||||
width: int
|
width: int
|
||||||
@@ -15,12 +17,14 @@ class SunshineClientSpecs:
|
|||||||
hdr: bool
|
hdr: bool
|
||||||
gcmap: int
|
gcmap: int
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SunshineClientOptions:
|
class SunshineClientOptions:
|
||||||
host_audio: bool
|
host_audio: bool
|
||||||
enable_sops: bool
|
enable_sops: bool
|
||||||
audio_config: SunshineAudioConfig
|
audio_config: SunshineAudioConfig
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SunshineEnvironmentVariables:
|
class SunshineEnvironmentVariables:
|
||||||
info: SunshineClientInfo
|
info: SunshineClientInfo
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
from .abstracts import AbstractDisplay
|
from .abstracts import AbstractDisplay
|
||||||
from .enums import DisplayManagerDriver, Error
|
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:
|
match requested_driver:
|
||||||
case DisplayManagerDriver.INFER:
|
case DisplayManagerDriver.INFER:
|
||||||
for display in DisplayManagerDriver.get_display_list():
|
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)
|
raise RuntimeError(Error.DISPLAY_INFER_TYPE_FAILURE)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from .structs import MonitorInfo, Modeline
|
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .structs import Modeline, MonitorInfo
|
||||||
|
|
||||||
|
|
||||||
class AbstractDisplay(ABC):
|
class AbstractDisplay(ABC):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -12,15 +14,14 @@ class AbstractDisplay(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def save_current_mode(self, location: str) -> None:
|
def save_current_mode(self, location: str | Path) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def read_saved_mode(self, location: str) -> Modeline:
|
def read_saved_mode(self, location: str | Path) -> Modeline:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def detect(self) -> bool:
|
def detect() -> bool:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
from .abstracts import AbstractDisplay
|
|
||||||
|
|
||||||
from . import gnome, plasma, wlroots
|
|
||||||
|
|
||||||
from enum import Enum, StrEnum
|
from enum import Enum, StrEnum
|
||||||
from typing import Optional, Type
|
from typing import Optional, Type
|
||||||
|
|
||||||
|
from . import gnome, plasma, wlroots
|
||||||
|
from .abstracts import AbstractDisplay
|
||||||
|
|
||||||
|
|
||||||
class MetaMixin(str):
|
class MetaMixin(str):
|
||||||
def __new__(cls, text: str, meta: AbstractDisplay):
|
def __new__(cls, text: str, meta: AbstractDisplay):
|
||||||
obj = str.__new__(cls, text)
|
obj = str.__new__(cls, text)
|
||||||
@@ -12,6 +12,7 @@ class MetaMixin(str):
|
|||||||
obj.meta = meta
|
obj.meta = meta
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class DisplayManagerDriver(MetaMixin, Enum):
|
class DisplayManagerDriver(MetaMixin, Enum):
|
||||||
GNOME = ("GNOME (Mutter)", gnome.Display)
|
GNOME = ("GNOME (Mutter)", gnome.Display)
|
||||||
PLASMA = ("KDE Plasma 5/6 (KWin)", plasma.Display)
|
PLASMA = ("KDE Plasma 5/6 (KWin)", plasma.Display)
|
||||||
@@ -22,7 +23,10 @@ class DisplayManagerDriver(MetaMixin, Enum):
|
|||||||
def get_display_list(cls):
|
def get_display_list(cls):
|
||||||
return [display for display in cls if display.meta is not None]
|
return [display for display in cls if display.meta is not None]
|
||||||
|
|
||||||
|
|
||||||
class Error(StrEnum):
|
class Error(StrEnum):
|
||||||
DISPLAY_TYPE_NOT_IMPL = "Requested manager type has not been implemented!"
|
DISPLAY_TYPE_NOT_IMPL = "Requested manager type has not been implemented!"
|
||||||
DISPLAY_TYPE_UNKNOWN = "Requested manager type does not exist."
|
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 os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
### Dirty Implementation ###
|
from ..abstracts import AbstractDisplay
|
||||||
# Needs to be replaced with a proper dbus solution. Preferably by taking reference from gdctl. `cat /usr/bin/gdctl` #
|
from ..structs import Modeline, MonitorInfo
|
||||||
# Issues:
|
from ._gnome_types import GNOMEDisplayConfig
|
||||||
# - 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`.
|
|
||||||
|
|
||||||
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):
|
class Display(AbstractDisplay):
|
||||||
def get_modes(self):
|
def get_modes(self):
|
||||||
gdctl_output = subprocess.run(["gdctl", "show", "--modes"], capture_output=True, text=True, check=True)
|
mutter_display_state = GNOMEDisplayConfig()
|
||||||
modes = MODE_REGEX_PATTERN.findall(gdctl_output.stdout)
|
mutter_display_state.get_dbus_info()
|
||||||
|
|
||||||
# Hard coding my beloved
|
primary_connectors = {
|
||||||
return [
|
m[0]
|
||||||
MonitorInfo(
|
for lm in mutter_display_state.logical_monitors
|
||||||
connector = "HDMI-2",
|
if lm.is_primary
|
||||||
primary = True,
|
for m in lm.monitors
|
||||||
|
}
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for monitor in mutter_display_state.monitors:
|
||||||
modes = [
|
modes = [
|
||||||
Modeline(
|
Modeline(
|
||||||
width = int(w),
|
mode.width,
|
||||||
height = int(h),
|
mode.height,
|
||||||
refresh_rate = float(r)
|
mode.refresh_rate,
|
||||||
)
|
|
||||||
for _, (w, h, r) in enumerate(modes)
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
for mode in monitor.modes
|
||||||
]
|
]
|
||||||
|
|
||||||
def set_mode(self, modeline, requested_connector = None) -> None:
|
result.append(
|
||||||
subprocess.run(["gdctl", "set", "--logical-monitor", "--primary", "--monitor", requested_connector, "--mode", str(modeline)])
|
MonitorInfo(
|
||||||
|
monitor.connector,
|
||||||
|
monitor.connector in primary_connectors,
|
||||||
|
modes
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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):
|
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)
|
match = MODE_REGEX_PATTERN.search(gdctl_output.stdout)
|
||||||
w, h, r = match.groups()
|
w, h, r = match.groups()
|
||||||
with open(location, "w") as file:
|
with open(location, "w") as file:
|
||||||
@@ -49,15 +71,10 @@ class Display(AbstractDisplay):
|
|||||||
with open(location, "r") as file:
|
with open(location, "r") as file:
|
||||||
config_file_content = file.read()
|
config_file_content = file.read()
|
||||||
|
|
||||||
|
|
||||||
match = MODE_REGEX_PATTERN.search(config_file_content)
|
match = MODE_REGEX_PATTERN.search(config_file_content)
|
||||||
|
|
||||||
w, h, r = match.groups()
|
w, h, r = match.groups()
|
||||||
return Modeline(
|
return Modeline(width=int(w), height=int(h), refresh_rate=float(r))
|
||||||
width = int(w),
|
|
||||||
height = int(h),
|
|
||||||
refresh_rate = float(r)
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def detect():
|
def detect():
|
||||||
|
|||||||
@@ -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
|
from ..abstracts import AbstractDisplay
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
class Display(AbstractDisplay):
|
class Display(AbstractDisplay):
|
||||||
def get_modes(self):
|
def get_modes(self):
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Modeline:
|
class Modeline:
|
||||||
width: int
|
width: int
|
||||||
@@ -12,9 +13,9 @@ class Modeline:
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.__str__()
|
return self.__str__()
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MonitorInfo:
|
class MonitorInfo:
|
||||||
connector: str
|
connector: str
|
||||||
primary: bool
|
primary: bool
|
||||||
modes: list[Modeline]
|
modes: list[Modeline]
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +1,57 @@
|
|||||||
from ..structs import MonitorInfo, Modeline
|
import json
|
||||||
from ..abstracts import AbstractDisplay
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import json
|
|
||||||
|
from ..abstracts import AbstractDisplay
|
||||||
|
from ..structs import Modeline, MonitorInfo
|
||||||
|
|
||||||
# Matches "1920x1080 @ 60.000 Hz" or "1920x1080@60.000"
|
# 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):
|
class Display(AbstractDisplay):
|
||||||
def get_modes(self):
|
def get_modes(self):
|
||||||
# wlr-randr --json is the 'civilized' way to do this on Trixie
|
# 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)
|
data = json.loads(result.stdout)
|
||||||
|
|
||||||
monitors = []
|
monitors = []
|
||||||
for output in data:
|
for output in data:
|
||||||
modes = []
|
modes = []
|
||||||
for m in output.get("modes", []):
|
for m in output.get("modes", []):
|
||||||
modes.append(Modeline(
|
modes.append(
|
||||||
|
Modeline(
|
||||||
width=int(m["width"]),
|
width=int(m["width"]),
|
||||||
height=int(m["height"]),
|
height=int(m["height"]),
|
||||||
refresh_rate=float(m["refresh"])
|
refresh_rate=float(m["refresh"]),
|
||||||
))
|
)
|
||||||
|
)
|
||||||
|
|
||||||
monitors.append(MonitorInfo(
|
monitors.append(
|
||||||
|
MonitorInfo(
|
||||||
connector=output["name"], # Usually 'HEADLESS-1' in Cage
|
connector=output["name"], # Usually 'HEADLESS-1' in Cage
|
||||||
primary=output.get("focused", False),
|
primary=output.get("focused", False),
|
||||||
modes=modes
|
modes=modes,
|
||||||
))
|
)
|
||||||
|
)
|
||||||
return monitors
|
return monitors
|
||||||
|
|
||||||
def set_mode(self, modeline, requested_connector="HEADLESS-1") -> None:
|
def set_mode(self, modeline, requested_connector="HEADLESS-1") -> None:
|
||||||
# Format: 1920x1080@60Hz
|
# Format: 1920x1080@60Hz
|
||||||
mode_str = f"{modeline.width}x{modeline.height}@{modeline.refresh_rate}Hz"
|
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):
|
def save_current_mode(self, location):
|
||||||
# Implementation similar to your GNOME one but using wlr-randr
|
# Implementation similar to your GNOME one but using wlr-randr
|
||||||
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)[0] # Grab first monitor
|
data = json.loads(result.stdout)[0] # Grab first monitor
|
||||||
curr = data["current_mode"]
|
curr = data["current_mode"]
|
||||||
with open(location, "w") as f:
|
with open(location, "w") as f:
|
||||||
@@ -53,4 +65,7 @@ class Display(AbstractDisplay):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def detect():
|
def detect():
|
||||||
# Cage doesn't always set XDG_CURRENT_DESKTOP, but it's a wlroots compositor
|
# 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