History wipe because Zed is very annoying...
This commit is contained in:
+17
@@ -0,0 +1,17 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
venv/
|
||||
.venv/
|
||||
env/
|
||||
.env/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
@@ -0,0 +1,67 @@
|
||||
# Sunshine Snake Glue
|
||||
(Yes it's a silly name)
|
||||
|
||||
## What's This?
|
||||
A tool that changes the resolution of a Sunshine host to an approximation of the client Moonlight resolution.
|
||||
|
||||
## What is Sunshine and Moonlight?
|
||||
[Sunshine](https://github.com/lizardbyte/sunshine)
|
||||
|
||||
[Moonlight](https://moonlight-stream.org/)
|
||||
|
||||
**TLDR**: Sunshine is an ultra low latency display streaming server, and Moonlight is a client for it. I get around 1ms~ when using it for my tablet as a second display.
|
||||
|
||||
## What Does This Support?
|
||||
Linux, specifically only GNOME's Wayland session at the moment ([and in a very hacky state...](https://git.coolbea.nz/oscarg/sunshine-snake-glue/src/branch/main/pkg/wayland_display_manager/gnome/__init__.py)).
|
||||
|
||||
### Support list:
|
||||
| Compositor | Support |
|
||||
| :--- | ---: |
|
||||
| GNOME | ⚠️ (Work in progress / Hacky) |
|
||||
| KDE Plasma | ❌ |
|
||||
| Sway | ❌ |
|
||||
| Hyprland | ❌ |
|
||||
|
||||
### X11?
|
||||
I don't think I'll ever extend this to support X11. Sunshine has options to interact with an X server, Wayland compositors tend to be much stricter, and this is just a middle layer to make that communication pleasant. Sunshine's documentation [even has instructions for X11](https://docs.lizardbyte.dev/projects/sunshine/master/md_docs_2app__examples.html#x11). Yes, it does mention GNOME's Wayland session, but it makes the bold assumption that the client has an exact match with a resolution/refresh rate the compositor offers.
|
||||
|
||||
## Dependencies
|
||||
A version of GNOME that supports `gdctl`. That's all! Presumably you have `python3`, right?
|
||||
|
||||
## Usage
|
||||
### Arguments
|
||||
- `set` changes the resolution of the given display adapter
|
||||
- `create` generates a script, filling placeholders documented below. (It aligns the application with the matched resolution, not Sunshine's internal resolution info)
|
||||
- `reset` sets the resolution of the provided adapter back to what it was before connecting.
|
||||
### Common Usage
|
||||
`[path]` represents the full path of sunshine snake glue for brevity.
|
||||
`[display adapter]` represents the adapter identifier that Sunshine internally uses.
|
||||
`[string with placeholders]` is self explanatory.
|
||||
#### Do Instructions
|
||||
Do: `python3 [path] set [display adapter]`
|
||||
|
||||
Do: `python3 [path] create [display adapter] [string with placeholders]`
|
||||
(This saves a file in `~/.sunshine-snake-glue` called `script.sh`, and sets its permissions accordingly.)
|
||||
|
||||
#### Undo Instructions
|
||||
Undo: `python3 [path] reset [display adapter]`
|
||||
|
||||
#### Possible Placeholders
|
||||
- `{res_x}` (The horizontal resolution.)
|
||||
- `{res_y}` (The vertical resolution.)
|
||||
- `{rate}` (The refresh rate.)
|
||||
|
||||
### Example Usage
|
||||
#### Do:
|
||||
```
|
||||
python3 /home/agrey/Development/sunshine-snake-glue set HDMI-2
|
||||
python3 /home/agrey/Development/sunshine-snake-glue create HDMI-2 "gamescope -f -W {res_x} -H {res_y} -r {rate} -e -- steam -bigpicture -steamos3"
|
||||
```
|
||||
#### Command:
|
||||
```
|
||||
/home/agrey/.sunshine-snake-glue/script.sh
|
||||
```
|
||||
#### Undo:
|
||||
```
|
||||
python3 /home/agrey/Development/sunshine-snake-glue reset HDMI-2
|
||||
```
|
||||
Executable
+136
@@ -0,0 +1,136 @@
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
|
||||
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:
|
||||
target_w = sunshine_env.specs.width
|
||||
target_h = sunshine_env.specs.height
|
||||
target_fps = sunshine_env.specs.fps
|
||||
|
||||
target_ratio = target_w / target_h
|
||||
target_pixels = target_w * target_h
|
||||
|
||||
def _sorting_rules(mode: Modeline):
|
||||
# Shape difference
|
||||
mode_ratio = mode.width / mode.height
|
||||
shape_difference = abs(mode_ratio - target_ratio)
|
||||
|
||||
# Is refresh rate too slow? (rules out very low refresh rates)
|
||||
is_too_slow = mode.refresh_rate < target_fps
|
||||
|
||||
# How off is the refresh rate?
|
||||
fps_difference = abs(mode.refresh_rate - target_fps)
|
||||
|
||||
# For comparing total pixel quantities
|
||||
mode_pixels = mode.width * mode.height
|
||||
pixel_difference = abs(mode_pixels - target_pixels)
|
||||
|
||||
return (shape_difference, is_too_slow, fps_difference, pixel_difference)
|
||||
|
||||
return min(supported_modes, key=_sorting_rules)
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
os.mkdir(CONFIG_DIR)
|
||||
except FileExistsError:
|
||||
pass
|
||||
|
||||
sunshine_env = sunshine.load_env_variables()
|
||||
display_manager = wayland_display_manager.get_display()
|
||||
|
||||
if len(sys.argv) - 1 < Arguments.CONNECTOR:
|
||||
raise SyntaxError(Errors.INSUFFICIENT_INSTRUCTIONS)
|
||||
|
||||
match sys.argv[Arguments.INSTRUCTION]:
|
||||
case Instructions.SET:
|
||||
display_manager.save_current_mode(PREVIOUS_MODE_PATH)
|
||||
|
||||
display_modes = next(
|
||||
(
|
||||
i.modes
|
||||
for i in display_manager.get_modes()
|
||||
if i.connector == sys.argv[Arguments.CONNECTOR]
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if display_modes:
|
||||
modeline = calculate_closest_mode(sunshine_env, display_modes)
|
||||
|
||||
display_manager.set_mode(
|
||||
modeline=modeline, requested_connector=sys.argv[Arguments.CONNECTOR]
|
||||
)
|
||||
|
||||
print(modeline)
|
||||
|
||||
else:
|
||||
raise ValueError(Errors.CONNECTOR_INVALID)
|
||||
|
||||
case Instructions.RESET:
|
||||
modeline = display_manager.read_saved_mode(PREVIOUS_MODE_PATH)
|
||||
|
||||
display_manager.set_mode(modeline, sys.argv[Arguments.CONNECTOR])
|
||||
|
||||
print(modeline)
|
||||
|
||||
case Instructions.CREATE:
|
||||
if len(sys.argv) - 1 < Arguments.TEMPLATE:
|
||||
raise SyntaxError(Errors.INSUFFICIENT_INSTRUCTIONS)
|
||||
|
||||
display_modes = next(
|
||||
(
|
||||
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)
|
||||
|
||||
with open(GEN_SCRIPT_PATH, "w") as file:
|
||||
_ = file.write(
|
||||
"#!/bin/bash\n"
|
||||
+ sys.argv[Arguments.TEMPLATE].format(
|
||||
res_x=modeline.width,
|
||||
res_y=modeline.height,
|
||||
rate=modeline.refresh_rate,
|
||||
)
|
||||
)
|
||||
|
||||
os.chmod(
|
||||
GEN_SCRIPT_PATH,
|
||||
stat.S_IRUSR
|
||||
| stat.S_IWUSR
|
||||
| stat.S_IXUSR
|
||||
| stat.S_IRGRP
|
||||
| stat.S_IXGRP
|
||||
| stat.S_IROTH
|
||||
| stat.S_IXOTH,
|
||||
)
|
||||
|
||||
case _:
|
||||
raise ValueError(Errors.CONNECTOR_UNSPECIFIED)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,9 @@
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class Errors(StrEnum):
|
||||
INSUFFICIENT_INSTRUCTIONS = (
|
||||
"Not enough supplied instructions! Usage: {instruction} {connector}"
|
||||
)
|
||||
CONNECTOR_INVALID = "The requested connector is invalid!"
|
||||
CONNECTOR_UNSPECIFIED = "No requested connector!"
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import pathlib
|
||||
from enum import IntEnum, StrEnum
|
||||
|
||||
|
||||
class Config(StrEnum):
|
||||
CONFIG_DIR_NAME = ".sunshine-snake-glue"
|
||||
PREVIOUS_MODE_FILE = "previous_mode"
|
||||
GEN_SCRIPT_FILE = "script.sh"
|
||||
|
||||
|
||||
CONFIG_DIR = pathlib.Path.home() / Config.CONFIG_DIR_NAME
|
||||
|
||||
PREVIOUS_MODE_PATH = CONFIG_DIR / Config.PREVIOUS_MODE_FILE
|
||||
GEN_SCRIPT_PATH = CONFIG_DIR / Config.GEN_SCRIPT_FILE
|
||||
|
||||
|
||||
class Arguments(IntEnum):
|
||||
SCRIPT_NAME = 0
|
||||
INSTRUCTION = 1
|
||||
CONNECTOR = 2
|
||||
TEMPLATE = 3
|
||||
|
||||
|
||||
class Instructions(StrEnum):
|
||||
SET = "set"
|
||||
RESET = "reset"
|
||||
CREATE = "create"
|
||||
@@ -0,0 +1,40 @@
|
||||
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 ld_str(var: EnvKey):
|
||||
return environ.get(var.requested_key, var.default)
|
||||
|
||||
def ld_int(var: EnvKey):
|
||||
return int(ld_str(var))
|
||||
|
||||
def ld_bool(var: EnvKey):
|
||||
return ld_str(var).lower() == "true"
|
||||
|
||||
return SunshineEnvironmentVariables(
|
||||
SunshineClientInfo(
|
||||
app_id=ld_str(EnvKey.APP_ID), app_name=ld_str(EnvKey.APP_NAME)
|
||||
),
|
||||
SunshineClientSpecs(
|
||||
width=ld_int(EnvKey.WIDTH),
|
||||
height=ld_int(EnvKey.HEIGHT),
|
||||
fps=ld_int(EnvKey.FPS),
|
||||
hdr=ld_bool(EnvKey.HDR),
|
||||
gcmap=ld_int(EnvKey.GCMAP),
|
||||
),
|
||||
SunshineClientOptions(
|
||||
host_audio=ld_bool(EnvKey.HOST_AUDIO),
|
||||
enable_sops=ld_bool(EnvKey.ENABLE_SOPS),
|
||||
audio_config=SunshineAudioConfig(ld_str(EnvKey.AUDIO_CONFIG)),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class SunshineAudioConfig(StrEnum):
|
||||
STEREO = "2.0"
|
||||
SURROUND_5_1 = "5.1"
|
||||
SURROUND_7_1 = "7.1"
|
||||
@@ -0,0 +1,22 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class KeyDefaultMixin(str):
|
||||
def __new__(cls, requested_key: str, default: str):
|
||||
obj = str.__new__(cls, requested_key)
|
||||
obj.requested_key = requested_key
|
||||
obj.default = default
|
||||
return obj
|
||||
|
||||
|
||||
class SunshineEnvKey(KeyDefaultMixin, Enum):
|
||||
APP_ID = ("SUNSHINE_APP_ID", "Unknown ID")
|
||||
APP_NAME = ("SUNSHINE_APP_NAME", "Unknown App Name")
|
||||
WIDTH = ("SUNSHINE_CLIENT_WIDTH", "1920")
|
||||
HEIGHT = ("SUNSHINE_CLIENT_HEIGHT", "1080")
|
||||
FPS = ("SUNSHINE_CLIENT_FPS", "60")
|
||||
HDR = ("SUNSHINE_CLIENT_HDR", "0")
|
||||
GCMAP = ("SUNSHINE_CLIENT_GCMAP", "0")
|
||||
HOST_AUDIO = ("SUNSHINE_CLIENT_HOST_AUDIO", "0")
|
||||
ENABLE_SOPS = ("SUNSHINE_CLIENT_ENABLE_SOPS", "0")
|
||||
AUDIO_CONFIG = ("SUNSHINE_CLIENT_AUDIO_CONFIGURATION", "2.0")
|
||||
@@ -0,0 +1,32 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .enums import SunshineAudioConfig
|
||||
|
||||
|
||||
@dataclass
|
||||
class SunshineClientInfo:
|
||||
app_id: str
|
||||
app_name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class SunshineClientSpecs:
|
||||
width: int
|
||||
height: int
|
||||
fps: int
|
||||
hdr: bool
|
||||
gcmap: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class SunshineClientOptions:
|
||||
host_audio: bool
|
||||
enable_sops: bool
|
||||
audio_config: SunshineAudioConfig
|
||||
|
||||
|
||||
@dataclass
|
||||
class SunshineEnvironmentVariables:
|
||||
info: SunshineClientInfo
|
||||
specs: SunshineClientSpecs
|
||||
options: SunshineClientOptions
|
||||
@@ -0,0 +1,20 @@
|
||||
from .abstracts import AbstractDisplay
|
||||
from .enums import DisplayManagerDriver, Error
|
||||
|
||||
|
||||
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()
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,27 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
|
||||
from .structs import Modeline, MonitorInfo
|
||||
|
||||
|
||||
class AbstractDisplay(ABC):
|
||||
@abstractmethod
|
||||
def get_modes(self) -> list[MonitorInfo]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_mode(self, modeline: Modeline, requested_connector: str) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def save_current_mode(self, location: str | Path) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def read_saved_mode(self, location: str | Path) -> Modeline:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def detect() -> bool:
|
||||
pass
|
||||
@@ -0,0 +1,32 @@
|
||||
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)
|
||||
obj.text = text
|
||||
obj.meta = meta
|
||||
return obj
|
||||
|
||||
|
||||
class DisplayManagerDriver(MetaMixin, Enum):
|
||||
GNOME = ("GNOME (Mutter)", gnome.Display)
|
||||
PLASMA = ("KDE Plasma 5/6 (KWin)", plasma.Display)
|
||||
SWAY = ("Sway", wlroots.Display)
|
||||
INFER = ("Inferring Wayland Compositor", None)
|
||||
|
||||
@classmethod
|
||||
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."
|
||||
)
|
||||
@@ -0,0 +1,87 @@
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from typing import override
|
||||
|
||||
from ..abstracts import AbstractDisplay
|
||||
from ..structs import Modeline, MonitorInfo
|
||||
from ._gnome_types import GNOMEDisplayConfig
|
||||
|
||||
# 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):
|
||||
@override
|
||||
def get_modes(self):
|
||||
mutter_display_state = GNOMEDisplayConfig()
|
||||
mutter_display_state.get_dbus_info()
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@override
|
||||
def set_mode(self, modeline, requested_connector=None) -> None:
|
||||
subprocess.run(
|
||||
[
|
||||
"gdctl",
|
||||
"set",
|
||||
"--logical-monitor",
|
||||
"--primary",
|
||||
"--monitor",
|
||||
requested_connector,
|
||||
"--mode",
|
||||
str(modeline),
|
||||
]
|
||||
)
|
||||
|
||||
@override
|
||||
def save_current_mode(self, location):
|
||||
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:
|
||||
file.write(f"{w}x{h}@{r}")
|
||||
|
||||
@override
|
||||
def read_saved_mode(self, location):
|
||||
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))
|
||||
|
||||
@staticmethod
|
||||
@override
|
||||
def detect():
|
||||
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 not going to be nice
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import os
|
||||
from typing import override
|
||||
|
||||
from ..abstracts import AbstractDisplay
|
||||
from ..structs import MonitorInfo, Modeline
|
||||
|
||||
|
||||
class Display(AbstractDisplay):
|
||||
@override
|
||||
def get_modes(self) -> list[MonitorInfo]:
|
||||
pass
|
||||
|
||||
@override
|
||||
def set_mode(self, modeline: Modeline, requested_connector: str) -> None:
|
||||
pass
|
||||
|
||||
@override
|
||||
def read_saved_mode(self, location: str | Path) -> Modeline:
|
||||
pass
|
||||
|
||||
@override
|
||||
def save_current_mode(self, location: str | Path) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@override
|
||||
def detect():
|
||||
return "plasma" in os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
|
||||
@@ -0,0 +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]
|
||||
@@ -0,0 +1,26 @@
|
||||
import os
|
||||
from typing import override
|
||||
|
||||
from ..abstracts import AbstractDisplay
|
||||
from ..structs import MonitorInfo, Modeline
|
||||
|
||||
|
||||
class Display(AbstractDisplay):
|
||||
def get_modes(self) -> list[MonitorInfo]:
|
||||
pass
|
||||
|
||||
def set_mode(self, modeline: Modeline, requested_connector: str) -> None:
|
||||
pass
|
||||
|
||||
def read_saved_mode(self, location: str | Path) -> Modeline:
|
||||
pass
|
||||
|
||||
def save_current_mode(self, location: str | Path) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def detect():
|
||||
desktop = os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
|
||||
known = {"sway", "cage", "labwc", "wayfire", "hyprland"}
|
||||
|
||||
return bool(set(desktop) & known) or "WAYLAND_DISPLAY" in os.environ
|
||||
Reference in New Issue
Block a user