From 61078fae69c025d714380459b774b8519b469802 Mon Sep 17 00:00:00 2001 From: evertonstz Date: Sun, 12 Oct 2025 15:46:30 -0300 Subject: [PATCH 1/8] Add support for pre_script_args and post_script_args in Manager and WineExecutor --- bottles/backend/managers/manager.py | 2 ++ bottles/backend/wine/executor.py | 12 ++++++++++++ bottles/backend/wine/start.py | 4 ++++ 3 files changed, 18 insertions(+) diff --git a/bottles/backend/managers/manager.py b/bottles/backend/managers/manager.py index 2413fb11b6..e969feea47 100644 --- a/bottles/backend/managers/manager.py +++ b/bottles/backend/managers/manager.py @@ -723,7 +723,9 @@ def get_programs(self, config: BottleConfig) -> List[dict]: "path": _program.get("path"), "icon": "com.usebottles.bottles-program", "pre_script": _program.get("pre_script"), + "pre_script_args": _program.get("pre_script_args"), "post_script": _program.get("post_script"), + "post_script_args": _program.get("post_script_args"), "folder": _program.get("folder", program_folder), "dxvk": _program.get("dxvk"), "vkd3d": _program.get("vkd3d"), diff --git a/bottles/backend/wine/executor.py b/bottles/backend/wine/executor.py index 7c4646e551..cd83645c9e 100644 --- a/bottles/backend/wine/executor.py +++ b/bottles/backend/wine/executor.py @@ -33,6 +33,8 @@ def __init__( move_upd_fn: callable = None, pre_script: Optional[str] = None, post_script: Optional[str] = None, + pre_script_args: Optional[str] = None, + post_script_args: Optional[str] = None, cwd: Optional[str] = None, monitoring: Optional[list] = None, program_dxvk: Optional[bool] = None, @@ -62,6 +64,8 @@ def __init__( self.environment = environment self.pre_script = pre_script self.post_script = post_script + self.pre_script_args = pre_script_args + self.post_script_args = post_script_args self.cwd = self.__get_cwd(cwd) self.monitoring = monitoring self.use_gamescope = program_gamescope @@ -122,6 +126,8 @@ def run_program(cls, config: BottleConfig, program: dict, terminal: bool = False args=program.get("arguments"), pre_script=program.get("pre_script"), post_script=program.get("post_script"), + pre_script_args=program.get("pre_script_args"), + post_script_args=program.get("post_script_args"), cwd=program.get("folder"), terminal=terminal, program_dxvk=program.get("dxvk"), @@ -212,6 +218,8 @@ def run_cli(self): environment=self.environment, pre_script=self.pre_script, post_script=self.post_script, + pre_script_args=self.pre_script_args, + post_script_args=self.post_script_args, cwd=self.cwd, ) return Result(status=True, data={"output": res}) @@ -278,6 +286,8 @@ def __launch_exe(self): communicate=True, pre_script=self.pre_script, post_script=self.post_script, + pre_script_args=self.pre_script_args, + post_script_args=self.post_script_args, cwd=self.cwd, ) res = winecmd.run() @@ -316,6 +326,8 @@ def __launch_with_starter(self): environment=self.environment, pre_script=self.pre_script, post_script=self.post_script, + pre_script_args=self.pre_script_args, + post_script_args=self.post_script_args, cwd=self.cwd, ) self.__set_monitors() diff --git a/bottles/backend/wine/start.py b/bottles/backend/wine/start.py index b88738ec11..cb6b55ee47 100644 --- a/bottles/backend/wine/start.py +++ b/bottles/backend/wine/start.py @@ -19,6 +19,8 @@ def run( environment: Optional[dict] = None, pre_script: Optional[str] = None, post_script: Optional[str] = None, + pre_script_args: Optional[str] = None, + post_script_args: Optional[str] = None, cwd: Optional[str] = None, ): winepath = WinePath(self.config) @@ -41,6 +43,8 @@ def run( environment=environment, pre_script=pre_script, post_script=post_script, + pre_script_args=pre_script_args, + post_script_args=post_script_args, cwd=cwd, minimal=False, action_name="run", From 15c324270cef966ef6542fedf96e7687aca59dbf Mon Sep 17 00:00:00 2001 From: evertonstz Date: Sun, 12 Oct 2025 15:51:15 -0300 Subject: [PATCH 2/8] Add pre_script_args and post_script_args support in WineCommand and WineProgram Enhance the WineCommand and WineProgram classes to accept pre-run and post-run script arguments. Update the UI to include fields for these arguments in the launch options dialog. --- bottles/backend/wine/winecommand.py | 21 ++++++++++++++++--- bottles/backend/wine/wineprogram.py | 4 ++++ bottles/frontend/ui/dialog-launch-options.blp | 12 +++++++++++ bottles/frontend/windows/launchoptions.py | 21 +++++++++++++++++++ 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/bottles/backend/wine/winecommand.py b/bottles/backend/wine/winecommand.py index b8e747eba7..4954aab151 100644 --- a/bottles/backend/wine/winecommand.py +++ b/bottles/backend/wine/winecommand.py @@ -99,6 +99,8 @@ def __init__( minimal: bool = False, # avoid gamemode/gamescope usage pre_script: Optional[str] = None, post_script: Optional[str] = None, + pre_script_args: Optional[str] = None, + post_script_args: Optional[str] = None, cwd: Optional[str] = None, ): _environment = environment.copy() @@ -113,7 +115,7 @@ def __init__( else self.config.Parameters.gamescope ) self.command = self.get_cmd( - command, pre_script, post_script, environment=_environment + command, pre_script, post_script, pre_script_args, post_script_args, environment=_environment ) self.terminal = terminal self.env = self.get_env(_environment) @@ -489,6 +491,8 @@ def get_cmd( command, pre_script: Optional[str] = None, post_script: Optional[str] = None, + pre_script_args: Optional[str] = None, + post_script_args: Optional[str] = None, return_steam_cmd: bool = False, return_clean_cmd: bool = False, environment: Optional[dict] = None, @@ -598,10 +602,18 @@ def get_cmd( environment.update(extracted_env) if post_script not in (None, ""): - command = f"{command} ; sh '{post_script}'" + post_cmd_parts = [post_script] + if post_script_args not in (None, ""): + post_cmd_parts.extend(shlex.split(post_script_args)) + post_cmd = " ".join(shlex.quote(part) for part in post_cmd_parts) + command = f"{command} ; sh {post_cmd}" if pre_script not in (None, ""): - command = f"sh '{pre_script}' ; {command}" + pre_cmd_parts = [pre_script] + if pre_script_args not in (None, ""): + pre_cmd_parts.extend(shlex.split(pre_script_args)) + pre_cmd = " ".join(shlex.quote(part) for part in pre_cmd_parts) + command = f"sh {pre_cmd} ; {command}" return command @@ -698,6 +710,9 @@ def run(self) -> Result[Optional[str]]: False, message="runner or env is not ready, Wine command terminated." ) + # Log the final command that will be executed + logging.info(f"Executing command: {self.command}") + if vmtouch_available and self.config.Parameters.vmtouch and not self.terminal: self._vmtouch_preload() diff --git a/bottles/backend/wine/wineprogram.py b/bottles/backend/wine/wineprogram.py index df111ab313..a24b8dbeb3 100644 --- a/bottles/backend/wine/wineprogram.py +++ b/bottles/backend/wine/wineprogram.py @@ -45,6 +45,8 @@ def launch( environment: Optional[dict] = None, pre_script: Optional[str] = None, post_script: Optional[str] = None, + pre_script_args: Optional[str] = None, + post_script_args: Optional[str] = None, cwd: Optional[str] = None, action_name: str = "launch", ): @@ -72,6 +74,8 @@ def launch( environment=environment, pre_script=pre_script, post_script=post_script, + pre_script_args=pre_script_args, + post_script_args=post_script_args, cwd=cwd, arguments=program_args, ) diff --git a/bottles/frontend/ui/dialog-launch-options.blp b/bottles/frontend/ui/dialog-launch-options.blp index ebd35d227e..fe4433975f 100644 --- a/bottles/frontend/ui/dialog-launch-options.blp +++ b/bottles/frontend/ui/dialog-launch-options.blp @@ -79,6 +79,12 @@ template $LaunchOptionsDialog: Adw.Window { } } + Adw.EntryRow entry_pre_script_args { + title: _("Pre-run Script Arguments"); + tooltip-text: _("e.g.: ludusavi restore --force Game Name"); + visible: false; + } + Adw.ActionRow action_post_script { activatable-widget: btn_post_script; title: _("Post-run Script"); @@ -110,6 +116,12 @@ template $LaunchOptionsDialog: Adw.Window { } } + Adw.EntryRow entry_post_script_args { + title: _("Post-run Script Arguments"); + tooltip-text: _("e.g.: ludusavi backup --force Game Name"); + visible: false; + } + Adw.ActionRow action_cwd { activatable-widget: btn_cwd; title: _("Working Directory"); diff --git a/bottles/frontend/windows/launchoptions.py b/bottles/frontend/windows/launchoptions.py index 1b1550d013..9344fb261d 100644 --- a/bottles/frontend/windows/launchoptions.py +++ b/bottles/frontend/windows/launchoptions.py @@ -36,8 +36,10 @@ class LaunchOptionsDialog(Adw.Window): btn_save = Gtk.Template.Child() btn_pre_script = Gtk.Template.Child() btn_pre_script_reset = Gtk.Template.Child() + entry_pre_script_args = Gtk.Template.Child() btn_post_script = Gtk.Template.Child() btn_post_script_reset = Gtk.Template.Child() + entry_post_script_args = Gtk.Template.Child() btn_cwd = Gtk.Template.Child() btn_cwd_reset = Gtk.Template.Child() btn_reset_defaults = Gtk.Template.Child() @@ -172,10 +174,16 @@ def __init__(self, parent, config, program, **kwargs): if program.get("pre_script") not in ("", None): self.action_pre_script.set_subtitle(program["pre_script"]) self.btn_pre_script_reset.set_visible(True) + self.entry_pre_script_args.set_visible(True) + if program.get("pre_script_args") not in ("", None): + self.entry_pre_script_args.set_text(program["pre_script_args"]) if program.get("post_script") not in ("", None): self.action_post_script.set_subtitle(program["post_script"]) self.btn_post_script_reset.set_visible(True) + self.entry_post_script_args.set_visible(True) + if program.get("post_script_args") not in ("", None): + self.entry_post_script_args.set_text(program["post_script_args"]) if program.get("folder") not in ( "", @@ -218,6 +226,11 @@ def __idle_save(self, *_args): "virtual_desktop", program_virt_desktop, self.global_virt_desktop ) self.program["arguments"] = self.entry_arguments.get_text() + + pre_args = self.entry_pre_script_args.get_text() + post_args = self.entry_post_script_args.get_text() + self.program["pre_script_args"] = pre_args if pre_args else None + self.program["post_script_args"] = post_args if post_args else None self.config = self.manager.update_config( config=self.config, @@ -247,6 +260,7 @@ def set_path(dialog, result): self.program["pre_script"] = file_path self.action_pre_script.set_subtitle(file_path) self.btn_pre_script_reset.set_visible(True) + self.entry_pre_script_args.set_visible(True) except GLib.Error as error: # also thrown when dialog has been cancelled @@ -278,6 +292,7 @@ def set_path(dialog, result): self.program["post_script"] = file_path self.action_post_script.set_subtitle(file_path) self.btn_post_script_reset.set_visible(True) + self.entry_post_script_args.set_visible(True) except GLib.Error as error: # also thrown when dialog has been cancelled if error.code == 2: @@ -297,13 +312,19 @@ def set_path(dialog, result): def __reset_pre_script(self, *_args): self.program["pre_script"] = None + self.program["pre_script_args"] = None self.action_pre_script.set_subtitle(self.__default_pre_script_msg) self.btn_pre_script_reset.set_visible(False) + self.entry_pre_script_args.set_visible(False) + self.entry_pre_script_args.set_text("") def __reset_post_script(self, *_args): self.program["post_script"] = None + self.program["post_script_args"] = None self.action_post_script.set_subtitle(self.__default_post_script_msg) self.btn_post_script_reset.set_visible(False) + self.entry_post_script_args.set_visible(False) + self.entry_post_script_args.set_text("") def __choose_cwd(self, *_args): def set_path(dialog, result): From 15b3066bf780ca0047f3acacc8a18ed3a39f7718 Mon Sep 17 00:00:00 2001 From: evertonstz Date: Sun, 12 Oct 2025 17:09:30 -0300 Subject: [PATCH 3/8] Implement placeholder handling in WineExecutor and add unit tests --- bottles/backend/wine/executor.py | 53 ++++++++-- bottles/tests/backend/wine/__init__.py | 0 bottles/tests/backend/wine/test_executor.py | 106 ++++++++++++++++++++ 3 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 bottles/tests/backend/wine/__init__.py create mode 100644 bottles/tests/backend/wine/test_executor.py diff --git a/bottles/backend/wine/executor.py b/bottles/backend/wine/executor.py index cd83645c9e..a267322868 100644 --- a/bottles/backend/wine/executor.py +++ b/bottles/backend/wine/executor.py @@ -1,4 +1,5 @@ import os +import re import shlex import uuid from typing import Optional @@ -22,6 +23,7 @@ class WineExecutor: + _PLACEHOLDER_PATTERN = re.compile(r"%([A-Z_]+)%") def __init__( self, config: BottleConfig, @@ -120,15 +122,20 @@ def run_program(cls, config: BottleConfig, program: dict, terminal: bool = False if program is None: logging.warning("The program entry is not well formatted.") + placeholders = cls._build_placeholder_map(config, program or {}) + + def _resolve(field: str): + return cls._replace_placeholders((program or {}).get(field), placeholders) + return cls( config=config, exec_path=program.get("path"), - args=program.get("arguments"), - pre_script=program.get("pre_script"), - post_script=program.get("post_script"), - pre_script_args=program.get("pre_script_args"), - post_script_args=program.get("post_script_args"), - cwd=program.get("folder"), + args=_resolve("arguments"), + pre_script=cls._replace_placeholders(program.get("pre_script"), placeholders), + post_script=cls._replace_placeholders(program.get("post_script"), placeholders), + pre_script_args=_resolve("pre_script_args"), + post_script_args=_resolve("post_script_args"), + cwd=_resolve("folder"), terminal=terminal, program_dxvk=program.get("dxvk"), program_vkd3d=program.get("vkd3d"), @@ -138,6 +145,40 @@ def run_program(cls, config: BottleConfig, program: dict, terminal: bool = False program_virt_desktop=program.get("virtual_desktop"), ).run() + @staticmethod + def _build_placeholder_map(config: BottleConfig, program: dict) -> dict[str, str]: + program_path = program.get("path", "") or "" + program_dir = program.get("folder") or "" + if not program_dir and isinstance(program_path, str) and program_path: + program_dir = os.path.dirname(program_path) + + bottle_path = "" + if config: + try: + bottle_path = ManagerUtils.get_bottle_path(config) + except Exception: + bottle_path = "" + + placeholders = { + "PROGRAM_NAME": program.get("name", ""), + "PROGRAM_PATH": program_path, + "PROGRAM_DIR": program_dir, + "BOTTLE_NAME": getattr(config, "Name", "") or "", + "BOTTLE_PATH": bottle_path, + } + return {key: value for key, value in placeholders.items() if isinstance(value, str)} + + @classmethod + def _replace_placeholders(cls, value: Optional[str], placeholders: dict[str, str]) -> Optional[str]: + if not isinstance(value, str) or not value: + return value + + def _sub(match: re.Match[str]) -> str: + key = match.group(1) + return placeholders.get(key, match.group(0)) + + return cls._PLACEHOLDER_PATTERN.sub(_sub, value) + def __get_cwd(self, cwd: str) -> str | None: winepath = WinePath(self.config) if cwd in [None, ""]: diff --git a/bottles/tests/backend/wine/__init__.py b/bottles/tests/backend/wine/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bottles/tests/backend/wine/test_executor.py b/bottles/tests/backend/wine/test_executor.py new file mode 100644 index 0000000000..712a2eaba2 --- /dev/null +++ b/bottles/tests/backend/wine/test_executor.py @@ -0,0 +1,106 @@ +"""Unit tests for WineExecutor placeholder handling""" + +from bottles.backend.models.config import BottleConfig +from bottles.backend.models.result import Result +from bottles.backend.utils.manager import ManagerUtils +from bottles.backend.wine.executor import WineExecutor + + +def _make_config(name: str = "TestBottle", path: str = "TestBottlePath") -> BottleConfig: + return BottleConfig(Name=name, Path=path, Custom_Path="", Environment="Custom") + + +def test_build_placeholder_map_uses_program_values(): + config = _make_config() + program = { + "name": "My Game", + "path": "/opt/games/my-game.exe", + } + + placeholders = WineExecutor._build_placeholder_map(config, program) + + expected_bottle_path = ManagerUtils.get_bottle_path(config) + assert placeholders["PROGRAM_NAME"] == "My Game" + assert placeholders["PROGRAM_PATH"] == "/opt/games/my-game.exe" + assert placeholders["PROGRAM_DIR"] == "/opt/games" + assert placeholders["BOTTLE_NAME"] == "TestBottle" + assert placeholders["BOTTLE_PATH"] == expected_bottle_path + + +def test_replace_placeholders_handles_unknown_tokens(): + placeholders = {"PROGRAM_NAME": "Example", "BOTTLE_NAME": "Bottle"} + + result = WineExecutor._replace_placeholders( + "run-%PROGRAM_NAME%-on-%BOTTLE_NAME%-%UNKNOWN%", + placeholders, + ) + + assert result == "run-Example-on-Bottle-%UNKNOWN%" + + +def test_run_program_substitutes_placeholders(monkeypatch): + captured: dict[str, object] = {} + + def fake_init( + self, + *, + config, + exec_path, + args="", + terminal=False, + environment=None, + move_file=False, + move_upd_fn=None, + pre_script=None, + post_script=None, + pre_script_args=None, + post_script_args=None, + cwd=None, + monitoring=None, + program_dxvk=None, + program_vkd3d=None, + program_nvapi=None, + program_fsr=None, + program_gamescope=None, + program_virt_desktop=None, + ): + # mimic original __init__ contract enough for run() stub + self.config = config + self.captured = { + "exec_path": exec_path, + "args": args, + "pre_script": pre_script, + "post_script": post_script, + "pre_script_args": pre_script_args, + "post_script_args": post_script_args, + "cwd": cwd, + } + + def fake_run(self): + return Result(True, data=self.captured) + + monkeypatch.setattr(WineExecutor, "__init__", fake_init, raising=False) + monkeypatch.setattr(WineExecutor, "run", fake_run, raising=False) + + config = _make_config(name="Bottle", path="BottlePath") + program = { + "name": "Awesome Game", + "path": "/games/awesome/game.exe", + "arguments": "--title=%PROGRAM_NAME%", + "pre_script": "/scripts/%BOTTLE_NAME%/%PROGRAM_NAME%.sh", + "pre_script_args": "--prefix=%BOTTLE_PATH%", + "post_script": None, + "post_script_args": "--dir=%PROGRAM_DIR%", + "folder": "%PROGRAM_DIR%", + } + + result = WineExecutor.run_program(config=config, program=program, terminal=False) + + assert result.status is True + data = result.data + assert data["exec_path"] == "/games/awesome/game.exe" + assert data["args"] == "--title=Awesome Game" + assert data["pre_script"] == "/scripts/Bottle/Awesome Game.sh" + assert data["pre_script_args"] == f"--prefix={ManagerUtils.get_bottle_path(config)}" + assert data["post_script_args"] == "--dir=/games/awesome" + assert data["cwd"] == "/games/awesome" From 0031b7212cf9dfb4ea3c844e739960823a3c02d1 Mon Sep 17 00:00:00 2001 From: evertonstz Date: Sun, 12 Oct 2025 18:00:05 -0300 Subject: [PATCH 4/8] Enhance BottleConfig with YAML registration and improve placeholder handling in WineExecutor --- bottles/backend/models/config.py | 14 ++++++++++++-- bottles/backend/utils/yaml.py | 5 +++-- bottles/backend/wine/executor.py | 20 +++++++++++++++++--- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/bottles/backend/models/config.py b/bottles/backend/models/config.py index d42c6db74a..6099072a46 100644 --- a/bottles/backend/models/config.py +++ b/bottles/backend/models/config.py @@ -111,10 +111,10 @@ class BottleParams(DictCompatMixIn): versioning_exclusion_patterns: bool = False vmtouch: bool = False vmtouch_cache_cwd: bool = False - - + @dataclass class BottleConfig(DictCompatMixIn): + _yaml_registered = False Name: str = "" Arch: str = "win64" # Enum, Use bottles.backend.models.enum.Arch Windows: str = "win10" @@ -158,6 +158,7 @@ def dump(self, file: str | IO, mode="w", encoding=None, indent=4) -> Result: :param encoding: file content encoding, default is None(Decide by Python IO) :param indent: file indent width, default is 4 """ + self._ensure_yaml_registration() f = file if isinstance(file, IOBase) else open(file, mode=mode) try: yaml.dump(self.to_dict(), f, indent=indent, encoding=encoding) @@ -277,3 +278,12 @@ def _filter(cls, data: dict, clazz: object = None) -> dict: ) return new_data + + def __post_init__(self) -> None: + self._ensure_yaml_registration() + + @classmethod + def _ensure_yaml_registration(cls) -> None: + if not cls._yaml_registered: + yaml.register_dataclass(cls) + cls._yaml_registered = True diff --git a/bottles/backend/utils/yaml.py b/bottles/backend/utils/yaml.py index 37007d96e7..a8113a4f4a 100644 --- a/bottles/backend/utils/yaml.py +++ b/bottles/backend/utils/yaml.py @@ -1,6 +1,6 @@ import yaml as _yaml -from bottles.backend.models.config import BottleConfig +from typing import Type try: from yaml import CSafeLoader as SafeLoader, CSafeDumper as SafeDumper @@ -8,8 +8,9 @@ from yaml import SafeLoader, SafeDumper YAMLError = _yaml.YAMLError -SafeDumper.add_representer(BottleConfig, BottleConfig.yaml_serialize_handler) +def register_dataclass(dataclass_type: Type) -> None: + SafeDumper.add_representer(dataclass_type, dataclass_type.yaml_serialize_handler) def dump(data, stream=None, **kwargs): """ diff --git a/bottles/backend/wine/executor.py b/bottles/backend/wine/executor.py index a267322868..0fb14af5dc 100644 --- a/bottles/backend/wine/executor.py +++ b/bottles/backend/wine/executor.py @@ -2,7 +2,7 @@ import re import shlex import uuid -from typing import Optional +from typing import Optional, Pattern from bottles.backend.dlls.dxvk import DXVKComponent from bottles.backend.dlls.nvapi import NVAPIComponent @@ -23,7 +23,15 @@ class WineExecutor: - _PLACEHOLDER_PATTERN = re.compile(r"%([A-Z_]+)%") + _PLACEHOLDER_PATTERN: Pattern[str] = re.compile(r"%([A-Z_]+)%") + _KNOWN_PLACEHOLDERS: set[str] = { + "PROGRAM_NAME", + "PROGRAM_PATH", + "PROGRAM_DIR", + "BOTTLE_NAME", + "BOTTLE_PATH", + } + def __init__( self, config: BottleConfig, @@ -148,7 +156,13 @@ def _resolve(field: str): @staticmethod def _build_placeholder_map(config: BottleConfig, program: dict) -> dict[str, str]: program_path = program.get("path", "") or "" - program_dir = program.get("folder") or "" + program_dir_raw = program.get("folder") + program_dir = program_dir_raw or "" + if isinstance(program_dir_raw, str): + matches = WineExecutor._PLACEHOLDER_PATTERN.findall(program_dir_raw) + if any(match in WineExecutor._KNOWN_PLACEHOLDERS for match in matches): + # ignore unresolved placeholders that reference known tokens + program_dir = "" if not program_dir and isinstance(program_path, str) and program_path: program_dir = os.path.dirname(program_path) From dcea01793a5bac1635c6855e98d2e529fccf4941 Mon Sep 17 00:00:00 2001 From: evertonstz Date: Sun, 12 Oct 2025 18:32:19 -0300 Subject: [PATCH 5/8] Refactor BottleConfig to simplify YAML registration and enhance dataclass handling in yaml utility --- bottles/backend/models/config.py | 11 +---------- bottles/backend/utils/yaml.py | 6 ++++++ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/bottles/backend/models/config.py b/bottles/backend/models/config.py index 6099072a46..5ad8311915 100644 --- a/bottles/backend/models/config.py +++ b/bottles/backend/models/config.py @@ -114,7 +114,6 @@ class BottleParams(DictCompatMixIn): @dataclass class BottleConfig(DictCompatMixIn): - _yaml_registered = False Name: str = "" Arch: str = "win64" # Enum, Use bottles.backend.models.enum.Arch Windows: str = "win10" @@ -158,7 +157,7 @@ def dump(self, file: str | IO, mode="w", encoding=None, indent=4) -> Result: :param encoding: file content encoding, default is None(Decide by Python IO) :param indent: file indent width, default is 4 """ - self._ensure_yaml_registration() + yaml.register_dataclass(type(self)) f = file if isinstance(file, IOBase) else open(file, mode=mode) try: yaml.dump(self.to_dict(), f, indent=indent, encoding=encoding) @@ -279,11 +278,3 @@ def _filter(cls, data: dict, clazz: object = None) -> dict: return new_data - def __post_init__(self) -> None: - self._ensure_yaml_registration() - - @classmethod - def _ensure_yaml_registration(cls) -> None: - if not cls._yaml_registered: - yaml.register_dataclass(cls) - cls._yaml_registered = True diff --git a/bottles/backend/utils/yaml.py b/bottles/backend/utils/yaml.py index a8113a4f4a..d4feb19390 100644 --- a/bottles/backend/utils/yaml.py +++ b/bottles/backend/utils/yaml.py @@ -9,8 +9,14 @@ YAMLError = _yaml.YAMLError +_REGISTERED_DATACLASSES: set[Type] = set() + def register_dataclass(dataclass_type: Type) -> None: + """Register a custom representer for dumping dataclasses.""" + if dataclass_type in _REGISTERED_DATACLASSES: + return SafeDumper.add_representer(dataclass_type, dataclass_type.yaml_serialize_handler) + _REGISTERED_DATACLASSES.add(dataclass_type) def dump(data, stream=None, **kwargs): """ From e527c0405fe57383ccc8ab2c588a93a2f41a1c59 Mon Sep 17 00:00:00 2001 From: evertonstz Date: Sun, 12 Oct 2025 19:12:22 -0300 Subject: [PATCH 6/8] Update tooltip texts and add translations for pre-run and post-run script arguments in launch options dialog --- bottles/frontend/ui/dialog-launch-options.blp | 4 +-- po/bottles.pot | 20 +++++++++++++++ po/pt.po | 25 ++++++++++++++++--- po/pt_BR.po | 22 +++++++++++++++- 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/bottles/frontend/ui/dialog-launch-options.blp b/bottles/frontend/ui/dialog-launch-options.blp index fe4433975f..303b6cc6f6 100644 --- a/bottles/frontend/ui/dialog-launch-options.blp +++ b/bottles/frontend/ui/dialog-launch-options.blp @@ -81,7 +81,7 @@ template $LaunchOptionsDialog: Adw.Window { Adw.EntryRow entry_pre_script_args { title: _("Pre-run Script Arguments"); - tooltip-text: _("e.g.: ludusavi restore --force Game Name"); + tooltip-text: _("e.g.: ludusavi restore --force \"Game Name\""); visible: false; } @@ -118,7 +118,7 @@ template $LaunchOptionsDialog: Adw.Window { Adw.EntryRow entry_post_script_args { title: _("Post-run Script Arguments"); - tooltip-text: _("e.g.: ludusavi backup --force Game Name"); + tooltip-text: _("e.g.: ludusavi backup --force \"Game Name\""); visible: false; } diff --git a/po/bottles.pot b/po/bottles.pot index 68c5cb7cca..5a03501c3a 100644 --- a/po/bottles.pot +++ b/po/bottles.pot @@ -376,6 +376,10 @@ msgstr "" msgid "Launch Options" msgstr "" +#: bottles/frontend/ui/dialog-launch-options.blp:53 +msgid "Pre-run Script" +msgstr "" + #: bottles/frontend/ui/details-bottle.blp:135 msgid "Run in Terminal" msgstr "" @@ -853,6 +857,22 @@ msgstr "" msgid "Working Directory" msgstr "" +#: bottles/frontend/ui/dialog-launch-options.blp:83 +msgid "Pre-run Script Arguments" +msgstr "" + +#: bottles/frontend/ui/dialog-launch-options.blp:84 +msgid "e.g.: ludusavi restore --force \"Game Name\"" +msgstr "" + +#: bottles/frontend/ui/dialog-launch-options.blp:118 +msgid "Post-run Script Arguments" +msgstr "" + +#: bottles/frontend/ui/dialog-launch-options.blp:121 +msgid "e.g.: ludusavi backup --force \"Game Name\"" +msgstr "" + #: bottles/frontend/ui/details-preferences.blp:317 #: bottles/frontend/ui/dialog-launch-options.blp:59 #: bottles/frontend/ui/dialog-launch-options.blp:90 diff --git a/po/pt.po b/po/pt.po index 92a838e89e..7adbde17f5 100644 --- a/po/pt.po +++ b/po/pt.po @@ -380,6 +380,10 @@ msgstr "Reiniciar" msgid "Launch Options" msgstr "Opções de lançamento" +#: bottles/frontend/ui/dialog-launch-options.blp:53 +msgid "Pre-run Script" +msgstr "Script de pré-execução" + #: bottles/frontend/ui/details-bottle.blp:135 msgid "Run in Terminal" msgstr "Executar no Terminal" @@ -897,6 +901,22 @@ msgstr "" msgid "Working Directory" msgstr "Diretório de trabalho" +#: bottles/frontend/ui/dialog-launch-options.blp:83 +msgid "Pre-run Script Arguments" +msgstr "Argumentos do script de pré-execução" + +#: bottles/frontend/ui/dialog-launch-options.blp:84 +msgid "e.g.: ludusavi restore --force \"Game Name\"" +msgstr "ex.: ludusavi restore --force \"Nome do Jogo\"" + +#: bottles/frontend/ui/dialog-launch-options.blp:118 +msgid "Post-run Script Arguments" +msgstr "Argumentos do script de pós-execução" + +#: bottles/frontend/ui/dialog-launch-options.blp:121 +msgid "e.g.: ludusavi backup --force \"Game Name\"" +msgstr "ex.: ludusavi backup --force \"Nome do Jogo\"" + #: bottles/frontend/ui/details-preferences.blp:318 #: bottles/frontend/ui/dialog-launch-options.blp:59 #: bottles/frontend/ui/dialog-launch-options.blp:90 @@ -1358,7 +1378,7 @@ msgstr "ex.: VAR=valor %comando% -exemplo1 -exemplo2 -exemplo3=olá" #: bottles/frontend/ui/dialog-launch-options.blp:52 msgid "Post-run Script" -msgstr "Script pós-execução" +msgstr "Script de pós-execução" #. endregion #: bottles/frontend/ui/dialog-launch-options.blp:53 @@ -1375,8 +1395,7 @@ msgstr "Escolha um Script" msgid "Choose from where start the program." msgstr "Escolha de onde iniciar o programa." -#: bottles/frontend/ui/dialog-launch-options.blp:101 -#: bottles/frontend/ui/drive-entry.blp:22 +#: bottles/frontend/ui/dialog-launch-options.blp:118 msgid "Choose a Directory" msgstr "Escolher um diretório" diff --git a/po/pt_BR.po b/po/pt_BR.po index fd9294ef1d..8bf50692dc 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -379,6 +379,10 @@ msgstr "Reiniciar" msgid "Launch Options" msgstr "Opções de Lançamento" +#: bottles/frontend/ui/dialog-launch-options.blp:53 +msgid "Pre-run Script" +msgstr "Script de pré-execução" + #: bottles/frontend/ui/details-bottle.blp:135 msgid "Run in Terminal" msgstr "Executar no Terminal" @@ -897,6 +901,22 @@ msgstr "" msgid "Working Directory" msgstr "Diretório de trabalho" +#: bottles/frontend/ui/dialog-launch-options.blp:83 +msgid "Pre-run Script Arguments" +msgstr "Argumentos do script de pré-execução" + +#: bottles/frontend/ui/dialog-launch-options.blp:84 +msgid "e.g.: ludusavi restore --force \"Game Name\"" +msgstr "ex.: ludusavi restore --force \"Nome do Jogo\"" + +#: bottles/frontend/ui/dialog-launch-options.blp:118 +msgid "Post-run Script Arguments" +msgstr "Argumentos do script de pós-execução" + +#: bottles/frontend/ui/dialog-launch-options.blp:121 +msgid "e.g.: ludusavi backup --force \"Game Name\"" +msgstr "ex.: ludusavi backup --force \"Nome do Jogo\"" + #: bottles/frontend/ui/details-preferences.blp:318 #: bottles/frontend/ui/dialog-launch-options.blp:59 #: bottles/frontend/ui/dialog-launch-options.blp:90 @@ -1359,7 +1379,7 @@ msgstr "ex.: VAR=valor %comando% -exemplo1 -exemplo2 -exemplo3=olá" #: bottles/frontend/ui/dialog-launch-options.blp:52 msgid "Post-run Script" -msgstr "Script pós-execução" +msgstr "Script de pós-execução" #. endregion #: bottles/frontend/ui/dialog-launch-options.blp:53 From 7034ffd445ef18a5a210ba85258c0b5a0a828241 Mon Sep 17 00:00:00 2001 From: evertonstz Date: Mon, 3 Nov 2025 20:14:17 +0000 Subject: [PATCH 7/8] reset config.py --- bottles/backend/models/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bottles/backend/models/config.py b/bottles/backend/models/config.py index 21357817ad..909d8961d4 100644 --- a/bottles/backend/models/config.py +++ b/bottles/backend/models/config.py @@ -111,7 +111,8 @@ class BottleParams(DictCompatMixIn): versioning_exclusion_patterns: bool = False vmtouch: bool = False vmtouch_cache_cwd: bool = False - + + @dataclass class BottleConfig(DictCompatMixIn): Name: str = "" @@ -275,4 +276,3 @@ def _filter(cls, data: dict, clazz: object = None) -> dict: ) return new_data - From fcb6eba061bd58091f47fe6b44d9eb12f10750e4 Mon Sep 17 00:00:00 2001 From: evertonstz Date: Mon, 3 Nov 2025 20:35:33 +0000 Subject: [PATCH 8/8] revert yaml.py file --- bottles/backend/utils/yaml.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/bottles/backend/utils/yaml.py b/bottles/backend/utils/yaml.py index d4feb19390..37007d96e7 100644 --- a/bottles/backend/utils/yaml.py +++ b/bottles/backend/utils/yaml.py @@ -1,6 +1,6 @@ import yaml as _yaml -from typing import Type +from bottles.backend.models.config import BottleConfig try: from yaml import CSafeLoader as SafeLoader, CSafeDumper as SafeDumper @@ -8,15 +8,8 @@ from yaml import SafeLoader, SafeDumper YAMLError = _yaml.YAMLError +SafeDumper.add_representer(BottleConfig, BottleConfig.yaml_serialize_handler) -_REGISTERED_DATACLASSES: set[Type] = set() - -def register_dataclass(dataclass_type: Type) -> None: - """Register a custom representer for dumping dataclasses.""" - if dataclass_type in _REGISTERED_DATACLASSES: - return - SafeDumper.add_representer(dataclass_type, dataclass_type.yaml_serialize_handler) - _REGISTERED_DATACLASSES.add(dataclass_type) def dump(data, stream=None, **kwargs): """