Init
This commit is contained in:
127
ptlinux/settings.py
Normal file
127
ptlinux/settings.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""User-editable launcher settings with conservative defaults."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from .paths import AppPaths
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Settings:
|
||||
width: int = 1600
|
||||
height: int = 900
|
||||
output_width: int = 1600
|
||||
output_height: int = 900
|
||||
refresh_rate: int = 60
|
||||
fullscreen: bool = False
|
||||
borderless: bool = True
|
||||
scaler: str = "fit"
|
||||
filter: str = "linear"
|
||||
runtime: str = "auto"
|
||||
proton_path: str | None = None
|
||||
game_source: str | None = None
|
||||
|
||||
@property
|
||||
def source_override(self) -> Path | None:
|
||||
return Path(self.game_source).expanduser().resolve() if self.game_source else None
|
||||
|
||||
|
||||
def load_settings(paths: AppPaths) -> Settings:
|
||||
try:
|
||||
value = json.loads(paths.settings_file.read_text(encoding="utf-8"))
|
||||
except FileNotFoundError:
|
||||
return Settings()
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise ValueError(f"invalid settings file {paths.settings_file}: {exc}") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"settings file must contain an object: {paths.settings_file}")
|
||||
settings = Settings(
|
||||
width=_integer(value, "width", 1600),
|
||||
height=_integer(value, "height", 900),
|
||||
output_width=_integer(value, "output_width", 1600),
|
||||
output_height=_integer(value, "output_height", 900),
|
||||
refresh_rate=_integer(value, "refresh_rate", 60),
|
||||
fullscreen=_boolean(value, "fullscreen", False),
|
||||
borderless=_boolean(value, "borderless", True),
|
||||
scaler=_string(value, "scaler", "fit"),
|
||||
filter=_string(value, "filter", "linear"),
|
||||
runtime=_string(value, "runtime", "auto"),
|
||||
proton_path=_optional_string(value, "proton_path"),
|
||||
game_source=_optional_string(value, "game_source"),
|
||||
)
|
||||
_validate(settings)
|
||||
return settings
|
||||
|
||||
|
||||
def save_settings(paths: AppPaths, settings: Settings) -> None:
|
||||
_validate(settings)
|
||||
paths.config.mkdir(parents=True, exist_ok=True)
|
||||
temporary = paths.settings_file.with_name(
|
||||
f".{paths.settings_file.name}.{os.getpid()}.tmp"
|
||||
)
|
||||
temporary.write_text(
|
||||
json.dumps(asdict(settings), indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
temporary.replace(paths.settings_file)
|
||||
|
||||
|
||||
def ensure_settings(paths: AppPaths) -> Settings:
|
||||
if paths.settings_file.exists():
|
||||
return load_settings(paths)
|
||||
settings = Settings()
|
||||
save_settings(paths, settings)
|
||||
return settings
|
||||
|
||||
|
||||
def _validate(settings: Settings) -> None:
|
||||
for name, value in (
|
||||
("width", settings.width),
|
||||
("height", settings.height),
|
||||
("output_width", settings.output_width),
|
||||
("output_height", settings.output_height),
|
||||
):
|
||||
if value < 640 or value > 16384:
|
||||
raise ValueError(f"{name} must be between 640 and 16384")
|
||||
if settings.refresh_rate < 24 or settings.refresh_rate > 1000:
|
||||
raise ValueError("refresh_rate must be between 24 and 1000")
|
||||
if settings.scaler not in {"auto", "integer", "fit", "fill", "stretch"}:
|
||||
raise ValueError(f"unsupported Gamescope scaler: {settings.scaler}")
|
||||
if settings.filter not in {"linear", "nearest", "fsr", "nis", "pixel"}:
|
||||
raise ValueError(f"unsupported Gamescope filter: {settings.filter}")
|
||||
if settings.runtime not in {"auto", "proton7", "umu"}:
|
||||
raise ValueError(f"unsupported runtime selection: {settings.runtime}")
|
||||
|
||||
|
||||
def _integer(value: dict[str, object], key: str, default: int) -> int:
|
||||
item = value.get(key, default)
|
||||
if isinstance(item, bool) or not isinstance(item, int):
|
||||
raise ValueError(f"{key} must be an integer")
|
||||
return item
|
||||
|
||||
|
||||
def _boolean(value: dict[str, object], key: str, default: bool) -> bool:
|
||||
item = value.get(key, default)
|
||||
if not isinstance(item, bool):
|
||||
raise ValueError(f"{key} must be a boolean")
|
||||
return item
|
||||
|
||||
|
||||
def _string(value: dict[str, object], key: str, default: str) -> str:
|
||||
item = value.get(key, default)
|
||||
if not isinstance(item, str) or not item:
|
||||
raise ValueError(f"{key} must be a non-empty string")
|
||||
return item
|
||||
|
||||
|
||||
def _optional_string(value: dict[str, object], key: str) -> str | None:
|
||||
item = value.get(key)
|
||||
if item is None:
|
||||
return None
|
||||
if not isinstance(item, str) or not item:
|
||||
raise ValueError(f"{key} must be null or a non-empty string")
|
||||
return item
|
||||
Reference in New Issue
Block a user