diff --git a/ptdiag/assbin.py b/ptdiag/assbin.py index df129e0..138d1d9 100644 --- a/ptdiag/assbin.py +++ b/ptdiag/assbin.py @@ -924,7 +924,7 @@ def _rewrite_wrapped_file( output: Path, patches: list[_BinaryPatch], ) -> tuple[str, int]: - ordered = sorted(patches, key=lambda patch: patch.offset) + ordered = sorted(patches, key=lambda item: item.offset) previous_end = 0 for patch in ordered: if patch.offset < previous_end: diff --git a/ptdiag/config.py b/ptdiag/config.py index 1c299b7..1613161 100644 --- a/ptdiag/config.py +++ b/ptdiag/config.py @@ -40,19 +40,11 @@ class GameConfig: virtual_desktop: str -@dataclass(frozen=True) -class SafetyConfig: - allow_live_game_modification: bool - allow_prefix_modification: bool - allow_binary_patch: bool - - @dataclass(frozen=True) class AppConfig: paths: PathsConfig proton7: Proton7Config game: GameConfig - safety: SafetyConfig source: Path def to_dict(self) -> dict[str, Any]: @@ -81,7 +73,6 @@ def load_config(path: str | Path | None = None) -> AppConfig: paths = _section(data, "paths") proton7 = _section(data, "proton7") game = _section(data, "game") - safety = _section(data, "safety") virtual_desktop = _required_string(game, "virtual_desktop") if re.fullmatch(r"[1-9][0-9]*x[1-9][0-9]*", virtual_desktop) is None: raise ConfigError("game.virtual_desktop must look like 1600x900") @@ -105,15 +96,6 @@ def load_config(path: str | Path | None = None) -> AppConfig: args=_required_string_tuple(game, "args"), virtual_desktop=virtual_desktop, ), - safety=SafetyConfig( - allow_live_game_modification=_required_bool( - safety, "allow_live_game_modification" - ), - allow_prefix_modification=_required_bool( - safety, "allow_prefix_modification" - ), - allow_binary_patch=_required_bool(safety, "allow_binary_patch"), - ), source=config_path, ) @@ -145,13 +127,6 @@ def _required_string_tuple(section: Mapping[str, Any], key: str) -> tuple[str, . return tuple(value) -def _required_bool(section: Mapping[str, Any], key: str) -> bool: - value = section.get(key) - if not isinstance(value, bool): - raise ConfigError(f"{key} must be true or false") - return value - - def _paths_to_strings(value: Any) -> Any: if isinstance(value, Path): return str(value) diff --git a/ptdiag/experiment.py b/ptdiag/experiment.py index 29d6983..141d5ba 100644 --- a/ptdiag/experiment.py +++ b/ptdiag/experiment.py @@ -12,7 +12,7 @@ import signal import subprocess import sys import time -from typing import Any, Mapping, Sequence, TextIO +from typing import Any, Mapping, Sequence from .config import AppConfig, project_root from .filesystem import sha256_file, verify_known_game_hashes @@ -190,7 +190,6 @@ def _run_standard_experiment( _dry_run_commands( config, testcase, - result, logger, registry_before, launch_argv, @@ -1025,7 +1024,6 @@ def _preflight( def _dry_run_commands( config: AppConfig, testcase: Testcase, - result: ResultDirectory, logger: CommandLogger, registry_before: DesktopRegistrySnapshot, launch_argv: list[str], @@ -1087,8 +1085,6 @@ def _run_and_watch( process: subprocess.Popen[str] | None = None process_identity: dict[str, Any] | None = None failure_state: LogFileState | None = None - failure_analysis: dict[str, Any] | None = None - failure_text = "" failure_trigger_mtime_ns: int | None = None current_state: LogFileState | None = None current_analysis: dict[str, Any] | None = None @@ -1121,6 +1117,7 @@ def _run_and_watch( error=str(exc), ) raise ExperimentError(f"Could not launch experiment: {exc}") from exc + assert process is not None logger.record_lifecycle( argv, state="started", @@ -1156,8 +1153,6 @@ def _run_and_watch( current_text = read_current_run_log_text(state, baseline) if analysis.load_texture_to_material_failed: failure_state = state - failure_analysis = analysis.to_summary() - failure_text = read_current_run_log_text(state, baseline) failure_trigger_mtime_ns = state.mtime_ns detected_at_ns = now_ns post_failure_deadline_ns = now_ns + int( @@ -1235,12 +1230,9 @@ def _run_and_watch( final_states = snapshot_native_logs(config.paths.native_logs) final_state = final_states.get(str(failure_state.path.absolute())) if final_state is not None: - failure_state = final_state - failure_text = read_current_run_log_text(final_state, baseline) - failure_analysis = parse_current_run_log(final_state, baseline).to_summary() current_state = final_state - current_text = failure_text - current_analysis = failure_analysis + current_text = read_current_run_log_text(final_state, baseline) + current_analysis = parse_current_run_log(final_state, baseline).to_summary() if current_state is not None: _write_text(result.path / "native.log", current_text) whispering_vale = ( @@ -1381,8 +1373,13 @@ def _launch_argv(config: AppConfig, testcase: Testcase, result_directory: Path) def _proton_command(config: AppConfig, *arguments: str) -> list[str]: - shim = Path(__file__).with_name("proton_compat.py") - return [sys.executable, str(shim), str(config.proton7.proton), *arguments] + shim = project_root() / "ptlinux/proton_compat.py" + return [ + sys.executable, + str(shim), + str(config.proton7.proton), + *arguments, + ] def _runtime_environment( @@ -1659,7 +1656,7 @@ def _collect_hashes(config: AppConfig, testcase: Testcase) -> list[dict[str, Any config.proton7.prefix / "drive_c/windows/syswow64/d3dcompiler_43.dll": "native-d3dcompiler-43", config.proton7.prefix / "drive_c/windows/syswow64/d3d9.dll": "dxvk-d3d9", - Path(__file__).with_name("proton_compat.py"): "proton-python314-shim", + project_root() / "ptlinux/proton_compat.py": "proton-python314-shim", config.source: "local-config", testcase.source: "testcase", } diff --git a/ptdiag/logs.py b/ptdiag/logs.py index 6e3478e..1833c18 100644 --- a/ptdiag/logs.py +++ b/ptdiag/logs.py @@ -11,7 +11,7 @@ from typing import Iterable, Mapping DEFAULT_NATIVE_LOG_PATTERNS = ("log_*.txt", "*.log") _SCENE_RE = re.compile( - r"\[S1\]\s+after import\s+(?P[^:]+):\s*" + r"\[S1]\s+after import\s+(?P[^:]+):\s*" r"materials=(?P\d+)\s+textures=(?P\d+)", re.IGNORECASE, ) @@ -24,14 +24,14 @@ _FAILED_IMPORT_RE = re.compile( re.IGNORECASE, ) _MODULE_CODE_SYNC_RE = re.compile( - r"Module Code Sync Error\s*\(\[(?P-?\d+)\]\s*!=\s*" - r"\[(?P-?\d+)\]\)", + r"Module Code Sync Error\s*\(\[(?P-?\d+)]\s*!=\s*" + r"\[(?P-?\d+)]\)", re.IGNORECASE, ) _TIMESTAMP_RE = re.compile( - r"^\s*(?:\[)?(?P" + r"^\s*\[?(?P" r"\d{4}-\d{2}-\d{2}[T ][0-9:.+Z-]+|\d{2}:\d{2}:\d{2}(?:\.\d+)?)" - r"(?:\])?" + r"]?" ) @@ -90,9 +90,6 @@ class NativeLogAnalysis: return imports[-1] return None - def first_event(self, kind: str) -> LogEvent | None: - return next((event for event in self.events if event.kind == kind), None) - def to_summary(self) -> dict[str, object]: summary: dict[str, object] = { "scenes": { diff --git a/ptdiag/proton_compat.py b/ptdiag/proton_compat.py deleted file mode 100644 index 5e3af2e..0000000 --- a/ptdiag/proton_compat.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Run the retained Proton 7 script safely on Python 3.14. - -Proton 7 predates Python's tar extraction-filter API. The known-good v41 -runtime used this compatibility shim; keeping it in the harness makes the -exact launch recipe inspectable and reproducible. -""" - -from __future__ import annotations - -from pathlib import Path -import runpy -import sys -import tarfile -from typing import Sequence - - -def main(argv: Sequence[str] | None = None) -> None: - arguments = list(sys.argv[1:] if argv is None else argv) - if not arguments: - raise SystemExit("usage: proton_compat.py /path/to/proton [arguments...]") - proton = Path(arguments[0]).expanduser().resolve(strict=True) - proton_directory = str(proton.parent) - if proton_directory not in sys.path: - sys.path.insert(0, proton_directory) - if hasattr(tarfile, "fully_trusted_filter"): - tarfile.TarFile.extraction_filter = staticmethod(tarfile.fully_trusted_filter) - sys.argv = [str(proton), *arguments[1:]] - runpy.run_path(str(proton), run_name="__main__") - - -if __name__ == "__main__": - main() diff --git a/ptdiag/registry.py b/ptdiag/registry.py index 9781d60..5986aaa 100644 --- a/ptdiag/registry.py +++ b/ptdiag/registry.py @@ -11,7 +11,7 @@ from .filesystem import sha256_file _VALUE_RE = re.compile(r'^(?P"(?:\\.|[^"\\])*")=(?P.*)$') -_SECTION_RE = re.compile(r"^\[(?P.*)\](?:\s+[0-9]+)?$") +_SECTION_RE = re.compile(r"^\[(?P.*)](?:\s+[0-9]+)?$") @dataclass(frozen=True) diff --git a/ptdiag/testcase.py b/ptdiag/testcase.py index 0027184..642366b 100644 --- a/ptdiag/testcase.py +++ b/ptdiag/testcase.py @@ -29,18 +29,12 @@ class TestDisplay: @dataclass(frozen=True) class TestWatch: require_new_native_log: bool - patterns: tuple[str, ...] @dataclass(frozen=True) class TestCapture: process_maps: bool - cwd: bool - environment: bool dll_overrides: bool - hashes: bool - dxvk_log: bool - native_log: bool strace_files: bool performance_stats: bool focus_before_seconds: float @@ -123,16 +117,10 @@ def load_testcase(value: str | Path) -> Testcase: env=_string_mapping(environment, "env"), watch=TestWatch( require_new_native_log=_boolean(watch, "require_new_native_log"), - patterns=_string_tuple(watch, "patterns"), ), capture=TestCapture( process_maps=_boolean(capture, "process_maps"), - cwd=_boolean(capture, "cwd"), - environment=_boolean(capture, "environment"), dll_overrides=_boolean(capture, "dll_overrides"), - hashes=_boolean(capture, "hashes"), - dxvk_log=_boolean(capture, "dxvk_log"), - native_log=_boolean(capture, "native_log"), strace_files=_boolean(capture, "strace_files"), performance_stats=_optional_boolean(capture, "performance_stats", False), focus_before_seconds=_optional_nonnegative_number( diff --git a/ptlinux/changelog.py b/ptlinux/changelog.py index 0058bdd..109a7a0 100644 --- a/ptlinux/changelog.py +++ b/ptlinux/changelog.py @@ -176,7 +176,7 @@ def _parse_patch_note(value: object) -> tuple[float, PatchNote] | None: sections=_parse_sections(value.get("changes")), published_at=_format_timestamp(timestamp), ) - return (timestamp if timestamp is not None else -1.0, patch) + return timestamp if timestamp is not None else -1.0, patch def _parse_tags(value: object) -> tuple[str, ...]: diff --git a/ptlinux/depot.py b/ptlinux/depot.py index b24928e..2197639 100644 --- a/ptlinux/depot.py +++ b/ptlinux/depot.py @@ -782,13 +782,15 @@ class DepotClient: try: response = self._opener(request, timeout=self.timeout_seconds) except HTTPError as exc: - if exc.code == 416 and requested_offset: + status = exc.code + exc.close() + if status == 416 and requested_offset: raise _RetryableDownloadError( f"server rejected the saved offset for {file.path}", discard_partial=True, ) from exc raise _RetryableDownloadError( - f"HTTP {exc.code} from {server} for {file.path}" + f"HTTP {status} from {server} for {file.path}" ) from exc except (OSError, HTTPException) as exc: raise _RetryableDownloadError( diff --git a/ptlinux/download.py b/ptlinux/download.py index 1c4bfff..2838c53 100644 --- a/ptlinux/download.py +++ b/ptlinux/download.py @@ -64,9 +64,11 @@ def fetch_official_launcher( try: response = urlopen(request, timeout=timeout_seconds) except HTTPError as exc: - if exc.code == 304 and destination.is_file(): + status = exc.code + exc.close() + if status == 304 and destination.is_file(): return _existing_download(destination, metadata, url) - raise DownloadError(f"official launcher request failed: HTTP {exc.code}") from exc + raise DownloadError(f"official launcher request failed: HTTP {status}") from exc except OSError as exc: raise DownloadError(f"official launcher request failed: {exc}") from exc diff --git a/ptlinux/launcher.py b/ptlinux/launcher.py index 21eaf8f..7d49f01 100644 --- a/ptlinux/launcher.py +++ b/ptlinux/launcher.py @@ -2,7 +2,7 @@ from __future__ import annotations -from dataclasses import asdict, dataclass +from dataclasses import dataclass from datetime import datetime import hashlib import json @@ -266,10 +266,6 @@ def launch_official_updater( ) -def patch_result_dict(result: LauncherPatchResult) -> dict[str, object]: - return asdict(result) - - def _sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: diff --git a/ptlinux/login_trace.py b/ptlinux/login_trace.py index 27c4bca..2c013b0 100644 --- a/ptlinux/login_trace.py +++ b/ptlinux/login_trace.py @@ -1,7 +1,4 @@ -"""Retired login instrumentation retained only for command compatibility.""" - -from __future__ import annotations - +"""Retirement state for the disabled login-trace command.""" DEFAULT_TRACE_TIMEOUT_SECONDS = 10 * 60.0 LOGIN_TRACE_ENABLED = False @@ -12,9 +9,3 @@ LOGIN_TRACE_DISABLED_REASON = ( "credential-free check, then use wine-update for one controlled comparison " "through the unmodified publisher launcher." ) - - -def run_login_trace(*_args: object, **_kwargs: object) -> None: - """Refuse to reactivate the retired relay-based authentication trace.""" - - raise RuntimeError(LOGIN_TRACE_DISABLED_REASON) diff --git a/ptlinux/overlay.py b/ptlinux/overlay.py index 09baecf..8abd502 100644 --- a/ptlinux/overlay.py +++ b/ptlinux/overlay.py @@ -1113,14 +1113,6 @@ def _read_legacy_overlay_result(path: Path) -> LegacyOverlayResult | None: ) -def _runtime_game_binary_sources( - paths: AppPaths, - source: Path, -) -> tuple[str, Path, Path]: - action, game_exe, game_dll, _ = _runtime_game_binary_profile(paths, source) - return action, game_exe, game_dll - - def _runtime_game_binary_profile( paths: AppPaths, source: Path, diff --git a/ptlinux/paths.py b/ptlinux/paths.py index 6904ed2..5353317 100644 --- a/ptlinux/paths.py +++ b/ptlinux/paths.py @@ -239,28 +239,6 @@ class AppPaths: def compatibility_manifest(self) -> Path: return self.state / "compatibility-binaries.json" - @property - def runtime_patch_directory(self) -> Path: - """Retired 0.5.8 disk-patch cache; never selected for play.""" - - return self.data / "runtime-patches" / "v21-post-auth-reclaim" - - @property - def runtime_patch_dll(self) -> Path: - """Retired 0.5.8 DLL retained only so diagnostics can identify it.""" - - return self.runtime_patch_directory / "game.dll" - - @property - def runtime_patch_manifest(self) -> Path: - return self.state / "runtime-patch.json" - - @property - def retired_runtime_patch_dll(self) -> Path: - """The pre-0.5.7 all-session patch, retained only for diagnostics.""" - - return self.data / "runtime-patches" / "v21-texture-reclaim" / "game.dll" - @property def compat_data(self) -> Path: return self.data / "game-compat" diff --git a/ptlinux/runtime.py b/ptlinux/runtime.py index 6e186a5..8f2c33d 100644 --- a/ptlinux/runtime.py +++ b/ptlinux/runtime.py @@ -507,36 +507,6 @@ def _resolve_runtime_path_case(root: Path, relative: PurePosixPath) -> Path: return cursor -def run_proton_prefix_command( - paths: AppPaths, - selection: RuntimeSelection, - runner: CommandRunner, - arguments: Sequence[str], - *, - check: bool = True, - timeout_seconds: float = 60.0, -) -> CommandOutcome: - """Run a maintenance command in the pinned Proton 7 prefix.""" - - if selection.kind not in {"proton7", "proton7-container"}: - raise RuntimeErrorPT("the login trace requires the pinned Proton 7 runtime") - if not selection.proton_path or not selection.steam_root: - raise RuntimeErrorPT("incomplete Proton 7 runtime selection") - return runner.run( - [ - sys.executable, - "-m", - "ptlinux.proton_compat", - selection.proton_path, - "runinprefix", - *arguments, - ], - env_overrides=_proton_environment(paths, selection), - timeout_seconds=timeout_seconds, - check=check, - ) - - def run_runtime_prefix_command( paths: AppPaths, selection: RuntimeSelection, diff --git a/ptlinux/self_update.py b/ptlinux/self_update.py index 502889b..5c9e555 100644 --- a/ptlinux/self_update.py +++ b/ptlinux/self_update.py @@ -603,7 +603,9 @@ def _fetch_small_file( try: response = urlopen(request, timeout=timeout_seconds) except HTTPError as exc: - raise LauncherUpdateError(f"{label} request failed: HTTP {exc.code}") from exc + status = exc.code + exc.close() + raise LauncherUpdateError(f"{label} request failed: HTTP {status}") from exc except (URLError, OSError) as exc: raise LauncherUpdateError(f"{label} request failed: {exc}") from exc with response: @@ -695,8 +697,10 @@ def _download_archive( try: response = urlopen(request, timeout=timeout_seconds) except HTTPError as exc: + status = exc.code + exc.close() raise LauncherUpdateError( - f"launcher archive request failed: HTTP {exc.code}" + f"launcher archive request failed: HTTP {status}" ) from exc except (URLError, OSError) as exc: raise LauncherUpdateError(f"launcher archive request failed: {exc}") from exc diff --git a/ptlinux/ui/components/theme.py b/ptlinux/ui/components/theme.py index ce640b0..933b0fa 100644 --- a/ptlinux/ui/components/theme.py +++ b/ptlinux/ui/components/theme.py @@ -5,6 +5,130 @@ from __future__ import annotations from typing import Any +def _settings_checkbox_image( + root: Any, + *, + size: int, + gap: int, + fill: str, + border: str, + mark: str | None = None, +) -> Any: + """Create one scaled checkbox indicator from the launcher palette.""" + + import tkinter as tk + + image = tk.PhotoImage(master=root, width=size + gap, height=size) + border_width = max(1, round(size / 16)) + image.put(fill, to=(0, 0, size, size)) + image.put(border, to=(0, 0, size, border_width)) + image.put(border, to=(0, size - border_width, size, size)) + image.put(border, to=(0, 0, border_width, size)) + image.put(border, to=(size - border_width, 0, size, size)) + if mark is not None: + stroke = max(2, round(size / 8)) + for source_x, source_y in ( + (4, 8), + (5, 9), + (6, 10), + (7, 11), + (8, 10), + (9, 9), + (10, 8), + (11, 7), + (12, 6), + ): + x = round(source_x * size / 16) + y = round(source_y * size / 16) + image.put( + mark, + to=( + x, + y, + min(size - border_width, x + stroke), + min(size - border_width, y + stroke), + ), + ) + return image + + +def _configure_settings_checkbox_indicator( + root: Any, + style: Any, + launcher_ui_scale: float, +) -> None: + """Replace the platform X indicator with the launcher's checkmark.""" + + element_name = "Settings.Checkbutton.indicator" + if element_name in style.element_names(): + return + size = max(16, round(16 * launcher_ui_scale)) + gap = max(9, round(9 * launcher_ui_scale)) + palettes = { + "unchecked": ("#100d0c", "#6a4d38", None), + "unchecked_active": ("#211916", "#d98927", None), + "unchecked_focus": ("#100d0c", "#f4bd61", None), + "checked": ("#d98927", "#f4bd61", "#24170f"), + "checked_active": ("#efaa4c", "#fff0c2", "#24170f"), + "checked_focus": ("#d98927", "#fff0c2", "#24170f"), + "disabled": ("#171412", "#30251f", None), + "checked_disabled": ("#6c4826", "#776b62", "#30251f"), + } + images = { + name: _settings_checkbox_image( + root, + size=size, + gap=gap, + fill=fill, + border=border, + mark=mark, + ) + for name, (fill, border, mark) in palettes.items() + } + style.element_create( + element_name, + "image", + images["unchecked"], + ("disabled selected", images["checked_disabled"]), + ("disabled", images["disabled"]), + ("pressed selected", images["checked_active"]), + ("active selected", images["checked_active"]), + ("focus selected", images["checked_focus"]), + ("selected", images["checked"]), + ("pressed", images["unchecked_active"]), + ("active", images["unchecked_active"]), + ("focus", images["unchecked_focus"]), + ) + style.layout( + "Settings.TCheckbutton", + [ + ( + "Checkbutton.padding", + { + "sticky": "nswe", + "children": [ + (element_name, {"side": "left", "sticky": ""}), + ( + "Checkbutton.focus", + { + "side": "left", + "sticky": "w", + "children": [ + ( + "Checkbutton.label", + {"sticky": "nswe"}, + ) + ], + }, + ), + ], + }, + ), + ], + ) + setattr(root, "_ptlinux_settings_checkbox_images", tuple(images.values())) + + def configure_dashboard_style(root: Any, ttk: Any, launcher_ui_scale: float) -> None: style = ttk.Style(root) style.theme_use("clam") @@ -100,8 +224,8 @@ def configure_dashboard_style(root: Any, ttk: Any, launcher_ui_scale: float) -> style.configure( "SettingsText.TLabel", background="#191311", - foreground="#d8cbbd", - font=("Sans", 9), + foreground="#e5b66c", + font=("Sans", 8, "bold"), ) style.configure( "SettingsHint.TLabel", @@ -165,30 +289,110 @@ def configure_dashboard_style(root: Any, ttk: Any, launcher_ui_scale: float) -> "Settings.TEntry", fieldbackground="#100d0c", foreground="#f8ead2", - insertcolor="#f8fafc", + insertcolor="#f4bd61", + selectbackground="#6a482d", + selectforeground="#fff8e9", + font=("Sans", 9), + padding=(10, 7), bordercolor="#4a3528", lightcolor="#4a3528", darkcolor="#4a3528", + borderwidth=1, + relief="flat", focuscolor="#f4bd61", - focusthickness=2, + focusthickness=1, + ) + style.map( + "Settings.TEntry", + fieldbackground=[ + ("disabled", "#171412"), + ("!disabled", "#100d0c"), + ], + foreground=[ + ("disabled", "#776b62"), + ("!disabled", "#f8ead2"), + ], + bordercolor=[ + ("disabled", "#30251f"), + ("focus", "#f4bd61"), + ("!disabled", "#4a3528"), + ], + lightcolor=[ + ("focus", "#f4bd61"), + ("!focus", "#4a3528"), + ], + darkcolor=[ + ("focus", "#f4bd61"), + ("!focus", "#4a3528"), + ], ) - style.map("Settings.TEntry", bordercolor=[("focus", "#f4bd61")]) style.configure( "Settings.TCombobox", fieldbackground="#100d0c", - background="#211916", + background="#191311", foreground="#f8ead2", arrowcolor="#e5b66c", + selectbackground="#6a482d", + selectforeground="#fff8e9", + font=("Sans", 9), + padding=(10, 7), + arrowsize=max(12, round(12 * launcher_ui_scale)), bordercolor="#4a3528", + lightcolor="#4a3528", + darkcolor="#4a3528", + borderwidth=1, + relief="flat", focuscolor="#f4bd61", - focusthickness=2, + focusthickness=1, ) style.map( "Settings.TCombobox", - fieldbackground=[("readonly", "#100d0c")], - foreground=[("readonly", "#f8ead2")], - bordercolor=[("focus", "#f4bd61")], + fieldbackground=[ + ("disabled", "#171412"), + ("readonly", "#100d0c"), + ], + background=[ + ("disabled", "#171412"), + ("pressed", "#32251f"), + ("active", "#2a201c"), + ("readonly", "#191311"), + ], + foreground=[ + ("disabled", "#776b62"), + ("readonly", "#f8ead2"), + ], + arrowcolor=[ + ("disabled", "#776b62"), + ("pressed", "#fff0c2"), + ("active", "#f4bd61"), + ("readonly", "#e5b66c"), + ], + bordercolor=[ + ("disabled", "#30251f"), + ("focus", "#f4bd61"), + ("active", "#6a4d38"), + ("readonly", "#4a3528"), + ], + lightcolor=[ + ("focus", "#f4bd61"), + ("!focus", "#4a3528"), + ], + darkcolor=[ + ("focus", "#f4bd61"), + ("!focus", "#4a3528"), + ], ) + root.option_add("*TCombobox*Listbox.background", "#100d0c") + root.option_add("*TCombobox*Listbox.foreground", "#f8ead2") + root.option_add("*TCombobox*Listbox.selectBackground", "#6a482d") + root.option_add("*TCombobox*Listbox.selectForeground", "#fff8e9") + root.option_add("*TCombobox*Listbox.font", "Sans 9") + root.option_add("*TCombobox*Listbox.borderWidth", 1) + root.option_add("*TCombobox*Listbox.highlightThickness", 1) + root.option_add("*TCombobox*Listbox.highlightBackground", "#4a3528") + root.option_add("*TCombobox*Listbox.highlightColor", "#f4bd61") + root.option_add("*TCombobox*Listbox.relief", "flat") + root.option_add("*TCombobox*Listbox.activeStyle", "none") style.configure( "Realm.TCombobox", fieldbackground="#211916", @@ -239,18 +443,31 @@ def configure_dashboard_style(root: Any, ttk: Any, launcher_ui_scale: float) -> root.option_add("*realm_selector*Listbox.highlightThickness", 0) root.option_add("*realm_selector*Listbox.relief", "flat") root.option_add("*realm_selector*Listbox.activeStyle", "none") + _configure_settings_checkbox_indicator(root, style, launcher_ui_scale) style.configure( "Settings.TCheckbutton", - background="#191311", + background="#171210", foreground="#d8cbbd", font=("Sans", 9), + padding=(9, 7), + relief="flat", focuscolor="#f4bd61", - focusthickness=2, + focusthickness=1, ) style.map( "Settings.TCheckbutton", - background=[("active", "#191311")], - foreground=[("active", "#fff0d2")], + background=[ + ("disabled", "#15110f"), + ("pressed", "#2a201c"), + ("active", "#241b17"), + ("selected", "#211916"), + ("!selected", "#171210"), + ], + foreground=[ + ("disabled", "#776b62"), + ("active", "#fff0d2"), + ("selected", "#f8ead2"), + ], ) style.configure( "PreloaderStatus.TLabel", diff --git a/ptlinux/ui/dashboard.py b/ptlinux/ui/dashboard.py index 43f5c35..1a8bf89 100644 --- a/ptlinux/ui/dashboard.py +++ b/ptlinux/ui/dashboard.py @@ -199,8 +199,6 @@ def patch_change_colors(type_key: str) -> tuple[str, str]: }.get(type_key.casefold(), ("#2a201c", "#d8aa61")) - - def run_dashboard(paths: AppPaths) -> None: try: import tkinter as tk @@ -677,19 +675,9 @@ def run_dashboard(paths: AppPaths) -> None: responsive_layout_after: str | None = None buttons = ( play_button, - update_button, - repair_button, - doctor_button, - settings_button, - import_button, - launcher_update_button, - ) - game_mutation_buttons = ( - update_button, - repair_button, - import_button, - launcher_update_button, + *action_widgets.operation_buttons, ) + game_mutation_buttons = action_widgets.mutation_buttons def append_log(value: str) -> None: nonlocal activity_log_lines @@ -1050,11 +1038,11 @@ def run_dashboard(paths: AppPaths) -> None: append_log(f"$ {' '.join(command)}") def worker() -> None: - def publish_output(line: str) -> None: + def publish_output(output_line: str) -> None: if game_client_id is None: - events.put(("output", line)) + events.put(("output", output_line)) else: - events.put(("output", f"[Client {game_client_id}] {line}")) + events.put(("output", f"[Client {game_client_id}] {output_line}")) if ( game_client_id is not None diff --git a/ptlinux/ui/dialogs/settings_dialog.py b/ptlinux/ui/dialogs/settings_dialog.py index 8d9f306..1b2aa60 100644 --- a/ptlinux/ui/dialogs/settings_dialog.py +++ b/ptlinux/ui/dialogs/settings_dialog.py @@ -164,6 +164,7 @@ def open_settings_dialog( def add_choice( parent: ttk.LabelFrame, row: int, + name: str, label: str, description: str, variable: tk.StringVar, @@ -181,15 +182,20 @@ def open_settings_dialog( ) ttk.Combobox( parent, + name=name, textvariable=variable, values=choices, state="readonly", style="Settings.TCombobox", + cursor="hand2", + exportselection=False, + takefocus=True, ).grid(row=row + 1, column=0, columnspan=2, sticky="ew", pady=(3, 9)) def add_path( parent: ttk.LabelFrame, row: int, + name: str, label: str, description: str, variable: tk.StringVar, @@ -205,7 +211,14 @@ def open_settings_dialog( column=1, sticky="e", ) - ttk.Entry(parent, textvariable=variable, style="Settings.TEntry").grid( + ttk.Entry( + parent, + name=name, + textvariable=variable, + style="Settings.TEntry", + exportselection=False, + takefocus=True, + ).grid( row=row + 1, column=0, columnspan=1, @@ -295,22 +308,27 @@ def open_settings_dialog( wraplength=360, ).grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 11)) presentation = ttk.Frame(display_card, style="SettingsCard.TFrame") - presentation.grid(row=1, column=0, columnspan=2, sticky="w") + presentation.grid(row=1, column=0, columnspan=2, sticky="ew") ttk.Checkbutton( presentation, text="Remove title bar and borders (Borderless)", variable=borderless_value, style="Settings.TCheckbutton", - ).pack(anchor="w") + cursor="hand2", + takefocus=True, + ).pack(fill="x") ttk.Checkbutton( presentation, text="Suppress false 'not responding' warnings", variable=suppress_unresponsive_value, style="Settings.TCheckbutton", - ).pack(anchor="w", pady=(7, 0)) + cursor="hand2", + takefocus=True, + ).pack(fill="x", pady=(7, 0)) add_choice( display_card, 2, + "settings_selector_ui_scale", "Launcher size", "Applies after launcher restart", ui_scale_value, @@ -335,6 +353,7 @@ def open_settings_dialog( add_choice( rendering_card, 1, + "settings_selector_runtime", "Runtime", "Compatibility layer", runtime_value, @@ -343,6 +362,7 @@ def open_settings_dialog( add_choice( rendering_card, 3, + "settings_selector_proton_sync", "Proton synchronization", "Automatic selects the compatible mode", proton_sync_value, @@ -378,7 +398,9 @@ def open_settings_dialog( text="Low-bandwidth mode (cached news, up to 2 workers and 5 MB/s)", variable=low_bandwidth_value, style="Settings.TCheckbutton", - ).grid(row=1, column=0, sticky="w", padx=(0, 16)) + cursor="hand2", + takefocus=True, + ).grid(row=1, column=0, rowspan=2, sticky="ew", padx=(0, 16)) ttk.Label( downloads_card, text="Speed limit", @@ -391,12 +413,16 @@ def open_settings_dialog( ).grid(row=1, column=2, sticky="w", padx=(8, 0)) download_speed_selector = ttk.Combobox( downloads_card, + name="settings_selector_download_speed", textvariable=download_speed_value, values=tuple(DOWNLOAD_SPEED_PRESETS_MBPS) + ( DOWNLOAD_SPEED_CUSTOM_LABEL, ), state="readonly", style="Settings.TCombobox", + cursor="hand2", + exportselection=False, + takefocus=True, ) download_speed_selector.grid( row=2, @@ -406,9 +432,13 @@ def open_settings_dialog( ) custom_download_speed = ttk.Entry( downloads_card, + name="settings_download_speed", textvariable=custom_download_speed_value, style="Settings.TEntry", width=12, + justify="right", + exportselection=False, + takefocus=True, ) custom_download_speed.grid( row=2, @@ -449,6 +479,7 @@ def open_settings_dialog( add_path( overrides_card, 1, + "settings_proton_path", "Proton script", "Use a specific Proton executable", proton_path_value, @@ -457,6 +488,7 @@ def open_settings_dialog( add_path( overrides_card, 3, + "settings_game_source", "Import source", "Seed a new managed installation only", game_source_value, diff --git a/ptlinux/umu.py b/ptlinux/umu.py index 1a21955..7e29523 100644 --- a/ptlinux/umu.py +++ b/ptlinux/umu.py @@ -187,8 +187,10 @@ def _download_archive( try: response = opener(request, timeout=timeout_seconds) except HTTPError as exc: + status = exc.code + exc.close() raise UmuBootstrapError( - f"portable UMU request failed: HTTP {exc.code}" + f"portable UMU request failed: HTTP {status}" ) from exc except OSError as exc: raise UmuBootstrapError(f"portable UMU request failed: {exc}") from exc