diff --git a/bottles/backend/bottle.py b/bottles/backend/bottle.py new file mode 100644 index 00000000000..7170f3bddeb --- /dev/null +++ b/bottles/backend/bottle.py @@ -0,0 +1,75 @@ +# bottle.py +# +# Copyright 2025 The Bottles Contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, in version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import yaml +from dataclasses import dataclass + +from bottles.backend.typing import WindowsAPI, VersionedComponent, Environment +from bottles.backend.models.config import BottleConfig + + +# BottleConfig(Name='d', Arch='win64', Windows='win10', Runner='sys-wine-10.0', WorkingDir='', DXVK='', NVAPI='', VKD3D='', LatencyFleX='', Path='d', Custom_Path=False, Environment='Application', Creation_Date='', Update_Date='', Versioning=False, Versioning_Exclusion_Patterns=[], State=0, Parameters=BottleParams(dxvk=False, dxvk_nvapi=False, vkd3d=False, latencyflex=False, mangohud=False, mangohud_display_on_game_start=True, obsvkc=False, vkbasalt=False, gamemode=False, gamescope=False, gamescope_game_width=0, gamescope_game_height=0, gamescope_window_width=0, gamescope_window_height=0, gamescope_fps=0, gamescope_fps_no_focus=0, gamescope_scaling=False, gamescope_borderless=False, gamescope_fullscreen=True, sync='wine', fsr=False, fsr_sharpening_strength=2, fsr_quality_mode='none', custom_dpi=96, renderer='gl', discrete_gpu=False, virtual_desktop=False, virtual_desktop_res='1280x720', pulseaudio_latency=False, fullscreen_capture=False, take_focus=False, mouse_warp=True, decorated=True, fixme_logs=False, use_runtime=False, use_eac_runtime=True, use_be_runtime=True, use_steam_runtime=False, sandbox=False, versioning_compression=False, versioning_automatic=False, versioning_exclusion_patterns=False, vmtouch=False, vmtouch_cache_cwd=False), Sandbox=BottleSandboxParams(share_net=False, share_sound=False), Environment_Variables={}, Installed_Dependencies=[], DLL_Overrides={}, External_Programs={}, Uninstallers={}, session_arguments='', run_in_terminal=False, Language='sys', CompatData='', data={}, RunnerPath='') +@dataclass +class BottleClass: + name: str + runner: str + environment: Environment + mangohud: bool = False + vkbasalt: bool = False + gamemode: bool = False + gamescope: bool = False + fidelityfx_super_resolution: bool = False + dxvk: VersionedComponent = False + nvapi: VersionedComponent = False + vkd3d: VersionedComponent = False + latencyflex: VersionedComponent = False + architecture: WindowsAPI = WindowsAPI.WIN64 + + +class Bottle: + """Class representing a bottle.""" + + @staticmethod + def generate_local_bottles_list(bottles_dir: str) -> dict[str, BottleConfig]: + """Generate a list of local bottles.""" + + local_bottles = {} + local_bottles_list = os.listdir(bottles_dir) + + for local_bottle in local_bottles_list: + local_bottle_dir = os.path.join(bottles_dir, local_bottle) + bottle_config_file_path = os.path.join(local_bottle_dir, "bottle.yml") + placeholder_file_path = os.path.join(local_bottle_dir, "placeholder.yml") + + try: + with open(placeholder_file_path) as file: + configuration = yaml.safe_load(file) + bottle_config_file_path = configuration["Path"] + except FileNotFoundError: + pass + + if not os.path.isfile(bottle_config_file_path): + continue + + config_load = BottleConfig.load(bottle_config_file_path) + + if not config_load.status: + raise TypeError(f"Unable to load {bottle_config_file_path}") + + local_bottles[local_bottle] = config_load.data + + return local_bottles diff --git a/bottles/backend/cabextract.py b/bottles/backend/cabextract.py index bfe6cd00b5a..0e36b4a41f6 100644 --- a/bottles/backend/cabextract.py +++ b/bottles/backend/cabextract.py @@ -20,9 +20,7 @@ import shutil import subprocess -from bottles.backend.logger import Logger - -logging = Logger() +import logging class CabExtract: diff --git a/bottles/backend/dlls/dll.py b/bottles/backend/dlls/dll.py index 954d37b6f68..3bb90bd1cc8 100644 --- a/bottles/backend/dlls/dll.py +++ b/bottles/backend/dlls/dll.py @@ -20,14 +20,12 @@ from abc import abstractmethod from copy import deepcopy -from bottles.backend.logger import Logger +import logging from bottles.backend.models.config import BottleConfig from bottles.backend.models.enum import Arch from bottles.backend.utils.manager import ManagerUtils from bottles.backend.wine.reg import Reg -logging = Logger() - class DLLComponent: base_path: str diff --git a/bottles/backend/dlls/nvapi.py b/bottles/backend/dlls/nvapi.py index b4de3bd719e..013a797384c 100644 --- a/bottles/backend/dlls/nvapi.py +++ b/bottles/backend/dlls/nvapi.py @@ -21,12 +21,10 @@ from bottles.backend.dlls.dll import DLLComponent from bottles.backend.models.config import BottleConfig from bottles.backend.utils.manager import ManagerUtils -from bottles.backend.logger import Logger +import logging from bottles.backend.utils.nvidia import get_nvidia_dll_path -logging = Logger() - class NVAPIComponent(DLLComponent): dlls = { diff --git a/bottles/backend/downloader.py b/bottles/backend/downloader.py index 482e88f0908..d525e7aad32 100644 --- a/bottles/backend/downloader.py +++ b/bottles/backend/downloader.py @@ -21,13 +21,11 @@ import requests -from bottles.backend.logger import Logger +import logging from bottles.backend.models.result import Result from bottles.backend.state import TaskStreamUpdateHandler from bottles.backend.utils.file import FileUtils -logging = Logger() - class Downloader: """ diff --git a/bottles/backend/globals.py b/bottles/backend/globals.py index 8123b2396ba..71b7082c922 100644 --- a/bottles/backend/globals.py +++ b/bottles/backend/globals.py @@ -60,6 +60,24 @@ def is_vkbasalt_available(): return True return False + @classmethod + def get_components_paths(cls) -> list[str]: + """Retrieve list of components' paths.""" + + return [ + cls.temp, + cls.runtimes, + cls.winebridge, + cls.runners, + cls.bottles, + cls.steam, + cls.dxvk, + cls.vkd3d, + cls.nvapi, + cls.latencyflex, + cls.templates, + ] + class TrdyPaths: # External managers paths diff --git a/bottles/backend/health.py b/bottles/backend/health.py index 11e87824911..43119b9f1e5 100644 --- a/bottles/backend/health.py +++ b/bottles/backend/health.py @@ -19,15 +19,12 @@ from bottles.backend.utils import yaml import contextlib -from bottles.backend.logger import Logger from bottles.backend.utils.display import DisplayUtils from bottles.backend.utils.gpu import GPUUtils from bottles.backend.utils.generic import is_glibc_min_available from bottles.backend.utils.file import FileUtils from bottles.backend.params import APP_VERSION -logging = Logger() - class HealthChecker: x11: bool = False diff --git a/bottles/backend/logger.py b/bottles/backend/logger.py deleted file mode 100644 index 05bba87f5c0..00000000000 --- a/bottles/backend/logger.py +++ /dev/null @@ -1,113 +0,0 @@ -# logger.py -# -# Copyright 2022 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -import logging -import os -import re - -from bottles.backend.globals import Paths -from bottles.backend.managers.journal import JournalManager, JournalSeverity - -# Set default logging level -logging.basicConfig(level=logging.DEBUG) - - -class Logger(logging.getLoggerClass()): - """ - This class is a wrapper for the logging module. It provides - custom formats for the log messages. - """ - - __color_map = {"debug": 37, "info": 36, "warning": 33, "error": 31, "critical": 41} - __format_log = { - "fmt": "\033[80m%(asctime)s \033[1m(%(levelname)s)\033[0m %(message)s \033[0m", - "datefmt": "%H:%M:%S", - } - - def __color(self, level, message: str): - if message and "\n" in message: - message = message.replace("\n", "\n\t") + "\n" - color_id = self.__color_map[level] - return "\033[%dm%s\033[0m" % (color_id, message) - - def __init__(self, formatter=None): - if formatter is None: - formatter = self.__format_log - formatter = logging.Formatter(**formatter) - - self.root.setLevel(os.environ.get("LOG_LEVEL") or logging.INFO) - self.root.handlers = [] - - handler = logging.StreamHandler() - handler.setFormatter(formatter) - self.root.addHandler(handler) - - def debug(self, message, **kwargs): - self.root.debug( - self.__color("debug", message), - ) - - def info(self, message, jn=False, **kwargs): - self.root.info( - self.__color("info", message), - ) - if jn: - JournalManager.write(JournalSeverity.INFO, message) - - def warning(self, message, jn=True, **kwargs): - self.root.warning( - self.__color("warning", message), - ) - if jn: - JournalManager.write(JournalSeverity.WARNING, message) - - def error(self, message, jn=True, **kwargs): - self.root.error( - self.__color("error", message), - ) - if jn: - JournalManager.write(JournalSeverity.ERROR, message) - - def critical(self, message, jn=True, **kwargs): - self.root.critical( - self.__color("critical", message), - ) - if jn: - JournalManager.write(JournalSeverity.CRITICAL, message) - - @staticmethod - def write_log(data: list): - """ - Writes a crash.log file. It finds and replace the user's home directory - with "USER" as a proposed standard for crash reports. - """ - log_path = f"{Paths.xdg_data_home}/bottles/crash.log" - - with open(log_path, "w") as crash_log: - for d in data: - # replace username with "USER" as standard - if "/home/" in d: - d = re.sub(r"/home/([^/]*)/", r"/home/USER/", d) - - crash_log.write(d) - - # we write the same to the journal for convenience - JournalManager.write( - severity=JournalSeverity.CRASH, message="A crash has been detected." - ) - - def set_silent(self): - self.root.handlers = [] diff --git a/bottles/backend/managers/backup.py b/bottles/backend/managers/backup.py deleted file mode 100644 index 7a60311521a..00000000000 --- a/bottles/backend/managers/backup.py +++ /dev/null @@ -1,222 +0,0 @@ -# backup.py -# -# Copyright 2022 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import os -import shutil -import tarfile -import pathvalidate -from gettext import gettext as _ - -from bottles.backend.globals import Paths -from bottles.backend.logger import Logger -from bottles.backend.managers.manager import Manager -from bottles.backend.models.config import BottleConfig -from bottles.backend.models.result import Result -from bottles.backend.state import TaskManager, Task -from bottles.backend.utils import yaml -from bottles.backend.utils.manager import ManagerUtils - -logging = Logger() - - -class BackupManager: - @staticmethod - def _validate_path(path: str) -> bool: - """Validate if the path is not None or empty.""" - if not path: - logging.error(_("No path specified")) - return False - return True - - @staticmethod - def _create_tarfile( - source_path: str, destination_path: str, exclude_filter=None - ) -> bool: - """Helper function to create a tar.gz file from a source path.""" - try: - with tarfile.open(destination_path, "w:gz") as tar: - os.chdir(os.path.dirname(source_path)) - tar.add(os.path.basename(source_path), filter=exclude_filter) - return True - except (FileNotFoundError, PermissionError, tarfile.TarError, ValueError) as e: - logging.error(f"Error creating backup: {e}") - return False - - @staticmethod - def _safe_extract_tarfile(tar_path: str, extract_path: str) -> bool: - """ - Safely extract a tar.gz file to avoid directory traversal - vulnerabilities. - """ - try: - with tarfile.open(tar_path, "r:gz") as tar: - # Validate each member - for member in tar.getmembers(): - member_path = os.path.abspath( - os.path.join(extract_path, member.name) - ) - if not member_path.startswith(os.path.abspath(extract_path)): - raise Exception("Detected path traversal attempt in tar file") - tar.extractall(path=extract_path) - return True - except (tarfile.TarError, Exception) as e: - logging.error(f"Error extracting backup: {e}") - return False - - @staticmethod - def export_backup(config: BottleConfig, scope: str, path: str) -> Result: - """ - Exports a bottle backup to the specified path. - Use the scope parameter to specify the backup type: config, full. - Config will only export the bottle configuration, full will export - the full bottle in tar.gz format. - """ - if not BackupManager._validate_path(path): - return Result(status=False) - - logging.info(f"Exporting {scope} backup for [{config.Name}] to [{path}]") - - if scope == "config": - backup_created = config.dump(path).status - else: - task_id = TaskManager.add(Task(title=_("Backup {0}").format(config.Name))) - bottle_path = ManagerUtils.get_bottle_path(config) - backup_created = BackupManager._create_tarfile( - bottle_path, path, exclude_filter=BackupManager.exclude_filter - ) - TaskManager.remove(task_id) - - if backup_created: - logging.info(f"Backup successfully saved to: {path}.") - return Result(status=True) - else: - logging.error("Failed to save backup.") - return Result(status=False) - - @staticmethod - def exclude_filter(tarinfo: tarfile.TarInfo) -> tarfile.TarInfo | None: - """ - Filter which excludes some unwanted files from the backup. - """ - if "dosdevices" in tarinfo.name: - return None - return tarinfo - - @staticmethod - def import_backup(scope: str, path: str) -> Result: - """ - Imports a backup from the specified path. - Use the scope parameter to specify the backup type: config, full. - Config will make a new bottle reproducing the configuration, full will - import the full bottle from a tar.gz file. - """ - if not BackupManager._validate_path(path): - return Result(status=False) - - logging.info(f"Importing backup from: {path}") - - if scope == "config": - return BackupManager._import_config_backup(path) - else: - return BackupManager._import_full_backup(path) - - @staticmethod - def _import_config_backup(path: str) -> Result: - task_id = TaskManager.add(Task(title=_("Importing config backup"))) - config_load = BottleConfig.load(path) - manager = Manager() - if ( - config_load.status - and config_load.data - and manager.create_bottle_from_config(config_load.data) - ): - TaskManager.remove(task_id) - logging.info("Config backup imported successfully.") - return Result(status=True) - else: - TaskManager.remove(task_id) - logging.error("Failed to import config backup.") - return Result(status=False) - - @staticmethod - def _import_full_backup(path: str) -> Result: - task_id = TaskManager.add(Task(title=_("Importing full backup"))) - if BackupManager._safe_extract_tarfile(path, Paths.bottles): - Manager().update_bottles() - TaskManager.remove(task_id) - logging.info("Full backup imported successfully.") - return Result(status=True) - else: - TaskManager.remove(task_id) - logging.error("Failed to import full backup.") - return Result(status=False) - - @staticmethod - def duplicate_bottle(config: BottleConfig, name: str) -> Result: - """ - Duplicates the bottle with the specified new name. - """ - logging.info(f"Duplicating bottle: {config.Name} as {name}") - - sanitized_name = pathvalidate.sanitize_filename(name, platform="universal") - source_path = ManagerUtils.get_bottle_path(config) - destination_path = os.path.join(Paths.bottles, sanitized_name) - - return BackupManager._duplicate_bottle_directory( - config, source_path, destination_path, name - ) - - @staticmethod - def _duplicate_bottle_directory( - config: BottleConfig, source_path: str, destination_path: str, new_name: str - ) -> Result: - try: - if not os.path.exists(destination_path): - os.makedirs(destination_path) - for item in [ - "drive_c", - "system.reg", - "user.reg", - "userdef.reg", - "bottle.yml", - ]: - source_item = os.path.join(source_path, item) - destination_item = os.path.join(destination_path, item) - if os.path.isdir(source_item): - shutil.copytree( - source_item, - destination_item, - ignore=shutil.ignore_patterns(".*"), - symlinks=True, - ) - elif os.path.isfile(source_item): - shutil.copy(source_item, destination_item) - - # Update the bottle configuration - config_path = os.path.join(destination_path, "bottle.yml") - with open(config_path) as config_file: - config_data = yaml.load(config_file) - config_data["Name"] = new_name - config_data["Path"] = destination_path - with open(config_path, "w") as config_file: - yaml.dump(config_data, config_file, indent=4) - - logging.info(f"Bottle duplicated successfully as {new_name}.") - return Result(status=True) - except (FileNotFoundError, PermissionError, OSError) as e: - logging.error(f"Error duplicating bottle: {e}") - return Result(status=False) diff --git a/bottles/backend/managers/component.py b/bottles/backend/managers/component.py deleted file mode 100644 index 9065f7f337f..00000000000 --- a/bottles/backend/managers/component.py +++ /dev/null @@ -1,493 +0,0 @@ -# component.py -# -# Copyright 2022 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import contextlib -import os -import shutil -import tarfile -from functools import lru_cache - -import pycurl - -from bottles.backend.downloader import Downloader -from bottles.backend.globals import Paths -from bottles.backend.logger import Logger -from bottles.backend.models.result import Result -from bottles.backend.state import ( - Locks, - Task, - TaskStreamUpdateHandler, - Status, - TaskManager, - LockManager, -) -from bottles.backend.utils.file import FileUtils -from bottles.backend.utils.generic import is_glibc_min_available -from bottles.backend.utils.manager import ManagerUtils - -logging = Logger() - - -# noinspection PyTypeChecker -class ComponentManager: - def __init__(self, manager, offline: bool = False): - self.__manager = manager - self.__repo = manager.repository_manager.get_repo("components", offline) - self.__utils_conn = manager.utils_conn - - @lru_cache - def get_component(self, name: str, plain: bool = False) -> dict: - return self.__repo.get(name, plain) - - def fetch_catalog(self) -> dict: - """ - Fetch all components from the Bottles repository, mark the installed - ones and return a dict with the catalog. - """ - if not self.__utils_conn.check_connection(): - return {} - - catalog = { - "runtimes": {}, - "wine": {}, - "proton": {}, - "dxvk": {}, - "vkd3d": {}, - "nvapi": {}, - "latencyflex": {}, - "winebridge": {}, - } - components_available = { - "runtimes": self.__manager.runtimes_available, - "wine": self.__manager.runners_available, - "proton": self.__manager.runners_available, - "dxvk": self.__manager.dxvk_available, - "vkd3d": self.__manager.vkd3d_available, - "nvapi": self.__manager.nvapi_available, - "latencyflex": self.__manager.latencyflex_available, - "winebridge": self.__manager.winebridge_available, - } - - index = self.__repo.catalog - - for component in index.items(): - """ - For each component, append it to the corresponding - catalog and mark it as installed if it is. - """ - - if component[1]["Category"] == "runners": - if "soda" in component[0].lower() or "caffe" in component[0].lower(): - if not is_glibc_min_available(): - logging.warning( - f"{component[0]} was found but it requires " - "glibc >= 2.32 and your system is running an older " - "version. Use the Flatpak instead if you can't " - "upgrade your system. This runner will be ignored, " - "please keep in mind that Bottles and all our " - "installers are only tested with Soda and Caffe runners." - ) - continue - - sub_category = component[1]["Sub-category"] - catalog[sub_category][component[0]] = component[1] - if component[0] in components_available[sub_category]: - catalog[sub_category][component[0]]["Installed"] = True - else: - catalog[sub_category][component[0]].pop("Installed", None) - - continue - - category = component[1]["Category"] - if category not in catalog: - continue - - catalog[category][component[0]] = component[1] - if component[0] in components_available[category]: - catalog[category][component[0]]["Installed"] = True - else: - catalog[category][component[0]].pop("Installed", None) - - return catalog - - def download( - self, - download_url: str, - file: str, - rename: str = "", - checksum: str = "", - func: TaskStreamUpdateHandler | None = None, - ) -> bool: - """Download a component from the Bottles repository.""" - - # Check for missing Bottles paths before download - self.__manager.check_app_dirs() - - # Register this file download task to TaskManager - task = Task(title=file) - task_id = TaskManager.add(task) - update_func = task.stream_update if not func else func - - if download_url.startswith("temp/"): - """ - The caller is explicitly requesting a component from - the /temp directory. Nothing should be downloaded. - """ - return True - - existing_file = rename if rename else file - temp_dest = os.path.join(Paths.temp, file) - just_downloaded = False - - if os.path.isfile(os.path.join(Paths.temp, existing_file)): - """ - Check if the file already exists in the /temp directory. - If so, then skip the download process and set the update_func - to completed. - """ - logging.warning(f"File [{existing_file}] already exists in temp, skipping.") - else: - """ - As some urls can be redirect, we need to take care of this - and make sure to use the final url. This check should be - skipped for large files (e.g. runners). - """ - c = pycurl.Curl() - try: - c.setopt(c.URL, download_url) # type: ignore - c.setopt(c.FOLLOWLOCATION, True) # type: ignore - c.setopt(c.HTTPHEADER, ["User-Agent: curl/7.79.1"]) # type: ignore - c.setopt(c.NOBODY, True) # type: ignore - c.perform() - - req_code = c.getinfo(c.RESPONSE_CODE) # type: ignore - download_url = c.getinfo(c.EFFECTIVE_URL) # type: ignore - except pycurl.error: - logging.exception(f"Failed to download [{download_url}]") - TaskManager.remove(task_id) - return False - finally: - c.close() - - if req_code == 200: - """ - If the status code is 200, the resource should be available - and the download should be started. Any exceptions return - False and the download is removed from the download manager. - """ - res = Downloader( - url=download_url, file=temp_dest, update_func=update_func - ).download() - - if not res.ok: - TaskManager.remove(task_id) - return False - - if not os.path.isfile(temp_dest): - """Fail if the file is not available in the /temp directory.""" - TaskManager.remove(task_id) - return False - - just_downloaded = True - else: - logging.warning( - f"Failed to download [{download_url}] with code: {req_code} != 200" - ) - TaskManager.remove(task_id) - return False - - file_path = os.path.join(Paths.temp, existing_file) - if rename and just_downloaded: - """Renaming the downloaded file if requested.""" - logging.info(f"Renaming [{file}] to [{rename}].") - file_path = os.path.join(Paths.temp, rename) - os.rename(temp_dest, file_path) - - if checksum and not os.environ.get("BOTTLES_SKIP_CHECKSUM"): - """ - Compare the checksum of the downloaded file with the one - provided by the caller. If they don't match, remove the - file from the /temp directory, remove the entry from the - task manager and return False. - """ - checksum = checksum.lower() - local_checksum = FileUtils().get_checksum(file_path) - - if local_checksum and local_checksum != checksum: - logging.error(f"Downloaded file [{file}] looks corrupted.") - logging.error( - f"Source cksum: [{checksum}] downloaded: [{local_checksum}]" - ) - logging.error(f"Removing corrupted file [{file}].") - os.remove(file_path) - TaskManager.remove(task_id) - return False - - TaskManager.remove(task_id) - return True - - @staticmethod - def extract(name: str, component: str, archive: str) -> bool: - """Extract a component from an archive.""" - - if component in ["runner", "runner:proton"]: - path = Paths.runners - elif component == "dxvk": - path = Paths.dxvk - elif component == "vkd3d": - path = Paths.vkd3d - elif component == "nvapi": - path = Paths.nvapi - elif component == "latencyflex": - path = Paths.latencyflex - elif component == "runtime": - path = Paths.runtimes - elif component == "winebridge": - path = Paths.winebridge - else: - logging.error(f"Unknown component [{component}].") - return False - - try: - """ - Try to extract the archive in the /temp directory. - If the extraction fails, remove the archive from the /temp - directory and return False. The common cause of a failed - extraction is that the archive is corrupted. - """ - tar = tarfile.open(f"{Paths.temp}/{archive}") - root_dir = tar.getnames()[0] - tar.extractall(path) - tar.close() - except (tarfile.TarError, OSError, EOFError): - with contextlib.suppress(FileNotFoundError): - os.remove(os.path.join(Paths.temp, archive)) - with contextlib.suppress(FileNotFoundError): - shutil.rmtree(os.path.join(path, archive[:-7])) - - logging.error("Extraction failed! Archive ends earlier than expected.") - return False - - if root_dir.endswith("x86_64"): - try: - """ - If the folder ends with x86_64, remove this from its name. - Return False if an folder with the same name already exists. - """ - root_dir = os.path.join(path, root_dir) - shutil.move(src=root_dir, dst=root_dir[:-7]) - except (FileExistsError, shutil.Error): - logging.error("Extraction failed! Component already exists.") - return False - return True - - @LockManager.lock(Locks.ComponentsInstall) # avoid high resource usage - def install( - self, - component_type: str, - component_name: str, - func: TaskStreamUpdateHandler | None = None, - ): - """ - This function is used to install a component. It automatically - gets the manifest from the given component and then calls the - download and extract functions. - """ - manifest = self.get_component(component_name) - - if not manifest: - return Result(False) - - logging.info(f"Installing component: [{component_name}].") - file = manifest["File"][0] - - res = self.download( - download_url=file["url"], - file=file["file_name"], - rename=file["rename"], - checksum=file["file_checksum"], - func=func, - ) - - if not res: - """ - If the download fails, execute the given func passing - failed=True as a parameter. - """ - if func: - func(status=Status.FAILED) - return Result(False) - - archive = manifest["File"][0]["file_name"] - - if manifest["File"][0]["rename"]: - """ - If the component has a rename, rename the downloaded file - to the required name. - """ - archive = manifest["File"][0]["rename"] - - self.extract(component_name, component_type, archive) - - """ - Execute Post Install if the component has it defined - in the manifest. - """ - if "Post" in manifest: - print(f"Executing post install for [{component_name}].") - - for post in manifest.get("Post", []): - if post["action"] == "rename": - self.__post_rename(component_type, post) - - """ - Ask the manager to re-organize its components. - Note: I know that this is not the most efficient way to do this, - please give feedback if you know a better way to avoid this. - """ - if component_type in ["runtime", "winebridge"]: - with contextlib.suppress(FileNotFoundError): - os.remove(os.path.join(Paths.temp, archive)) - - if component_type in ["runner", "runner:proton"]: - self.__manager.check_runners() - - elif component_type == "dxvk": - self.__manager.check_dxvk() - - elif component_type == "vkd3d": - self.__manager.check_vkd3d() - - elif component_type == "nvapi": - self.__manager.check_nvapi() - - elif component_type == "runtime": - self.__manager.check_runtimes() - - elif component_type == "winebridge": - self.__manager.check_winebridge() - - self.__manager.organize_components() - logging.info(f"Component installed: {component_type} {component_name}", jn=True) - - return Result(True) - - @staticmethod - def __post_rename(component_type: str, post: dict): - source = post.get("source") - dest = post.get("dest") - - if component_type in ["runner", "runner:proton"]: - path = Paths.runners - elif component_type == "dxvk": - path = Paths.dxvk - elif component_type == "vkd3d": - path = Paths.vkd3d - elif component_type == "nvapi": - path = Paths.nvapi - else: - logging.error(f"Unknown component type: {component_type}") - return - - if ( - source is not None - and dest is not None - and not os.path.isdir(os.path.join(path, dest)) - ): - shutil.move(src=os.path.join(path, source), dst=os.path.join(path, dest)) - - def is_in_use(self, component_type: str, component_name: str): - bottles = self.__manager.local_bottles - - if component_type in ["runner", "runner:proton"]: - return component_name in [b["Runner"] for _, b in bottles.items()] - if component_type == "dxvk": - return component_name in [b["DXVK"] for _, b in bottles.items()] - if component_type == "vkd3d": - return component_name in [b["VKD3D"] for _, b in bottles.items()] - if component_type == "nvapi": - return component_name in [b["NVAPI"] for _, b in bottles.items()] - if component_type == "latencyflex": - return component_name in [b["LatencyFleX"] for _, b in bottles.items()] - if component_type in ["runtime", "winebridge"]: - return True - return False - - def uninstall(self, component_type: str, component_name: str): - if self.is_in_use(component_type, component_name): - return Result( - False, - data={ - "message": f"Component in use and cannot be removed: {component_name}" - }, - ) - - if component_type in ["runner", "runner:proton"]: - path = ManagerUtils.get_runner_path(component_name) - - elif component_type == "dxvk": - path = ManagerUtils.get_dxvk_path(component_name) - - elif component_type == "vkd3d": - path = ManagerUtils.get_vkd3d_path(component_name) - - elif component_type == "nvapi": - path = ManagerUtils.get_nvapi_path(component_name) - - elif component_type == "latencyflex": - path = ManagerUtils.get_latencyflex_path(component_name) - - else: - logging.error(f"Unknown component type: {component_type}") - return Result(False, data={"message": "Unknown component type."}) - - if not os.path.isdir(path): - return Result(False, data={"message": "Component not installed."}) - - try: - shutil.rmtree(path) - except Exception as e: - logging.error(f"Failed to uninstall component: {component_name}, {e}") - return Result(False, data={"message": "Failed to uninstall component."}) - - """ - Ask the manager to re-organize its components. - Note: I know that this is not the most efficient way to do this, - please give feedback if you know a better way to avoid this. - """ - if component_type in ["runner", "runner:proton"]: - self.__manager.check_runners() - - elif component_type == "dxvk": - self.__manager.check_dxvk() - - elif component_type == "vkd3d": - self.__manager.check_vkd3d() - - elif component_type == "nvapi": - self.__manager.check_nvapi() - - elif component_type == "runtime": - self.__manager.check_runtimes() - - elif component_type == "winebridge": - self.__manager.check_winebridge() - - self.__manager.organize_components() - logging.info(f"Component uninstalled: {component_type} {component_name}") - - return Result(True) diff --git a/bottles/backend/managers/conf.py b/bottles/backend/managers/conf.py deleted file mode 100644 index 2936700746b..00000000000 --- a/bottles/backend/managers/conf.py +++ /dev/null @@ -1,139 +0,0 @@ -import os -from configparser import ConfigParser - -from bottles.backend.utils import yaml, json - - -class ConfigManager: - def __init__( - self, - config_file: str | None = None, - config_type: str = "ini", - config_string: str | None = None, - ): - self.config_file = config_file - self.config_string = config_string - self.config_type = config_type - - if self.config_file is not None: - self.checks() - - self.config_dict = self.read() - - if self.config_file is not None and self.config_string is not None: - raise ValueError( - "Passing both config_file and config_string is not allowed" - ) - - def checks(self): - """Checks if the configuration file exists, if not, create it.""" - if not os.path.exists(self.config_file): - base_path = os.path.dirname(self.config_file) - os.makedirs(base_path, exist_ok=True) - - with open(self.config_file, "w") as f: - f.write("") - - def read(self): - if self.config_file is not None: - """Reads the configuration file and returns it as a dictionary""" - if self.config_type == "ini": - config = ConfigParser() - config.read(self.config_file) - # noinspection PyProtectedMember - res = config._sections - elif self.config_type == "json": - with open(self.config_file) as f: - res = json.load(f) - elif self.config_type == "yaml" or self.config_type == "yml": - with open(self.config_file) as f: - res = yaml.load(f) - else: - raise ValueError("Invalid configuration type") - elif self.config_string is not None: - if self.config_type == "ini": - config = ConfigParser() - config.read_string(self.config_string) - res = config._sections - elif self.config_type == "json": - res = json.loads(self.config_string) - elif self.config_type == "yaml" or self.config_type == "yml": - res = yaml.load(self.config_string) - else: - raise ValueError("Invalid configuration type") - else: - res = None - - return res or {} - - def get_dict(self): - """Returns the configuration as a dictionary""" - return self.config_dict - - def write_json(self): - """Writes the configuration to a JSON file""" - with open(self.config_file, "w") as f: - json.dump(self.config_dict, f, indent=4) - - def write_yaml(self): - """Writes the configuration to a YAML file""" - with open(self.config_file, "w") as f: - yaml.dump(self.config_dict, f) - - def write_ini(self): - """Writes the configuration to an INI file""" - config = ConfigParser() - - for section in self.config_dict: - config.add_section(section) - - for key, value in self.config_dict[section].items(): - config.set(section, key, value) - - with open(self.config_file, "w") as f: - config.write(f) - - def write_dict(self, config_file: str | None = None): - if self.config_file is None and config_file is None: - raise ValueError("No config path specified") - elif self.config_file is None and config_file is not None: - self.config_file = config_file - - """Writes the configuration to the file""" - if self.config_type == "ini": - self.write_ini() - elif self.config_type == "json": - self.write_json() - elif self.config_type == "yaml": - self.write_yaml() - else: - raise ValueError("Invalid configuration type") - - def merge_dict(self, changes: dict): - """Merges a dictionary into the configuration""" - for section in changes: - if section in self.config_dict: - for key, value in changes[section].items(): - if isinstance(value, dict): - if key in self.config_dict[section]: - self.config_dict[section][key].update(value) - else: - self.config_dict[section][key] = value - else: - self.config_dict[section][key] = value - else: - self.config_dict[section] = changes[section] - - self.write_dict() - - def del_key(self, key_struct: dict): - """Deletes a key from the configuration""" - key = self.config_dict - - for i, k in enumerate(key_struct): - if i == len(key_struct) - 1: - del key[k] - continue - key = key[k] - - self.write_dict() diff --git a/bottles/backend/managers/data.py b/bottles/backend/managers/data.py deleted file mode 100644 index fc463840ca4..00000000000 --- a/bottles/backend/managers/data.py +++ /dev/null @@ -1,101 +0,0 @@ -# data.py -# -# Copyright 2022 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import contextlib -import os - -from bottles.backend.globals import Paths -from bottles.backend.logger import Logger -from bottles.backend.models.samples import Samples -from bottles.backend.utils import yaml - -logging = Logger() - - -class UserDataKeys: - CustomBottlesPath = "custom_bottles_path" - - -class DataManager: - """ - The DataManager class is used to store and retrieve data - from the user data.yml file. Should be stored only info - and settings that should not be stored in gsettings. - """ - - __data: dict = {} - __p_data = f"{Paths.base}/data.yml" - - def __init__(self): - self.__get_data() - - def __get_data(self): - try: - with open(self.__p_data) as s: - self.__data = yaml.load(s) - if self.__data is None: - raise AttributeError - except FileNotFoundError: - logging.error( - "Data file not found. Creating new one.", - ) - self.__create_data_file() - except AttributeError: - logging.error( - "Data file is empty. Creating new one.", - ) - self.__create_data_file() - - def __create_data_file(self): - if not os.path.exists(Paths.base): - os.makedirs(Paths.base) - with open(self.__p_data, "w") as s: - yaml.dump(Samples.data, s) - self.__get_data() - - def list(self): - """Returns the whole data dictionary.""" - return self.__data - - def set(self, key, value, of_type=None): - """Sets a value in the data dictionary.""" - if self.__data.get(key): - if isinstance(self.__data[key], list): - self.__data[key].append(value) - else: - self.__data[key] = value - else: - if of_type is list: - self.__data[key] = [value] - else: - self.__data[key] = value - - with contextlib.suppress(FileNotFoundError): - with open(self.__p_data, "w") as s: - yaml.dump(self.__data, s) - - def remove(self, key): - """Removes a key from the data dictionary.""" - if self.__data.get(key): - del self.__data[key] - with contextlib.suppress(FileNotFoundError): - with open(self.__p_data, "w") as s: - yaml.dump(self.__data, s) - - def get(self, key): - """Returns the value of a key in the data dictionary.""" - return self.__data.get(key) diff --git a/bottles/backend/managers/dependency.py b/bottles/backend/managers/dependency.py deleted file mode 100644 index 0c95dfc8dbf..00000000000 --- a/bottles/backend/managers/dependency.py +++ /dev/null @@ -1,643 +0,0 @@ -# dependency.py -# -# Copyright 2022 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import os -import shutil -import traceback -from functools import lru_cache -from glob import glob - -import patoolib # type: ignore [import-untyped] - -from bottles.backend.cabextract import CabExtract -from bottles.backend.globals import Paths -from bottles.backend.logger import Logger -from bottles.backend.models.config import BottleConfig -from bottles.backend.models.enum import Arch -from bottles.backend.models.result import Result -from bottles.backend.state import TaskManager, Task -from bottles.backend.utils.generic import validate_url -from bottles.backend.utils.manager import ManagerUtils -from bottles.backend.wine.executor import WineExecutor -from bottles.backend.wine.reg import Reg, RegItem -from bottles.backend.wine.regkeys import RegKeys -from bottles.backend.wine.regsvr32 import Regsvr32 -from bottles.backend.wine.uninstaller import Uninstaller -from bottles.backend.wine.winedbg import WineDbg - -logging = Logger() - - -class DependencyManager: - def __init__(self, manager, offline: bool = False): - self.__manager = manager - self.__repo = manager.repository_manager.get_repo("dependencies", offline) - self.__utils_conn = manager.utils_conn - - @lru_cache - def get_dependency(self, name: str, plain: bool = False) -> str | dict | bool: - return self.__repo.get(name, plain) - - @lru_cache - def fetch_catalog(self) -> dict: - """ - Fetch all dependencies from the Bottles repository - and return these as a dictionary. It also returns an empty dictionary - if there are no dependencies or fails to fetch them. - """ - if not self.__utils_conn.check_connection(): - return {} - - catalog = {} - index = self.__repo.catalog - - for dependency in index.items(): - catalog[dependency[0]] = dependency[1] - - catalog = dict(sorted(catalog.items())) - return catalog - - def install(self, config: BottleConfig, dependency: list) -> Result: - """ - Install a given dependency in a bottle. It will - return True if the installation was successful. - """ - uninstaller = True - - if config.Parameters.versioning_automatic: - """ - If the bottle has the versioning system enabled, we need - to create a new version of the bottle, before installing - the dependency. - """ - self.__manager.versioning_manager.create_state( - config=config, message=f"Before installing {dependency[0]}" - ) - - task_id = TaskManager.add(Task(title=dependency[0])) - - logging.info( - "Installing dependency [{}] in bottle [{}].".format( - dependency[0], config.Name - ), - ) - manifest = self.get_dependency(dependency[0]) - if not manifest: - """ - If the manifest is not found, return a Result - object with the error. - """ - TaskManager.remove(task_id) - return Result( - status=False, message=f"Cannot find manifest for {dependency[0]}." - ) - - if manifest.get("Dependencies"): - """ - If the manifest has dependencies, we need to install them - before installing the current one. - """ - for _ext_dep in manifest.get("Dependencies"): - if _ext_dep in config.Installed_Dependencies: - continue - if _ext_dep in self.__manager.supported_dependencies: - _dep = self.__manager.supported_dependencies[_ext_dep] - _res = self.install(config, [_ext_dep, _dep]) - if not _res.status: - return _res - - for step in manifest.get("Steps"): - """ - Here we execute all steps in the manifest. - Steps are the actions performed to install the dependency. - """ - arch = step.get("for", "win64_win32") - if config.Arch not in arch: - continue - - res = self.__perform_steps(config, step) - if not res.ok: - TaskManager.remove(task_id) - return Result( - status=False, - message=f"One or more steps failed for {dependency[0]}.", - ) - if not res.data.get("uninstaller"): - uninstaller = False - - if dependency[0] not in config.Installed_Dependencies: - """ - If the dependency is not already listed in the installed - dependencies list of the bottle, add it. - """ - dependencies = [dependency[0]] - - if config.Installed_Dependencies: - dependencies = config.Installed_Dependencies + [dependency[0]] - - self.__manager.update_config( - config=config, key="Installed_Dependencies", value=dependencies - ) - - if manifest.get("Uninstaller"): - """ - If the manifest has an uninstaller, add it to the - uninstaller list in the bottle config. - Set it to NO_UNINSTALLER if the dependency cannot be uninstalled. - """ - uninstaller = manifest.get("Uninstaller") - - if dependency[0] not in config.Installed_Dependencies: - self.__manager.update_config( - config, dependency[0], uninstaller, "Uninstallers" - ) - - # Remove entry from task manager - TaskManager.remove(task_id) - - # Hide installation button and show remove button - logging.info(f"Dependency installed: {dependency[0]} in {config.Name}", jn=True) - if not uninstaller: - return Result(status=True, data={"uninstaller": False}) - return Result(status=True, data={"uninstaller": True}) - - def __perform_steps(self, config: BottleConfig, step: dict) -> Result: - """ - This method execute a step in the bottle (e.g. changing the Windows - version, installing fonts, etc.) - --- - Returns True if the dependency cannot be uninstalled. - """ - uninstaller = True - - if step["action"] == "delete_dlls": - self.__step_delete_dlls(config, step) - - if step["action"] == "download_archive": - if not self.__step_download_archive(step): - return Result(status=False) - - if step["action"] in ["install_exe", "install_msi"]: - if not self.__step_install_exe_msi(config=config, step=step): - return Result(status=False) - - if step["action"] == "uninstall": - self.__step_uninstall(config=config, file_name=step["file_name"]) - - if step["action"] == "cab_extract": - uninstaller = False - if not self.__step_cab_extract(step=step): - return Result(status=False) - - if step["action"] == "get_from_cab": - uninstaller = False - if not self.__step_get_from_cab(config=config, step=step): - return Result(status=False) - - if step["action"] == "archive_extract": - uninstaller = False - if not self.__step_archive_extract(step): - return Result(status=False) - - if step["action"] in ["install_cab_fonts", "install_fonts"]: - uninstaller = False - if not self.__step_install_fonts(config=config, step=step): - return Result(status=False) - - if step["action"] in ["copy_dll", "copy_file"]: - uninstaller = False - if not self.__step_copy_dll(config=config, step=step): - return Result(status=False) - - if step["action"] == "register_dll": - self.__step_register_dll(config=config, step=step) - - if step["action"] == "override_dll": - self.__step_override_dll(config=config, step=step) - - if step["action"] == "set_register_key": - self.__step_set_register_key(config=config, step=step) - - if step["action"] == "register_font": - self.__step_register_font(config=config, step=step) - - if step["action"] == "replace_font": - self.__step_replace_font(config=config, step=step) - - if step["action"] == "set_windows": - self.__step_set_windows(config=config, step=step) - - if step["action"] == "use_windows": - self.__step_use_windows(config=config, step=step) - - return Result(status=True, data={"uninstaller": uninstaller}) - - @staticmethod - def __get_real_dest(config: BottleConfig, dest: str) -> str | bool: - """This function return the real destination path.""" - bottle = ManagerUtils.get_bottle_path(config) - _dest = dest - - if dest.startswith("temp/"): - dest = dest.replace("temp/", f"{Paths.temp}/") - elif dest.startswith("windows/"): - dest = f"{bottle}/drive_c/{dest}" - elif dest.startswith("win32"): - dest = f"{bottle}/drive_c/windows/system32/" - if config.Arch == Arch.WIN64: - dest = f"{bottle}/drive_c/windows/syswow64/" - dest = _dest.replace("win32", dest) - elif dest.startswith("win64"): - if config.Arch == Arch.WIN64: - dest = f"{bottle}/drive_c/windows/system32/" - dest = _dest.replace("win64", dest) - else: - return True - else: - logging.error("Destination path not supported!") - return False - - return dest - - def __step_download_archive(self, step: dict): - """ - This function download an archive from the given step. - Can be used for any file type (cab, zip, ...). Please don't - use this method for exe/msi files as the install_exe already - download the exe/msi file before installation. - """ - download = self.__manager.component_manager.download( - download_url=step.get("url"), - file=step.get("file_name"), - rename=step.get("rename"), - checksum=step.get("file_checksum"), - ) - - return download - - def __step_install_exe_msi(self, config: BottleConfig, step: dict) -> bool: - """ - Download and install the .exe or .msi file - declared in the step, in a bottle. - """ - winedbg = WineDbg(config) - download = self.__manager.component_manager.download( - download_url=step.get("url"), - file=step.get("file_name"), - rename=step.get("rename"), - checksum=step.get("file_checksum"), - ) - file = step.get("file_name") - if step.get("rename"): - file = step.get("rename") - - if download: - if step.get("url").startswith("temp/"): - _file = step.get("url").replace("temp/", f"{Paths.temp}/") - file = f"{_file}/{file}" - else: - file = f"{Paths.temp}/{file}" - executor = WineExecutor( - config, - exec_path=file, - args=step.get("arguments"), - environment=step.get("environment"), - ) - executor.run() - winedbg.wait_for_process(file) - return True - - return False - - @staticmethod - def __step_uninstall(config: BottleConfig, file_name: str) -> bool: - """ - This function find an uninstaller in the bottle by the given - file name and execute it. - """ - Uninstaller(config).from_name(file_name) - return True - - def __step_cab_extract(self, step: dict): - """ - This function download and extract a Windows Cabinet to the - temp folder. - """ - dest = step.get("dest") - if dest.startswith("temp/"): - dest = dest.replace("temp/", f"{Paths.temp}/") - else: - logging.error("Destination path not supported!") - return False - - if validate_url(step["url"]): - download = self.__manager.component_manager.download( - download_url=step.get("url"), - file=step.get("file_name"), - rename=step.get("rename"), - checksum=step.get("file_checksum"), - ) - - if download: - if step.get("rename"): - file = step.get("rename") - else: - file = step.get("file_name") - - if not CabExtract().run( - path=os.path.join(Paths.temp, file), name=file, destination=dest - ): - return False - else: - return False - - elif step["url"].startswith("temp/"): - path = step["url"] - path = path.replace("temp/", f"{Paths.temp}/") - - if step.get("rename"): - file_path = os.path.splitext(f"{step.get('rename')}")[0] - else: - file_path = os.path.splitext(f"{step.get('file_name')}")[0] - - if not CabExtract().run( - f"{path}/{step.get('file_name')}", file_path, destination=dest - ): - return False - - return True - - def __step_delete_dlls(self, config: BottleConfig, step: dict): - """Deletes the given dlls from the system32 or syswow64 paths""" - dest = self.__get_real_dest(config, step.get("dest")) - - for d in step.get("dlls", []): - _d = os.path.join(dest, d) - if os.path.exists(_d): - os.remove(_d) - - return True - - def __step_get_from_cab(self, config: BottleConfig, step: dict): - """Take a file from a cabiner and extract to a path.""" - source = step.get("source") - file_name = step.get("file_name") - rename = step.get("rename") - dest = self.__get_real_dest(config, step.get("dest")) - - if isinstance(dest, bool): - return dest - - res = CabExtract().run( - path=os.path.join(Paths.temp, source), files=[file_name], destination=dest - ) - - if rename: - _file_name = file_name.split("/")[-1] - - if os.path.exists(os.path.join(dest, rename)): - os.remove(os.path.join(dest, rename)) - - shutil.move(os.path.join(dest, _file_name), os.path.join(dest, rename)) - - if not res: - return False - return True - - def __step_archive_extract(self, step: dict): - """Download and extract an archive to the temp folder.""" - download = self.__manager.component_manager.download( - download_url=step.get("url"), - file=step.get("file_name"), - rename=step.get("rename"), - checksum=step.get("file_checksum"), - ) - - if not download: - return False - - if step.get("rename"): - file = step.get("rename") - else: - file = step.get("file_name") - - archive_path = os.path.join(Paths.temp, os.path.splitext(file)[0]) - - if os.path.exists(archive_path): - shutil.rmtree(archive_path) - - os.makedirs(archive_path) - try: - patoolib.extract_archive( - os.path.join(Paths.temp, file), outdir=archive_path - ) - if archive_path.endswith(".tar") and os.path.isfile( - os.path.join(archive_path, os.path.basename(archive_path)) - ): - tar_path = os.path.join(archive_path, os.path.basename(archive_path)) - patoolib.extract_archive(tar_path, outdir=archive_path) - except Exception as e: - logging.error("Something wrong happened during extraction.") - logging.error(f"{e}") - logging.error(f"{traceback.format_exc()}") - return False - return True - - @staticmethod - def __step_install_fonts(config: BottleConfig, step: dict): - """Move fonts to the drive_c/windows/Fonts path.""" - path = step["url"] - path = path.replace("temp/", f"{Paths.temp}/") - bottle_path = ManagerUtils.get_bottle_path(config) - - for font in step.get("fonts"): - font_path = f"{bottle_path}/drive_c/windows/Fonts/" - if not os.path.exists(font_path): - os.makedirs(font_path) - - try: - shutil.copyfile(f"{path}/{font}", f"{font_path}/{font}") - except (FileNotFoundError, FileExistsError): - logging.warning(f"Font {font} already exists or is not found.") - - # print(f"Copying {font} to {bottle_path}/drive_c/windows/Fonts/") - - return True - - # noinspection PyTypeChecker - def __step_copy_dll(self, config: BottleConfig, step: dict): - """ - This function copy dlls from temp folder to a directory - declared in the step. The bottle drive_c path will be used as - root path. - """ - path = step["url"] - path = path.replace("temp/", f"{Paths.temp}/") - dest = self.__get_real_dest(config, step.get("dest")) - - if isinstance(dest, bool): - return dest - - if not os.path.exists(dest): - os.makedirs(dest) - - try: - if "*" in step.get("file_name"): - files = glob(f"{path}/{step.get('file_name')}") - if not files: - logging.info(f"File(s) not found in {path}") - return False - for fg in files: - _name = fg.split("/")[-1] - _path = os.path.join(path, _name) - _dest = os.path.join(dest, _name) - logging.info(f"Copying {_name} to {_dest}") - - if os.path.exists(_dest) and os.path.islink(_dest): - os.unlink(_dest) - - try: - shutil.copyfile(_path, _dest) - except shutil.SameFileError: - logging.info( - f"{_name} already exists at the same version, skipping." - ) - else: - _name = step.get("file_name") - _dest = os.path.join(dest, _name) - logging.info(f"Copying {_name} to {_dest}") - - if os.path.exists(_dest) and os.path.islink(_dest): - os.unlink(_dest) - - try: - shutil.copyfile(os.path.join(path, _name), _dest) - except shutil.SameFileError: - logging.info( - f"{_name} already exists at the same version, skipping." - ) - - except Exception as e: - print(e) - logging.warning("An error occurred while copying dlls.") - return False - - return True - - @staticmethod - def __step_register_dll(config: BottleConfig, step: dict): - """Register one or more dll and ActiveX control""" - regsvr32 = Regsvr32(config) - - for dll in step.get("dlls", []): - regsvr32.register(dll) - - return True - - @staticmethod - def __step_override_dll(config: BottleConfig, step: dict): - """Register a new override for each dll.""" - reg = Reg(config) - - if step.get("url") and step.get("url").startswith("temp/"): - path = step["url"].replace("temp/", f"{Paths.temp}/") - dlls = glob(os.path.join(path, step.get("dll"))) - - bundle = {"HKEY_CURRENT_USER\\Software\\Wine\\DllOverrides": []} - - for dll in dlls: - dll_name = os.path.splitext(os.path.basename(dll))[0] - bundle["HKEY_CURRENT_USER\\Software\\Wine\\DllOverrides"].append( - {"value": dll_name, "data": step.get("type")} - ) - - reg.import_bundle(bundle) - return True - - if step.get("bundle"): - _bundle = { - "HKEY_CURRENT_USER\\Software\\Wine\\DllOverrides": step.get("bundle") - } - reg.import_bundle(_bundle) - return True - - reg.add( - key="HKEY_CURRENT_USER\\Software\\Wine\\DllOverrides", - value=step.get("dll"), - data=step.get("type"), - ) - return True - - @staticmethod - def __step_set_register_key(config: BottleConfig, step: dict): - """Set a registry key.""" - reg = Reg(config) - reg.add( - key=step.get("key"), - value=step.get("value"), - data=step.get("data"), - value_type=step.get("type"), - ) - return True - - @staticmethod - def __step_register_font(config: BottleConfig, step: dict): - """Register a font in the registry.""" - reg = Reg(config) - reg.add( - key="HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts", - value=step.get("name"), - data=step.get("file"), - ) - return True - - @staticmethod - def __step_replace_font(config: BottleConfig, step: dict): - """Register a font replacement in the registry.""" - reg = Reg(config) - target_font = step.get("font") - replaces = step.get("replace") - - if not isinstance(replaces, list): - logging.warning("Invalid replace_font, 'replace' field should be list.") - return False - - regs = [ - RegItem( - key="HKEY_CURRENT_USER\\Software\\Wine\\Fonts\\Replacements", - value=r, - value_type="", - data=target_font, - ) - for r in replaces - ] - reg.bulk_add(regs) - return True - - @staticmethod - def __step_set_windows(config: BottleConfig, step: dict): - """Set the Windows version.""" - rk = RegKeys(config) - rk.lg_set_windows(step.get("version")) - return True - - @staticmethod - def __step_use_windows(config: BottleConfig, step: dict): - """Set a Windows version per program.""" - rk = RegKeys(config) - rk.set_app_default(step.get("version"), step.get("executable")) - return True diff --git a/bottles/backend/managers/epicgamesstore.py b/bottles/backend/managers/epicgamesstore.py deleted file mode 100644 index 127fe616f95..00000000000 --- a/bottles/backend/managers/epicgamesstore.py +++ /dev/null @@ -1,86 +0,0 @@ -# epicgamesstore.py -# -# Copyright 2022 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import os -import uuid - -from bottles.backend.models.config import BottleConfig -from bottles.backend.utils import json -from bottles.backend.utils.manager import ManagerUtils - - -class EpicGamesStoreManager: - @staticmethod - def find_dat_path(config: BottleConfig) -> str | None: - """ - Finds the Epic Games dat file path. - """ - paths = [ - os.path.join( - ManagerUtils.get_bottle_path(config), - "drive_c/ProgramData/Epic/UnrealEngineLauncher/LauncherInstalled.dat", - ) - ] - - for path in paths: - if os.path.exists(path): - return path - return None - - @staticmethod - def is_epic_supported(config: BottleConfig) -> bool: - """ - Checks if Epic Games is supported. - """ - return EpicGamesStoreManager.find_dat_path(config) is not None - - @staticmethod - def get_installed_games(config: BottleConfig) -> list: - """ - Gets the games. - """ - games = [] - dat_path = EpicGamesStoreManager.find_dat_path(config) - - if dat_path is None: - return [] - - with open(dat_path) as dat: - data = json.load(dat) - - for game in data["InstallationList"]: - _uri = f"-com.epicgames.launcher://apps/{game['AppName']}?action=launch&silent=true" - _args = f"-opengl -SkipBuildPatchPrereq {_uri}" - _name = game["InstallLocation"].split("\\")[-1] - _path = ( - "C:\\Program Files (x86)\\Epic Games\\Launcher\\Portal\\Binaries\\Win32\\" - "EpicGamesLauncher.exe" - ) - _executable = _path.split("\\")[-1] - _folder = ManagerUtils.get_exe_parent_dir(config, _path) - games.append( - { - "executable": _path, - "arguments": _args, - "name": _name, - "path": _path, - "folder": _folder, - "icon": "com.usebottles.bottles-program", - "id": str(uuid.uuid4()), - } - ) - return games diff --git a/bottles/backend/managers/importer.py b/bottles/backend/managers/importer.py deleted file mode 100644 index 8d3b14cf8ed..00000000000 --- a/bottles/backend/managers/importer.py +++ /dev/null @@ -1,129 +0,0 @@ -# importer.py -# -# Copyright 2022 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import os - -from bottles.backend.models.config import BottleConfig -import subprocess -from glob import glob -from datetime import datetime - -from bottles.backend.logger import Logger -from bottles.backend.globals import TrdyPaths, Paths -from bottles.backend.models.result import Result - -logging = Logger() - - -class ImportManager: - def __init__(self, manager): - self.manager = manager - - @staticmethod - def search_wineprefixes() -> Result: - """Look and return all 3rd party available wine prefixes""" - importer_wineprefixes = [] - - # search wine prefixes in external managers paths - wine_standard = glob(TrdyPaths.wine) - lutris_results = glob(f"{TrdyPaths.lutris}/*/") - playonlinux_results = glob(f"{TrdyPaths.playonlinux}/*/") - bottlesv1_results = glob(f"{TrdyPaths.bottlesv1}/*/") - - results = ( - wine_standard + lutris_results + playonlinux_results + bottlesv1_results - ) - - # count results - is_wine = len(wine_standard) - is_lutris = len(lutris_results) - is_playonlinux = len(playonlinux_results) - i = 1 - - for wineprefix in results: - wineprefix_name = wineprefix.split("/")[-2] - - # identify manager by index - if i <= is_wine: - wineprefix_manager = "Legacy Wine" - elif i <= is_wine + is_lutris: - wineprefix_manager = "Lutris" - elif i <= is_wine + is_lutris + is_playonlinux: - wineprefix_manager = "PlayOnLinux" - else: - wineprefix_manager = "Bottles v1" - - # check the drive_c path exists - if os.path.isdir(os.path.join(wineprefix, "drive_c")): - wineprefix_lock = os.path.isfile( - os.path.join(wineprefix, "bottle.lock") - ) - importer_wineprefixes.append( - { - "Name": wineprefix_name, - "Manager": wineprefix_manager, - "Path": wineprefix, - "Lock": wineprefix_lock, - } - ) - i += 1 - - logging.info(f"Found {len(importer_wineprefixes)} wine prefixes…") - - return Result(status=True, data={"wineprefixes": importer_wineprefixes}) - - def import_wineprefix(self, wineprefix: dict) -> Result: - """Import wineprefix from external manager and convert in a bottle""" - logging.info(f"Importing wineprefix {wineprefix['Name']} as bottle…") - - # prepare bottle path for the wine prefix - bottle_path = f"Imported_{wineprefix.get('Name')}" - bottle_complete_path = os.path.join(Paths.bottles, bottle_path) - - try: - os.makedirs(bottle_complete_path, exist_ok=False) - except (FileExistsError, OSError): - logging.error(f"Error creating bottle directory for {wineprefix['Name']}") - return Result(False) - - # create lockfile in source path - logging.info(f"Creating lock file in {wineprefix['Path']}…") - open(f'{wineprefix.get("Path")}/bottle.lock', "a").close() - - # copy wineprefix files in the new bottle - command = f"cp -a {wineprefix.get('Path')}/* {bottle_complete_path}/" - subprocess.Popen(command, shell=True).communicate() - - # create bottle config - new_config = BottleConfig() - new_config.Name = wineprefix["Name"] - new_config.Runner = self.manager.get_latest_runner() - new_config.Path = bottle_path - new_config.Environment = "Custom" - new_config.Creation_Date = str(datetime.now()) - new_config.Update_Date = str(datetime.now()) - - # save config - saved = new_config.dump(os.path.join(bottle_complete_path, "bottle.yml")) - if not saved.status: - return Result(False) - - # update bottles view - self.manager.update_bottles(silent=True) - - logging.info(f"Wine prefix {wineprefix['Name']} imported as bottle.", jn=True) - return Result(True) diff --git a/bottles/backend/managers/installer.py b/bottles/backend/managers/installer.py deleted file mode 100644 index 5bf63a53786..00000000000 --- a/bottles/backend/managers/installer.py +++ /dev/null @@ -1,489 +0,0 @@ -# installer_manager.py -# -# Copyright 2022 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -import os -import subprocess -import uuid -from functools import lru_cache - -import markdown -import pycurl - -from bottles.backend.globals import Paths -from bottles.backend.logger import Logger -from bottles.backend.managers.conf import ConfigManager -from bottles.backend.models.config import BottleConfig -from bottles.backend.models.result import Result -from bottles.backend.utils.manager import ManagerUtils -from bottles.backend.utils.wine import WineUtils -from bottles.backend.wine.executor import WineExecutor -from bottles.backend.wine.winecommand import WineCommand - -logging = Logger() - - -class InstallerManager: - def __init__(self, manager, offline: bool = False): - self.__manager = manager - self.__repo = manager.repository_manager.get_repo("installers", offline) - self.__utils_conn = manager.utils_conn - self.__component_manager = manager.component_manager - self.__local_resources = {} - - @lru_cache - def get_review(self, installer_name, parse: bool = True) -> str: - """Return an installer review from the repository (as HTML)""" - review = self.__repo.get_review(installer_name) - if not review: - return "No review found for this installer." - if parse: - return markdown.markdown(review) - return review - - @lru_cache - def get_installer( - self, installer_name: str, plain: bool = False - ) -> str | dict | bool: - """ - Return an installer manifest from the repository. Use the plain - argument to get the manifest as plain text. - """ - return self.__repo.get(installer_name, plain) - - @lru_cache - def fetch_catalog(self) -> dict: - """Fetch the installers catalog from the repository""" - catalog = {} - index = self.__repo.catalog - if not self.__utils_conn.check_connection(): - return {} - - for installer in index.items(): - catalog[installer[0]] = installer[1] - - catalog = dict(sorted(catalog.items())) - return catalog - - def get_icon_url(self, installer): - """Wrapper for the repo method.""" - return self.__repo.get_icon(installer) - - def __download_icon(self, config, executable: dict, manifest): - """ - Download the installer icon from the repository to the bottle - icons path. - """ - icon_url = self.__repo.get_icon(manifest.get("Name")) - bottle_icons_path = f"{ManagerUtils.get_bottle_path(config)}/icons" - icon_path = f"{bottle_icons_path}/{executable.get('icon')}" - - if icon_url is not None: - if not os.path.exists(bottle_icons_path): - os.makedirs(bottle_icons_path) - - if not os.path.isfile(icon_path): - c = pycurl.Curl() - c.setopt(c.URL, icon_url) - c.setopt(c.WRITEDATA, open(icon_path, "wb")) - c.perform() - c.close() - - def __process_local_resources(self, exe_msi_steps, installer): - files = self.has_local_resources(installer) - if not files: - return True - for file in files: - if file not in exe_msi_steps.keys(): - return False - self.__local_resources[file] = exe_msi_steps[file] - return True - - def __install_dependencies( - self, - config: BottleConfig, - dependencies: list, - step_fn: callable, - is_final: bool = False, - ): - """Install a list of dependencies""" - _config = config - - for dep in dependencies: - if is_final: - step_fn() - - if dep in config.Installed_Dependencies: - continue - - _dep = [dep, self.__manager.supported_dependencies.get(dep)] - res = self.__manager.dependency_manager.install(_config, _dep) - - if not res.ok: - return False - - return True - - @staticmethod - def __perform_checks(config, checks: dict): - """Perform a list of checks""" - bottle_path = ManagerUtils.get_bottle_path(config) - - if files := checks.get("files"): - for f in files: - if f.startswith("userdir/"): - current_user = os.getenv("USER") - f = f.replace("userdir/", f"users/{current_user}/") - - _f = os.path.join(bottle_path, "drive_c", f) - if not os.path.exists(_f): - logging.error( - f"During checks, file {_f} was not found, assuming it is not installed. Aborting." - ) - return False - - return True - - def __perform_steps(self, config: BottleConfig, steps: list): - """Perform a list of actions""" - for st in steps: - # Step type: run_script - if st.get("action") == "run_script": - self.__step_run_script(config, st) - - # Step type: run_winecommand - if st.get("action") == "run_winecommand": - self.__step_run_winecommand(config, st) - - # Step type: update_config - if st.get("action") == "update_config": - self.__step_update_config(config, st) - - # Step type: install_exe, install_msi - if st["action"] in ["install_exe", "install_msi"]: - if st["url"] != "local": - download = self.__component_manager.download( - st.get("url"), - st.get("file_name"), - st.get("rename"), - checksum=st.get("file_checksum"), - ) - else: - download = True - - if download: - if st["url"] != "local": - if st.get("rename"): - file = st.get("rename") - else: - file = st.get("file_name") - file_path = f"{Paths.temp}/{file}" - else: - file_path = self.__local_resources[st.get("file_name")] - - executor = WineExecutor( - config, - exec_path=file_path, - args=st.get("arguments"), - environment=st.get("environment"), - monitoring=st.get("monitoring", []), - ) - executor.run() - else: - logging.error( - f"Failed to download {st.get('file_name')}, or checksum failed." - ) - return False - return True - - @staticmethod - def __step_run_winecommand(config: BottleConfig, step: dict): - """Run a wine command""" - commands = step.get("commands") - - if not commands: - return - - for command in commands: - _winecommand = WineCommand( - config, - command=command.get("command"), - arguments=command.get("arguments"), - minimal=command.get("minimal"), - ) - _winecommand.run() - - @staticmethod - def __step_run_script(config: BottleConfig, step: dict): - placeholders = { - "!bottle_path": ManagerUtils.get_bottle_path(config), - "!bottle_drive": f"{ManagerUtils.get_bottle_path(config)}/drive_c", - "!bottle_name": config.Name, - "!bottle_arch": config.Arch, - } - preventions = {"bottle.yml": "Bottle configuration cannot be modified."} - script = step.get("script") - - for key, value in placeholders.items(): - script = script.replace(key, value) - - for key, value in preventions.items(): - if script.find(key) != -1: - logging.error( - value, - ) - return False - - logging.info("Executing installer script…") - subprocess.Popen( - f"bash -c '{script}'", - shell=True, - cwd=ManagerUtils.get_bottle_path(config), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ).communicate() - logging.info("Finished executing installer script.") - - @staticmethod - def __step_update_config(config: BottleConfig, step: dict): - bottle = ManagerUtils.get_bottle_path(config) - conf_path = step.get("path") - conf_type = step.get("type") - del_keys = step.get("del_keys", {}) - upd_keys = step.get("upd_keys", {}) - - if conf_path.startswith("userdir/"): - current_user = os.getenv("USER") - conf_path = conf_path.replace("userdir/", f"drive_c/users/{current_user}/") - - conf_path = f"{bottle}/{conf_path}" - _conf = ConfigManager(config_file=conf_path, config_type=conf_type) - - for d in del_keys: - _conf.del_key(d) - - _conf.merge_dict(upd_keys) - - def __set_parameters(self, config: BottleConfig, new_params: dict): - _config = config - - if "dxvk" in new_params and isinstance(new_params["dxvk"], bool): - if new_params["dxvk"] != config.Parameters.dxvk: - self.__manager.install_dll_component( - _config, "dxvk", remove=not new_params["dxvk"] - ) - - if "vkd3d" in new_params and isinstance(new_params["vkd3d"], bool): - if new_params["vkd3d"] != config.Parameters.vkd3d: - self.__manager.install_dll_component( - _config, "vkd3d", remove=not new_params["vkd3d"] - ) - - if "dxvk_nvapi" in new_params and isinstance(new_params["dxvk_nvapi"], bool): - if new_params["dxvk_nvapi"] != config.Parameters.dxvk_nvapi: - self.__manager.install_dll_component( - _config, "nvapi", remove=not new_params["dxvk_nvapi"] - ) - - if "latencyflex" in new_params and isinstance(new_params["latencyflex"], bool): - if new_params["latencyflex"] != config.Parameters.latencyflex: - self.__manager.install_dll_component( - _config, "latencyflex", remove=not new_params["latencyflex"] - ) - - # avoid sync type change if not set to "wine" - if "sync" in new_params and config.Parameters.sync != "wine": - del new_params["sync"] - - for k, v in new_params.items(): - self.__manager.update_config( - config=config, key=k, value=v, scope="Parameters" - ) - - def count_steps(self, installer) -> dict: - manifest = self.get_installer(installer[0]) - steps = {"total": 0, "sections": []} - if manifest.get("Dependencies"): - i = int(len(manifest.get("Dependencies"))) - steps["sections"] += i * ["deps"] - steps["total"] += i - if manifest.get("Parameters"): - steps["sections"].append("params") - steps["total"] += 1 - if manifest.get("Steps"): - i = int(len(manifest.get("Steps"))) - steps["sections"] += i * ["steps"] - steps["total"] += i - if manifest.get("Executable"): - steps["sections"].append("exe") - steps["total"] += 1 - if manifest.get("Checks"): - steps["sections"].append("checks") - steps["total"] += 1 - - return steps - - def has_local_resources(self, installer): - manifest = self.get_installer(installer[0]) - steps = manifest.get("Steps", []) - exe_msi_steps = [ - s - for s in steps - if s.get("action", "") in ["install_exe", "install_msi"] - and s.get("url", "") == "local" - ] - - if len(exe_msi_steps) == 0: - return [] - - files = [s.get("file_name", "") for s in exe_msi_steps] - return files - - def install( - self, - config: BottleConfig, - installer: dict, - step_fn: callable, - is_final: bool = True, - local_resources: dict | None = None, - ): - manifest = self.get_installer(installer[0]) - _config = config - - bottle = ManagerUtils.get_bottle_path(config) - installers = manifest.get("Installers") - dependencies = manifest.get("Dependencies") - parameters = manifest.get("Parameters") - executable = manifest.get("Executable") - steps = manifest.get("Steps") - checks = manifest.get("Checks") - - # download icon - if executable.get("icon"): - self.__download_icon(_config, executable, manifest) - - # install dependent installers - if installers: - logging.info("Installing dependent installers") - for i in installers: - if not self.install(config, i, step_fn, False): - logging.error("Failed to install dependent installer(s)") - return Result( - False, - data={"message": "Failed to install dependent installer(s)"}, - ) - - # ask for local resources - if local_resources: - if not self.__process_local_resources(local_resources, installer): - return Result( - False, data={"message": "Local resources not found or invalid"} - ) - - # install dependencies - if dependencies: - logging.info("Installing dependencies") - if not self.__install_dependencies( - _config, dependencies, step_fn, is_final - ): - return Result( - False, data={"message": "Dependencies installation failed."} - ) - - # set parameters - if parameters: - logging.info("Updating bottle parameters") - if is_final: - step_fn() - - self.__set_parameters(_config, parameters) - - # execute steps - if steps: - logging.info("Executing installer steps") - if is_final: - step_fn() - - if not self.__perform_steps(_config, steps): - return Result( - False, data={"message": "Installer is not well configured."} - ) - - # execute checks - if checks: - logging.info("Executing installer checks") - if is_final: - step_fn() - if not self.__perform_checks(_config, checks): - return Result( - False, - data={ - "message": "Checks failed, the program is not installed." - }, - ) - - # register executable - if executable["path"].startswith("userdir/"): - _userdir = WineUtils.get_user_dir(bottle) - executable["path"] = executable["path"].replace( - "userdir/", f"/users/{_userdir}/" - ) - - _path = f'C:\\{executable["path"]}'.replace("/", "\\") - _uuid = str(uuid.uuid4()) - _program = { - "executable": executable["file"], - "arguments": executable.get("arguments", ""), - "name": executable["name"], - "path": _path, - "id": _uuid, - } - - if "dxvk" in executable: - _program["dxvk"] = executable["dxvk"] - if "vkd3d" in executable: - _program["vkd3d"] = executable["vkd3d"] - if "dxvk_nvapi" in executable: - _program["dxvk_nvapi"] = executable["dxvk_nvapi"] - - duplicates = [ - k for k, v in config.External_Programs.items() if v["path"] == _path - ] - ext = config.External_Programs - - if duplicates: - for d in duplicates: - del ext[d] - ext[_uuid] = _program - self.__manager.update_config( - config=config, key="External_Programs", value=ext - ) - else: - self.__manager.update_config( - config=config, key=_uuid, value=_program, scope="External_Programs" - ) - - # create Desktop entry - bottles_icons_path = os.path.join(ManagerUtils.get_bottle_path(config), "icons") - icon_path = os.path.join(bottles_icons_path, executable.get("icon")) - ManagerUtils.create_desktop_entry(_config, _program, False, icon_path) - - if is_final: - step_fn() - - logging.info( - f"Program installed: {manifest['Name']} in {config.Name}.", jn=True - ) - return Result(True) diff --git a/bottles/backend/managers/journal.py b/bottles/backend/managers/journal.py deleted file mode 100644 index 8c471ff180b..00000000000 --- a/bottles/backend/managers/journal.py +++ /dev/null @@ -1,190 +0,0 @@ -# journal.py -# -# Copyright 2022 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import contextlib -import os -import shutil -import uuid -from datetime import datetime, timedelta - -from bottles.backend.globals import Paths -from bottles.backend.utils import yaml - - -class JournalSeverity: - """Represents the severity of a journal entry.""" - - DEBUG = "debug" - INFO = "info" - WARNING = "warning" - ERROR = "error" - CRITICAL = "critical" - CRASH = "crash" - - -class JournalManager: - """ - Store and retrieve data from the journal file (YAML). This should - contain only important Bottles events. - """ - - path = f"{Paths.base}/journal.yml" - - @staticmethod - def __get_journal() -> dict: - """Return the journal as a dictionary.""" - if not os.path.exists(JournalManager.path): - with open(JournalManager.path, "w") as f: - yaml.dump({}, f) - - with open(JournalManager.path) as f: - try: - journal = yaml.load(f) - except yaml.YAMLError: - journal_backup = f"{JournalManager.path}_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.bak" - shutil.copy2(JournalManager.path, journal_backup) - journal = {} - - if journal is None: - return {} - - try: - journal = { - k: v - for k, v in sorted( - journal.items(), key=lambda item: item[1]["timestamp"], reverse=True - ) - } - except (KeyError, TypeError): - journal = {} - - return journal - - @staticmethod - def __clean_old(): - """Clean old journal entries (1 month).""" - journal = JournalManager.__get_journal() - old_events = [] - latest = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - for event_id, event in journal.items(): - if event.get("timestamp", None) is None: - latest_datetime = datetime.strptime(latest, "%Y-%m-%d %H:%M:%S") - else: - latest_datetime = datetime.strptime( - event["timestamp"], "%Y-%m-%d %H:%M:%S" - ) - latest = event["timestamp"] - - if latest_datetime < datetime.now() - timedelta(days=30): - old_events.append(event_id) - - for event_id in old_events: - del journal[event_id] - - JournalManager.__save_journal(journal) - - @staticmethod - def __save_journal(journal: dict | None = None): - """Save the journal to the journal file.""" - if journal is None: - journal = JournalManager.__get_journal() - - with contextlib.suppress(IOError, OSError): - with open(JournalManager.path, "w") as f: - yaml.dump(journal, f) - - @staticmethod - def get(period: str = "today", plain: bool = False): - """ - Return all events for the given period. - Supported periods: all, today, yesterday, week, month - Set plain to True to get the response as plain text. - """ - journal = JournalManager.__get_journal() - periods = [ - "all", - "today", - "yesterday", - "week", - "month", - ] - if period not in periods: - period = "today" - - _journal = JournalManager.__filter_by_date(journal, period) - - if plain: - _journal = yaml.dump(_journal, sort_keys=False, indent=4) - - return _journal - - @staticmethod - def __filter_by_date(journal: dict, period: str): - """Filter the journal by date.""" - _journal = {} - if period == "today": - start = datetime.now().date() - end = start + timedelta(days=1) - elif period == "yesterday": - start = datetime.now().date() - timedelta(days=1) - end = start + timedelta(days=1) - elif period == "week": - start = datetime.now().date() - timedelta(days=7) - end = datetime.now().date() + timedelta(days=1) - elif period == "month": - start = datetime.now().date() - timedelta(days=30) - end = datetime.now().date() + timedelta(days=1) - elif period == "all": - return journal - else: - start = datetime.now().date() - end = start + timedelta(days=1) - - for event_id, event in journal.items(): - timestamp = datetime.strptime( - event["timestamp"], "%Y-%m-%d %H:%M:%S" - ).date() - - if start <= timestamp <= end: - _journal[event_id] = event - - return _journal - - @staticmethod - def get_event(event_id: str): - """Return the event with the given id.""" - journal = JournalManager.__get_journal() - return journal.get(event_id, None) - - @staticmethod - def write(severity: JournalSeverity, message: str): - """Write an event to the journal.""" - journal = JournalManager.__get_journal() - event_id = str(uuid.uuid4()) - now = datetime.now() - - if severity not in JournalSeverity.__dict__.values(): - severity = JournalSeverity.INFO - - journal[event_id] = { - "severity": severity, - "message": message, - "timestamp": now.strftime("%Y-%m-%d %H:%M:%S"), - } - JournalManager.__save_journal(journal) - JournalManager.__clean_old() diff --git a/bottles/backend/managers/library.py b/bottles/backend/managers/library.py deleted file mode 100644 index d462c8de511..00000000000 --- a/bottles/backend/managers/library.py +++ /dev/null @@ -1,134 +0,0 @@ -# library.py -# -# Copyright 2022 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import os -import uuid - -from bottles.backend.models.config import BottleConfig -from bottles.backend.utils import yaml - -from bottles.backend.logger import Logger -from bottles.backend.globals import Paths -from bottles.backend.managers.steamgriddb import SteamGridDBManager - -logging = Logger() - - -class LibraryManager: - """ - The LibraryManager class is used to store and retrieve data - from the user library.yml file. - """ - - library_path: str = Paths.library - __library: dict = {} - - def __init__(self): - self.load_library(silent=True) - - def load_library(self, silent=False): - """ - Loads data from the library.yml file. - """ - if not os.path.exists(self.library_path): - logging.warning("Library file not found, creating new one") - self.__library = {} - self.save_library() - else: - with open(self.library_path) as library_file: - self.__library = yaml.load(library_file) - - if self.__library is None: - self.__library = {} - - _tmp = self.__library.copy() - for k, v in _tmp.items(): - if "id" not in v: - del self.__library[k] - - self.save_library(silent=silent) - - def add_to_library(self, data: dict, config: BottleConfig): - """ - Adds a new entry to the library.yml file. - """ - if self.__already_in_library(data): - logging.warning(f"Entry already in library, nothing to add: {data}") - return - - _uuid = str(uuid.uuid4()) - logging.info(f"Adding new entry to library: {_uuid}") - - if not data.get("thumbnail"): - data["thumbnail"] = SteamGridDBManager.get_game_grid(data["name"], config) - - self.__library[_uuid] = data - self.save_library() - - def download_thumbnail(self, _uuid: str, config: BottleConfig): - if not self.__library.get(_uuid): - logging.warning( - f"Entry not found in library, can't download thumbnail: {_uuid}" - ) - return False - - data = self.__library.get(_uuid) - value = SteamGridDBManager.get_game_grid(data["name"], config) - - if not value: - return False - - self.__library[_uuid]["thumbnail"] = value - self.save_library() - return True - - def __already_in_library(self, data: dict): - """ - Checks if the entry UUID is already in the library.yml file. - """ - for k, v in self.__library.items(): - if v["id"] == data["id"]: - return True - - return False - - def remove_from_library(self, _uuid: str): - """ - Removes an entry from the library.yml file. - """ - if self.__library.get(_uuid): - logging.info(f"Removing entry from library: {_uuid}") - del self.__library[_uuid] - self.save_library() - return - logging.warning(f"Entry not found in library, nothing to remove: {_uuid}") - - def save_library(self, silent=False): - """ - Saves the library.yml file. - """ - with open(self.library_path, "w") as library_file: - yaml.dump(self.__library, library_file) - - if not silent: - logging.info("Library saved") - - def get_library(self): - """ - Returns the library.yml file. - """ - return self.__library diff --git a/bottles/backend/managers/manager.py b/bottles/backend/managers/manager.py index c3d990c4722..be62edee099 100644 --- a/bottles/backend/managers/manager.py +++ b/bottles/backend/managers/manager.py @@ -35,25 +35,12 @@ from bottles.backend.dlls.nvapi import NVAPIComponent from bottles.backend.dlls.vkd3d import VKD3DComponent from bottles.backend.globals import Paths -from bottles.backend.logger import Logger -from bottles.backend.managers.component import ComponentManager -from bottles.backend.managers.data import DataManager, UserDataKeys -from bottles.backend.managers.dependency import DependencyManager -from bottles.backend.managers.epicgamesstore import EpicGamesStoreManager -from bottles.backend.managers.importer import ImportManager -from bottles.backend.managers.installer import InstallerManager -from bottles.backend.managers.library import LibraryManager -from bottles.backend.managers.repository import RepositoryManager -from bottles.backend.managers.steam import SteamManager -from bottles.backend.managers.template import TemplateManager -from bottles.backend.managers.ubisoftconnect import UbisoftConnectManager -from bottles.backend.managers.versioning import VersioningManager +import logging from bottles.backend.models.config import BottleConfig from bottles.backend.models.result import Result from bottles.backend.models.samples import Samples from bottles.backend.state import SignalManager, Signals, Events, EventManager from bottles.backend.utils import yaml -from bottles.backend.utils.connection import ConnectionUtils from bottles.backend.utils.file import FileUtils from bottles.backend.utils.generic import sort_by_version from bottles.backend.utils.gpu import GPUUtils @@ -62,7 +49,6 @@ from bottles.backend.utils.lnk import LnkUtils from bottles.backend.utils.manager import ManagerUtils from bottles.backend.utils.singleton import Singleton -from bottles.backend.utils.steam import SteamUtils from bottles.backend.utils.threading import RunAsync from bottles.backend.wine.reg import Reg from bottles.backend.wine.regkeys import RegKeys @@ -71,8 +57,6 @@ from bottles.backend.wine.winepath import WinePath from bottles.backend.wine.wineserver import WineServer -logging = Logger() - class Manager(metaclass=Singleton): """ @@ -106,8 +90,6 @@ class Manager(metaclass=Singleton): def __init__( self, g_settings: Any = None, - check_connection: bool = True, - is_cli: bool = False, **kwargs, ): super().__init__(**kwargs) @@ -115,48 +97,10 @@ def __init__( times = {"start": time.time()} # common variables - self.is_cli = is_cli self.settings = g_settings or GSettingsStub - self.utils_conn = ConnectionUtils( - force_offline=self.is_cli or self.settings.get_boolean("force-offline") - ) - self.data_mgr = DataManager() - _offline = True - - if check_connection: - _offline = not self.utils_conn.check_connection() + _offline = False - # validating user-defined Paths.bottles - if user_bottles_path := self.data_mgr.get(UserDataKeys.CustomBottlesPath): - if os.path.exists(user_bottles_path): - Paths.bottles = user_bottles_path - else: - logging.error( - f"Custom bottles path {user_bottles_path} does not exist! " - f"Falling back to default path." - ) - - # sub-managers - self.repository_manager = RepositoryManager(get_index=not _offline) - if self.repository_manager.aborted_connections > 0: - self.utils_conn.status = False - _offline = True - - times["RepositoryManager"] = time.time() - self.versioning_manager = VersioningManager(self) - times["VersioningManager"] = time.time() - self.component_manager = ComponentManager(self, _offline) - self.installer_manager = InstallerManager(self, _offline) - self.dependency_manager = DependencyManager(self, _offline) - self.import_manager = ImportManager(self) - times["ImportManager"] = time.time() - self.steam_manager = SteamManager() - times["SteamManager"] = time.time() - - if not self.is_cli: - times.update(self.checks(install_latest=False, first_run=True).data) - else: - logging.set_silent() + times.update(self.checks(install_latest=False, first_run=True).data) if "BOOT_TIME" in os.environ: _temp_times = times.copy() @@ -177,7 +121,6 @@ def checks(self, install_latest=False, first_run=False) -> Result: rv = Result(status=True, data={}) self.check_app_dirs() - rv.data["check_app_dirs"] = time.time() self.check_dxvk(install_latest) or rv.set_status(False) rv.data["check_dxvk"] = time.time() @@ -200,134 +143,14 @@ def checks(self, install_latest=False, first_run=False) -> Result: self.check_runners(install_latest) or rv.set_status(False) rv.data["check_runners"] = time.time() - if first_run: - self.organize_components() - self.__clear_temp() - - self.organize_dependencies() - - self.organize_installers() - - self.check_bottles() - rv.data["check_bottles"] = time.time() - return rv - def __clear_temp(self, force: bool = False): - """Clears the temp directory if user setting allows it. Use the force - parameter to force clearing the directory. - """ - if self.settings.get_boolean("temp") or force: - try: - shutil.rmtree(Paths.temp) - os.makedirs(Paths.temp, exist_ok=True) - logging.info("Temp directory cleaned successfully!") - except FileNotFoundError: - self.check_app_dirs() - - def update_bottles(self, silent: bool = False): - """Checks for new bottles and update the list view.""" - self.check_bottles(silent) - SignalManager.send(Signals.ManagerLocalBottlesLoaded) - def check_app_dirs(self): """ Checks for the existence of the bottles' directories, and creates them if they don't exist. """ - if not os.path.isdir(Paths.runners): - logging.info("Runners path doesn't exist, creating now.") - os.makedirs(Paths.runners, exist_ok=True) - - if not os.path.isdir(Paths.runtimes): - logging.info("Runtimes path doesn't exist, creating now.") - os.makedirs(Paths.runtimes, exist_ok=True) - - if not os.path.isdir(Paths.winebridge): - logging.info("WineBridge path doesn't exist, creating now.") - os.makedirs(Paths.winebridge, exist_ok=True) - - if not os.path.isdir(Paths.bottles): - logging.info("Bottles path doesn't exist, creating now.") - os.makedirs(Paths.bottles, exist_ok=True) - - if ( - self.settings.get_boolean("steam-proton-support") - and self.steam_manager.is_steam_supported - ): - if not os.path.isdir(Paths.steam): - logging.info("Steam path doesn't exist, creating now.") - os.makedirs(Paths.steam, exist_ok=True) - - if not os.path.isdir(Paths.dxvk): - logging.info("Dxvk path doesn't exist, creating now.") - os.makedirs(Paths.dxvk, exist_ok=True) - - if not os.path.isdir(Paths.vkd3d): - logging.info("Vkd3d path doesn't exist, creating now.") - os.makedirs(Paths.vkd3d, exist_ok=True) - - if not os.path.isdir(Paths.nvapi): - logging.info("Nvapi path doesn't exist, creating now.") - os.makedirs(Paths.nvapi, exist_ok=True) - - if not os.path.isdir(Paths.templates): - logging.info("Templates path doesn't exist, creating now.") - os.makedirs(Paths.templates, exist_ok=True) - - if not os.path.isdir(Paths.temp): - logging.info("Temp path doesn't exist, creating now.") - os.makedirs(Paths.temp, exist_ok=True) - - if not os.path.isdir(Paths.latencyflex): - logging.info("LatencyFleX path doesn't exist, creating now.") - os.makedirs(Paths.latencyflex, exist_ok=True) - - @RunAsync.run_async - def organize_components(self): - """Get components catalog and organizes into supported_ lists.""" - EventManager.wait(Events.ComponentsFetching) - catalog = self.component_manager.fetch_catalog() - if len(catalog) == 0: - EventManager.done(Events.ComponentsOrganizing) - logging.info("No components found.") - return - - self.supported_wine_runners = catalog["wine"] - self.supported_proton_runners = catalog["proton"] - self.supported_runtimes = catalog["runtimes"] - self.supported_winebridge = catalog["winebridge"] - self.supported_dxvk = catalog["dxvk"] - self.supported_vkd3d = catalog["vkd3d"] - self.supported_nvapi = catalog["nvapi"] - self.supported_latencyflex = catalog["latencyflex"] - EventManager.done(Events.ComponentsOrganizing) - - @RunAsync.run_async - def organize_dependencies(self): - """Organizes dependencies into supported_dependencies.""" - EventManager.wait(Events.DependenciesFetching) - catalog = self.dependency_manager.fetch_catalog() - if len(catalog) == 0: - EventManager.done(Events.DependenciesOrganizing) - logging.info("No dependencies found!") - return - - self.supported_dependencies = catalog - EventManager.done(Events.DependenciesOrganizing) - - @RunAsync.run_async - def organize_installers(self): - """Organizes installers into supported_installers.""" - EventManager.wait(Events.InstallersFetching) - catalog = self.installer_manager.fetch_catalog() - if len(catalog) == 0: - EventManager.done(Events.InstallersOrganizing) - logging.info("No installers found!") - return - - self.supported_installers = catalog - EventManager.done(Events.InstallersOrganizing) + map(lambda path: os.makedirs(path, exist_ok=True), Paths.get_components_paths()) def remove_dependency(self, config: BottleConfig, dependency: list): """Uninstall a dependency and remove it from the bottle config.""" @@ -360,16 +183,15 @@ def check_runners(self, install_latest: bool = True) -> bool: # lock winemenubuilder.exe for runner in runners: - if not SteamUtils.is_proton(runner): - winemenubuilder_paths = [ - f"{runner}lib64/wine/x86_64-windows/winemenubuilder.exe", - f"{runner}lib/wine/x86_64-windows/winemenubuilder.exe", - f"{runner}lib32/wine/i386-windows/winemenubuilder.exe", - f"{runner}lib/wine/i386-windows/winemenubuilder.exe", - ] - for winemenubuilder in winemenubuilder_paths: - if os.path.isfile(winemenubuilder): - os.rename(winemenubuilder, f"{winemenubuilder}.lock") + winemenubuilder_paths = [ + f"{runner}lib64/wine/x86_64-windows/winemenubuilder.exe", + f"{runner}lib/wine/x86_64-windows/winemenubuilder.exe", + f"{runner}lib32/wine/i386-windows/winemenubuilder.exe", + f"{runner}lib/wine/i386-windows/winemenubuilder.exe", + ] + for winemenubuilder in winemenubuilder_paths: + if os.path.isfile(winemenubuilder): + os.rename(winemenubuilder, f"{winemenubuilder}.lock") # check system wine if shutil.which("wine") is not None: @@ -420,25 +242,7 @@ def check_runners(self, install_latest: bool = True) -> bool: if len(tmp_runners) == 0 and install_latest: logging.warning("No managed runners found.") - - if self.utils_conn.check_connection(): - # if connected, install the latest runner from repository - try: - if not self.settings.get_boolean("release-candidate"): - tmp_runners = [] - for runner in self.supported_wine_runners.items(): - if runner[1]["Channel"] not in ["rc", "unstable"]: - tmp_runners.append(runner) - break - runner_name = next(iter(tmp_runners))[0] - else: - tmp_runners = self.supported_wine_runners - runner_name = next(iter(tmp_runners)) - self.component_manager.install("runner", runner_name) - except StopIteration: - return False - else: - return False + return False return True @@ -451,13 +255,6 @@ def check_runtimes(self, install_latest: bool = True) -> bool: runtimes = os.listdir(Paths.runtimes) if len(runtimes) == 0: - if install_latest and self.utils_conn.check_connection(): - logging.warning("No runtime found.") - try: - version = next(iter(self.supported_runtimes)) - return self.component_manager.install("runtime", version) - except StopIteration: - return False return False runtime = runtimes[0] # runtimes cannot be more than one @@ -480,15 +277,6 @@ def check_winebridge( winebridge = os.listdir(Paths.winebridge) if len(winebridge) == 0 or update: - if install_latest and self.utils_conn.check_connection(): - logging.warning("No WineBridge found.") - try: - version = next(iter(self.supported_winebridge)) - self.component_manager.install("winebridge", version) - self.winebridge_available = [version] - return True - except StopIteration: - return False return False version_file = os.path.join(Paths.winebridge, "VERSION") @@ -566,16 +354,7 @@ def get_offline_components( if component_type == "runner": offline_components = [ - runner - for runner in offline_components - if not runner.startswith("sys-") - and not SteamUtils.is_proton(ManagerUtils.get_runner_path(runner)) - ] - elif component_type == "runner:proton": - offline_components = [ - runner - for runner in offline_components - if SteamUtils.is_proton(ManagerUtils.get_runner_path(runner)) + runner for runner in offline_components if not runner.startswith("sys-") ] if ( @@ -637,26 +416,7 @@ def __check_component( if len(component["available"]) == 0 and install_latest: logging.warning(f"No {component_type} found.") - - if self.utils_conn.check_connection(): - # if connected, install the latest component from repository - try: - if not self.settings.get_boolean("release-candidate"): - tmp_components = [] - for cpnt in component["supported"].items(): - if cpnt[1]["Channel"] not in ["rc", "unstable"]: - tmp_components.append(cpnt) - break - component_version = next(iter(tmp_components))[0] - else: - tmp_components = component["supported"] - component_version = next(iter(tmp_components)) - self.component_manager.install(component_type, component_version) - component["available"] = [component_version] - except StopIteration: - return False - else: - return False + return False try: return sort_by_version(component["available"]) @@ -780,189 +540,8 @@ def get_programs(self, config: BottleConfig) -> list[dict]: ) found.append(executable_name) - win_steam_manager = SteamManager(config, is_windows=True) - - if ( - self.settings.get_boolean("steam-programs") - and win_steam_manager.is_steam_supported - ): - programs_names = [p.get("name", "") for p in installed_programs] - for app in win_steam_manager.get_installed_apps_as_programs(): - if app["name"] not in programs_names: - installed_programs.append(app) - - if self.settings.get_boolean( - "epic-games" - ) and EpicGamesStoreManager.is_epic_supported(config): - programs_names = [p.get("name", "") for p in installed_programs] - for app in EpicGamesStoreManager.get_installed_games(config): - if app["name"] not in programs_names: - installed_programs.append(app) - - if self.settings.get_boolean( - "ubisoft-connect" - ) and UbisoftConnectManager.is_uconnect_supported(config): - programs_names = [p.get("name", "") for p in installed_programs] - for app in UbisoftConnectManager.get_installed_games(config): - if app["name"] not in programs_names: - installed_programs.append(app) - return installed_programs - def check_bottles(self, silent: bool = False): - """ - Check for local bottles and update the local_bottles list. - Will also mark the broken ones if the configuration file is missing - """ - bottles = os.listdir(Paths.bottles) - - # Empty local bottles - self.local_bottles = {} - - def process_bottle(bottle): - _name = bottle - _bottle = str(os.path.join(Paths.bottles, bottle)) - _placeholder = os.path.join(_bottle, "placeholder.yml") - _config = os.path.join(_bottle, "bottle.yml") - - if os.path.exists(_placeholder): - with open(_placeholder) as f: - try: - placeholder_yaml = yaml.load(f) - if placeholder_yaml.get("Path"): - _config = os.path.join( - placeholder_yaml.get("Path"), "bottle.yml" - ) - else: - raise ValueError("Missing Path in placeholder.yml") - except (yaml.YAMLError, ValueError): - return - - config_load = BottleConfig.load(_config) - - if not config_load.status: - return - - config = config_load.data - - # Clear Run Executable parameters on new session start - if config.session_arguments: - config.session_arguments = "" - - if config.run_in_terminal: - config.run_in_terminal = False - - # Check if the path in the bottle config corresponds to the folder name - # if not, change the config to reflect the folder name - # if the folder name is "illegal" across all platforms, rename the folder - - # "universal" platform works for all filesystem/OSes - sane_name = pathvalidate.sanitize_filepath(_name, platform="universal") - if config.Custom_Path is False: # There shouldn't be problems with this - if config.Path != _name or sane_name != _name: - logging.warning( - 'Illegal bottle folder or mismatch between config "Path" and folder name' - ) - if sane_name != _name: - # This hopefully doesn't happen, but it's managed - logging.warning(f"Broken path in bottle {_name}, fixing...") - shutil.move( - _bottle, str(os.path.join(Paths.bottles, sane_name)) - ) - # Restart the process bottle function. Normally, can't be recursive! - process_bottle(sane_name) - return - - config.Path = sane_name - self.update_config(config=config, key="Path", value=sane_name) - - sample = BottleConfig() - miss_keys = sample.keys() - config.keys() - for key in miss_keys: - logging.warning(f"Key {key} is missing for bottle {_name}, updating…") - self.update_config(config=config, key=key, value=sample[key]) - - miss_params_keys = sample.Parameters.keys() - config.Parameters.keys() - - for key in miss_params_keys: - """ - For each missing key in the bottle configuration, set - it to the default value. - """ - logging.warning( - f"Parameters key {key} is missing for bottle {_name}, updating…" - ) - self.update_config( - config=config, - key=key, - value=sample.Parameters[key], - scope="Parameters", - ) - self.local_bottles[config.Name] = config - - real_path = ManagerUtils.get_bottle_path(config) - for p in [ - os.path.join(real_path, "cache", "dxvk_state"), - os.path.join(real_path, "cache", "gl_shader"), - os.path.join(real_path, "cache", "mesa_shader"), - os.path.join(real_path, "cache", "vkd3d_shader"), - ]: - if not os.path.exists(p): - os.makedirs(p) - - for c in os.listdir(real_path): - c = str(c) - if c.endswith(".dxvk-cache"): - # NOTE: the following code tries to create the caching directories - # if one or more already exist, it will fail silently as there - # is no need to create them again. - try: - shutil.move( - os.path.join(real_path, c), - os.path.join(real_path, "cache", "dxvk_state"), - ) - except shutil.Error: - pass - elif "vkd3d-proton.cache" in c: - try: - shutil.move( - os.path.join(real_path, c), - os.path.join(real_path, "cache", "vkd3d_shader"), - ) - except shutil.Error: - pass - elif c == "GLCache": - try: - shutil.move( - os.path.join(real_path, c), - os.path.join(real_path, "cache", "gl_shader"), - ) - except shutil.Error: - pass - - if config.Parameters.dxvk_nvapi: - NVAPIComponent.check_bottle_nvngx(real_path, config) - - for b in bottles: - """ - For each bottle add the path name to the `local_bottles` variable - and append the config. - """ - process_bottle(b) - - if len(self.local_bottles) > 0 and not silent: - logging.info( - "Bottles found:\n - {}".format("\n - ".join(self.local_bottles)) - ) - - if ( - self.settings.get_boolean("steam-proton-support") - and self.steam_manager.is_steam_supported - and not self.is_cli - ): - self.steam_manager.update_bottles() - self.local_bottles.update(self.steam_manager.list_prefixes()) - # Update parameters in bottle config def update_config( self, @@ -1016,9 +595,6 @@ def update_config( config.Update_Date = str(datetime.now()) - if config.Environment == "Steam": - self.steam_manager.update_bottle(config) - return Result(status=True, data={"config": config}) def create_bottle_from_config(self, config: BottleConfig) -> bool: @@ -1108,21 +684,7 @@ def create_bottle_from_config(self, config: BottleConfig) -> bool: """ self.install_dll_component(config, "vkd3d") - for dependency in config.Installed_Dependencies: - """ - Install each declared dependency in the new bottle. - """ - if dependency in self.supported_dependencies.keys(): - dep = [dependency, self.supported_dependencies[dependency]] - res = self.dependency_manager.install(config, dep) - if not res.ok: - logging.error( - _("Failed to install dependency: %s") % dependency, - jn=True, - ) - return False logging.info(f"New bottle from config created: {config.Path}") - self.update_bottles(silent=True) return True def create_bottle( @@ -1157,7 +719,7 @@ def components_check(): nonlocal check_attempts if check_attempts > 2: - logging.error("Fail to install components, tried 3 times.", jn=True) + logging.error("Fail to install components, tried 3 times.") log_update(_("Fail to install components, tried 3 times.")) return False @@ -1175,7 +737,6 @@ def components_check(): self.check_vkd3d() self.check_nvapi() self.check_latencyflex() - self.organize_components() check_attempts += 1 return components_check() @@ -1251,9 +812,7 @@ def components_check(): os.makedirs(bottle_drive_c) FileUtils.chattr_f(bottle_drive_c) except: - logging.error( - f"Failed to create bottle directory: {bottle_complete_path}", jn=True - ) + logging.error(f"Failed to create bottle directory: {bottle_complete_path}") log_update(_("Failed to create bottle directory.")) return Result(False) @@ -1267,7 +826,6 @@ def components_check(): except: logging.error( f"Failed to create placeholder directory/file at: {placeholder_dir}", - jn=True, ) log_update(_("Failed to create placeholder directory/file.")) return Result(False) @@ -1293,18 +851,7 @@ def components_check(): if versioning: config.Versioning = True - # get template - template = TemplateManager.get_env_template(environment) - template_updated = False - if template: - log_update(_("Template found, applying…")) - TemplateManager.unpack_template(template, config) - config.Installed_Dependencies = template["config"]["Installed_Dependencies"] - config.Uninstallers = template["config"]["Uninstallers"] - # initialize wineprefix - reg = Reg(config) - rk = RegKeys(config) wineboot = WineBoot(config) wineserver = WineServer(config) @@ -1353,37 +900,6 @@ def components_check(): # wait for registry files to be created FileUtils.wait_for_files(reg_files) - # apply Windows version - if not template and not custom_environment: - logging.info("Setting Windows version…") - log_update(_("Setting Windows version…")) - if ( - "soda" not in runner_name.lower() and "caffe" not in runner_name.lower() - ): # Caffe/Soda came with win10 by default - rk.lg_set_windows(config.Windows) - wineboot.update() - - FileUtils.wait_for_files(reg_files) - - # apply CMD settings - logging.info("Setting CMD default settings…") - log_update(_("Apply CMD default settings…")) - rk.apply_cmd_settings() - wineboot.update() - - FileUtils.wait_for_files(reg_files) - - # blacklisting processes - logging.info("Optimizing environment…") - log_update(_("Optimizing environment…")) - _blacklist_dll = ["winemenubuilder.exe"] - for _dll in _blacklist_dll: - reg.add( - key="HKEY_CURRENT_USER\\Software\\Wine\\DllOverrides", - value=_dll, - data="", - ) - # apply environment configuration logging.info(f"Applying environment: [{environment}]…") log_update(_("Applying environment: {0}…").format(environment)) @@ -1412,72 +928,11 @@ def components_check(): if prm in env.get("Parameters", {}): config.Parameters[prm] = env["Parameters"][prm] - if (not template and config.Parameters.dxvk) or ( - template and template["config"]["DXVK"] != dxvk - ): - # perform dxvk installation if configured - logging.info("Installing DXVK…") - log_update(_("Installing DXVK…")) - self.install_dll_component(config, "dxvk", version=dxvk_name) - template_updated = True - - if ( - not template - and config.Parameters.vkd3d - or (template and template["config"]["VKD3D"] != vkd3d) - ): - # perform vkd3d installation if configured - logging.info("Installing VKD3D…") - log_update(_("Installing VKD3D…")) - self.install_dll_component(config, "vkd3d", version=vkd3d_name) - template_updated = True - - if ( - not template - and config.Parameters.dxvk_nvapi - or (template and template["config"]["NVAPI"] != nvapi) - ): - if GPUUtils.is_gpu(GPUVendors.NVIDIA): - # perform nvapi installation if configured - logging.info("Installing DXVK-NVAPI…") - log_update(_("Installing DXVK-NVAPI…")) - self.install_dll_component(config, "nvapi", version=nvapi_name) - template_updated = True - - for dep in env.get("Installed_Dependencies", []): - if template and dep in template["config"]["Installed_Dependencies"]: - continue - if dep in self.supported_dependencies: - _dep = self.supported_dependencies[dep] - log_update( - _("Installing dependency: %s …") - % _dep.get("Description", "n/a") - ) - res = self.dependency_manager.install(config, [dep, _dep]) - if not res.ok: - logging.error( - _("Failed to install dependency: %s") - % _dep.get("Description", "n/a"), - jn=True, - ) - log_update( - _("Failed to install dependency: %s") - % _dep.get("Description", "n/a") - ) - return Result(False) - template_updated = True - # save bottle config config.dump(f"{bottle_complete_path}/bottle.yml") - if versioning: - # create first state if versioning enabled - logging.info("Creating versioning state 0…") - log_update(_("Creating versioning state 0…")) - self.versioning_manager.create_state(config=config, message="First boot") - # set status created and UI usability - logging.info(f"New bottle created: {bottle_name}", jn=True) + logging.info(f"New bottle created: {bottle_name}") log_update(_("Finalizing…")) # wait for all registry changes to be applied @@ -1486,12 +941,6 @@ def components_check(): # perform wineboot wineboot.update() - # caching template - if not template or template_updated: - logging.info("Caching template…") - log_update(_("Caching template…")) - TemplateManager.new(environment, config) - return Result(status=True, data={"config": config}) @staticmethod @@ -1536,13 +985,6 @@ def delete_bottle(self, config: BottleConfig) -> bool: for inst in glob(f"{Paths.applications}/{config.Name}--*"): os.remove(inst) - logging.info("Removing library entries associated with this bottle…") - library_manager = LibraryManager() - entries = library_manager.get_library().copy() - for _uuid, entry in entries.items(): - if entry.get("bottle").get("name") == config.Name: - library_manager.remove_from_library(_uuid) - if config.Custom_Path: logging.info("Removing placeholder…") with contextlib.suppress(FileNotFoundError): @@ -1556,43 +998,9 @@ def delete_bottle(self, config: BottleConfig) -> bool: path = ManagerUtils.get_bottle_path(config) subprocess.run(["rm", "-rf", path], stdout=subprocess.DEVNULL) - self.update_bottles(silent=True) - logging.info(f"Deleted the bottle in: {path}") return True - def repair_bottle(self, config: BottleConfig) -> bool: - """ - This function tries to repair a broken bottle, creating a - new bottle configuration with the latest runner. Each fixed - bottle will use the Custom environment. - TODO: will be replaced by the BottlesManager class. - """ - logging.info(f"Trying to repair the bottle: [{config.Name}]…") - - wineboot = WineBoot(config) - bottle_path = f"{Paths.bottles}/{config.Name}" - - # create new config with path as name and Custom environment - new_config = BottleConfig() - new_config.Name = config.Name - new_config.Runner = self.get_latest_runner() - new_config.Path = config.Name - new_config.Environment = "Custom" - new_config.Creation_Date = str(datetime.now()) - new_config.Update_Date = str(datetime.now()) - - saved = new_config.dump(os.path.join(bottle_path, "bottle.yml")) - if not saved.status: - return False - - # Execute wineboot in bottle to generate missing files - wineboot.init() - - # Update bottles - self.update_bottles() - return True - def install_dll_component( self, config: BottleConfig, diff --git a/bottles/backend/managers/meson.build b/bottles/backend/managers/meson.build index e682c0c3b44..36411328757 100644 --- a/bottles/backend/managers/meson.build +++ b/bottles/backend/managers/meson.build @@ -3,28 +3,7 @@ managersdir = join_paths(pkgdatadir, 'bottles/backend/managers') bottles_sources = [ '__init__.py', - 'backup.py', - 'component.py', - 'dependency.py', - 'installer.py', - 'library.py', 'manager.py', - 'versioning.py', - 'data.py', - 'runtime.py', - 'importer.py', - 'conf.py', - 'journal.py', - 'repository.py', - 'template.py', - 'sandbox.py', - 'steam.py', - 'epicgamesstore.py', - 'ubisoftconnect.py', - 'origin.py', - 'queue.py', - 'steamgriddb.py', - 'thumbnail.py' ] install_data(bottles_sources, install_dir: managersdir) diff --git a/bottles/backend/managers/origin.py b/bottles/backend/managers/origin.py deleted file mode 100644 index 1e50b490519..00000000000 --- a/bottles/backend/managers/origin.py +++ /dev/null @@ -1,55 +0,0 @@ -# origin.py -# -# Copyright 2022 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import os - -from bottles.backend.models.config import BottleConfig -from bottles.backend.utils.manager import ManagerUtils - - -class OriginManager: - @staticmethod - def find_manifests_path(config: BottleConfig) -> str | None: - """ - Finds the Origin manifests path. - """ - paths = [ - os.path.join( - ManagerUtils.get_bottle_path(config), - "drive_c/ProgramData/Origin/LocalContent", - ) - ] - - for path in paths: - if os.path.exists(path): - return path - return None - - @staticmethod - def is_origin_supported(config: BottleConfig) -> bool: - """ - Checks if Origin is supported. - """ - return OriginManager.find_manifests_path(config) is not None - - @staticmethod - def get_installed_games(config: BottleConfig) -> list: - """ - Gets the games. - """ - games = [] - return games diff --git a/bottles/backend/managers/repository.py b/bottles/backend/managers/repository.py deleted file mode 100644 index 78da3401f7d..00000000000 --- a/bottles/backend/managers/repository.py +++ /dev/null @@ -1,152 +0,0 @@ -# repository.py -# -# Copyright 2022 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import os - -import pycurl - -from bottles.backend.logger import Logger -from bottles.backend.models.result import Result -from bottles.backend.params import APP_VERSION -from bottles.backend.repos.component import ComponentRepo -from bottles.backend.repos.dependency import DependencyRepo -from bottles.backend.repos.installer import InstallerRepo -from bottles.backend.state import SignalManager, Signals -from bottles.backend.utils.threading import RunAsync - -logging = Logger() - - -class RepositoryManager: - __repositories = { - "components": { - "url": "https://proxy.usebottles.com/repo/components/", - "index": "", - "cls": ComponentRepo, - }, - "dependencies": { - "url": "https://proxy.usebottles.com/repo/dependencies/", - "index": "", - "cls": DependencyRepo, - }, - "installers": { - "url": "https://proxy.usebottles.com/repo/programs/", - "index": "", - "cls": InstallerRepo, - }, - } - - def __init__(self, get_index=True): - self.do_get_index = True - self.aborted_connections = 0 - SignalManager.connect(Signals.ForceStopNetworking, self.__stop_index) - - self.__check_personals() - if get_index: - self.__get_index() - - def get_repo(self, name: str, offline: bool = False): - if name in self.__repositories: - repo = self.__repositories[name] - return repo["cls"](repo["url"], repo["index"], offline=offline) - - logging.error(f"Repository {name} not found") - - def __check_personals(self): - _personals = {} - - if "PERSONAL_COMPONENTS" in os.environ: - _personals["components"] = os.environ["PERSONAL_COMPONENTS"] - - if "PERSONAL_DEPENDENCIES" in os.environ: - _personals["dependencies"] = os.environ["PERSONAL_DEPENDENCIES"] - - if "PERSONAL_INSTALLERS" in os.environ: - _personals["installers"] = os.environ["PERSONAL_INSTALLERS"] - - if not _personals: - return - - for repo in self.__repositories: - if repo not in _personals: - continue - - _url = _personals[repo] - self.__repositories[repo]["url"] = _url - logging.info(f"Using personal {repo} repository at {_url}") - - def __curl_progress(self, _download_t, _download_d, _upload_t, _upload_d): - if self.do_get_index: - return pycurl.E_OK - else: - self.aborted_connections += 1 - return pycurl.E_ABORTED_BY_CALLBACK - - def __stop_index(self, res: Result): - if res.status: - self.do_get_index = False - - def __get_index(self): - total = len(self.__repositories) - - threads = [] - - for repo, data in self.__repositories.items(): - - def query(_repo, _data): - __index = os.path.join(_data["url"], f"{APP_VERSION}.yml") - __fallback = os.path.join(_data["url"], "index.yml") - - for url in (__index, __fallback): - c = pycurl.Curl() - c.setopt(c.URL, url) - c.setopt(c.NOBODY, True) - c.setopt(c.FOLLOWLOCATION, True) - c.setopt(c.TIMEOUT, 10) - c.setopt(c.NOPROGRESS, False) - c.setopt(c.XFERINFOFUNCTION, self.__curl_progress) - - try: - c.perform() - except pycurl.error as e: - if url is not __index: - logging.error( - f"Could not get index for {_repo} repository: {e}" - ) - continue - - if url.startswith("file://") or c.getinfo(c.RESPONSE_CODE) == 200: - _data["index"] = url - SignalManager.send( - Signals.RepositoryFetched, Result(True, data=total) - ) - break - - c.close() - else: - SignalManager.send( - Signals.RepositoryFetched, Result(False, data=total) - ) - logging.error(f"Could not get index for {_repo} repository") - - thread = RunAsync(query, _repo=repo, _data=data) - threads.append(thread) - - for t in threads: - t.join() - - self.do_get_index = True diff --git a/bottles/backend/managers/runtime.py b/bottles/backend/managers/runtime.py deleted file mode 100644 index a56e31d992c..00000000000 --- a/bottles/backend/managers/runtime.py +++ /dev/null @@ -1,161 +0,0 @@ -# runtime.py -# -# Copyright 2022 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import os -from functools import lru_cache - -from bottles.backend.globals import Paths - - -class RuntimeManager: - @staticmethod - @lru_cache - def get_runtimes(_filter: str = "bottles"): - runtimes = { - "bottles": RuntimeManager.__get_bottles_runtime(), - "steam": RuntimeManager.__get_steam_runtime(), - } - - if _filter == "steam": - if len(runtimes.get("steam", {})) == 0: - return False - - return runtimes.get(_filter, False) - - @staticmethod - def get_runtime_env(_filter: str = "bottles"): - runtime = RuntimeManager.get_runtimes(_filter) - env = "" - - if runtime: - for p in runtime: - if "EasyAntiCheatRuntime" in p or "BattlEyeRuntime" in p: - continue - env += f":{p}" - - else: - return False - - ld = os.environ.get("LD_LIBRARY_PATH") - if ld: - env += f":{ld}" - - return env - - @staticmethod - def get_eac(): - runtime = RuntimeManager.get_runtimes("bottles") - - if runtime: - for p in runtime: - if "EasyAntiCheatRuntime" in p: - return p - - return False - - @staticmethod - def get_be(): - runtime = RuntimeManager.get_runtimes("bottles") - - if runtime: - for p in runtime: - if "BattlEyeRuntime" in p: - return p - - return False - - @staticmethod - def __get_runtime(paths: list, structure: list): - def check_structure(found, expected): - for e in expected: - if e not in found: - return False - return True - - for runtime_path in paths: - if not os.path.exists(runtime_path): - continue - - structure_found = [] - for root, dirs, files in os.walk(runtime_path): - for d in dirs: - structure_found.append(d) - - if not check_structure(structure_found, structure): - return [] - - res = [f"{runtime_path}/{s}" for s in structure] - eac_path = os.path.join(runtime_path, "EasyAntiCheatRuntime") - be_path = os.path.join(runtime_path, "BattlEyeRuntime") - - if os.path.isdir(eac_path): - res.append(eac_path) - - if os.path.isdir(be_path): - res.append(be_path) - - return res - - return False - - @staticmethod - def __get_bottles_runtime(): - paths = ["/app/etc/runtime", Paths.runtimes] - structure = ["lib", "lib32"] - - return RuntimeManager.__get_runtime(paths, structure) - - @staticmethod - def __get_steam_runtime(): - from bottles.backend.managers.steam import SteamManager - - available_runtimes = {} - steam_manager = SteamManager(check_only=True) - - if not steam_manager.is_steam_supported: - return available_runtimes - - lookup = { - "sniper": { - "name": "sniper", - "entry_point": os.path.join( - steam_manager.steam_path, - "steamapps/common/SteamLinuxRuntime_sniper/_v2-entry-point", - ), - }, - "soldier": { - "name": "soldier", - "entry_point": os.path.join( - steam_manager.steam_path, - "steamapps/common/SteamLinuxRuntime_soldier/_v2-entry-point", - ), - }, - "scout": { - "name": "scout", - "entry_point": os.path.join( - steam_manager.steam_path, "ubuntu12_32/steam-runtime/run.sh" - ), - }, - } - - for name, data in lookup.items(): - if not os.path.exists(data["entry_point"]): - continue - - available_runtimes[name] = data - - return available_runtimes diff --git a/bottles/backend/managers/sandbox.py b/bottles/backend/managers/sandbox.py deleted file mode 100644 index 2f5661d994e..00000000000 --- a/bottles/backend/managers/sandbox.py +++ /dev/null @@ -1,109 +0,0 @@ -# steam.py -# -# Copyright 2022 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - - -import os -import shlex -import subprocess - - -class SandboxManager: - def __init__( - self, - envs: dict | None = None, - chdir: str | None = None, - clear_env: bool = False, - share_paths_ro: list | None = None, - share_paths_rw: list | None = None, - share_net: bool = False, - share_user: bool = False, - share_host_ro: bool = True, - share_display: bool = True, - share_sound: bool = True, - share_gpu: bool = True, - ): - self.envs = envs - self.chdir = chdir - self.clear_env = clear_env - self.share_paths_ro = share_paths_ro - self.share_paths_rw = share_paths_rw - self.share_net = share_net - self.share_user = share_user - self.share_host_ro = share_host_ro - self.share_display = share_display - self.share_sound = share_sound - self.share_gpu = share_gpu - self.__uid = os.environ.get("UID", "1000") - - def __get_flatpak_spawn(self, cmd: str): - _cmd = ["flatpak-spawn"] - - if self.envs: - _cmd += [f"--env={k}={shlex.quote(v)}" for k, v in self.envs.items()] - - if self.share_host_ro: - _cmd.append("--sandbox") - _cmd.append("--sandbox-expose-path-ro=/") - - if self.chdir: - _cmd.append(f"--directory={shlex.quote(self.chdir)}") - _cmd.append(f"--sandbox-expose-path={shlex.quote(self.chdir)}") - - if self.clear_env: - _cmd.append("--clear-env") - - if self.share_paths_ro: - _cmd += [ - f"--sandbox-expose-path-ro={shlex.quote(p)}" - for p in self.share_paths_ro - ] - - if self.share_paths_rw: - _cmd += [ - f"--sandbox-expose-path={shlex.quote(p)}" for p in self.share_paths_rw - ] - - if not self.share_net: - _cmd.append("--no-network") - - if self.share_display: - _cmd.append("--sandbox-flag=share-display") - - if self.share_sound: - _cmd.append("--sandbox-flag=share-sound") - - if self.share_gpu: - _cmd.append("--sandbox-flag=share-gpu") - - _cmd.append(cmd) - - return _cmd - - def get_cmd(self, cmd: str): - _cmd = "" - if "FLATPAK_ID" in os.environ: - _cmd = self.__get_flatpak_spawn(cmd) - - return " ".join(_cmd) - - def run(self, cmd: str) -> subprocess.Popen[bytes]: - return subprocess.Popen( - self.get_cmd(cmd), - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) diff --git a/bottles/backend/managers/steam.py b/bottles/backend/managers/steam.py deleted file mode 100644 index 495214ad52d..00000000000 --- a/bottles/backend/managers/steam.py +++ /dev/null @@ -1,567 +0,0 @@ -# steam.py -# -# Copyright 2022 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import contextlib -import os -import shlex -import shutil -import uuid -from datetime import datetime -from functools import lru_cache -from glob import glob -from pathlib import Path - -from bottles.backend.globals import Paths -from bottles.backend.models.config import BottleConfig -from bottles.backend.models.result import Result -from bottles.backend.models.samples import Samples -from bottles.backend.models.vdict import VDFDict -from bottles.backend.state import Signals, SignalManager -from bottles.backend.utils import vdf -from bottles.backend.utils.manager import ManagerUtils -from bottles.backend.utils.steam import SteamUtils -from bottles.backend.wine.winecommand import WineCommand - -from bottles.backend.logger import Logger - -logging = Logger() - - -class SteamManager: - steamapps_path = None - userdata_path = None - localconfig_path = None - localconfig = {} - library_folders = [] - - def __init__( - self, - config: BottleConfig | None = None, - is_windows: bool = False, - check_only: bool = False, - ): - self.config = config - self.is_windows = is_windows - self.steam_path = self.__find_steam_path() - self.is_steam_supported = self.steam_path is not None - if self.is_steam_supported and not check_only: - self.steamapps_path = self.__get_scoped_path("steamapps") - self.userdata_path = self.__get_scoped_path("userdata") - self.localconfig_path = self.__get_local_config_path() - self.localconfig = self.__get_local_config() - self.library_folders = self.__get_library_folders() - - def __find_steam_path(self) -> str | None: - if self.is_windows and self.config: - paths = [ - os.path.join( - ManagerUtils.get_bottle_path(self.config), - "drive_c/Program Files (x86)/Steam", - ) - ] - else: - paths = [ - os.path.join( - Path.home(), ".var/app/com.valvesoftware.Steam/data/Steam" - ), - os.path.join(Path.home(), ".local/share/Steam"), - os.path.join(Path.home(), ".steam/debian-installation"), - os.path.join(Path.home(), ".steam/steam"), - os.path.join(Path.home(), ".steam"), - ] - - for path in paths: - if os.path.isdir(path): - return path - return None - - def __get_scoped_path(self, scope: str = "steamapps"): - """scopes: steamapps, userdata""" - if scope not in ["steamapps", "userdata"]: - raise ValueError("scope must be either 'steamapps' or 'userdata'") - - path = os.path.join(self.steam_path, scope) - if os.path.isdir(path): - return path - return None - - @staticmethod - def get_acf_data(libraryfolder: str, app_id: str) -> dict | None: - acf_path = os.path.join(libraryfolder, f"steamapps/appmanifest_{app_id}.acf") - if not os.path.isfile(acf_path): - return None - - with open(acf_path, errors="replace") as f: - data = SteamUtils.parse_acf(f.read()) - - return data - - def __get_local_config_path(self) -> str | None: - if self.userdata_path is None: - return None - - confs = glob(os.path.join(self.userdata_path, "*/config/localconfig.vdf")) - if len(confs) == 0: - logging.warning("Could not find any localconfig.vdf file in Steam userdata") - return None - - return confs[0] - - def __get_library_folders(self) -> list | None: - if not self.steamapps_path: - return None - - library_folders_path = os.path.join(self.steamapps_path, "libraryfolders.vdf") - library_folders = [] - - if not os.path.exists(library_folders_path): - logging.warning("Could not find the libraryfolders.vdf file") - return None - - with open(library_folders_path, errors="replace") as f: - _library_folders = SteamUtils.parse_vdf(f.read()) - - if _library_folders is None or not _library_folders.get("libraryfolders"): - logging.warning("Could not parse libraryfolders.vdf") - return None - - for _, folder in _library_folders["libraryfolders"].items(): - if ( - not isinstance(folder, dict) - or not folder.get("path") - or not folder.get("apps") - ): - continue - - library_folders.append(folder) - - return library_folders if len(library_folders) > 0 else None - - @lru_cache - def get_appid_library_path(self, appid: str) -> str | None: - if self.library_folders is None: - return None - - # This will always be a list because of the check before - # pylint: disable=E1133 - for folder in self.library_folders: - if appid in folder["apps"].keys(): - return folder["path"] - return None - - def __get_local_config(self) -> dict: - if self.localconfig_path is None: - return {} - - with open(self.localconfig_path, errors="replace") as f: - data = SteamUtils.parse_vdf(f.read()) - - if data is None: - logging.warning("Could not parse localconfig.vdf") - return {} - - return data - - def save_local_config(self, new_data: dict): - if self.localconfig_path is None: - return - - if os.path.isfile(self.localconfig_path): - now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") - shutil.copy(self.localconfig_path, f"{self.localconfig_path}.bck.{now}") - - with open(self.localconfig_path, "w") as f: - SteamUtils.to_vdf(VDFDict(new_data), f) - - logging.info("Steam config saved") - - @staticmethod - @lru_cache - def get_runner_path(pfx_path: str) -> str | None: - """Get runner path from config_info file""" - config_info = os.path.join(pfx_path, "config_info") - - if not os.path.isfile(config_info): - return None - - with open(config_info) as f: - lines = f.readlines() - if len(lines) < 10: - logging.error( - f"{config_info} is not valid, cannot get Steam Proton path" - ) - return None - - proton_path = lines[1].strip().removesuffix("/share/fonts/") - - if proton_path.endswith("/files"): - proton_path = proton_path.removesuffix("/files") - elif proton_path.endswith("/dist"): - proton_path = proton_path.removesuffix("/dist") - - if not SteamUtils.is_proton(proton_path): - logging.error(f"{proton_path} is not a valid Steam Proton path") - return None - - return proton_path - - def list_apps_ids(self) -> dict: - """List all apps in Steam""" - apps = ( - self.localconfig.get("UserLocalConfigStore", {}) - .get("Software", {}) - .get("Valve", {}) - .get("Steam", {}) - ) - if "apps" in apps: - apps = apps.get("apps") - elif "Apps" in apps: - apps = apps.get("Apps") - else: - apps = {} - return apps - - def get_installed_apps_as_programs(self) -> list: - """This is a Steam for Windows only function""" - if not self.is_windows: - raise NotImplementedError( - "This function is only implemented for Windows versions of Steam" - ) - - apps_ids = self.list_apps_ids() - apps = [] - - if len(apps_ids) == 0: - return [] - - for app_id in apps_ids: - _acf = self.get_acf_data(self.steam_path, app_id) - if _acf is None: - continue - - _path = _acf["AppState"].get( - "LauncherPath", "C:\\Program Files (x86)\\Steam\\steam.exe" - ) - _executable = _path.split("\\")[-1] - _folder = ManagerUtils.get_exe_parent_dir(self.config, _path) - apps.append( - { - "executable": _executable, - "arguments": f"steam://run/{app_id}", - "name": _acf["AppState"]["name"], - "path": _path, - "folder": _folder, - "icon": "com.usebottles.bottles-program", - "id": str(uuid.uuid4()), - } - ) - - return apps - - def list_prefixes(self) -> dict[str, BottleConfig]: - apps = self.list_apps_ids() - prefixes = {} - - if len(apps) == 0: - return {} - - for appid, appdata in apps.items(): - _library_path = self.get_appid_library_path(appid) - if _library_path is None: - continue - - _path = os.path.join(_library_path, "steamapps/compatdata", appid) - - if not os.path.isdir(os.path.join(_path, "pfx")): - logging.debug(f"{appid} does not contain a prefix") - continue - - _launch_options = self.get_launch_options(appid, appdata) - _dir_name = os.path.basename(_path) - _acf = self.get_acf_data(_library_path, _dir_name) - _runner_path = self.get_runner_path(_path) - _creation_date = datetime.fromtimestamp(os.path.getctime(_path)).strftime( - "%Y-%m-%d %H:%M:%S.%f" - ) - - if not isinstance(_acf, dict): - # WORKAROUND: for corrupted acf files, this is not at our fault - continue - - if _acf is None or not _acf.get("AppState"): - logging.warning( - f"A Steam prefix was found, but there is no ACF for it: {_dir_name}, skipping…" - ) - continue - - if SteamUtils.is_proton( - os.path.join( - _library_path, - "steamapps/common", - _acf["AppState"].get("installdir", ""), - ) - ): - # skip Proton default prefix - logging.warning( - f"A Steam prefix was found, but it is a Proton one: {_dir_name}, skipping…" - ) - continue - - if _runner_path is None: - logging.warning( - f"A Steam prefix was found, but there is no Proton for it: {_dir_name}, skipping…" - ) - continue - - _conf = BottleConfig() - _conf.Name = _acf["AppState"].get("name", "Unknown") - _conf.Environment = "Steam" - _conf.CompatData = _dir_name - _conf.Path = os.path.join(_path, "pfx") - _conf.Runner = os.path.basename(_runner_path) - _conf.RunnerPath = _runner_path - _conf.WorkingDir = os.path.join(_conf.get("Path", ""), "drive_c") - _conf.Creation_Date = _creation_date - _conf.Update_Date = datetime.fromtimestamp( - int(_acf["AppState"].get("LastUpdated", 0)) - ).strftime("%Y-%m-%d %H:%M:%S.%f") - - # Launch options - _conf.Parameters.mangohud = "mangohud" in _launch_options.get("command", "") - _conf.Parameters.gamemode = "gamemode" in _launch_options.get("command", "") - _conf.Environment_Variables = _launch_options.get("env_vars", {}) - for p in _launch_options.get("env_params", {}): - _conf.Parameters[p] = _launch_options["env_params"].get(p, "") - - prefixes[_dir_name] = _conf - - return prefixes - - def update_bottles(self): - prefixes = self.list_prefixes() - - with contextlib.suppress(FileNotFoundError): - shutil.rmtree(Paths.steam) # generate new configs at start - - for _, conf in prefixes.items(): - _bottle = os.path.join(Paths.steam, conf.CompatData) - - os.makedirs(_bottle, exist_ok=True) - - conf.dump(os.path.join(_bottle, "bottle.yml")) - - def get_app_config(self, prefix: str) -> dict: - _fail_msg = f"Fail to get app config from Steam for: {prefix}" - - if len(self.localconfig) == 0: - logging.warning(_fail_msg) - return {} - - apps = ( - self.localconfig.get("UserLocalConfigStore", {}) - .get("Software", {}) - .get("Valve", {}) - .get("Steam", {}) - ) - if "apps" in apps: - apps = apps.get("apps") - elif "Apps" in apps: - apps = apps.get("Apps") - else: - apps = {} - - if len(apps) == 0 or prefix not in apps: - logging.warning(_fail_msg) - return {} - - return apps[prefix] - - def get_launch_options(self, prefix: str, app_conf: dict | None = None) -> {}: - if app_conf is None: - app_conf = self.get_app_config(prefix) - - launch_options = app_conf.get("LaunchOptions", "") - _fail_msg = f"Fail to get launch options from Steam for: {prefix}" - res = {"command": "", "args": "", "env_vars": {}, "env_params": {}} - - if len(launch_options) == 0: - logging.debug(_fail_msg) - return res - - command, args, env_vars = SteamUtils.handle_launch_options(launch_options) - res = {"command": command, "args": args, "env_vars": env_vars, "env_params": {}} - tmp_env_vars = res["env_vars"].copy() - - for e in tmp_env_vars: - if e in Samples.bottles_to_steam_relations: - k, v = Samples.bottles_to_steam_relations[e] - if v is None: - v = tmp_env_vars[e] - res["env_params"][k] = v - del res["env_vars"][e] - - return res - - # noinspection PyTypeChecker - def set_launch_options(self, prefix: str, options: dict): - original_launch_options = self.get_launch_options(prefix) - _fail_msg = f"Fail to set launch options for: {prefix}" - - if 0 in [len(self.localconfig), len(original_launch_options)]: - logging.warning(_fail_msg) - return - - command = options.get("command", "") - env_vars = options.get("env_vars", {}) - - if len(env_vars) > 0: - for k, v in env_vars.items(): - v = shlex.quote(v) if " " in v else v - original_launch_options["env_vars"][k] = v - - launch_options = "" - - for e, v in original_launch_options["env_vars"].items(): - launch_options += f"{e}={v} " - launch_options += f"{command} %command% {original_launch_options['args']}" - - try: - self.localconfig["UserLocalConfigStore"]["Software"]["Valve"]["Steam"][ - "apps" - ][prefix]["LaunchOptions"] = launch_options - except (KeyError, TypeError): - self.localconfig["UserLocalConfigStore"]["Software"]["Valve"]["Steam"][ - "Apps" - ][prefix]["LaunchOptions"] = launch_options - - self.save_local_config(self.localconfig) - - # noinspection PyTypeChecker - def del_launch_option(self, prefix: str, key_type: str, key: str): - original_launch_options = self.get_launch_options(prefix) - key_types = ["env_vars", "command"] - _fail_msg = f"Fail to delete a launch option for: {prefix}" - - if 0 in [len(self.localconfig), len(original_launch_options)]: - logging.warning(_fail_msg) - return - - if key_type not in key_types: - logging.warning(_fail_msg + f"\nKey type: {key_type} is not valid") - return - - if key_type == "env_vars": - if key in original_launch_options["env_vars"]: - del original_launch_options["env_vars"][key] - elif key_type == "command": - if key in original_launch_options["command"]: - original_launch_options["command"] = original_launch_options[ - "command" - ].replace(key, "") - - launch_options = "" - - for e, v in original_launch_options["env_vars"].items(): - launch_options += f"{e}={v} " - - launch_options += f"{original_launch_options['command']} %command% {original_launch_options['args']}" - try: - self.localconfig["UserLocalConfigStore"]["Software"]["Valve"]["Steam"][ - "apps" - ][prefix]["LaunchOptions"] = launch_options - except (KeyError, TypeError): - self.localconfig["UserLocalConfigStore"]["Software"]["Valve"]["Steam"][ - "Apps" - ][prefix]["LaunchOptions"] = launch_options - - self.save_local_config(self.localconfig) - - def update_bottle(self, config: BottleConfig) -> BottleConfig: - pfx = config.CompatData - launch_options = self.get_launch_options(pfx) - _fail_msg = f"Fail to update bottle for: {pfx}" - - args = launch_options.get("args", "") - if isinstance(args, dict) or args == "{}": - args = "" - - winecmd = WineCommand(config, "%command%", arguments=args) - command = winecmd.get_cmd("%command%", return_steam_cmd=True) - env_vars = winecmd.get_env(launch_options["env_vars"], return_steam_env=True) - - if "%command%" in command: - command, _args = command.split("%command%") - args = args + " " + _args - - options = {"command": command, "args": args, "env_vars": env_vars} - self.set_launch_options(pfx, options) - self.config = config - return config - - @staticmethod - def launch_app(prefix: str): - logging.info(f"Launching AppID {prefix} with Steam") - uri = f"steam://rungameid/{prefix}" - SignalManager.send(Signals.GShowUri, Result(data=uri)) - - def add_shortcut(self, program_name: str, program_path: str): - logging.info(f"Adding shortcut for {program_name}") - cmd = "xdg-open" - args = "bottles:run/'{0}'/'{1}'" - - if self.userdata_path is None: - logging.warning("Userdata path is not set") - return Result(False) - - confs = glob(os.path.join(self.userdata_path, "*/config/")) - shortcut = { - "AppName": program_name, - "Exe": cmd, - "StartDir": ManagerUtils.get_bottle_path(self.config), - "icon": ManagerUtils.extract_icon(self.config, program_name, program_path), - "ShortcutPath": "", - "LaunchOptions": args.format(self.config.Name, program_name), - "IsHidden": 0, - "AllowDesktopConfig": 1, - "AllowOverlay": 1, - "OpenVR": 0, - "Devkit": 0, - "DevkitGameID": "", - "DevkitOverrideAppID": "", - "LastPlayTime": 0, - "tags": {"0": "Bottles"}, - } - - for c in confs: - _shortcuts = {} - _existing = {} - - if os.path.exists(os.path.join(c, "shortcuts.vdf")): - with open(os.path.join(c, "shortcuts.vdf"), "rb") as f: - try: - _existing = vdf.binary_loads(f.read()).get("shortcuts", {}) - except: - continue - - _all = list(_existing.values()) + [shortcut] - _shortcuts = {"shortcuts": {str(i): s for i, s in enumerate(_all)}} - - with open(os.path.join(c, "shortcuts.vdf"), "wb") as f: - f.write(vdf.binary_dumps(_shortcuts)) - - logging.info(f"Added shortcut for {program_name}") - return Result(True) diff --git a/bottles/backend/managers/steamgriddb.py b/bottles/backend/managers/steamgriddb.py deleted file mode 100644 index 7670f7b85ef..00000000000 --- a/bottles/backend/managers/steamgriddb.py +++ /dev/null @@ -1,57 +0,0 @@ -# steamgriddb.py -# -# Copyright 2022 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import os -import uuid -import requests - -from bottles.backend.logger import Logger -from bottles.backend.models.config import BottleConfig -from bottles.backend.utils.manager import ManagerUtils - -logging = Logger() - - -class SteamGridDBManager: - @staticmethod - def get_game_grid(name: str, config: BottleConfig): - try: - res = requests.get(f"https://steamgrid.usebottles.com/api/search/{name}") - except: - return - - if res.status_code == 200: - return SteamGridDBManager.__save_grid(res.json(), config) - - @staticmethod - def __save_grid(url: str, config: BottleConfig): - grids_path = os.path.join(ManagerUtils.get_bottle_path(config), "grids") - if not os.path.exists(grids_path): - os.makedirs(grids_path) - - ext = url.split(".")[-1] - filename = str(uuid.uuid4()) + "." + ext - path = os.path.join(grids_path, filename) - - try: - r = requests.get(url) - with open(path, "wb") as f: - f.write(r.content) - except Exception: - return - - return f"grid:{filename}" diff --git a/bottles/backend/managers/template.py b/bottles/backend/managers/template.py deleted file mode 100644 index 04bb82355fd..00000000000 --- a/bottles/backend/managers/template.py +++ /dev/null @@ -1,200 +0,0 @@ -# template.py -# -# Copyright 2022 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import os - -from bottles.backend.models.config import BottleConfig -from bottles.backend.utils import yaml -import uuid -import shutil -import contextlib -from datetime import datetime -from pathlib import Path - -from bottles.backend.logger import Logger -from bottles.backend.utils.manager import ManagerUtils -from bottles.backend.globals import Paths -from bottles.backend.models.samples import Samples - -logging = Logger() - - -class TemplateManager: - @staticmethod - def new(env: str, config: BottleConfig): - env = env.lower() - templates = TemplateManager.get_templates() - - for template in templates: - if template["env"] == env: - logging.info(f"Caching new template for {env}…") - TemplateManager.delete_template(template["uuid"]) - - _uuid = str(uuid.uuid4()) - logging.info(f"Creating new template: {_uuid}") - bottle = ManagerUtils.get_bottle_path(config) - - delattr(config, "Name") - delattr(config, "Path") - delattr(config, "Creation_Date") - delattr(config, "Update_Date") - - ignored = ["dosdevices", "states", ".fvs", "*.yml" ".*"] - - _path = os.path.join(Paths.templates, _uuid) - logging.info("Copying files …") - - with contextlib.suppress(FileNotFoundError): - shutil.copytree( - bottle, _path, symlinks=True, ignore=shutil.ignore_patterns(*ignored) - ) - - template = { - "uuid": _uuid, - "env": env, - "created": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "config": config, - } - - with open(os.path.join(_path, "template.yml"), "w") as f: - yaml.dump(template, f) - - logging.info(f"New template {env} created", jn=True) - - if not TemplateManager.__validate_template(_uuid): - logging.error("Template validation failed, will retry with next bottle.") - shutil.rmtree(_path) - - @staticmethod - def __validate_template(template_uuid: str): - result = True - template_path = os.path.join(Paths.templates, template_uuid) - essentials = [ - "drive_c/ProgramData", - "drive_c/windows", - ] - - if not os.path.exists(template_path): - logging.error(f"Template {template_uuid} not found!") - result = False - - for essential in essentials: - if not os.path.exists(os.path.join(template_path, essential)): - logging.error( - f"Template {template_uuid} is missing essential path: {essential}" - ) - result = False - - path_size = sum(file.stat().st_size for file in Path(template_path).rglob("*")) - if path_size < 300000000: - logging.error(f"Template {template_uuid} is too small!") - result = False - - with open(os.path.join(template_path, "template.yml")) as f: - template = yaml.load(f) - if template["uuid"] != template_uuid: - logging.error(f"Template {template_uuid} has invalid uuid!") - result = False - - return result - - @staticmethod - def get_template_manifest(template: str): - with open(os.path.join(Paths.templates, template, "template.yml")) as f: - return yaml.load(f) - - @staticmethod - def get_templates(): - res = [] - templates = os.listdir(Paths.templates) - - for template in templates: - if os.path.exists(os.path.join(Paths.templates, template, "template.yml")): - _manifest = TemplateManager.get_template_manifest(template) - if _manifest is not None: - res.append(_manifest) - - return res - - @staticmethod - def delete_template(template_uuid: str): - if not template_uuid: - logging.error("Template uuid is not defined!") - return - - if not os.path.exists(os.path.join(Paths.templates, template_uuid)): - logging.error(f"Template {template_uuid} not found!") - return - - logging.info(f"Deleting template: {template_uuid}") - shutil.rmtree(os.path.join(Paths.templates, template_uuid)) - logging.info("Template deleted successfully!") - - @staticmethod - def check_outdated(template: dict): - env = template.get("env", "") - if env not in Samples.environments: - TemplateManager.delete_template(template.get("uuid")) - return True - - _sample = Samples.environments[env] - for p in _sample.get("Parameters", {}): - _params = template.get("config", {}).get("Parameters", {}) - if p not in _params or _params[p] != _sample["Parameters"][p]: - TemplateManager.delete_template(template.get("uuid")) - return True - - for d in _sample.get("Installed_Dependencies", []): - _deps = template.get("config", {}).get("Installed_Dependencies", []) - if d not in _deps: - TemplateManager.delete_template(template.get("uuid")) - return True - - return False - - @staticmethod - def get_env_template(env: str): - _templates = TemplateManager.get_templates() - for template in _templates: - if template["env"] == env.lower(): - if TemplateManager.check_outdated(template): - logging.info(f"Deleting outdated template: {template['uuid']}") - return None - return template - return None - - @staticmethod - def unpack_template(template: dict, config: BottleConfig): - def copy_func(source: str, dest: str): - if os.path.islink(source): - # we don't want symlinks from templates - return - shutil.copy2(source, dest) - - logging.info(f"Unpacking template: {template['uuid']}") - bottle = ManagerUtils.get_bottle_path(config) - _path = os.path.join(Paths.templates, template["uuid"]) - - shutil.copytree( - _path, - bottle, - symlinks=True, - dirs_exist_ok=True, - ignore=shutil.ignore_patterns(".*"), - ignore_dangling_symlinks=True, - ) - logging.info("Template unpacked successfully!") diff --git a/bottles/backend/managers/thumbnail.py b/bottles/backend/managers/thumbnail.py deleted file mode 100644 index ea6183059a6..00000000000 --- a/bottles/backend/managers/thumbnail.py +++ /dev/null @@ -1,49 +0,0 @@ -# thumbnail.py -# -# Copyright 2022 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import os - -from bottles.backend.logger import Logger -from bottles.backend.models.config import BottleConfig -from bottles.backend.utils.manager import ManagerUtils - -logging = Logger() - - -class ThumbnailManager: - @staticmethod - def get_path(config: BottleConfig, uri: str): - if uri.startswith("grid:"): - return ThumbnailManager.__load_grid(config, uri) - # elif uri.startswith("epic:"): - # return ThumbnailManager.__load_epic(config, uri) - # elif uri.startswith("origin:"): - # return ThumbnailManager.__load_origin(config, uri) - logging.error("Unknown URI: " + uri) - return None - - @staticmethod - def __load_grid(config: BottleConfig, uri: str): - bottle_path = ManagerUtils.get_bottle_path(config) - file_name = uri[5:] - path = os.path.join(bottle_path, "grids", file_name) - - if not os.path.exists(path): - logging.error("Grid not found: " + path) - return None - - return path diff --git a/bottles/backend/managers/ubisoftconnect.py b/bottles/backend/managers/ubisoftconnect.py deleted file mode 100644 index e56633e4094..00000000000 --- a/bottles/backend/managers/ubisoftconnect.py +++ /dev/null @@ -1,131 +0,0 @@ -# ubisoftconnect.py -# -# Copyright 2022 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import os -import uuid - -from bottles.backend.models.config import BottleConfig -from bottles.backend.utils.manager import ManagerUtils - - -class UbisoftConnectManager: - @staticmethod - def find_conf_path(config: BottleConfig) -> str | None: - """ - Finds the Ubisoft Connect configurations file path. - """ - paths = [ - os.path.join( - ManagerUtils.get_bottle_path(config), - "drive_c/Program Files (x86)/Ubisoft/Ubisoft Game Launcher/cache/configuration/configurations", - ) - ] - - for path in paths: - if os.path.exists(path): - return path - return None - - @staticmethod - def is_uconnect_supported(config: BottleConfig) -> bool: - """ - Checks if Ubisoft Connect is supported. - """ - return UbisoftConnectManager.find_conf_path(config) is not None - - # noinspection PyTypeChecker - @staticmethod - def get_installed_games(config: BottleConfig) -> list: - """ - Gets the games. - """ - found = {} - games = [] - key: str | None = None - appid: str | None = None - thumb: str | None = None - reg_key = ( - "register: HKEY_LOCAL_MACHINE\\SOFTWARE\\Ubisoft\\Launcher\\Installs\\" - ) - conf_path = UbisoftConnectManager.find_conf_path(config) - games_path = os.path.join( - ManagerUtils.get_bottle_path(config), - "drive_c/Program Files (x86)/Ubisoft/Ubisoft Game Launcher/games", - ) - - if conf_path is None: - return [] - - with open(conf_path, encoding="iso-8859-15") as c: - for r in c.readlines(): - r = r.strip() - - if r.startswith("name:"): - _key = r.replace("name:", "").strip() - if _key != "" and _key not in found.keys(): - key = _key - found[key] = {"name": None, "appid": None, "thumb_image": None} - - elif key and r.startswith("- shortcut_name:"): - _name = r.replace("- shortcut_name:", "").strip() - if _name != "": - name = _name - found[key]["name"] = name - - elif ( - key and found[key]["name"] is None and r.startswith("display_name:") - ): - name = r.replace("display_name:", "").strip() - found[key]["name"] = name - - elif key and r.startswith("thumb_image:"): - thumb = r.replace("thumb_image:", "").strip() - found[key]["thumb_image"] = thumb - - elif key and r.startswith(reg_key): - appid = r.replace(reg_key, "").replace("\\InstallDir", "").strip() - found[key]["appid"] = appid - - if None not in [key, appid, thumb]: - key, name, appid, thumb = None, None, None, None - - for k, v in found.items(): - if v["name"] is None or not os.path.exists( - os.path.join(games_path, v["name"]) - ): - continue - - _args = f"uplay://launch/{v['appid']}/0" - _path = "C:\\Program Files (x86)\\Ubisoft\\Ubisoft Game Launcher\\UbisoftConnect.exe" - _executable = _path.split("\\")[-1] - _folder = ManagerUtils.get_exe_parent_dir(config, _path) - _thumb = ( - "" if v["thumb_image"] is None else f"ubisoft:{v['thumb_image']}" - ) - games.append( - { - "executable": _path, - "arguments": _args, - "name": v["name"], - "thumb": _thumb, - "path": _path, - "folder": _folder, - "icon": "com.usebottles.bottles-program", - "id": str(uuid.uuid4()), - } - ) - return games diff --git a/bottles/backend/managers/versioning.py b/bottles/backend/managers/versioning.py deleted file mode 100644 index 6f5163a1519..00000000000 --- a/bottles/backend/managers/versioning.py +++ /dev/null @@ -1,278 +0,0 @@ -# versioning.py -# -# Copyright 2022 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import os -import shutil -from datetime import datetime -from gettext import gettext as _ -from glob import glob - -from typing import Any - -from fvs.exceptions import ( # type: ignore [import-untyped] - FVSNothingToCommit, - FVSStateNotFound, - FVSNothingToRestore, - FVSStateZeroNotDeletable, -) -from fvs.repo import FVSRepo # type: ignore [import-untyped] - -from bottles.backend.logger import Logger -from bottles.backend.models.config import BottleConfig -from bottles.backend.models.result import Result -from bottles.backend.state import TaskManager, Task -from bottles.backend.utils import yaml -from bottles.backend.utils.file import FileUtils -from bottles.backend.utils.manager import ManagerUtils - -logging = Logger() - - -# noinspection PyTypeChecker -class VersioningManager: - def __init__(self, manager): - self.manager = manager - - @staticmethod - def __get_patterns(config: BottleConfig): - patterns = ["*dosdevices*", "*cache*"] - if config.Parameters.versioning_exclusion_patterns: - patterns += config.Versioning_Exclusion_Patterns - return patterns - - @staticmethod - def is_initialized(config: BottleConfig): - try: - repo = FVSRepo( - repo_path=ManagerUtils.get_bottle_path(config), - use_compression=config.Parameters.versioning_compression, - no_init=True, - ) - except FileNotFoundError: - return False - return not repo.has_no_states - - @staticmethod - def re_initialize(config: BottleConfig): - fvs_path = os.path.join(ManagerUtils.get_bottle_path(config), ".fvs") - if os.path.exists(fvs_path): - shutil.rmtree(fvs_path) - - def update_system(self, config: BottleConfig): - states_path = os.path.join(ManagerUtils.get_bottle_path(config), "states") - if os.path.exists(states_path): - shutil.rmtree(states_path) - return self.manager.update_config(config, "Versioning", False) - - def create_state(self, config: BottleConfig, message: str = "No message"): - patterns = self.__get_patterns(config) - repo = FVSRepo( - repo_path=ManagerUtils.get_bottle_path(config), - use_compression=config.Parameters.versioning_compression, - ) - task_id = TaskManager.add(Task(title=_("Committing state …"))) - try: - repo.commit(message, ignore=patterns) - except FVSNothingToCommit: - TaskManager.remove(task_id) - return Result(status=False, message=_("Nothing to commit")) - - TaskManager.remove(task_id) - return Result( - status=True, - message=_("New state [{0}] created successfully!").format( - repo.active_state_id - ), - data={"state_id": repo.active_state_id, "states": repo.states}, - ) - - def list_states( - self, config: BottleConfig - ) -> dict[str, Any] | Result[dict[str, Any]]: - """ - This function take all the states from the states.yml file - of the given bottle and return them as a dict. - """ - if not config.Versioning: - try: - repo = FVSRepo( - repo_path=ManagerUtils.get_bottle_path(config), - use_compression=config.Parameters.versioning_compression, - ) - except FVSStateNotFound: - logging.warning( - "The FVS repository may be corrupted, trying to re-initialize it" - ) - self.re_initialize(config) - repo = FVSRepo( - repo_path=ManagerUtils.get_bottle_path(config), - use_compression=config.Parameters.versioning_compression, - ) - return Result( - status=True, - message=_("States list retrieved successfully!"), - data={"state_id": repo.active_state_id, "states": repo.states}, - ) - - bottle_path = ManagerUtils.get_bottle_path(config) - states = {} - - try: - states_file = open("%s/states/states.yml" % bottle_path) - states_file_yaml = yaml.load(states_file) - states_file.close() - states = states_file_yaml.get("States") - logging.info(f"Found [{len(states)}] states for bottle: [{config.Name}]") - except (FileNotFoundError, yaml.YAMLError): - logging.info(f"No states found for bottle: [{config.Name}]") - - return states - - def set_state( - self, config: BottleConfig, state_id: int, after: callable = None - ) -> Result: - if not config.Versioning: - patterns = self.__get_patterns(config) - repo = FVSRepo( - repo_path=ManagerUtils.get_bottle_path(config), - use_compression=config.Parameters.versioning_compression, - ) - res = Result( - status=True, - message=_("State {0} restored successfully!").format(state_id), - ) - task_id = TaskManager.add(Task(title=_(f"Restoring state {state_id} …"))) - try: - repo.restore_state(state_id, ignore=patterns) - except FVSStateNotFound: - logging.error(f"State {state_id} not found.") - res = Result(status=False, message=_("State not found")) - except (FVSNothingToRestore, FVSStateZeroNotDeletable): - logging.error(f"State {state_id} is the active state.") - res = Result( - status=False, - message=_("State {} is already the active state").format(state_id), - ) - TaskManager.remove(task_id) - return res - - bottle_path = ManagerUtils.get_bottle_path(config) - logging.info(f"Restoring to state: [{state_id}]") - - # get bottle and state indexes - bottle_index = self.get_index(config) - state_index = self.get_state_files(config, state_id) - - search_sources = list(range(int(state_id) + 1)) - search_sources.reverse() - - # check for removed and changed files - remove_files = [] - edit_files = [] - for file in bottle_index.get("Files"): - if file["file"] not in [f["file"] for f in state_index.get("Files")]: - remove_files.append(file) - elif file["checksum"] not in [ - f["checksum"] for f in state_index.get("Files") - ]: - edit_files.append(file) - logging.info(f"[{len(remove_files)}] files to remove.") - logging.info(f"[{len(edit_files)}] files to replace.") - - # check for new files - add_files = [] - for file in state_index.get("Files"): - if file["file"] not in [f["file"] for f in bottle_index.get("Files")]: - add_files.append(file) - logging.info(f"[{len(add_files)}] files to add.") - - # perform file updates - for file in remove_files: - os.remove("{}/drive_c/{}".format(bottle_path, file["file"])) - - for file in add_files: - source = "{}/states/{}/drive_c/{}".format( - bottle_path, - str(state_id), - file["file"], - ) - target = "{}/drive_c/{}".format(bottle_path, file["file"]) - shutil.copy2(source, target) - - for file in edit_files: - for i in search_sources: - source = "{}/states/{}/drive_c/{}".format( - bottle_path, str(i), file["file"] - ) - if os.path.isfile(source): - checksum = FileUtils().get_checksum(source) - if file["checksum"] == checksum: - break - target = "{}/drive_c/{}".format(bottle_path, file["file"]) - shutil.copy2(source, target) - - # update State in bottle config - self.manager.update_config(config, "State", state_id) - - # execute caller function after all - if after: - after() - - return Result(True) - - @staticmethod - def get_state_files( - config: BottleConfig, state_id: int, plain: bool = False - ) -> str | Any: - """ - Return the files.yml content of the state. Use the plain argument - to return the content as plain text. - """ - try: - file = open( - "%s/states/%s/files.yml" - % (ManagerUtils.get_bottle_path(config), state_id) - ) - files = file.read() if plain else yaml.load(file.read()) - file.close() - return files - except (OSError, yaml.YAMLError): - logging.error("Could not read the state files file.") - return {} - - @staticmethod - def get_index(config: BottleConfig): - """List all files in a bottle and return as dict.""" - bottle_path = ManagerUtils.get_bottle_path(config) - cur_index = {"Update_Date": str(datetime.now()), "Files": []} - for file in glob("%s/drive_c/**" % bottle_path, recursive=True): - if not os.path.isfile(file): - continue - - if os.path.islink(os.path.dirname(file)): - continue - - if file[len(bottle_path) + 9 :].split("/")[0] in ["users"]: - continue - - cur_index["Files"].append( - { - "file": file[len(bottle_path) + 9 :], - "checksum": FileUtils().get_checksum(file), - } - ) - return cur_index diff --git a/bottles/backend/meson.build b/bottles/backend/meson.build index e3ef11e826f..284bb66dce4 100644 --- a/bottles/backend/meson.build +++ b/bottles/backend/meson.build @@ -16,14 +16,15 @@ subdir('managers') bottles_sources = [ '__init__.py', + 'bottle.py', 'globals.py', 'runner.py', 'diff.py', 'health.py', 'downloader.py', - 'logger.py', 'cabextract.py', 'state.py', + 'typing.py', params_file ] diff --git a/bottles/backend/models/config.py b/bottles/backend/models/config.py index 9136f027ae2..01882202aaf 100644 --- a/bottles/backend/models/config.py +++ b/bottles/backend/models/config.py @@ -3,7 +3,7 @@ import os from dataclasses import dataclass, field, replace, asdict, is_dataclass from io import IOBase -from typing import Optional, IO +from typing import IO, Self from collections.abc import ItemsView, Container from bottles.backend.models.result import Result @@ -170,7 +170,7 @@ def dump(self, file: str | IO, mode="w", encoding=None, indent=4) -> Result: f.close() @classmethod - def load(cls, file: str | IO, mode="r") -> Result[Optional["BottleConfig"]]: + def load(cls, file: str | IO, mode="r") -> Result[Self]: """ Load config from file @@ -204,7 +204,7 @@ def load(cls, file: str | IO, mode="r") -> Result[Optional["BottleConfig"]]: f.close() @classmethod - def _fill_with(cls, data: dict) -> Result[Optional["BottleConfig"]]: + def _fill_with(cls, data: dict) -> Result[Self | None]: """fill with dict""" try: data = data.copy() diff --git a/bottles/backend/repos/repo.py b/bottles/backend/repos/repo.py index 3601cc82a2b..657a10020ad 100644 --- a/bottles/backend/repos/repo.py +++ b/bottles/backend/repos/repo.py @@ -19,13 +19,11 @@ import pycurl -from bottles.backend.logger import Logger +import logging from bottles.backend.state import EventManager, Events from bottles.backend.utils import yaml from bottles.backend.utils.threading import RunAsync -logging = Logger() - class Repo: name: str = "" diff --git a/bottles/backend/runner.py b/bottles/backend/runner.py index 270916674a8..8a7438da6c8 100644 --- a/bottles/backend/runner.py +++ b/bottles/backend/runner.py @@ -18,19 +18,15 @@ import os from typing import TYPE_CHECKING -from bottles.backend.logger import Logger -from bottles.backend.managers.runtime import RuntimeManager +import logging from bottles.backend.models.config import BottleConfig from bottles.backend.models.result import Result from bottles.backend.utils.manager import ManagerUtils -from bottles.backend.utils.steam import SteamUtils from bottles.backend.wine.wineboot import WineBoot if TYPE_CHECKING: from bottles.backend.managers.manager import Manager -logging = Logger() - class Runner: """ @@ -79,17 +75,4 @@ def runner_update( if config.Parameters.vkd3d: manager.install_dll_component(config, "vkd3d", overrides_only=True) - """ - enable Steam runtime if using Proton. - - NOTE: Official Proton runners need to be launched with the Steam Runtime to works - properly, since it relies on a lot of libraries that should not be present on - the host system. There are some exceptions, like the Soda and Wine-GE runners, - which are built to work without the Steam Runtime. - """ - if SteamUtils.is_proton( - ManagerUtils.get_runner_path(runner) - ) and RuntimeManager.get_runtimes("steam"): - manager.update_config(config, "use_steam_runtime", True, "Parameters") - return Result(status=True, data={"config": up_config}) diff --git a/bottles/backend/state.py b/bottles/backend/state.py index e29bbe18862..9beee1433df 100644 --- a/bottles/backend/state.py +++ b/bottles/backend/state.py @@ -6,11 +6,9 @@ from collections.abc import Callable from uuid import UUID, uuid4 -from bottles.backend.logger import Logger +import logging from bottles.backend.models.result import Result -logging = Logger() - class Locks(Enum): ComponentsInstall = "components.install" @@ -33,7 +31,6 @@ class Signals(Enum): ForceStopNetworking = ( "LoadingView.stop_networking" # status(bool): Force Stop network operations ) - RepositoryFetched = "RepositoryManager.repo_fetched" # status: fetch success or not, data(int): total repositories NetworkStatusChanged = ( "ConnectionUtils.status_changed" # status(bool): network ready or not ) diff --git a/bottles/backend/managers/queue.py b/bottles/backend/typing.py similarity index 57% rename from bottles/backend/managers/queue.py rename to bottles/backend/typing.py index 450f1d95fdb..676100e5363 100644 --- a/bottles/backend/managers/queue.py +++ b/bottles/backend/typing.py @@ -1,6 +1,6 @@ -# queue.py +# typing.py # -# Copyright 2022 brombinmirko +# Copyright 2025 The Bottles Contributors # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,22 +13,20 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -# +from typing import Literal +from enum import Enum + +type VersionedComponent = str | Literal[False] -class QueueManager: - __queue = 0 - def __init__(self, end_fn, add_fn=None): - self.__add_fn = add_fn - self.__end_fn = end_fn +class WindowsAPI(Enum): + WIN64 = "Win64" + WIN32 = "Win32" + WIN16 = "Win16" - def add_task(self): - self.__queue += 1 - if self.__add_fn and self.__queue == 1: - self.__add_fn() - def end_task(self): - self.__queue -= 1 - if self.__queue <= 0: - self.__end_fn() +class Environment(Enum): + APPLICATION = "Application" + GAMING = "Gaming" + CUSTOM = "Custom" diff --git a/bottles/backend/utils/connection.py b/bottles/backend/utils/connection.py deleted file mode 100644 index acc84a33636..00000000000 --- a/bottles/backend/utils/connection.py +++ /dev/null @@ -1,110 +0,0 @@ -# connection.py -# -# Copyright 2022 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import os -from datetime import datetime -from gettext import gettext as _ - -import pycurl - -from bottles.backend.logger import Logger -from bottles.backend.models.result import Result -from bottles.backend.state import SignalManager, Signals, Notification - -logging = Logger() - - -class ConnectionUtils: - """ - This class is used to check the connection, pinging the official - Bottle's website. If the connection is offline, the user will be - notified and False will be returned, otherwise True. - """ - - _status: bool | None = None - last_check = None - - def __init__(self, force_offline=False, **kwargs): - super().__init__(**kwargs) - self.force_offline = force_offline - self.do_check_connection = True - self.aborted_connections = 0 - SignalManager.connect(Signals.ForceStopNetworking, self.stop_check) - - @property - def status(self) -> bool | None: - return self._status - - @status.setter - def status(self, value: bool): - if value is None: - logging.error("Cannot set network status to None") - return - self._status = value - SignalManager.send(Signals.NetworkStatusChanged, Result(status=self.status)) - - def __curl_progress(self, _download_t, _download_d, _upload_t, _upload_d): - if self.do_check_connection: - return pycurl.E_OK - else: - self.aborted_connections += 1 - return pycurl.E_ABORTED_BY_CALLBACK - - def stop_check(self, res: Result): - if res.status: - self.do_check_connection = False - - def check_connection(self, show_notification=False) -> bool | None: - """check network status, send result through signal NetworkReady and return""" - if self.force_offline or "FORCE_OFFLINE" in os.environ: - logging.info("Forcing offline mode") - self.status = False - return False - - try: - c = pycurl.Curl() - c.setopt(c.URL, "https://ping.usebottles.com") - c.setopt(c.FOLLOWLOCATION, True) - c.setopt(c.NOBODY, True) - c.setopt(c.NOPROGRESS, False) - c.setopt(c.XFERINFOFUNCTION, self.__curl_progress) - c.perform() - - if c.getinfo(pycurl.HTTP_CODE) != 200: - raise Exception("Connection status: offline …") - - self.last_check = datetime.now() - self.status = True - except Exception: - logging.warning("Connection status: offline …") - if show_notification: - SignalManager.send( - Signals.GNotification, - Result( - True, - Notification( - title="Bottles", - text=_("You are offline, unable to download."), - image="network-wireless-disabled-symbolic", - ), - ), - ) - self.last_check = datetime.now() - self.status = False - finally: - self.do_check_connection = True - return self.status diff --git a/bottles/backend/utils/gpu.py b/bottles/backend/utils/gpu.py index 00207971d92..8b930e971a5 100644 --- a/bottles/backend/utils/gpu.py +++ b/bottles/backend/utils/gpu.py @@ -22,9 +22,7 @@ from bottles.backend.utils.nvidia import get_nvidia_dll_path from bottles.backend.utils.vulkan import VulkanUtils -from bottles.backend.logger import Logger - -logging = Logger() +import logging class GPUVendors(Enum): diff --git a/bottles/backend/utils/gsettings_stub.py b/bottles/backend/utils/gsettings_stub.py index d150c49276e..08d8df105a7 100644 --- a/bottles/backend/utils/gsettings_stub.py +++ b/bottles/backend/utils/gsettings_stub.py @@ -1,6 +1,4 @@ -from bottles.backend.logger import Logger - -logging = Logger() +import logging class GSettingsStub: diff --git a/bottles/backend/utils/manager.py b/bottles/backend/utils/manager.py index 1a7e29d0ed8..55e0791ff1b 100644 --- a/bottles/backend/utils/manager.py +++ b/bottles/backend/utils/manager.py @@ -24,15 +24,13 @@ import icoextract # type: ignore [import-untyped] from bottles.backend.globals import Paths -from bottles.backend.logger import Logger +import logging from bottles.backend.models.config import BottleConfig from bottles.backend.models.result import Result from bottles.backend.state import SignalManager, Signals from bottles.backend.utils.generic import get_mime from bottles.backend.utils.imagemagick import ImageMagickUtils -logging = Logger() - class ManagerUtils: """ diff --git a/bottles/backend/utils/meson.build b/bottles/backend/utils/meson.build index ab74896ff1b..3d17a23ff1c 100644 --- a/bottles/backend/utils/meson.build +++ b/bottles/backend/utils/meson.build @@ -6,13 +6,11 @@ bottles_sources = [ 'display.py', 'gpu.py', 'manager.py', - 'midi.py', 'vulkan.py', 'terminal.py', 'file.py', 'generic.py', 'wine.py', - 'steam.py', 'lnk.py', 'decorators.py', 'snake.py', @@ -22,7 +20,6 @@ bottles_sources = [ 'yaml.py', 'nvidia.py', 'threading.py', - 'connection.py', 'gsettings_stub.py', 'json.py', 'singleton.py' diff --git a/bottles/backend/utils/midi.py b/bottles/backend/utils/midi.py deleted file mode 100644 index a8bce4e8347..00000000000 --- a/bottles/backend/utils/midi.py +++ /dev/null @@ -1,110 +0,0 @@ -# midi.py -# -# Copyright 2025 The Bottles Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from typing import Self - -from ctypes import c_void_p -from fluidsynth import cfunc, Synth # type: ignore [import-untyped] - -from bottles.backend.logger import Logger -from bottles.backend.models.config import BottleConfig -from bottles.backend.wine.reg import Reg - -logging = Logger() - - -class FluidSynth: - """FluidSynth instance bounded to a unique SoundFont (.sf2, .sf3) file.""" - - __active_instances: dict[int, Self] = {} - - @classmethod - def find_or_create(cls, soundfont_path: str) -> Self: - """ - Search for running FluidSynth instance and return it. - If nonexistent, create and add it to active ones beforehand. - """ - - for fs in cls.__active_instances.values(): - if fs.soundfont_path == soundfont_path: - fs.program_count += 1 - return fs - - fs = cls(soundfont_path) - cls.__active_instances[fs.id] = fs - return fs - - def __init__(self, soundfont_path: str): - """Build a new FluidSynth object from SoundFont file path.""" - self.soundfont_path = soundfont_path - self.id = self.__get_vacant_id() - self.__start() - self.program_count = 1 - - @classmethod - def __get_vacant_id(cls) -> int: - """Get smallest 0-indexed ID currently not in use by a SoundFont.""" - n = len(cls.__active_instances) - return next(i for i in range(n + 1) if i not in cls.__active_instances) - - def __start(self): - """Start FluidSynth synthetizer with loaded SoundFont.""" - logging.info( - "Starting new FluidSynth server with SoundFont" - f" #{self.id} ('{self.soundfont_path}')…" - ) - synth = Synth(channels=16) - synth.start() - sfid = synth.sfload(self.soundfont_path) - synth.program_select(0, sfid, 0, 0) - self.synth = synth - - def register_as_current(self, config: BottleConfig): - """ - Update Wine registry with this instance's ID, instructing - MIDI mapping to load the correct instrument set on program startup. - """ - reg = Reg(config) - reg.add( - key="HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Multimedia\\MIDIMap", - value="CurrentInstrument", - data=f"#{self.id}", - value_type="REG_SZ", - ) - - def decrement_program_counter(self): - """Decrement program counter; if it reaches zero, delete this instance.""" - self.program_count -= 1 - if self.program_count == 0: - self.__delete() - - def __delete(self): - """Kill underlying synthetizer and remove FluidSynth instance from dict.""" - - def __delete_synth(synth: Synth): - """Bind missing function and run deletion routines.""" - delete_fluid_midi_driver = cfunc( - "delete_fluid_midi_driver", c_void_p, ("driver", c_void_p, 1) - ) - delete_fluid_midi_driver(synth.midi_driver) - synth.delete() - - logging.info( - "Killing FluidSynth server with SoundFont" - f" #{self.id} ('{self.soundfont_path}')…" - ) - __delete_synth(self.synth) - self.__active_instances.pop(self.id) diff --git a/bottles/backend/utils/nvidia.py b/bottles/backend/utils/nvidia.py index ea49e588f7d..46d3d41fcb3 100644 --- a/bottles/backend/utils/nvidia.py +++ b/bottles/backend/utils/nvidia.py @@ -5,9 +5,9 @@ import os from ctypes import CDLL, POINTER, Structure, addressof, c_char_p, c_int, c_void_p, cast -from bottles.backend.logger import Logger +import logging + -logging = Logger() RTLD_DI_LINKMAP = 2 diff --git a/bottles/backend/utils/steam.py b/bottles/backend/utils/steam.py deleted file mode 100644 index d420d2c2125..00000000000 --- a/bottles/backend/utils/steam.py +++ /dev/null @@ -1,135 +0,0 @@ -# steam.py -# -# Copyright 2022 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import os -import shlex -from typing import TextIO - -from bottles.backend.logger import Logger -from bottles.backend.models.vdict import VDFDict -from bottles.backend.utils import vdf - -logging = Logger() - - -class SteamUtils: - @staticmethod - def parse_acf(data: str) -> VDFDict: - """ - Parses an ACF file. Just a wrapper for vdf.loads. - """ - return vdf.loads(data) - - @staticmethod - def parse_vdf(data: str) -> VDFDict: - """ - Parses a VDF file. Just a wrapper for vdf.loads. - """ - return vdf.loads(data) - - @staticmethod - def to_vdf(data: VDFDict, fp: TextIO): - """ - Saves a VDF file. Just a wrapper for vdf.dumps. - """ - vdf.dump(data, fp, pretty=True) - - @staticmethod - def is_proton(path: str) -> bool: - """ - Checks if a directory is a Proton directory. - """ - toolmanifest = os.path.join(path, "toolmanifest.vdf") - if not os.path.isfile(toolmanifest): - return False - - f = open(toolmanifest, errors="replace") - data = SteamUtils.parse_vdf(f.read()) - compat_layer_name = data.get("manifest", {}).get("compatmanager_layer_name", {}) - - commandline = data.get("manifest", {}).get("commandline", {}) - - return "proton" in compat_layer_name or "proton" in commandline - - @staticmethod - def get_associated_runtime(path: str) -> str | None: - """ - Get the associated runtime of a Proton directory. - """ - toolmanifest = os.path.join(path, "toolmanifest.vdf") - if not os.path.isfile(toolmanifest): - logging.error(f"toolmanifest.vdf not found in Proton directory: {path}") - return None - - runtime = "scout" - f = open(toolmanifest, errors="replace") - data = SteamUtils.parse_vdf(f.read()) - tool_appid = data.get("manifest", {}).get("require_tool_appid", {}) - - if "1628350" in tool_appid: - runtime = "sniper" - elif "1391110" in tool_appid: - runtime = "soldier" - - return runtime - - @staticmethod - def get_dist_directory(path: str) -> str: - """ - Get the sub-directory containing the wine libraries and binaries. - """ - dist_directory = path - if os.path.isdir(os.path.join(path, "dist")): - dist_directory = os.path.join(path, "dist") - elif os.path.isdir(os.path.join(path, "files")): - dist_directory = os.path.join(path, "files") - else: - logging.warning( - f"No /dist or /files sub-directory was found under this Proton directory: {path}" - ) - - return dist_directory - - @staticmethod - def handle_launch_options(launch_options: str) -> tuple[str, str, dict[str, str]]: - """ - Handle launch options. Supports the %command% pattern. - Return prefix, arguments, and environment variables. - """ - env_vars = {} - prefix, args = "", "" - if "%command%" in launch_options: - _c = launch_options.split("%command%") - prefix = _c[0] if len(_c) > 0 else "" - args = _c[1] if len(_c) > 1 else "" - else: - args = launch_options - - try: - prefix_list = shlex.split(prefix.strip()) - except ValueError: - prefix_list = prefix.split(shlex.quote(prefix.strip())) - - for p in prefix_list.copy(): - if "=" in p: - k, v = p.split("=", 1) - v = shlex.quote(v) if " " in v else v - env_vars[k] = v - prefix_list.remove(p) - - prefix = " ".join(prefix_list) - return prefix, args, env_vars diff --git a/bottles/backend/utils/terminal.py b/bottles/backend/utils/terminal.py index 32513bb71fb..66c0c93ee94 100644 --- a/bottles/backend/utils/terminal.py +++ b/bottles/backend/utils/terminal.py @@ -19,9 +19,7 @@ import subprocess import shlex -from bottles.backend.logger import Logger - -logging = Logger() +import logging class TerminalUtils: diff --git a/bottles/backend/utils/threading.py b/bottles/backend/utils/threading.py index 911dc0fb3a1..f0f137f8ade 100644 --- a/bottles/backend/utils/threading.py +++ b/bottles/backend/utils/threading.py @@ -21,9 +21,7 @@ import traceback from typing import Any -from bottles.backend.logger import Logger - -logging = Logger() +import logging class RunAsync(threading.Thread): diff --git a/bottles/backend/wine/cmd.py b/bottles/backend/wine/cmd.py index 268e152a454..42f751bc6ce 100644 --- a/bottles/backend/wine/cmd.py +++ b/bottles/backend/wine/cmd.py @@ -1,8 +1,5 @@ -from bottles.backend.logger import Logger from bottles.backend.wine.wineprogram import WineProgram -logging = Logger() - class CMD(WineProgram): program = "Wine Command Line" diff --git a/bottles/backend/wine/control.py b/bottles/backend/wine/control.py index c9b2800f5a9..f40657c2f06 100644 --- a/bottles/backend/wine/control.py +++ b/bottles/backend/wine/control.py @@ -1,8 +1,5 @@ -from bottles.backend.logger import Logger from bottles.backend.wine.wineprogram import WineProgram -logging = Logger() - class Control(WineProgram): program = "Wine Control Panel" diff --git a/bottles/backend/wine/drives.py b/bottles/backend/wine/drives.py index a87b3dc9400..0702414bd21 100644 --- a/bottles/backend/wine/drives.py +++ b/bottles/backend/wine/drives.py @@ -1,11 +1,9 @@ import os -from bottles.backend.logger import Logger +import logging from bottles.backend.models.config import BottleConfig from bottles.backend.utils.manager import ManagerUtils -logging = Logger() - class Drives: def __init__(self, config: BottleConfig): diff --git a/bottles/backend/wine/eject.py b/bottles/backend/wine/eject.py index 703c047a1da..e4621f8a6ba 100644 --- a/bottles/backend/wine/eject.py +++ b/bottles/backend/wine/eject.py @@ -1,8 +1,5 @@ -from bottles.backend.logger import Logger from bottles.backend.wine.wineprogram import WineProgram -logging = Logger() - class Eject(WineProgram): program = "Wine Eject CLI" diff --git a/bottles/backend/wine/executor.py b/bottles/backend/wine/executor.py index f3eaee0d6b2..7af98bb95f6 100644 --- a/bottles/backend/wine/executor.py +++ b/bottles/backend/wine/executor.py @@ -5,11 +5,10 @@ from bottles.backend.dlls.dxvk import DXVKComponent from bottles.backend.dlls.nvapi import NVAPIComponent from bottles.backend.dlls.vkd3d import VKD3DComponent -from bottles.backend.logger import Logger +import logging from bottles.backend.models.config import BottleConfig from bottles.backend.models.result import Result from bottles.backend.utils.manager import ManagerUtils -from bottles.backend.utils.midi import FluidSynth from bottles.backend.wine.cmd import CMD from bottles.backend.wine.explorer import Explorer from bottles.backend.wine.msiexec import MsiExec @@ -18,8 +17,6 @@ from bottles.backend.wine.winedbg import WineDbg from bottles.backend.wine.winepath import WinePath -logging = Logger() - class WineExecutor: def __init__( @@ -71,13 +68,6 @@ def __init__( env_dll_overrides = [] - self.fluidsynth = None - if (soundfont_path := midi_soundfont) not in (None, ""): - # FluidSynth instance is bound to WineExecutor as a member to control - # the former's lifetime (deleted when no more references from executors) - self.fluidsynth = FluidSynth.find_or_create(soundfont_path) - self.fluidsynth.register_as_current(config) - # None = use global DXVK value if program_dxvk is not None: # DXVK is globally activated, but disabled for the program @@ -363,8 +353,3 @@ def __set_monitors(self): winedbg = WineDbg(self.config, silent=True) for m in self.monitoring: winedbg.wait_for_process(name=m) - - def __del__(self): - """On exit, kill FluidSynth instance if this was the last executor using it.""" - if self.fluidsynth: - self.fluidsynth.decrement_program_counter() diff --git a/bottles/backend/wine/expand.py b/bottles/backend/wine/expand.py index 7832169c12f..a2a233b3b32 100644 --- a/bottles/backend/wine/expand.py +++ b/bottles/backend/wine/expand.py @@ -1,8 +1,5 @@ -from bottles.backend.logger import Logger from bottles.backend.wine.wineprogram import WineProgram -logging = Logger() - class Expand(WineProgram): program = "Wine cabinet expander" diff --git a/bottles/backend/wine/explorer.py b/bottles/backend/wine/explorer.py index d6588cda075..259a13a8c08 100644 --- a/bottles/backend/wine/explorer.py +++ b/bottles/backend/wine/explorer.py @@ -1,8 +1,5 @@ -from bottles.backend.logger import Logger from bottles.backend.wine.wineprogram import WineProgram -logging = Logger() - class Explorer(WineProgram): program = "Wine Explorer" diff --git a/bottles/backend/wine/hh.py b/bottles/backend/wine/hh.py deleted file mode 100644 index d294be0d731..00000000000 --- a/bottles/backend/wine/hh.py +++ /dev/null @@ -1,9 +0,0 @@ -from bottles.backend.logger import Logger -from bottles.backend.wine.wineprogram import WineProgram - -logging = Logger() - - -class Hh(WineProgram): - program = "Wine HTML help viewer" - command = "hh" diff --git a/bottles/backend/wine/icinfo.py b/bottles/backend/wine/icinfo.py index 9cc1fd3a813..2a34356022e 100644 --- a/bottles/backend/wine/icinfo.py +++ b/bottles/backend/wine/icinfo.py @@ -1,8 +1,5 @@ -from bottles.backend.logger import Logger from bottles.backend.wine.wineprogram import WineProgram -logging = Logger() - class Icinfo(WineProgram): program = "List installed video compressors" diff --git a/bottles/backend/wine/meson.build b/bottles/backend/wine/meson.build index c707c904182..7b734db90b4 100644 --- a/bottles/backend/wine/meson.build +++ b/bottles/backend/wine/meson.build @@ -29,7 +29,6 @@ bottles_sources = [ 'drives.py', 'eject.py', 'expand.py', - 'hh.py', 'icinfo.py', 'notepad.py', 'oleview.py', diff --git a/bottles/backend/wine/msiexec.py b/bottles/backend/wine/msiexec.py index 340d4f5aabb..cfbac308062 100644 --- a/bottles/backend/wine/msiexec.py +++ b/bottles/backend/wine/msiexec.py @@ -1,8 +1,5 @@ -from bottles.backend.logger import Logger from bottles.backend.wine.wineprogram import WineProgram -logging = Logger() - class MsiExec(WineProgram): program = "Wine MSI Installer" diff --git a/bottles/backend/wine/net.py b/bottles/backend/wine/net.py index 41da0793b2f..aaa352396bd 100644 --- a/bottles/backend/wine/net.py +++ b/bottles/backend/wine/net.py @@ -1,8 +1,5 @@ -from bottles.backend.logger import Logger from bottles.backend.wine.wineprogram import WineProgram -logging = Logger() - class Net(WineProgram): program = "Wine Services manager" diff --git a/bottles/backend/wine/notepad.py b/bottles/backend/wine/notepad.py index c2e361d7148..5651e364ea6 100644 --- a/bottles/backend/wine/notepad.py +++ b/bottles/backend/wine/notepad.py @@ -1,8 +1,5 @@ -from bottles.backend.logger import Logger from bottles.backend.wine.wineprogram import WineProgram -logging = Logger() - class Notepad(WineProgram): program = "Wine Notepad" diff --git a/bottles/backend/wine/oleview.py b/bottles/backend/wine/oleview.py index 93da9a76b55..f0cb1c6144e 100644 --- a/bottles/backend/wine/oleview.py +++ b/bottles/backend/wine/oleview.py @@ -1,8 +1,5 @@ -from bottles.backend.logger import Logger from bottles.backend.wine.wineprogram import WineProgram -logging = Logger() - class Oleview(WineProgram): program = "OLE/COM object viewer" diff --git a/bottles/backend/wine/progman.py b/bottles/backend/wine/progman.py index 8996a0e3016..bb1eaaeb114 100644 --- a/bottles/backend/wine/progman.py +++ b/bottles/backend/wine/progman.py @@ -1,8 +1,5 @@ -from bottles.backend.logger import Logger from bottles.backend.wine.wineprogram import WineProgram -logging = Logger() - class Progman(WineProgram): program = "Wine Program Manager" diff --git a/bottles/backend/wine/reg.py b/bottles/backend/wine/reg.py index 83c78a1e162..dfbc27e0bd3 100644 --- a/bottles/backend/wine/reg.py +++ b/bottles/backend/wine/reg.py @@ -6,14 +6,12 @@ from itertools import groupby from bottles.backend.globals import Paths -from bottles.backend.logger import Logger +import logging from bottles.backend.utils.generic import random_string from bottles.backend.utils.manager import ManagerUtils from bottles.backend.wine.winedbg import WineDbg from bottles.backend.wine.wineprogram import WineProgram -logging = Logger() - @dataclasses.dataclass class RegItem: diff --git a/bottles/backend/wine/regedit.py b/bottles/backend/wine/regedit.py index 25ffa665339..fb0d16857ed 100644 --- a/bottles/backend/wine/regedit.py +++ b/bottles/backend/wine/regedit.py @@ -1,8 +1,5 @@ -from bottles.backend.logger import Logger from bottles.backend.wine.wineprogram import WineProgram -logging = Logger() - class Regedit(WineProgram): program = "Wine Registry Editor" diff --git a/bottles/backend/wine/regkeys.py b/bottles/backend/wine/regkeys.py index 63f0883e533..343e724ebb7 100644 --- a/bottles/backend/wine/regkeys.py +++ b/bottles/backend/wine/regkeys.py @@ -1,4 +1,3 @@ -from bottles.backend.logger import Logger from bottles.backend.models.config import BottleConfig from bottles.backend.models.enum import Arch from bottles.backend.wine.catalogs import win_versions @@ -6,8 +5,6 @@ from bottles.backend.wine.wineboot import WineBoot from bottles.backend.wine.winecfg import WineCfg -logging = Logger() - class RegKeys: def __init__(self, config: BottleConfig): diff --git a/bottles/backend/wine/regsvr32.py b/bottles/backend/wine/regsvr32.py index 420c829865d..02f4d5a45fd 100644 --- a/bottles/backend/wine/regsvr32.py +++ b/bottles/backend/wine/regsvr32.py @@ -1,8 +1,5 @@ -from bottles.backend.logger import Logger from bottles.backend.wine.wineprogram import WineProgram -logging = Logger() - class Regsvr32(WineProgram): program = "Wine DLL Registration Server" diff --git a/bottles/backend/wine/rundll32.py b/bottles/backend/wine/rundll32.py index c4ed2f9de3e..f5308d6bb54 100644 --- a/bottles/backend/wine/rundll32.py +++ b/bottles/backend/wine/rundll32.py @@ -1,8 +1,5 @@ -from bottles.backend.logger import Logger from bottles.backend.wine.wineprogram import WineProgram -logging = Logger() - class RunDLL32(WineProgram): program = "32-bit DLLs loader and runner" diff --git a/bottles/backend/wine/start.py b/bottles/backend/wine/start.py index d144dae66a4..e491a93db25 100644 --- a/bottles/backend/wine/start.py +++ b/bottles/backend/wine/start.py @@ -1,9 +1,6 @@ -from bottles.backend.logger import Logger from bottles.backend.wine.wineprogram import WineProgram from bottles.backend.wine.winepath import WinePath -logging = Logger() - class Start(WineProgram): program = "Wine Starter" diff --git a/bottles/backend/wine/taskmgr.py b/bottles/backend/wine/taskmgr.py index 7f27f415f76..0216fe32ce7 100644 --- a/bottles/backend/wine/taskmgr.py +++ b/bottles/backend/wine/taskmgr.py @@ -1,8 +1,5 @@ -from bottles.backend.logger import Logger from bottles.backend.wine.wineprogram import WineProgram -logging = Logger() - class Taskmgr(WineProgram): program = "Wine Task Manager" diff --git a/bottles/backend/wine/uninstaller.py b/bottles/backend/wine/uninstaller.py index 511db791bdc..f6c824224fa 100644 --- a/bottles/backend/wine/uninstaller.py +++ b/bottles/backend/wine/uninstaller.py @@ -1,8 +1,5 @@ -from bottles.backend.logger import Logger from bottles.backend.wine.wineprogram import WineProgram -logging = Logger() - class Uninstaller(WineProgram): program = "Wine Uninstaller" diff --git a/bottles/backend/wine/wineboot.py b/bottles/backend/wine/wineboot.py index b489f321f79..e7905228ae8 100644 --- a/bottles/backend/wine/wineboot.py +++ b/bottles/backend/wine/wineboot.py @@ -1,12 +1,10 @@ -from bottles.backend.logger import Logger +import logging from bottles.backend.wine.wineprogram import WineProgram from bottles.backend.wine.wineserver import WineServer import os import signal -logging = Logger() - class WineBoot(WineProgram): program = "Wine Runtime tool" diff --git a/bottles/backend/wine/winebridge.py b/bottles/backend/wine/winebridge.py index 8bd1351c4d3..31988ac4b3f 100644 --- a/bottles/backend/wine/winebridge.py +++ b/bottles/backend/wine/winebridge.py @@ -1,11 +1,9 @@ import os -from bottles.backend.logger import Logger +import logging from bottles.backend.wine.wineprogram import WineProgram from bottles.backend.wine.wineserver import WineServer -logging = Logger() - class WineBridge(WineProgram): program = "Wine Bridge" diff --git a/bottles/backend/wine/winecfg.py b/bottles/backend/wine/winecfg.py index 67b28456556..f5041a782ff 100644 --- a/bottles/backend/wine/winecfg.py +++ b/bottles/backend/wine/winecfg.py @@ -1,12 +1,10 @@ import os -from bottles.backend.logger import Logger +import logging from bottles.backend.wine.wineprogram import WineProgram from bottles.backend.wine.winedbg import WineDbg from bottles.backend.wine.wineboot import WineBoot -logging = Logger() - class WineCfg(WineProgram): program = "Wine Configuration" diff --git a/bottles/backend/wine/winecommand.py b/bottles/backend/wine/winecommand.py index bf89ff2a6d5..bb59e891f63 100644 --- a/bottles/backend/wine/winecommand.py +++ b/bottles/backend/wine/winecommand.py @@ -13,9 +13,7 @@ obs_vkc_available, vmtouch_available, ) -from bottles.backend.logger import Logger -from bottles.backend.managers.runtime import RuntimeManager -from bottles.backend.managers.sandbox import SandboxManager +import logging from bottles.backend.models.config import BottleConfig from bottles.backend.models.result import Result from bottles.backend.utils.display import DisplayUtils @@ -23,9 +21,6 @@ from bottles.backend.utils.gpu import GPUUtils from bottles.backend.utils.manager import ManagerUtils from bottles.backend.utils.terminal import TerminalUtils -from bottles.backend.utils.steam import SteamUtils - -logging = Logger() class WineEnv: @@ -134,10 +129,7 @@ def _get_config(self, config: BottleConfig) -> BottleConfig: def _get_cwd(self, cwd) -> str: config = self.config - if config.Environment == "Steam": - bottle = config.Path - else: - bottle = ManagerUtils.get_bottle_path(config) + bottle = ManagerUtils.get_bottle_path(config) if not cwd: """ @@ -157,10 +149,8 @@ def _get_cwd(self, cwd) -> str: def get_env( self, environment: dict | None = None, - return_steam_env: bool = False, - return_clean_env: bool = False, ) -> dict: - env = WineEnv(clean=return_steam_env or return_clean_env) + env = WineEnv() config = self.config arch = config.Arch params = config.Parameters @@ -177,13 +167,6 @@ def get_env( bottle = ManagerUtils.get_bottle_path(config) runner_path = ManagerUtils.get_runner_path(config.Runner) - if config.Environment == "Steam": - bottle = config.Path - runner_path = config.RunnerPath - - if SteamUtils.is_proton(runner_path): - runner_path = SteamUtils.get_dist_directory(runner_path) - # Clean some env variables which can cause trouble # ref: # env.remove("XDG_DATA_HOME") @@ -220,39 +203,7 @@ def get_env( dll_overrides.append(f"{k}={v}") # Default DLL overrides - if not return_steam_env: - dll_overrides.append("winemenubuilder=''") - - # Get Runtime libraries - if ( - (params.use_runtime or params.use_eac_runtime or params.use_be_runtime) - and not self.terminal - and not return_steam_env - ): - _rb = RuntimeManager.get_runtime_env("bottles") - if _rb: - _eac = RuntimeManager.get_eac() - _be = RuntimeManager.get_be() - - if params.use_runtime: - logging.info("Using Bottles runtime") - ld += _rb - - if ( - _eac and not self.minimal - ): # NOTE: should check for runner compatibility with "eac" (?) - logging.info("Using EasyAntiCheat runtime") - env.add("PROTON_EAC_RUNTIME", _eac) - dll_overrides.append("easyanticheat_x86,easyanticheat_x64=b,n") - - if ( - _be and not self.minimal - ): # NOTE: should check for runner compatibility with "be" (?) - logging.info("Using BattlEye runtime") - env.add("PROTON_BATTLEYE_RUNTIME", _be) - dll_overrides.append("beclient,beclient_x64=b,n") - else: - logging.warning("Bottles runtime was requested but not found") + dll_overrides.append("winemenubuilder=''") # Get Runner libraries if arch == "win64": @@ -286,7 +237,7 @@ def get_env( ld.append(_path) # Embedded GStreamer environment variables - if not env.has("BOTTLES_USE_SYSTEM_GSTREAMER") and not return_steam_env: + if not env.has("BOTTLES_USE_SYSTEM_GSTREAMER"): gst_env_path = [] for lib in gst_libs: if os.path.exists(os.path.join(runner_path, lib)): @@ -295,7 +246,7 @@ def get_env( env.add("GST_PLUGIN_SYSTEM_PATH", ":".join(gst_env_path), override=True) # DXVK environment variables - if params.dxvk and not return_steam_env: + if params.dxvk: env.add("WINE_LARGE_ADDRESS_AWARE", "1") env.add( "DXVK_STATE_CACHE_PATH", os.path.join(bottle, "cache", "dxvk_state") @@ -314,13 +265,13 @@ def get_env( ) # VKD3D environment variables - if params.vkd3d and not return_steam_env: + if params.vkd3d: env.add( "VKD3D_SHADER_CACHE_PATH", os.path.join(bottle, "cache", "vkd3d_shader") ) # LatencyFleX environment variables - if params.latencyflex and not return_steam_env: + if params.latencyflex: _lf_path = ManagerUtils.get_latencyflex_path(config.LatencyFleX) _lf_layer_path = os.path.join( _lf_path, "layer/usr/share/vulkan/implicit_layer.d" @@ -358,7 +309,7 @@ def get_env( env.add("OBS_USE_EGL", "1") # DXVK-Nvapi environment variables - if params.dxvk_nvapi and not return_steam_env: + if params.dxvk_nvapi: # NOTE: users reported that DXVK_ENABLE_NVAPI and DXVK_NVAPIHACK must be set to make # DLSS works. I don't have a GPU compatible with this tech, so I'll trust them env.add("DXVK_NVAPIHACK", "0") @@ -373,11 +324,10 @@ def get_env( env.add("WINEFSYNC", "1") # Wine debug level - if not return_steam_env: - debug_level = "fixme-all" - if params.fixme_logs: - debug_level = "+fixme-all" - env.add("WINEDEBUG", debug_level) + debug_level = "fixme-all" + if params.fixme_logs: + debug_level = "+fixme-all" + env.add("WINEDEBUG", debug_level) # Aco compiler # if params["aco_compiler"]: @@ -395,39 +345,38 @@ def get_env( env.add("PULSE_LATENCY_MSEC", "60") # Discrete GPU - if not return_steam_env: - if params.discrete_gpu: - discrete = gpu["prime"]["discrete"] - if discrete is not None: - gpu_envs = discrete["envs"] - for p in gpu_envs: - env.add(p, gpu_envs[p]) - env.concat("VK_ICD_FILENAMES", discrete["icd"]) - - # VK_ICD - if not env.has("VK_ICD_FILENAMES"): - if gpu["prime"]["integrated"] is not None: - """ - System support PRIME but user disabled the discrete GPU - setting (previus check skipped), so using the integrated one. - """ - env.concat("VK_ICD_FILENAMES", gpu["prime"]["integrated"]["icd"]) + if params.discrete_gpu: + discrete = gpu["prime"]["discrete"] + if discrete is not None: + gpu_envs = discrete["envs"] + for p in gpu_envs: + env.add(p, gpu_envs[p]) + env.concat("VK_ICD_FILENAMES", discrete["icd"]) + + # VK_ICD + if not env.has("VK_ICD_FILENAMES"): + if gpu["prime"]["integrated"] is not None: + """ + System support PRIME but user disabled the discrete GPU + setting (previus check skipped), so using the integrated one. + """ + env.concat("VK_ICD_FILENAMES", gpu["prime"]["integrated"]["icd"]) + else: + """ + System doesn't support PRIME, so using the first result + from the gpu vendors list. + """ + if "vendors" in gpu and len(gpu["vendors"]) > 0: + _first = list(gpu["vendors"].keys())[0] + env.concat("VK_ICD_FILENAMES", gpu["vendors"][_first]["icd"]) else: - """ - System doesn't support PRIME, so using the first result - from the gpu vendors list. - """ - if "vendors" in gpu and len(gpu["vendors"]) > 0: - _first = list(gpu["vendors"].keys())[0] - env.concat("VK_ICD_FILENAMES", gpu["vendors"][_first]["icd"]) - else: - logging.warning( - "No GPU vendor found, keep going without setting VK_ICD_FILENAMES…" - ) - - # Add ld to LD_LIBRARY_PATH - if ld: - env.concat("LD_LIBRARY_PATH", ld) + logging.warning( + "No GPU vendor found, keep going without setting VK_ICD_FILENAMES…" + ) + + # Add ld to LD_LIBRARY_PATH + if ld: + env.concat("LD_LIBRARY_PATH", ld) # Vblank # env.add("__GL_SYNC_TO_VBLANK", "0") @@ -438,11 +387,10 @@ def get_env( if env.is_empty("WINEDLLOVERRIDES"): env.remove("WINEDLLOVERRIDES") - if not return_steam_env: - # Wine prefix - env.add("WINEPREFIX", bottle, override=True) - # Wine arch - env.add("WINEARCH", arch) + # Wine prefix + env.add("WINEPREFIX", bottle, override=True) + # Wine arch + env.add("WINEARCH", arch) return env.get()["envs"] @@ -452,22 +400,10 @@ def _get_runner_info(self) -> tuple[str, str]: arch = config.Arch runner_runtime = "" - if config.Environment == "Steam": - runner = config.RunnerPath - if runner in [None, ""]: return "", "" - if SteamUtils.is_proton(runner): - """ - If the runner is Proton, set the path to /dist or /files - based on check if files exists. - Additionally, check for its corresponding runtime. - """ - runner_runtime = SteamUtils.get_associated_runtime(runner) - runner = os.path.join(SteamUtils.get_dist_directory(runner), "bin/wine") - - elif runner.startswith("sys-"): + if runner.startswith("sys-"): """ If the runner type is system, set the runner binary path to the system command. Else set it to the full path. @@ -490,8 +426,6 @@ def get_cmd( pre_script: str | None = None, post_script: str | None = None, midi_soundfont: str | None = None, - return_steam_cmd: bool = False, - return_clean_cmd: bool = False, environment: dict | None = None, ) -> str: config = self.config @@ -501,24 +435,14 @@ def get_cmd( if environment is None: environment = {} - if return_clean_cmd: - return_steam_cmd = True - - if not return_steam_cmd and not return_clean_cmd: - command = f"{runner} {command}" + command = f"{runner} {command}" if not self.minimal: if gamemode_available and params.gamemode: - if not return_steam_cmd: - command = f"{gamemode_available} {command}" - else: - command = f"gamemode {command}" + command = f"{gamemode_available} {command}" if mangohud_available and params.mangohud and not self.gamescope_activated: - if not return_steam_cmd: - command = f"{mangohud_available} {command}" - else: - command = f"mangohud {command}" + command = f"{mangohud_available} {command}" if gamescope_available and self.gamescope_activated: gamescope_run = tempfile.NamedTemporaryFile(mode="w", suffix=".sh").name @@ -532,9 +456,7 @@ def get_cmd( f.write("".join(file)) # Update command - command = ( - f"{self._get_gamescope_cmd(return_steam_cmd)} -- {gamescope_run}" - ) + command = f"{self._get_gamescope_cmd()} -- {gamescope_run}" logging.info(f"Running Gamescope command: '{command}'") logging.info(f"{gamescope_run} contains:") with open(gamescope_run) as f: @@ -547,57 +469,6 @@ def get_cmd( if obs_vkc_available and params.obsvkc: command = f"{obs_vkc_available} {command}" - if params.use_steam_runtime: - _rs = RuntimeManager.get_runtimes("steam") - _picked = {} - - if _rs: - if "sniper" in _rs.keys() and "sniper" in self.runner_runtime: - """ - Sniper is the default runtime used by Proton version >= 8.0 - """ - _picked = _rs["sniper"] - elif "soldier" in _rs.keys() and "soldier" in self.runner_runtime: - """ - Sniper is the default runtime used by Proton version >= 5.13 and < 8.0 - """ - _picked = _rs["soldier"] - elif "scout" in _rs.keys(): - """ - For Wine runners, we cannot make assumption about which runtime would suits - them the best, as it would depend on their build environment. - Sniper/Soldier are not backward-compatible, defaulting to Scout should maximize compatibility. - """ - _picked = _rs["scout"] - else: - logging.warning("Steam runtime was requested but not found") - - if _picked: - logging.info(f"Using Steam runtime {_picked['name']}") - command = f"{_picked['entry_point']} {command}" - else: - logging.warning( - "Steam runtime was requested and found but there are no valid combinations" - ) - - if self.arguments: - prefix, suffix, extracted_env = SteamUtils.handle_launch_options( - self.arguments - ) - if prefix: - command = f"{prefix} {command}" - if suffix: - command = f"{command} {suffix}" - if extracted_env: - if extracted_env.get("WINEDLLOVERRIDES") and environment.get( - "WINEDLLOVERRIDES" - ): - environment["WINEDLLOVERRIDES"] += ";" + extracted_env.get( - "WINEDLLOVERRIDES" - ) - del extracted_env["WINEDLLOVERRIDES"] - environment.update(extracted_env) - if post_script not in (None, ""): command = f"{command} ; sh '{post_script}'" @@ -606,15 +477,13 @@ def get_cmd( return command - def _get_gamescope_cmd(self, return_steam_cmd: bool = False) -> str: + def _get_gamescope_cmd(self) -> str: config = self.config params = config.Parameters gamescope_cmd = [] if gamescope_available and self.gamescope_activated: gamescope_cmd = [gamescope_available] - if return_steam_cmd: - gamescope_cmd = ["gamescope"] if params.gamescope_fullscreen: gamescope_cmd.append("-f") if params.gamescope_borderless: @@ -677,16 +546,6 @@ def _vmtouch_free(self): cwd=self.cwd, ) - def _get_sandbox_manager(self) -> SandboxManager: - return SandboxManager( - envs=self.env, - chdir=self.cwd, - share_paths_rw=[ManagerUtils.get_bottle_path(self.config)], - share_paths_ro=[Paths.runners, Paths.temp], - share_net=self.config.Sandbox.share_net, - share_sound=self.config.Sandbox.share_sound, - ) - def run(self) -> Result[str | None]: """ Run command with pre-configured parameters @@ -702,42 +561,28 @@ def run(self) -> Result[str | None]: if vmtouch_available and self.config.Parameters.vmtouch and not self.terminal: self._vmtouch_preload() - sandbox = ( - self._get_sandbox_manager() if self.config.Parameters.sandbox else None - ) - # run command in external terminal if terminal is True if self.terminal: - if sandbox: - return Result( - status=TerminalUtils().execute( - sandbox.get_cmd(self.command), self.env, self.colors, self.cwd - ) - ) - else: - return Result( - status=TerminalUtils().execute( - self.command, self.env, self.colors, self.cwd - ) + return Result( + status=TerminalUtils().execute( + self.command, self.env, self.colors, self.cwd ) + ) # prepare proc if we are going to execute command internally # proc should always be `Popen[bytes]` to make sure # stdout_data's type is `bytes` proc: subprocess.Popen[bytes] - if sandbox: - proc = sandbox.run(self.command) - else: - try: - proc = subprocess.Popen( - self.command, - stdout=subprocess.PIPE, - shell=True, - env=self.env, - cwd=self.cwd, - ) - except FileNotFoundError: - return Result(False, message="File not found") + try: + proc = subprocess.Popen( + self.command, + stdout=subprocess.PIPE, + shell=True, + env=self.env, + cwd=self.cwd, + ) + except FileNotFoundError: + return Result(False, message="File not found") stdout_data, _ = proc.communicate() diff --git a/bottles/backend/wine/winedbg.py b/bottles/backend/wine/winedbg.py index 738d1137c33..4d99e5131bf 100644 --- a/bottles/backend/wine/winedbg.py +++ b/bottles/backend/wine/winedbg.py @@ -2,14 +2,11 @@ import time import subprocess -from bottles.backend.logger import Logger from bottles.backend.wine.wineprogram import WineProgram from bottles.backend.wine.wineserver import WineServer from bottles.backend.wine.wineboot import WineBoot from bottles.backend.utils.decorators import cache -logging = Logger() - class WineDbg(WineProgram): program = "Wine debug tool" diff --git a/bottles/backend/wine/winefile.py b/bottles/backend/wine/winefile.py index 28152f8e1ac..ddd8030ef2c 100644 --- a/bottles/backend/wine/winefile.py +++ b/bottles/backend/wine/winefile.py @@ -1,8 +1,5 @@ -from bottles.backend.logger import Logger from bottles.backend.wine.wineprogram import WineProgram -logging = Logger() - class WineFile(WineProgram): program = "Wine File Explorer" diff --git a/bottles/backend/wine/winepath.py b/bottles/backend/wine/winepath.py index aaff7ba266b..36b81e1b153 100644 --- a/bottles/backend/wine/winepath.py +++ b/bottles/backend/wine/winepath.py @@ -1,12 +1,9 @@ import re from functools import lru_cache -from bottles.backend.logger import Logger from bottles.backend.wine.wineprogram import WineProgram from bottles.backend.utils.manager import ManagerUtils -logging = Logger() - class WinePath(WineProgram): program = "Wine path converter" diff --git a/bottles/backend/wine/wineprogram.py b/bottles/backend/wine/wineprogram.py index 32dd650343b..570f0dd647f 100644 --- a/bottles/backend/wine/wineprogram.py +++ b/bottles/backend/wine/wineprogram.py @@ -1,12 +1,10 @@ import os -from bottles.backend.logger import Logger +import logging from bottles.backend.globals import Paths from bottles.backend.models.config import BottleConfig from bottles.backend.wine.winecommand import WineCommand -logging = Logger() - class WineProgram: program: str = "unknown" diff --git a/bottles/backend/wine/wineserver.py b/bottles/backend/wine/wineserver.py index 60709b1279c..0618db98e6c 100644 --- a/bottles/backend/wine/wineserver.py +++ b/bottles/backend/wine/wineserver.py @@ -2,14 +2,10 @@ import subprocess import time -from bottles.backend.logger import Logger from bottles.backend.utils.manager import ManagerUtils from bottles.backend.utils.proc import ProcUtils -from bottles.backend.utils.steam import SteamUtils from bottles.backend.wine.wineprogram import WineProgram -logging = Logger() - class WineServer(WineProgram): program = "Wine Server" @@ -31,13 +27,6 @@ def is_alive(self): bottle = ManagerUtils.get_bottle_path(config) runner = ManagerUtils.get_runner_path(config.Runner) - if config.Environment == "Steam": - bottle = config.Path - runner = config.RunnerPath - - if SteamUtils.is_proton(runner): - runner = SteamUtils.get_dist_directory(runner) - env = os.environ.copy() env["WINEPREFIX"] = bottle env["PATH"] = f"{runner}/bin:{env['PATH']}" @@ -60,13 +49,6 @@ def wait(self): bottle = ManagerUtils.get_bottle_path(config) runner = ManagerUtils.get_runner_path(config.Runner) - if config.Environment == "Steam": - bottle = config.Path - runner = config.RunnerPath - - if SteamUtils.is_proton(runner): - runner = SteamUtils.get_dist_directory(runner) - env = os.environ.copy() env["WINEPREFIX"] = bottle env["PATH"] = f"{runner}/bin:{env['PATH']}" diff --git a/bottles/backend/wine/winhelp.py b/bottles/backend/wine/winhelp.py index b09e940fa24..d84ec10583d 100644 --- a/bottles/backend/wine/winhelp.py +++ b/bottles/backend/wine/winhelp.py @@ -1,8 +1,5 @@ -from bottles.backend.logger import Logger from bottles.backend.wine.wineprogram import WineProgram -logging = Logger() - class WinHelp(WineProgram): program = "Microsoft help file viewer" diff --git a/bottles/backend/wine/xcopy.py b/bottles/backend/wine/xcopy.py index ba8e8c97daa..b7cfe446563 100644 --- a/bottles/backend/wine/xcopy.py +++ b/bottles/backend/wine/xcopy.py @@ -1,10 +1,7 @@ from datetime import datetime -from bottles.backend.logger import Logger from bottles.backend.wine.wineprogram import WineProgram -logging = Logger() - class Xcopy(WineProgram): program = "Wine Xcopy implementation" diff --git a/bottles/frontend/bottle-details-page.blp b/bottles/frontend/bottle-details-page.blp index 670faec8798..73a263201e3 100644 --- a/bottles/frontend/bottle-details-page.blp +++ b/bottles/frontend/bottle-details-page.blp @@ -24,20 +24,6 @@ Popover pop_context { text: _("Browse Files…"); } - $GtkModelButton btn_duplicate { - text: _("Duplicate Bottle…"); - } - - $GtkModelButton btn_backup_full { - tooltip-text: _("This is the complete archive of your bottle, including personal files."); - text: _("Full Backup…"); - } - - $GtkModelButton btn_backup_config { - tooltip-text: _("This is just the bottle configuration, it\'s perfect if you want to create a new one but without personal files."); - text: _("Export Configuration…"); - } - Separator {} $GtkModelButton btn_toggle_removed { @@ -373,16 +359,6 @@ template $BottleDetailsPage: Adw.PreferencesPage { } } - Adw.ActionRow row_snapshots { - activatable: true; - title: _("Snapshots"); - subtitle: _("Create and manage bottle states."); - - Image { - icon-name: "go-next-symbolic"; - } - } - Adw.ActionRow row_taskmanager { activatable: true; title: _("Task Manager"); diff --git a/bottles/frontend/bottle_details_page.py b/bottles/frontend/bottle_details_page.py index 57e44ee046a..2a6fdeec88f 100644 --- a/bottles/frontend/bottle_details_page.py +++ b/bottles/frontend/bottle_details_page.py @@ -21,7 +21,6 @@ from gi.repository import Gtk, Gio, Adw, Gdk, GLib, Xdp -from bottles.backend.managers.backup import BackupManager from bottles.backend.models.config import BottleConfig from bottles.backend.utils.manager import ManagerUtils from bottles.backend.utils.terminal import TerminalUtils @@ -41,8 +40,6 @@ from bottles.frontend.filters import add_executable_filters, add_all_filters from bottles.frontend.gtk import GtkUtils from bottles.frontend.program_row import ProgramRow -from bottles.frontend.duplicate_dialog import DuplicateDialog -from bottles.frontend.upgrade_versioning_dialog import UpgradeVersioningDialog @Gtk.Template(resource_path="/com/usebottles/bottles/bottle-details-page.ui") @@ -64,7 +61,6 @@ class BottleDetailsPage(Adw.PreferencesPage): row_winecfg = Gtk.Template.Child() row_preferences = Gtk.Template.Child() row_dependencies = Gtk.Template.Child() - row_snapshots = Gtk.Template.Child() row_taskmanager = Gtk.Template.Child() row_debug = Gtk.Template.Child() row_explorer = Gtk.Template.Child() @@ -80,9 +76,6 @@ class BottleDetailsPage(Adw.PreferencesPage): btn_nv_forcestop = Gtk.Template.Child() btn_update = Gtk.Template.Child() btn_toggle_removed = Gtk.Template.Child() - btn_backup_config = Gtk.Template.Child() - btn_backup_full = Gtk.Template.Child() - btn_duplicate = Gtk.Template.Child() btn_delete = Gtk.Template.Child() btn_flatpak_doc = Gtk.Template.Child() label_name = Gtk.Template.Child() @@ -123,7 +116,6 @@ def __init__(self, details, config, **kwargs): self.popover_exec_settings.connect("closed", self.__run_executable_with_args) self.row_preferences.connect("activated", self.__change_page, "preferences") self.row_dependencies.connect("activated", self.__change_page, "dependencies") - self.row_snapshots.connect("activated", self.__change_page, "versioning") self.row_taskmanager.connect("activated", self.__change_page, "taskmanager") self.row_winecfg.connect("activated", self.run_winecfg) self.row_debug.connect("activated", self.run_debug) @@ -141,9 +133,6 @@ def __init__(self, details, config, **kwargs): self.btn_nv_forcestop.connect("clicked", self.wineboot, -2) self.btn_update.connect("clicked", self.__scan_programs) self.btn_toggle_removed.connect("clicked", self.__toggle_removed) - self.btn_backup_config.connect("clicked", self.__backup, "config") - self.btn_backup_full.connect("clicked", self.__backup, "full") - self.btn_duplicate.connect("clicked", self.__duplicate) self.btn_flatpak_doc.connect( "clicked", open_doc_url, "flatpak/black-screen-or-silent-crash" ) @@ -220,12 +209,6 @@ def set_config(self, config: BottleConfig): self.grid_versioning.set_visible(self.config.Versioning) self.label_state.set_text(str(self.config.State)) - self.__set_steam_rules() - - # check for old versioning system enabled - if config.Versioning: - self.__upgrade_versioning() - if ( config.Runner not in self.manager.runners_available and not self.config.Environment == "Steam" @@ -442,74 +425,6 @@ def callback(a, b): else: show_chooser() - def __backup(self, widget, backup_type): - """ - This function pop up the file chooser where the user - can select the path where to export the bottle backup. - Use the backup_type param to export config or full. - """ - if backup_type == "config": - title = _("Select the location where to save the backup config") - hint = f"backup_{self.config.Path}.yml" - accept_label = _("Export") - else: - title = _("Select the location where to save the backup archive") - hint = f"backup_{self.config.Path}.tar.gz" - accept_label = _("Backup") - - @GtkUtils.run_in_main_loop - def finish(result, error=False): - if result.ok: - self.window.show_toast( - _('Backup created for "{0}"').format(self.config.Name) - ) - else: - self.window.show_toast( - _('Backup failed for "{0}"').format(self.config.Name) - ) - - def set_path(_dialog, response): - if response != Gtk.ResponseType.ACCEPT: - return - - path = dialog.get_file().get_path() - - RunAsync( - task_func=BackupManager.export_backup, - callback=finish, - config=self.config, - scope=backup_type, - path=path, - ) - - dialog = Gtk.FileChooserNative.new( - title=title, - action=Gtk.FileChooserAction.SAVE, - parent=self.window, - accept_label=accept_label, - ) - - dialog.set_modal(True) - dialog.connect("response", set_path) - dialog.set_current_name(hint) - dialog.show() - - def __duplicate(self, widget): - """ - This function pop up the duplicate dialog, so the user can - choose the new bottle name and perform duplication. - """ - new_window = DuplicateDialog(self) - new_window.present() - - def __upgrade_versioning(self): - """ - This function pop up the upgrade versioning dialog, so the user can - upgrade the versioning system from old Bottles built-in to FVS. - """ - new_window = UpgradeVersioningDialog(self) - new_window.present() - def __confirm_delete(self, widget): """ This function pop up to delete confirm dialog. If user confirm @@ -637,10 +552,3 @@ def handle_response(_widget, response_id): dialog.set_response_appearance("ok", Adw.ResponseAppearance.DESTRUCTIVE) dialog.connect("response", handle_response) dialog.present() - - def __set_steam_rules(self): - status = False if self.config.Environment == "Steam" else True - - for w in [self.btn_delete, self.btn_backup_full, self.btn_duplicate]: - w.set_visible(status) - w.set_sensitive(status) diff --git a/bottles/frontend/bottle_details_view.py b/bottles/frontend/bottle_details_view.py index afe50ab1f82..8177b8d828c 100644 --- a/bottles/frontend/bottle_details_view.py +++ b/bottles/frontend/bottle_details_view.py @@ -20,16 +20,13 @@ from gi.repository import Gtk, Adw, GLib -from bottles.backend.managers.queue import QueueManager from bottles.backend.models.config import BottleConfig from bottles.backend.utils.threading import RunAsync from bottles.frontend.gtk import GtkUtils from bottles.frontend.bottle_details_page import BottleDetailsPage -from bottles.frontend.details_installers_view import DetailsInstallersView from bottles.frontend.details_dependencies_view import DetailsDependenciesView from bottles.frontend.details_preferences_page import DetailsPreferencesPage -from bottles.frontend.details_versioning_page import DetailsVersioningPage from bottles.frontend.details_task_manager_view import DetailsTaskManagerView @@ -70,15 +67,11 @@ def __init__(self, window, config: BottleConfig | None = None, **kwargs): self.window = window self.manager = window.manager - self.versioning_manager = window.manager.versioning_manager self.config = config - self.queue = QueueManager(add_fn=self.lock_back, end_fn=self.unlock_back) self.view_bottle = BottleDetailsPage(self, config) - self.view_installers = DetailsInstallersView(self, config) self.view_dependencies = DetailsDependenciesView(self, config) self.view_preferences = DetailsPreferencesPage(self, config) - self.view_versioning = DetailsVersioningPage(self, config) self.view_taskmanager = DetailsTaskManagerView(self, config) self.btn_back.connect("clicked", self.go_back) @@ -120,10 +113,6 @@ def __on_page_change(self, *_args): if page == "dependencies": self.set_actions(self.view_dependencies.actions) self.view_dependencies.update(config=self.config) - elif page == "versioning": - self.set_actions(self.view_versioning.actions) - elif page == "installers": - self.set_actions(self.view_installers.actions) elif page == "taskmanager": self.set_actions(self.view_taskmanager.actions) else: @@ -167,8 +156,6 @@ def ui_update(): self.stack_bottle.add_named(self.view_preferences, "preferences") self.stack_bottle.add_named(self.view_dependencies, "dependencies") - self.stack_bottle.add_named(self.view_versioning, "versioning") - self.stack_bottle.add_named(self.view_installers, "installers") self.stack_bottle.add_named(self.view_taskmanager, "taskmanager") if self.view_bottle.actions.get_parent() is None: @@ -199,8 +186,6 @@ def set_config(self, config: BottleConfig, rebuild_pages=True): self.view_bottle.set_config(config=config) self.view_preferences.set_config(config=config) self.view_taskmanager.set_config(config=config) - self.view_installers.update(config=config) - self.view_versioning.update(config=config) if rebuild_pages: self.build_pages() diff --git a/bottles/frontend/bottle_picker_dialog.py b/bottles/frontend/bottle_picker_dialog.py index 9332fc9327a..561040b3c8d 100644 --- a/bottles/frontend/bottle_picker_dialog.py +++ b/bottles/frontend/bottle_picker_dialog.py @@ -50,7 +50,7 @@ class BottlePickerDialog(Adw.ApplicationWindow): def __init__(self, arg_exe, **kwargs): super().__init__(**kwargs) self.arg_exe = arg_exe - mng = Manager(g_settings=self.settings, is_cli=True) + mng = Manager(g_settings=self.settings) mng.check_bottles() bottles = mng.local_bottles diff --git a/bottles/frontend/bottles-list-view.blp b/bottles/frontend/bottles-list-view.blp index 299ca52141d..baead640520 100644 --- a/bottles/frontend/bottles-list-view.blp +++ b/bottles/frontend/bottles-list-view.blp @@ -24,18 +24,6 @@ template $BottlesListView: Adw.Bin { ] } } - - Adw.PreferencesGroup group_steam { - title: _("Steam Proton"); - - ListBox list_steam { - selection-mode: none; - - styles [ - "boxed-list", - ] - } - } } Adw.StatusPage bottle_status { diff --git a/bottles/frontend/bottles.gresource.xml b/bottles/frontend/bottles.gresource.xml index de4e8a76360..67ee91433ae 100644 --- a/bottles/frontend/bottles.gresource.xml +++ b/bottles/frontend/bottles.gresource.xml @@ -7,37 +7,26 @@ window.ui new-bottle-dialog.ui bottles-list-view.ui - loading-view.ui bottle-row.ui check-row.ui task-row.ui dependency-entry-row.ui program-row.ui - importer-row.ui - state-row.ui - installer-row.ui dll-override-entry.ui env-var-entry.ui component-entry-row.ui drive-entry.ui - library-entry.ui local-resource-row.ui - exclusion-pattern-row.ui bottle-details-view.ui bottle-details-page.ui details-dependencies-view.ui - details-installers-view.ui details-preferences-page.ui - details-versioning-page.ui details-task-manager-view.ui preferences.ui - importer-view.ui - library-view.ui launch-options-dialog.ui dll-overrides-dialog.ui environment-variables-dialog.ui crash-report-dialog.ui - duplicate-dialog.ui rename-program-dialog.ui gamescope-dialog.ui vkbasalt-dialog.ui @@ -45,14 +34,10 @@ mangohud-dialog.ui display-dialog.ui drives-dialog.ui - journal-dialog.ui sandbox-dialog.ui - installer-dialog.ui bottle-picker-dialog.ui proton-alert-dialog.ui dependencies-check-dialog.ui - exclusion-patterns-dialog.ui - upgrade-versioning-dialog.ui vmtouch-dialog.ui onboard-dialog.ui diff --git a/bottles/frontend/bottles_list_view.py b/bottles/frontend/bottles_list_view.py index e2cffa18c25..27a8025b52b 100644 --- a/bottles/frontend/bottles_list_view.py +++ b/bottles/frontend/bottles_list_view.py @@ -25,6 +25,7 @@ from bottles.backend.state import Signals, SignalManager from bottles.backend.utils.threading import RunAsync from bottles.backend.wine.executor import WineExecutor +from bottles.frontend.gtk import GtkUtils from bottles.frontend.filters import add_executable_filters, add_all_filters from bottles.frontend.params import APP_ID @@ -41,12 +42,11 @@ class BottleRow(Adw.ActionRow): # endregion - def __init__(self, window, config: BottleConfig, **kwargs): + def __init__(self, config: BottleConfig, **kwargs): super().__init__(**kwargs) # common variables and references - self.window = window - self.manager = window.manager + self.window = GtkUtils.get_parent_window() self.config = config # Format update date @@ -130,9 +130,7 @@ class BottlesListView(Adw.Bin): # region Widgets list_bottles = Gtk.Template.Child() - list_steam = Gtk.Template.Child() group_bottles = Gtk.Template.Child() - group_steam = Gtk.Template.Child() pref_page = Gtk.Template.Child() bottle_status = Gtk.Template.Child() btn_create = Gtk.Template.Child() @@ -142,22 +140,16 @@ class BottlesListView(Adw.Bin): # endregion - def __init__(self, window, arg_bottle=None, **kwargs): + def __init__(self, **kwargs): super().__init__(**kwargs) # common variables and references - self.window = window - self.arg_bottle = arg_bottle + self.window = GtkUtils.get_parent_window() # connect signals self.btn_create.connect("clicked", self.window.show_add_view) self.entry_search.connect("changed", self.__search_bottles) - # backend signals - SignalManager.connect( - Signals.ManagerLocalBottlesLoaded, self.update_bottles_list - ) - self.bottle_status.set_icon_name(APP_ID) self.update_bottles_list() @@ -169,7 +161,6 @@ def __search_bottles(self, widget, event=None, data=None): """ terms = widget.get_text() self.list_bottles.set_filter_func(self.__filter_bottles, terms) - self.list_steam.set_filter_func(self.__filter_bottles, terms) @staticmethod def __filter_bottles(row, terms=None): @@ -177,38 +168,21 @@ def __filter_bottles(row, terms=None): return terms.lower() in text def update_bottles_list(self, *args) -> None: - self.__bottles = {} + application = self.window.get_application() while self.list_bottles.get_first_child(): self.list_bottles.remove(self.list_bottles.get_first_child()) - while self.list_steam.get_first_child(): - self.list_steam.remove(self.list_steam.get_first_child()) - - local_bottles = self.window.manager.local_bottles - is_empty_local_bottles = len(local_bottles) == 0 + is_empty_local_bottles = len(application.local_bottles) == 0 self.pref_page.set_visible(not is_empty_local_bottles) self.bottle_status.set_visible(is_empty_local_bottles) - for name, config in local_bottles.items(): - _entry = BottleRow(self.window, config) - self.__bottles[config.Path] = _entry + for name, config in application.local_bottles.items(): + _entry = BottleRow(config) - if config.Environment != "Steam": - self.list_bottles.append(_entry) - else: - self.list_steam.append(_entry) - - if self.list_steam.get_first_child() is None: - self.group_steam.set_visible(False) - self.group_bottles.set_title("") - else: - self.group_steam.set_visible(True) - self.group_bottles.set_title(_("Your Bottles")) + self.list_bottles.append(_entry) def show_page(self, page: str) -> None: - if config := self.window.manager.local_bottles.get(page): + application = self.window.get_application() + if config := application.local_bottles.get(page): self.window.show_details_view(config=config) - - def disable_bottle(self, config): - self.__bottles[config.Path].disable() diff --git a/bottles/frontend/cli.py b/bottles/frontend/cli.py deleted file mode 100644 index 4008ec9e3a6..00000000000 --- a/bottles/frontend/cli.py +++ /dev/null @@ -1,759 +0,0 @@ -#!@PYTHON@ - -# cli.in -# -# Copyright 2020 brombinmirko -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - -import argparse -import os -import signal -import sys -import uuid -import warnings - -import gi - -warnings.filterwarnings("ignore") # suppress GTK warnings -gi.require_version("Gtk", "4.0") - -APP_VERSION = "@APP_VERSION@" -pkgdatadir = "@pkgdatadir@" -# noinspection DuplicatedCode -gresource_path = f"{pkgdatadir}/bottles.gresource" -sys.path.insert(1, pkgdatadir) - -signal.signal(signal.SIGINT, signal.SIG_DFL) - -# ruff: noqa: E402 -from gi.repository import Gio - -from bottles.frontend.params import APP_ID -from bottles.backend.globals import Paths -from bottles.backend.health import HealthChecker -from bottles.backend.managers.manager import Manager -from bottles.backend.models.config import BottleConfig -from bottles.backend.wine.cmd import CMD -from bottles.backend.wine.control import Control -from bottles.backend.wine.executor import WineExecutor -from bottles.backend.wine.winecommand import WineCommand -from bottles.backend.wine.reg import Reg -from bottles.backend.wine.winepath import WinePath -from bottles.backend.wine.regedit import Regedit -from bottles.backend.wine.taskmgr import Taskmgr -from bottles.backend.wine.uninstaller import Uninstaller -from bottles.backend.wine.winecfg import WineCfg -from bottles.backend.wine.explorer import Explorer -from bottles.backend.wine.regkeys import RegKeys -from bottles.backend.runner import Runner -from bottles.backend.utils import json -from bottles.backend.utils.manager import ManagerUtils - - -# noinspection DuplicatedCode -class CLI: - settings = Gio.Settings.new(APP_ID) - - def __init__(self): - # self.__clear() - - self.parser = argparse.ArgumentParser( - description="Bottles is a tool to manage your bottles" - ) - self.parser.add_argument( - "-v", "--version", action="version", version=f"Bottles {APP_VERSION}" - ) - self.parser.add_argument( - "-j", "--json", action="store_true", help="Outputs in JSON format" - ) - - subparsers = self.parser.add_subparsers(dest="command", help="sub-command help") - - info_parser = subparsers.add_parser( - "info", help="Show information about Bottles" - ) - info_parser.add_argument( - "type", choices=["bottles-path", "health-check"], help="Type of information" - ) - - list_parser = subparsers.add_parser("list", help="List entities") - list_parser.add_argument( - "type", choices=["bottles", "components"], help="Type of entity" - ) - list_parser.add_argument( - "-f", - "--filter", - help="Filter bottles and components (e.g. '-f 'environment:gaming')", - ) - - programs_parser = subparsers.add_parser("programs", help="List programs") - programs_parser.add_argument( - "-b", "--bottle", help="Bottle name", required=True - ) - - add_parser = subparsers.add_parser("add", help="Add program") - add_parser.add_argument("-b", "--bottle", help="Bottle name", required=True) - add_parser.add_argument("-n", "--name", help="Program name", required=True) - add_parser.add_argument("-p", "--path", help="Program path", required=True) - add_parser.add_argument("-l", "--launch-options", help="Program launch options") - add_parser.add_argument( - "--no-dxvk", action="store_true", help="Disable DXVK for the program" - ) - add_parser.add_argument( - "--no-vkd3d", action="store_true", help="Disable VKD3D for the program" - ) - add_parser.add_argument( - "--no-dxvk-nvapi", - action="store_true", - help="Disable DXVK Nvapi for the program", - ) - - tools_parser = subparsers.add_parser("tools", help="Launch Wine tools") - tools_parser.add_argument( - "tool", - choices=[ - "cmd", - "winecfg", - "uninstaller", - "regedit", - "taskmgr", - "control", - "explorer", - ], - help="Tool to launch", - ) - tools_parser.add_argument("-b", "--bottle", help="Bottle name", required=True) - - reg_parser = subparsers.add_parser("reg", help="Manage registry") - reg_parser.add_argument( - "action", choices=["add", "edit", "del"], help="Action to perform" - ) - reg_parser.add_argument("-b", "--bottle", help="Bottle name", required=True) - reg_parser.add_argument("-k", "--key", help="Registry key", required=True) - reg_parser.add_argument("-v", "--value", help="Registry value", required=True) - reg_parser.add_argument("-d", "--data", help="Data to be set") - reg_parser.add_argument( - "-t", - "--key-type", - help="Data type", - choices=["REG_DWORD", "REG_SZ", "REG_BINARY", "REG_MULTI_SZ"], - ) - - edit_parser = subparsers.add_parser("edit", help="Edit a bottle configuration") - edit_parser.add_argument("-b", "--bottle", help="Bottle name", required=True) - edit_parser.add_argument( - "--params", help="Set parameters (e.g. '-p dxvk:true')" - ) - edit_parser.add_argument( - "--env-var", - help="Add new environment variable (e.g. '-env-var WINEDEBUG=-all')", - ) - edit_parser.add_argument( - "--win", help="Change Windows version (e.g. '--win win7')" - ) - edit_parser.add_argument( - "--runner", help="Change Runner (e.g. '--runner caffe-7.2')" - ) - edit_parser.add_argument( - "--dxvk", help="Change DXVK (e.g. '--dxvk dxvk-1.9.0')" - ) - edit_parser.add_argument( - "--vkd3d", help="Change VKD3D (e.g. '--vkd3d vkd3d-proton-2.6')" - ) - edit_parser.add_argument( - "--nvapi", help="Change DXVK-Nvapi (e.g. '--nvapi dxvk-nvapi-1.9.0')" - ) - edit_parser.add_argument( - "--latencyflex", - help="Change LatencyFleX (e.g. '--latencyflex latencyflex-v0.1.0')", - ) - - new_parser = subparsers.add_parser("new", help="Create a new bottle") - new_parser.add_argument("--bottle-name", help="Bottle name", required=True) - new_parser.add_argument( - "--environment", - help="Environment to apply (gaming|application|custom)", - required=True, - ) - new_parser.add_argument( - "--custom-environment", help="Path to a custom environment.yml file" - ) - new_parser.add_argument("--arch", help="Architecture (win32|win64)") - new_parser.add_argument("--runner", help="Name of the runner to be used") - new_parser.add_argument("--dxvk", help="Name of the dxvk to be used") - new_parser.add_argument("--vkd3d", help="Name of the vkd3d to be used") - new_parser.add_argument("--nvapi", help="Name of the dxvk-nvapi to be used") - new_parser.add_argument( - "--latencyflex", help="Name of the latencyflex to be used" - ) - - run_parser = subparsers.add_parser("run", help="Run a program") - run_parser.add_argument("-b", "--bottle", help="Bottle name", required=True) - run_parser.add_argument("-e", "--executable", help="Path to the executable") - run_parser.add_argument("-p", "--program", help="Program to run") - run_parser.add_argument( - "--args-replace", - action="store_false", - dest="keep_args", - help="Replace current program arguments, instead of append", - ) - run_parser.add_argument( - "args", - nargs="*", - action="extend", - help="Arguments to pass to the executable", - ) - - standalone_parser = subparsers.add_parser( - "standalone", - help="Generate a standalone script to launch commands " - "without passing trough Bottles", - ) - standalone_parser.add_argument( - "-b", "--bottle", help="Bottle name", required=True - ) - - shell_parser = subparsers.add_parser( - "shell", help="Launch commands in a Wine shell" - ) - shell_parser.add_argument("-b", "--bottle", help="Bottle name", required=True) - shell_parser.add_argument( - "-i", "--input", help="Command to execute", required=True - ) - - self.__process_args() - - @staticmethod - def __clear(): - os.system("clear") - - def __process_args(self): - self.args = self.parser.parse_args() - - # INFO parser - if self.args.command == "info": - self.show_info() - - # LIST parser - elif self.args.command == "list": - _filter = None if self.args.filter is None else self.args.filter - _type = self.args.type - - if _type == "bottles": - self.list_bottles(c_filter=_filter) - elif _type == "components": - self.list_components(c_filter=_filter) - - # PROGRAMS parser - elif self.args.command == "programs": - self.list_programs() - - # TOOLS parser - elif self.args.command == "tools": - self.launch_tool() - - # ADD parser - elif self.args.command == "add": - self.add_program() - - # REG parser - elif self.args.command == "reg": - self.manage_reg() - - # EDIT parser - elif self.args.command == "edit": - self.edit_bottle() - - # NEW parser - elif self.args.command == "new": - self.new_bottle() - - # RUN parser - elif self.args.command == "run": - self.run_program() - - # SHELL parser - elif self.args.command == "shell": - self.run_shell() - - # STANDALONE parser - elif self.args.command == "standalone": - self.generate_standalone() - - else: - self.parser.print_help() - - # region INFO - def show_info(self): - _type = self.args.type - if _type == "bottles-path": - res = Paths.bottles - sys.stdout.write(res) - exit(0) - elif _type == "health-check": - hc = HealthChecker() - if self.args.json: - sys.stdout.write(json.dumps(hc.get_results()) + "\n") - exit(0) - sys.stdout.write(hc.get_results(plain=True)) - - # endregion - - # region LIST - def list_bottles(self, c_filter=None): - mng = Manager(g_settings=self.settings, is_cli=True) - mng.check_bottles() - bottles = mng.local_bottles - - if c_filter and c_filter.startswith("environment:"): - environment = c_filter.split(":")[1].lower() - bottles = [ - name - for name, bottle in bottles.items() - if bottle.Environment.lower() == environment - ] - - if self.args.json: - sys.stdout.write(json.dumps(bottles)) - exit(0) - - if len(bottles) > 0: - sys.stdout.write(f"Found {len(bottles)} bottles:\n") - for b in bottles: - sys.stdout.write(f"- {b}\n") - - def list_components(self, c_filter=None): - mng = Manager(g_settings=self.settings, is_cli=True) - mng.check_runners(False) - mng.check_dxvk(False) - mng.check_vkd3d(False) - mng.check_nvapi(False) - mng.check_latencyflex(False) - - components = { - "runners": mng.runners_available, - "dxvk": mng.dxvk_available, - "vkd3d": mng.vkd3d_available, - "nvapi": mng.nvapi_available, - "latencyflex": mng.latencyflex_available, - } - - if c_filter and c_filter.startswith("category:"): - category = c_filter.split(":")[1].lower() - if category in components: - components = {category: components[category]} - - if self.args.json: - sys.stdout.write(json.dumps(components)) - exit(0) - - for c in components: - sys.stdout.write(f"Found {len(components[c])} {c}\n") - for i in components[c]: - sys.stdout.write(f"- {i}\n") - - # endregion - - # region PROGRAMS - def list_programs(self): - mng = Manager(g_settings=self.settings, is_cli=True) - mng.check_bottles() - _bottle = self.args.bottle - - if _bottle not in mng.local_bottles: - sys.stderr.write(f"Bottle {_bottle} not found\n") - exit(1) - - bottle = mng.local_bottles[_bottle] - programs = mng.get_programs(bottle) - programs = [p for p in programs if not p.get("removed", False)] - - if self.args.json: - sys.stdout.write(json.dumps(programs)) - exit(0) - - if len(programs) > 0: - sys.stdout.write(f"Found {len(programs)} programs:\n") - for p in programs: - sys.stdout.write(f"- {p['name']}\n") - - # endregion - - # region TOOLS - def launch_tool(self): - _bottle = self.args.bottle - _tool = self.args.tool - mng = Manager(g_settings=self.settings, is_cli=True) - mng.check_bottles() - - if _bottle not in mng.local_bottles: - sys.stderr.write(f"Bottle {_bottle} not found\n") - exit(1) - - bottle = mng.local_bottles[_bottle] - - if _tool == "cmd": - CMD(bottle).launch() - elif _tool == "winecfg": - WineCfg(bottle).launch() - elif _tool == "uninstaller": - Uninstaller(bottle).launch() - elif _tool == "regedit": - Regedit(bottle).launch() - elif _tool == "taskmgr": - Taskmgr(bottle).launch() - elif _tool == "control": - Control(bottle).launch() - elif _tool == "explorer": - Explorer(bottle).launch() - - # endregion - - # region ADD - def add_program(self): - _bottle = self.args.bottle - _name = self.args.name - _path = self.args.path - _launch_options = self.args.launch_options - _no_dxvk = self.args.no_dxvk - _no_vkd3d = self.args.no_vkd3d - _no_dxvk_nvapi = self.args.no_dxvk_nvapi - _executable = "" - _folder = "" - _uuid = str(uuid.uuid4()) - mng = Manager(g_settings=self.settings, is_cli=True) - mng.check_bottles() - - if _bottle not in mng.local_bottles: - sys.stderr.write(f"Bottle {_bottle} not found\n") - exit(1) - - bottle = mng.local_bottles[_bottle] - winepath = WinePath(bottle) - - if winepath.is_unix(_path): - if not os.path.exists(_path): - sys.stderr.write(f"Path doesn't exists or is unreachable: {_path}") - exit(1) - _executable = os.path.basename(_path) - _folder = os.path.dirname(_path) - elif winepath.is_windows(_path): - _executable = _path.split("\\")[-1] - _folder = ManagerUtils.get_exe_parent_dir(bottle, _path) - else: - sys.stderr.write(f"Unsupported path type: {_path}") - exit(1) - - _program = { - "arguments": _launch_options if _launch_options else "", - "executable": _executable, - "name": _name, - "folder": _folder, - "icon": "", - "id": _uuid, - "path": _path, - "dxvk": not _no_dxvk if _no_dxvk else bottle.Parameters.dxvk, - "vkd3d": not _no_vkd3d if _no_vkd3d else bottle.Parameters.vkd3d, - "dxvk_nvapi": ( - not _no_dxvk_nvapi if _no_dxvk_nvapi else bottle.Parameters.dxvk_nvapi - ), - } - mng.update_config(bottle, _uuid, _program, scope="External_Programs") - sys.stdout.write(f"'{_name}' added to '{bottle.Name}'!") - - # endregion - - # region REG - def manage_reg(self): - _bottle = self.args.bottle - _action = self.args.action - _key = self.args.key - _value = self.args.value - _data = self.args.data - _key_type = self.args.key_type - mng = Manager(g_settings=self.settings, is_cli=True) - mng.check_bottles() - - if _bottle not in mng.local_bottles: - sys.stderr.write(f"Bottle {_bottle} not found\n") - exit(1) - - bottle = mng.local_bottles[_bottle] - allowed_types = ["REG_SZ", "REG_DWORD", "REG_BINARY", "REG_MULTI_SZ"] - _key_type = "REG_SZ" if _key_type is None else _key_type.upper() - - if _action in ["add", "edit"]: - if _data is None or _key_type not in allowed_types: - sys.stderr.write("Missing or invalid data or key type\n") - exit(1) - Reg(bottle).add(_key, _value, _data, _key_type) - elif _action == "del": - Reg(bottle).remove(_key, _value) - - # endregion - - # region EDIT - def edit_bottle(self): - _bottle = self.args.bottle - _params = self.args.params - _env_var = self.args.env_var - _win = self.args.win - _runner = self.args.runner - _dxvk = self.args.dxvk - _vkd3d = self.args.vkd3d - _nvapi = self.args.nvapi - _latencyflex = self.args.latencyflex - mng = Manager(g_settings=self.settings, is_cli=True) - mng.check_bottles() - - valid_parameters = BottleConfig().Parameters.keys() - - if _bottle not in mng.local_bottles: - sys.stderr.write(f"Bottle {_bottle} not found\n") - exit(1) - - bottle = mng.local_bottles[_bottle] - - if _params is not None: - _params = _params.split(",") - _params = [p.split(":") for p in _params] - for k, v in _params: - if k not in valid_parameters: - sys.stderr.write(f"Invalid parameter {k}\n") - exit(1) - - if v.lower() == "true": - v = True - elif v.lower() == "false": - v = False - else: - try: - v = int(v) - except ValueError: - pass - - mng.update_config(bottle, k, v, scope="Parameters") - - if _env_var is not None and "=" in _env_var: - k, v = _env_var.split("=", 1) - mng.update_config(bottle, k, v, scope="Environment_Variables") - - if _win is not None: - RegKeys(bottle).lg_set_windows(_win) - - if _runner is not None: - Runner.runner_update(bottle, mng, _runner) - - if _dxvk is not None: - mng.check_dxvk(False) - - if _dxvk not in mng.dxvk_available: - sys.stderr.write(f"DXVK version {_dxvk} not available\n") - exit(1) - - if mng.install_dll_component(bottle, "dxvk", version=_dxvk): - mng.update_config(bottle, "DXVK", _dxvk) - - if _vkd3d is not None: - mng.check_vkd3d(False) - - if _vkd3d not in mng.vkd3d_available: - sys.stderr.write(f"VKD3D version {_vkd3d} not available\n") - exit(1) - - if mng.install_dll_component(bottle, "vkd3d", version=_vkd3d): - mng.update_config(bottle, "VKD3D", _vkd3d) - - if _nvapi is not None: - mng.check_nvapi(False) - - if _nvapi not in mng.nvapi_available: - sys.stderr.write(f"NVAPI version {_nvapi} not available\n") - exit(1) - - if mng.install_dll_component(bottle, "nvapi", version=_nvapi): - mng.update_config(bottle, "NVAPI", _nvapi) - - if _latencyflex is not None: - mng.check_latencyflex(False) - - if _latencyflex not in mng.latencyflex_available: - sys.stderr.write(f"LatencyFlex version {_latencyflex} not available\n") - exit(1) - - if mng.install_dll_component(bottle, "latencyflex", version=_latencyflex): - mng.update_config(bottle, "LatencyFlex", _latencyflex) - - # endregion - - # region NEW - def new_bottle(self): - _name = self.args.bottle_name - _environment = self.args.environment - _custom_environment = self.args.custom_environment - _arch = "win64" if self.args.arch is None else self.args.arch - _runner = self.args.runner - _dxvk = self.args.dxvk - _vkd3d = self.args.vkd3d - _nvapi = self.args.nvapi - _latencyflex = self.args.latencyflex - mng = Manager(g_settings=self.settings, is_cli=True) - mng.checks() - - mng.create_bottle( - name=_name, - environment=_environment, - runner=_runner, - dxvk=_dxvk, - vkd3d=_vkd3d, - nvapi=_nvapi, - latencyflex=_latencyflex, - arch=_arch, - custom_environment=_custom_environment, - ) - - # endregion - - # region RUN - def run_program(self): - _bottle = self.args.bottle - _program = self.args.program - _keep = self.args.keep_args - _args = " ".join(self.args.args) - _executable = self.args.executable - - mng = Manager(g_settings=self.settings, is_cli=True) - mng.checks() - - if _bottle.startswith('"') and _bottle.endswith('"'): - _bottle = _bottle[1:-1] - elif _bottle.startswith("'") and _bottle.endswith("'"): - _bottle = _bottle[1:-1] - - for b in mng.local_bottles.keys(): - if b == _bottle: - break - else: - sys.stderr.write(f"Bottle {_bottle} not found\n") - exit(1) - - bottle = mng.local_bottles[_bottle] - programs = mng.get_programs(bottle) - - if _program is not None: - if _executable is not None: - sys.stderr.write("Cannot specify both --program and --executable\n") - exit(1) - - if _program not in [p["name"] for p in programs]: - sys.stderr.write(f"Program {_program} not found\n") - exit(1) - - program = [p for p in programs if p["name"] == _program][0] - _executable = program.get("path", "") - _program_args = program.get("arguments") - if _keep and _program_args: - _args = _program_args + " " + _args - program.get("pre_script", None) - program.get("post_script", None) - program.get("folder", None) - program.get("midi_soundfont", None) - - program.get("dxvk") - program.get("vkd3d") - program.get("dxvk_nvapi") - program.get("fsr") - program.get("gamescope") - program.get("virtual_desktop") - - WineExecutor.run_program(bottle, program | {"arguments": _args}) - - elif _executable: - _executable = _executable.replace("file://", "") - if _executable.startswith('"') and _executable.endswith('"'): - _executable = _executable[1:-1] - elif _executable.startswith("'") and _executable.endswith("'"): - _executable = _executable[1:-1] - - WineExecutor( - bottle, - exec_path=_executable, - args=_args, - ).run_cli() - else: - sys.stderr.write( - "No program or executable specified, you must use either --program or --executable\n" - ) - exit(1) - - # endregion - - # region SHELL - def run_shell(self): - _bottle = self.args.bottle - _input = self.args.input - mng = Manager(g_settings=self.settings, is_cli=True) - mng.checks() - - if _bottle not in mng.local_bottles: - sys.stderr.write(f"Bottle {_bottle} not found\n") - exit(1) - - bottle = mng.local_bottles[_bottle] - winecommand = WineCommand(config=bottle, command=_input, communicate=True) - res = winecommand.run() - if not res.ok: - sys.stdout.write(res.message) - sys.stdout.write(res.data) - - # endregion - - # region STANDALONE - def generate_standalone(self): - _bottle = self.args.bottle - mng = Manager(g_settings=self.settings, is_cli=True) - mng.checks() - - if _bottle not in mng.local_bottles: - sys.stderr.write(f"Bottle {_bottle} not found\n") - exit(1) - - bottle = mng.local_bottles[_bottle] - path = ManagerUtils.get_bottle_path(bottle) - standalone_path = os.path.join(path, "standalone") - winecommand = WineCommand(config=bottle, command='"$@"') - env = winecommand.get_env(return_clean_env=True) - cmd = winecommand.get_cmd('"$@"', return_clean_cmd=True) - winecommand.command.replace( - "/usr/lib/extensions/vulkan/MangoHud/bin/mangohud", "" - ) - - if os.path.isfile(standalone_path): - os.remove(standalone_path) - - with open(standalone_path, "w") as f: - f.write("#!/bin/bash\n") - for k, v in env.items(): - f.write(f"export {k}='{v}'\n") - f.write(f"{cmd}\n") - - os.chmod(os.path.join(path, "standalone"), 0o755) - sys.stdout.write(f"Standalone generated in {path}\n") - sys.stdout.write("Re-generate after every bottle change.\n") - - -if __name__ == "__main__": - cli = CLI() diff --git a/bottles/frontend/component_entry_row.py b/bottles/frontend/component_entry_row.py index f8ae055f2e8..569d2f1f73c 100644 --- a/bottles/frontend/component_entry_row.py +++ b/bottles/frontend/component_entry_row.py @@ -19,14 +19,12 @@ from gi.repository import Gtk, GObject, Adw -from bottles.backend.logger import Logger +import logging from bottles.backend.state import Status from bottles.backend.utils.manager import ManagerUtils from bottles.backend.utils.threading import RunAsync from bottles.frontend.gtk import GtkUtils -logging = Logger() - @Gtk.Template(resource_path="/com/usebottles/bottles/component-entry-row.ui") class ComponentEntryRow(Adw.ActionRow): diff --git a/bottles/frontend/dependency_entry_row.py b/bottles/frontend/dependency_entry_row.py index 99646567f2e..b1ce08a7cb5 100644 --- a/bottles/frontend/dependency_entry_row.py +++ b/bottles/frontend/dependency_entry_row.py @@ -53,7 +53,6 @@ def __init__(self, window, config: BottleConfig, dependency, plain=False, **kwar self.manager = window.manager self.config = config self.dependency = dependency - self.queue = window.page_details.queue if plain: """ @@ -137,7 +136,6 @@ def install_dependency(self, _widget): and set the dependency as installed in the bottle configuration """ - self.queue.add_task() self.get_parent().set_sensitive(False) self.btn_install.set_visible(False) self.spinner.show() @@ -170,7 +168,6 @@ def set_install_status(self, result: Result, error=None): if the installation is successful, or uninstalled if the uninstallation is successful. """ - self.queue.end_task() if result is not None and result.status: if self.config.Parameters.versioning_automatic: self.window.page_details.view_versioning.update() diff --git a/bottles/frontend/details-installers-view.blp b/bottles/frontend/details-installers-view.blp deleted file mode 100644 index c4dec9dcb5b..00000000000 --- a/bottles/frontend/details-installers-view.blp +++ /dev/null @@ -1,71 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -template $DetailsInstallersView: Adw.Bin { - Box { - orientation: vertical; - - SearchBar search_bar { - SearchEntry entry_search { - placeholder-text: _("Search for Programs…"); - } - } - - Adw.PreferencesPage pref_page { - Adw.PreferencesGroup { - description: _("Install programs curated by our community.\n\nFiles on this page are provided by third parties under a proprietary license. By installing them, you agree with their respective licensing terms."); - - ListBox list_installers { - selection-mode: none; - - styles [ - "boxed-list", - ] - } - } - } - - Adw.StatusPage status_page { - icon-name: "system-software-install-symbolic"; - title: _("No Installers Found"); - vexpand: true; - hexpand: true; - description: _("The repository is unreachable or no installer is compatible with this bottle."); - } - } -} - -Popover pop_context { - styles [ - "menu", - ] - - Box { - orientation: vertical; - margin-top: 6; - margin-bottom: 6; - margin-start: 6; - margin-end: 6; - - $GtkModelButton btn_help { - tooltip-text: _("Read Documentation"); - text: _("Documentation"); - } - } -} - -Box actions { - spacing: 6; - - ToggleButton btn_toggle_search { - active: bind search_bar.search-mode-enabled no-sync-create bidirectional; - tooltip-text: _("Search"); - icon-name: "system-search-symbolic"; - } - - MenuButton { - popover: pop_context; - icon-name: "view-more-symbolic"; - tooltip-text: _("Secondary Menu"); - } -} diff --git a/bottles/frontend/details-preferences-page.blp b/bottles/frontend/details-preferences-page.blp index 58db442ef24..83ba7defa02 100644 --- a/bottles/frontend/details-preferences-page.blp +++ b/bottles/frontend/details-preferences-page.blp @@ -402,48 +402,4 @@ template $DetailsPreferencesPage: Adw.PreferencesPage { } } } - - Adw.PreferencesGroup { - title: _("Snapshots"); - - Adw.ActionRow { - activatable-widget: switch_auto_versioning; - title: _("Automatic Snapshots"); - subtitle: _("Automatically create snapshots before installing software or changing settings."); - - Switch switch_auto_versioning { - valign: center; - } - } - - Adw.ActionRow { - activatable-widget: switch_versioning_compression; - title: _("Compression"); - subtitle: _("Compress snapshots to reduce space. This will slow down the creation of snapshots."); - - Switch switch_versioning_compression { - valign: center; - } - } - - Adw.ActionRow { - activatable-widget: switch_versioning_patterns; - title: _("Use Exclusion Patterns"); - subtitle: _("Exclude paths in snapshots."); - - Button btn_manage_versioning_patterns { - tooltip-text: _("Manage Patterns"); - valign: center; - icon-name: "applications-system-symbolic"; - - styles [ - "flat", - ] - } - - Switch switch_versioning_patterns { - valign: center; - } - } - } } diff --git a/bottles/frontend/details-versioning-page.blp b/bottles/frontend/details-versioning-page.blp deleted file mode 100644 index 99bc65a74bf..00000000000 --- a/bottles/frontend/details-versioning-page.blp +++ /dev/null @@ -1,88 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -template $DetailsVersioningPage: Adw.PreferencesPage { - Adw.PreferencesPage pref_page { - Adw.PreferencesGroup { - ListBox list_states { - selection-mode: none; - - styles [ - "boxed-list", - ] - } - } - } - - Adw.StatusPage status_page { - icon-name: "preferences-system-time-symbolic"; - title: _("No Snapshots Found"); - description: _("Create your first snapshot to start saving states of your preferences."); - } -} - -Popover pop_context { - styles [ - "menu", - ] - - Box { - orientation: vertical; - margin-top: 6; - margin-bottom: 6; - margin-start: 6; - margin-end: 6; - - $GtkModelButton btn_help { - tooltip-text: _("Read Documentation"); - text: _("Documentation"); - } - } -} - -Popover pop_state { - Box { - orientation: vertical; - spacing: 6; - - styles [ - "menu", - ] - - Box { - Entry entry_state_message { - hexpand: true; - placeholder-text: _("A short comment"); - } - - Button btn_save { - tooltip-text: _("Save the bottle state."); - halign: end; - icon-name: "object-select-symbolic"; - - styles [ - "suggested-action", - ] - } - - styles [ - "linked", - ] - } - } -} - -Box actions { - spacing: 6; - - MenuButton btn_add { - tooltip-text: _("Create new Snapshot"); - popover: pop_state; - icon-name: "list-add-symbolic"; - } - - MenuButton { - popover: pop_context; - icon-name: "view-more-symbolic"; - } -} diff --git a/bottles/frontend/details_dependencies_view.py b/bottles/frontend/details_dependencies_view.py index 41cc42ce427..d9b1572ee20 100644 --- a/bottles/frontend/details_dependencies_view.py +++ b/bottles/frontend/details_dependencies_view.py @@ -52,7 +52,6 @@ def __init__(self, details, config: BottleConfig, **kwargs): self.window = details.window self.manager = details.window.manager self.config = config - self.queue = details.queue self.ev_controller.connect("key-released", self.__search_dependencies) @@ -64,9 +63,6 @@ def __init__(self, details, config: BottleConfig, **kwargs): ) self.btn_help.connect("clicked", open_doc_url, "bottles/dependencies") - if not self.manager.utils_conn.status: - self.stack.set_visible_child_name("page_offline") - self.spinner_loading.start() def __search_dependencies(self, *_args): @@ -100,9 +96,6 @@ def update(self, _widget=False, config: BottleConfig | None = None): self.config = config # Not sure if it's the best place to make this check - if not self.manager.utils_conn.status: - return - self.stack.set_visible_child_name("page_loading") def new_dependency(dependency, plain=False): diff --git a/bottles/frontend/details_installers_view.py b/bottles/frontend/details_installers_view.py deleted file mode 100644 index e6059475fd7..00000000000 --- a/bottles/frontend/details_installers_view.py +++ /dev/null @@ -1,130 +0,0 @@ -# details_installers_view.py -# -# Copyright 2025 The Bottle Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import time -from gi.repository import Gtk, GLib, Adw - -from bottles.backend.models.config import BottleConfig -from bottles.backend.models.result import Result - -from bottles.backend.utils.threading import RunAsync -from bottles.frontend.common import open_doc_url -from bottles.frontend.installer_row import InstallerRow - - -@Gtk.Template(resource_path="/com/usebottles/bottles/details-installers-view.ui") -class DetailsInstallersView(Adw.Bin): - __gtype_name__ = "DetailsInstallersView" - __registry = [] - - # region Widgets - list_installers = Gtk.Template.Child() - btn_help = Gtk.Template.Child() - btn_toggle_search = Gtk.Template.Child() - entry_search = Gtk.Template.Child() - search_bar = Gtk.Template.Child() - actions = Gtk.Template.Child() - pref_page = Gtk.Template.Child() - status_page = Gtk.Template.Child() - ev_controller = Gtk.EventControllerKey.new() - - # endregion - - def __init__(self, details, config, **kwargs): - super().__init__(**kwargs) - - # common variables and references - self.window = details.window - self.manager = details.window.manager - self.config = config - - self.ev_controller.connect("key-released", self.__search_installers) - self.entry_search.add_controller(self.ev_controller) - - self.search_bar.set_key_capture_widget(self.window) - self.btn_help.connect("clicked", open_doc_url, "bottles/installers") - self.entry_search.connect("changed", self.__search_installers) - - def __search_installers(self, *_args): - """ - This function search in the list of installers the - text written in the search entry. - """ - terms = self.entry_search.get_text() - self.list_installers.set_filter_func(self.__filter_installers, terms) - - @staticmethod - def __filter_installers(row, terms=None): - text = row.get_title().lower() + row.get_subtitle().lower() - if terms.lower() in text: - return True - return False - - def empty_list(self): - for r in self.__registry: - if r.get_parent() is not None: - r.get_parent().remove(r) - self.__registry = [] - - def update(self, widget=False, config=None): - """ - This function update the installers list with the - supported by the manager. - """ - if config is None: - config = BottleConfig() - self.config = config - installers = self.manager.supported_installers.items() - - self.list_installers.set_sensitive(False) - - def new_installer(_installer): - entry = InstallerRow( - window=self.window, config=self.config, installer=_installer - ) - self.list_installers.append(entry) - self.__registry.append(entry) - - def callback(result, error=False): - self.status_page.set_visible(not result.status) - self.pref_page.set_visible(result.status) - self.list_installers.set_visible(result.status) - self.list_installers.set_sensitive(result.status) - - def process_installers(): - time.sleep(0.5) # workaround for freezing bug on bottle load - GLib.idle_add(self.empty_list) - - if len(installers) == 0: - return Result(False) - - i = 0 - - for installer in installers: - if len(installer) != 2: - continue - if installer[1].get("Arch", "win64") != self.config.Arch: - continue - GLib.idle_add(new_installer, installer) - i += 1 - - if i == 0: - return Result(False) # there are no arch-compatible installers - - return Result(True) - - RunAsync(process_installers, callback) diff --git a/bottles/frontend/details_preferences_page.py b/bottles/frontend/details_preferences_page.py index b08dc861aeb..46458b1551e 100644 --- a/bottles/frontend/details_preferences_page.py +++ b/bottles/frontend/details_preferences_page.py @@ -31,9 +31,7 @@ gamescope_available, base_version, ) -from bottles.backend.logger import Logger -from bottles.backend.managers.library import LibraryManager -from bottles.backend.managers.runtime import RuntimeManager +import logging from bottles.backend.models.config import BottleConfig from bottles.backend.models.enum import Arch from bottles.backend.models.result import Result @@ -49,7 +47,6 @@ from bottles.frontend.environment_variables_dialog import ( EnvironmentVariablesDialog, ) -from bottles.frontend.exclusion_patterns_dialog import ExclusionPatternsDialog from bottles.frontend.fsr_dialog import FsrDialog from bottles.frontend.gamescope_dialog import GamescopeDialog from bottles.frontend.mangohud_dialog import MangoHudDialog @@ -58,8 +55,6 @@ from bottles.frontend.vkbasalt_dialog import VkBasaltDialog from bottles.frontend.vmtouch_dialog import VmtouchDialog -logging = Logger() - # noinspection PyUnusedLocal @Gtk.Template(resource_path="/com/usebottles/bottles/details-preferences-page.ui") @@ -72,7 +67,6 @@ class DetailsPreferencesPage(Adw.PreferencesPage): btn_manage_fsr = Gtk.Template.Child() btn_manage_mangohud = Gtk.Template.Child() btn_manage_sandbox = Gtk.Template.Child() - btn_manage_versioning_patterns = Gtk.Template.Child() btn_manage_vmtouch = Gtk.Template.Child() btn_cwd_reset = Gtk.Template.Child() btn_cwd = Gtk.Template.Child() @@ -98,9 +92,6 @@ class DetailsPreferencesPage(Adw.PreferencesPage): switch_discrete = Gtk.Template.Child() switch_steam_runtime = Gtk.Template.Child() switch_sandbox = Gtk.Template.Child() - switch_versioning_compression = Gtk.Template.Child() - switch_auto_versioning = Gtk.Template.Child() - switch_versioning_patterns = Gtk.Template.Child() switch_vmtouch = Gtk.Template.Child() combo_runner = Gtk.Template.Child() combo_dxvk = Gtk.Template.Child() @@ -134,9 +125,7 @@ def __init__(self, details, config, **kwargs): # common variables and references self.window = details.window - self.manager = details.window.manager self.config = config - self.queue = details.queue self.details = details if not gamemode_available or not Xdp.Portal.running_under_sandbox(): @@ -221,9 +210,6 @@ def __init__(self, details, config, **kwargs): self.btn_manage_vmtouch.connect( "clicked", self.__show_feature_dialog, VmtouchDialog ) - self.btn_manage_versioning_patterns.connect( - "clicked", self.__show_feature_dialog, ExclusionPatternsDialog - ) self.btn_cwd.connect("clicked", self.choose_cwd) self.btn_cwd_reset.connect("clicked", self.reset_cwd, True) self.switch_mangohud.connect("state-set", self.__toggle_feature, "mangohud") @@ -235,15 +221,6 @@ def __init__(self, details, config, **kwargs): self.switch_gamescope.connect("state-set", self.__toggle_feature, "gamescope") self.switch_sandbox.connect("state-set", self.__toggle_feature, "sandbox") self.switch_discrete.connect("state-set", self.__toggle_feature, "discrete_gpu") - self.switch_versioning_compression.connect( - "state-set", self.__toggle_versioning_compression - ) - self.switch_auto_versioning.connect( - "state-set", self.__toggle_feature, "versioning_automatic" - ) - self.switch_versioning_patterns.connect( - "state-set", self.__toggle_feature, "versioning_exclusion_patterns" - ) self.switch_vmtouch.connect("state-set", self.__toggle_feature, "vmtouch") self.combo_runner.connect("notify::selected", self.__set_runner) self.combo_dxvk.connect("notify::selected", self.__set_dxvk) @@ -262,12 +239,6 @@ def __init__(self, details, config, **kwargs): self.row_nvapi.set_visible(is_nvidia_gpu) self.combo_nvapi.set_visible(is_nvidia_gpu) - if RuntimeManager.get_runtimes("steam"): - self.row_steam_runtime.set_visible(True) - self.switch_steam_runtime.connect( - "state-set", self.__toggle_feature, "use_steam_runtime" - ) - """Toggle some utilities according to its availability""" self.switch_gamemode.set_sensitive(gamemode_available) self.switch_gamescope.set_sensitive(gamescope_available) @@ -297,24 +268,10 @@ def __save_name(self, *_args): return new_name = self.entry_name.get_text() - old_name = self.config.Name - - library_manager = LibraryManager() - entries = library_manager.get_library() - - for uuid, entry in entries.items(): - bottle = entry.get("bottle") - if bottle.get("name") == old_name: - logging.info(f"Updating library entry for {entry.get('name')}") - entries[uuid]["bottle"]["name"] = new_name - break - - library_manager.__library = entries - library_manager.save_library() + self.config.Name self.manager.update_config(config=self.config, key="Name", value=new_name) - self.manager.update_bottles(silent=True) # Updates backend bottles list and UI self.window.page_library.update() self.details.view_bottle.label_name.set_text(self.config.Name) @@ -415,11 +372,6 @@ def set_config(self, config: BottleConfig): self.switch_gamescope.handler_block_by_func(self.__toggle_feature) self.switch_sandbox.handler_block_by_func(self.__toggle_feature) self.switch_discrete.handler_block_by_func(self.__toggle_feature) - self.switch_versioning_compression.handler_block_by_func( - self.__toggle_versioning_compression - ) - self.switch_auto_versioning.handler_block_by_func(self.__toggle_feature) - self.switch_versioning_patterns.handler_block_by_func(self.__toggle_feature) with contextlib.suppress(TypeError): self.switch_steam_runtime.handler_block_by_func(self.__toggle_feature) self.combo_runner.handler_block_by_func(self.__set_runner) @@ -437,11 +389,6 @@ def set_config(self, config: BottleConfig): self.switch_gamemode.set_active(parameters.gamemode) self.switch_gamescope.set_active(parameters.gamescope) self.switch_sandbox.set_active(parameters.sandbox) - self.switch_versioning_compression.set_active(parameters.versioning_compression) - self.switch_auto_versioning.set_active(parameters.versioning_automatic) - self.switch_versioning_patterns.set_active( - parameters.versioning_exclusion_patterns - ) self.switch_steam_runtime.set_active(parameters.use_steam_runtime) self.switch_vmtouch.set_active(parameters.vmtouch) @@ -549,11 +496,6 @@ def set_config(self, config: BottleConfig): self.switch_gamescope.handler_unblock_by_func(self.__toggle_feature) self.switch_sandbox.handler_unblock_by_func(self.__toggle_feature) self.switch_discrete.handler_unblock_by_func(self.__toggle_feature) - self.switch_versioning_compression.handler_unblock_by_func( - self.__toggle_versioning_compression - ) - self.switch_auto_versioning.handler_unblock_by_func(self.__toggle_feature) - self.switch_versioning_patterns.handler_unblock_by_func(self.__toggle_feature) with contextlib.suppress(TypeError): self.switch_steam_runtime.handler_unblock_by_func(self.__toggle_feature) self.combo_runner.handler_unblock_by_func(self.__set_runner) @@ -571,7 +513,6 @@ def __show_display_settings(self, widget): parent_window=self.window, config=self.config, details=self.details, - queue=self.queue, widget=widget, spinner_display=self.spinner_display, ) @@ -597,7 +538,6 @@ def __set_sync_type(self, *_args): "esync", "fsync", ] - self.queue.add_task() self.combo_sync.set_sensitive(False) RunAsync( self.manager.update_config, @@ -607,11 +547,9 @@ def __set_sync_type(self, *_args): scope="Parameters", ) self.combo_sync.set_sensitive(True) - self.queue.end_task() def __toggle_nvapi(self, widget=False, state=False): """Install/Uninstall NVAPI from the bottle""" - self.queue.add_task() self.set_nvapi_status(pending=True) RunAsync( @@ -635,26 +573,7 @@ def update(): scope="Parameters", ).data["config"] - def handle_response(_widget, response_id): - if response_id == "ok": - RunAsync( - self.manager.versioning_manager.re_initialize, config=self.config - ) - _widget.destroy() - - if self.manager.versioning_manager.is_initialized(self.config): - dialog = Adw.MessageDialog.new( - self.window, - _("Are you sure you want to delete all snapshots?"), - _("This will delete all snapshots but keep your files."), - ) - dialog.add_response("cancel", _("_Cancel")) - dialog.add_response("ok", _("_Delete")) - dialog.set_response_appearance("ok", Adw.ResponseAppearance.DESTRUCTIVE) - dialog.connect("response", handle_response) - dialog.present() - else: - update() + update() def __set_runner(self, *_args): """Set the runner to use for the bottle""" @@ -694,7 +613,6 @@ def update(result: Result[dict], error=False): ) set_widgets_status(True) - self.queue.end_task() set_widgets_status(False) runner = self.manager.runners_available[self.combo_runner.get_selected()] @@ -706,7 +624,6 @@ def run_task(status=True): self.combo_runner.handler_unblock_by_func(self.__set_runner) return - self.queue.add_task() RunAsync( Runner.runner_update, callback=update, @@ -734,7 +651,6 @@ def __dll_component_task_func(self, *args, **kwargs): def __set_dxvk(self, *_args): """Set the DXVK version to use for the bottle""" self.set_dxvk_status(pending=True) - self.queue.add_task() if (self.combo_dxvk.get_selected()) == 0: self.set_dxvk_status(pending=True) @@ -774,7 +690,6 @@ def __set_dxvk(self, *_args): def __set_vkd3d(self, *_args): """Set the VKD3D version to use for the bottle""" self.set_vkd3d_status(pending=True) - self.queue.add_task() if (self.combo_vkd3d.get_selected()) == 0: self.set_vkd3d_status(pending=True) @@ -814,7 +729,6 @@ def __set_vkd3d(self, *_args): def __set_nvapi(self, *_args): """Set the NVAPI version to use for the bottle""" self.set_nvapi_status(pending=True) - self.queue.add_task() self.switch_nvapi.set_active(True) @@ -836,7 +750,6 @@ def __set_nvapi(self, *_args): def __set_latencyflex(self, *_args): """Set the latency flex value""" - self.queue.add_task() if self.combo_latencyflex.get_selected() == 0: RunAsync( task_func=self.manager.install_dll_component, @@ -876,9 +789,7 @@ def update(result, error=False): self.spinner_windows.stop() self.spinner_windows.set_visible(False) self.combo_windows.set_sensitive(True) - self.queue.end_task() - self.queue.add_task() self.spinner_windows.start() self.spinner_windows.set_visible(True) self.combo_windows.set_sensitive(False) @@ -913,7 +824,6 @@ def set_dxvk_status(self, status=None, error=None, pending=False): else: self.spinner_dxvk.stop() self.spinner_dxvk.set_visible(False) - self.queue.end_task() @GtkUtils.run_in_main_loop def set_vkd3d_status(self, status=None, error=None, pending=False): @@ -925,7 +835,6 @@ def set_vkd3d_status(self, status=None, error=None, pending=False): else: self.spinner_vkd3d.stop() self.spinner_vkd3d.set_visible(False) - self.queue.end_task() @GtkUtils.run_in_main_loop def set_nvapi_status(self, status=None, error=None, pending=False): @@ -942,7 +851,6 @@ def set_nvapi_status(self, status=None, error=None, pending=False): self.spinner_nvapibool.stop() self.spinner_nvapi.set_visible(False) self.spinner_nvapibool.set_visible(False) - self.queue.end_task() @GtkUtils.run_in_main_loop def set_latencyflex_status(self, status=None, error=None, pending=False): @@ -954,7 +862,6 @@ def set_latencyflex_status(self, status=None, error=None, pending=False): else: self.spinner_latencyflex.stop() self.spinner_latencyflex.set_visible(False) - self.queue.end_task() def __set_steam_rules(self): """Set the Steam Environment specific rules""" diff --git a/bottles/frontend/details_versioning_page.py b/bottles/frontend/details_versioning_page.py deleted file mode 100644 index d83408f3287..00000000000 --- a/bottles/frontend/details_versioning_page.py +++ /dev/null @@ -1,161 +0,0 @@ -# details_versioning_page.py -# -# Copyright 2025 The Bottles Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import re -from gettext import gettext as _ - -from gi.repository import Gtk, GLib, Adw - -from bottles.backend.models.result import Result -from bottles.backend.utils.threading import RunAsync -from bottles.frontend.common import open_doc_url -from bottles.frontend.gtk import GtkUtils -from bottles.frontend.state_row import StateRow - - -@Gtk.Template(resource_path="/com/usebottles/bottles/details-versioning-page.ui") -class DetailsVersioningPage(Adw.PreferencesPage): - __gtype_name__ = "DetailsVersioningPage" - __registry = [] - - # region Widgets - list_states = Gtk.Template.Child() - actions = Gtk.Template.Child() - pop_state = Gtk.Template.Child() - btn_save = Gtk.Template.Child() - btn_help = Gtk.Template.Child() - entry_state_message = Gtk.Template.Child() - status_page = Gtk.Template.Child() - pref_page = Gtk.Template.Child() - btn_add = Gtk.Template.Child() - ev_controller = Gtk.EventControllerKey.new() - - # endregion - - def __init__(self, details, config, **kwargs): - super().__init__(**kwargs) - - # common variables and references - self.window = details.window - self.manager = details.window.manager - self.versioning_manager = details.window.manager.versioning_manager - self.config = config - - self.ev_controller.connect("key-released", self.check_entry_state_message) - self.entry_state_message.add_controller(self.ev_controller) - - self.btn_save.connect("clicked", self.add_state) - self.btn_help.connect("clicked", open_doc_url, "bottles/versioning") - self.entry_state_message.connect("activate", self.add_state) - - def empty_list(self): - for r in self.__registry: - if r.get_parent() is not None: - r.get_parent().remove(r) - self.__registry = [] - - @GtkUtils.run_in_main_loop - def update(self, widget=None, config=None, states=None, active=0): - """ - This function update the states list with the - ones from the bottle configuration. - """ - if config is None: - config = self.config - if states is None: - states = self.versioning_manager.list_states(config) - if not config.Versioning: - active = states.data.get("state_id") - states = states.data.get("states") - - self.config = config - self.list_states.set_sensitive(False) - - if self.config.Versioning: - self.btn_add.set_sensitive(False) - self.btn_add.set_tooltip_text( - _("Please migrate to the new Versioning system to create new states.") - ) - - def new_state(_state, active): - entry = StateRow( - parent=self, config=self.config, state=_state, active=active - ) - self.__registry.append(entry) - self.list_states.append(entry) - - def callback(result, error=False): - self.status_page.set_visible(not result.status) - self.pref_page.set_visible(result.status) - self.list_states.set_visible(result.status) - self.list_states.set_sensitive(result.status) - - def process_states(): - GLib.idle_add(self.empty_list) - - if len(states) == 0: - return Result(False) - - for state in states.items(): - _active = int(state[0]) == int(active) - GLib.idle_add(new_state, state, _active) - - return Result(True) - - RunAsync(process_states, callback) - - def check_entry_state_message(self, *_args): - """ - This function check if the entry state message is valid, - looking for special characters. It also toggles the widget icon - and the save button sensitivity according to the result. - """ - regex = re.compile('[@!#$%^&*()<>?/|}{~:.;,"]') - message = self.entry_state_message.get_text() - check = regex.search(message) is None - - self.btn_save.set_sensitive(check) - self.entry_state_message.set_icon_from_icon_name( - 1, "" if check else 'dialog-warning-symbolic"' - ) - - def add_state(self, widget): - """ - This function create ask the versioning manager to - create a new bottle state with the given message. - """ - if not self.btn_save.get_sensitive(): - return - - @GtkUtils.run_in_main_loop - def update(result, error): - self.window.show_toast(result.message) - if result.ok: - self.update( - states=result.data.get("states"), active=result.data.get("state_id") - ) - - message = self.entry_state_message.get_text() - if message != "": - RunAsync( - task_func=self.versioning_manager.create_state, - callback=update, - config=self.config, - message=message, - ) - self.entry_state_message.set_text("") - self.pop_state.popdown() diff --git a/bottles/frontend/display_dialog.py b/bottles/frontend/display_dialog.py index d5a605a3941..d792f072e00 100644 --- a/bottles/frontend/display_dialog.py +++ b/bottles/frontend/display_dialog.py @@ -19,13 +19,11 @@ from gi.repository import Gtk, GLib, Adw -from bottles.backend.logger import Logger from bottles.backend.utils.threading import RunAsync from bottles.backend.wine.reg import Reg from bottles.backend.wine.regkeys import RegKeys from bottles.frontend.gtk import GtkUtils -logging = Logger() renderers = ["gl", "gdi", "vulkan"] @@ -56,7 +54,6 @@ def __init__( self.window = parent_window self.manager = parent_window.manager self.config = config - self.queue = queue self.widget = widget self.spinner_display = spinner_display @@ -98,7 +95,6 @@ def __idle_save(self, *args): """Queue system""" self.started = 0 - self.queued = 0 self.completed = 0 def add_queue(): @@ -106,17 +102,10 @@ def add_queue(): self.window.show_toast(_("Updating display settings, please wait…")) self.spinner_display.start() self.started = 1 - self.queue.add_task() self.widget.set_sensitive(False) - self.queued += 1 def complete_queue(): self.completed += 1 - if self.queued == self.completed: - self.widget.set_sensitive(True) - self.spinner_display.stop() - self.window.show_toast(_("Display settings updated")) - self.queue.end_task() if ( self.expander_virtual_desktop.get_enable_expansion() diff --git a/bottles/frontend/duplicate-dialog.blp b/bottles/frontend/duplicate-dialog.blp deleted file mode 100644 index 024ed9bebf4..00000000000 --- a/bottles/frontend/duplicate-dialog.blp +++ /dev/null @@ -1,99 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -template $DuplicateDialog: Adw.Window { - modal: true; - default-width: 400; - default-height: 400; - destroy-with-parent: true; - - Box { - orientation: vertical; - - Adw.HeaderBar { - show-end-title-buttons: false; - - title-widget: Adw.WindowTitle { - title: _("Duplicate Bottle"); - }; - - Button btn_cancel { - label: _("_Cancel"); - use-underline: true; - action-name: "window.close"; - } - - ShortcutController { - scope: managed; - - Shortcut { - trigger: "Escape"; - action: "action(window.close)"; - } - } - - [end] - Button btn_duplicate { - label: _("Duplicate"); - - styles [ - "suggested-action", - ] - } - } - - Stack stack_switcher { - Adw.PreferencesPage page_name { - Adw.PreferencesGroup { - description: _("Enter a name for the duplicate of the Bottle."); - - Adw.EntryRow entry_name { - title: _("Name"); - } - } - } - - StackPage { - name: "page_duplicating"; - - child: Box page_duplicating { - margin-top: 24; - margin-bottom: 24; - orientation: vertical; - - Label { - halign: center; - margin-top: 12; - margin-bottom: 12; - label: _("Duplicating…"); - - styles [ - "title-1", - ] - } - - Label { - margin-bottom: 6; - label: _("This could take a while."); - } - - ProgressBar progressbar { - width-request: 300; - halign: center; - margin-top: 24; - margin-bottom: 12; - } - }; - } - - StackPage { - name: "page_duplicated"; - - child: Adw.StatusPage page_duplicated { - icon-name: "object-select-symbolic"; - title: _("Bottle Duplicated"); - }; - } - } - } -} diff --git a/bottles/frontend/duplicate_dialog.py b/bottles/frontend/duplicate_dialog.py deleted file mode 100644 index a3b839430de..00000000000 --- a/bottles/frontend/duplicate_dialog.py +++ /dev/null @@ -1,92 +0,0 @@ -# duplicate_dialog.py -# -# Copyright 2025 The Bottles Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import time - -from gi.repository import Gtk, Adw - -from bottles.backend.managers.backup import BackupManager -from bottles.backend.utils.threading import RunAsync -from bottles.frontend.gtk import GtkUtils - - -@Gtk.Template(resource_path="/com/usebottles/bottles/duplicate-dialog.ui") -class DuplicateDialog(Adw.Window): - __gtype_name__ = "DuplicateDialog" - - # region Widgets - entry_name = Gtk.Template.Child() - btn_cancel = Gtk.Template.Child() - btn_duplicate = Gtk.Template.Child() - stack_switcher = Gtk.Template.Child() - progressbar = Gtk.Template.Child() - - # endregion - - def __init__(self, parent, **kwargs): - super().__init__(**kwargs) - self.set_transient_for(parent.window) - - # common variables and references - self.parent = parent - self.config = parent.config - - self.entry_name.connect("changed", self.__check_entry_name) - - # connect signals - self.btn_duplicate.connect("clicked", self.__duplicate_bottle) - - def __check_entry_name(self, *_args): - is_duplicate = self.entry_name.get_text() in self.parent.manager.local_bottles - if is_duplicate: - self.entry_name.add_css_class("error") - self.btn_duplicate.set_sensitive(False) - else: - self.entry_name.remove_css_class("error") - self.btn_duplicate.set_sensitive(True) - - def __duplicate_bottle(self, widget): - """ - This function take the new bottle name from the entry - and create a new duplicate of the bottle. It also change the - stack_switcher page when the process is finished. - """ - self.stack_switcher.set_visible_child_name("page_duplicating") - self.btn_duplicate.set_visible(False) - self.btn_cancel.set_label("Close") - - RunAsync(self.pulse) - name = self.entry_name.get_text() - - RunAsync( - task_func=BackupManager.duplicate_bottle, - callback=self.finish, - config=self.config, - name=name, - ) - - @GtkUtils.run_in_main_loop - def finish(self, result, error=None): - # TODO: handle result.status == False - self.parent.manager.update_bottles() - self.stack_switcher.set_visible_child_name("page_duplicated") - - def pulse(self): - # This function update the progress bar every half second. - while True: - time.sleep(0.5) - self.progressbar.pulse() diff --git a/bottles/frontend/environment_variables_dialog.py b/bottles/frontend/environment_variables_dialog.py index 8252a135f0a..0a8b1b8ed81 100644 --- a/bottles/frontend/environment_variables_dialog.py +++ b/bottles/frontend/environment_variables_dialog.py @@ -19,12 +19,10 @@ from gi.repository import Gtk, GLib, Adw -from bottles.backend.logger import Logger +import logging from bottles.frontend.gtk import GtkUtils from bottles.frontend.sh import ShUtils -logging = Logger() - @Gtk.Template(resource_path="/com/usebottles/bottles/env-var-entry.ui") class EnvironmentVariableEntryRow(Adw.EntryRow): diff --git a/bottles/frontend/exclusion-pattern-row.blp b/bottles/frontend/exclusion-pattern-row.blp deleted file mode 100644 index 7965fd65882..00000000000 --- a/bottles/frontend/exclusion-pattern-row.blp +++ /dev/null @@ -1,15 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -template $ExclusionPatternRow: Adw.ActionRow { - title: _("Value"); - - Button btn_remove { - valign: center; - icon-name: "user-trash-symbolic"; - - styles [ - "flat", - ] - } -} diff --git a/bottles/frontend/exclusion-patterns-dialog.blp b/bottles/frontend/exclusion-patterns-dialog.blp deleted file mode 100644 index 06f12c1a090..00000000000 --- a/bottles/frontend/exclusion-patterns-dialog.blp +++ /dev/null @@ -1,40 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -template $ExclusionPatternsDialog: Adw.Window { - modal: true; - default-width: 500; - default-height: 500; - - ShortcutController { - Shortcut { - trigger: "Escape"; - action: "action(window.close)"; - } - } - - Box { - orientation: vertical; - - Adw.HeaderBar { - title-widget: Adw.WindowTitle { - title: _("Exclusion Patterns"); - }; - } - - Adw.PreferencesPage { - Adw.PreferencesGroup { - description: _("Define patterns that will be used to prevent some directories to being versioned."); - - Adw.EntryRow entry_name { - title: _("Pattern"); - show-apply-button: true; - } - } - - Adw.PreferencesGroup group_patterns { - title: _("Existing Patterns"); - } - } - } -} diff --git a/bottles/frontend/exclusion_patterns_dialog.py b/bottles/frontend/exclusion_patterns_dialog.py deleted file mode 100644 index e406a8c954a..00000000000 --- a/bottles/frontend/exclusion_patterns_dialog.py +++ /dev/null @@ -1,111 +0,0 @@ -# exclusion_patterns_dialog.py -# -# Copyright 2025 The Bottles Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from gettext import gettext as _ - -from gi.repository import Gtk, GLib, Adw - - -@Gtk.Template(resource_path="/com/usebottles/bottles/exclusion-pattern-row.ui") -class ExclusionPatternRow(Adw.ActionRow): - __gtype_name__ = "ExclusionPatternRow" - - # region Widgets - btn_remove = Gtk.Template.Child() - # endregion - - def __init__(self, parent, pattern, **kwargs): - super().__init__(**kwargs) - - # common variables and references - self.parent = parent - self.manager = parent.window.manager - self.config = parent.config - self.pattern = pattern - - self.set_title(self.pattern) - - # connect signals - self.btn_remove.connect("clicked", self.__remove) - - def __remove(self, *_args): - """ - Remove the env var from the bottle configuration and - destroy the widget - """ - patterns = self.config.Versioning_Exclusion_Patterns - if self.pattern in patterns: - patterns.remove(self.pattern) - - self.manager.update_config( - config=self.config, key="Versioning_Exclusion_Patterns", value=patterns - ) - self.parent.group_patterns.remove(self) - - -@Gtk.Template(resource_path="/com/usebottles/bottles/exclusion-patterns-dialog.ui") -class ExclusionPatternsDialog(Adw.Window): - __gtype_name__ = "ExclusionPatternsDialog" - - # region Widgets - entry_name = Gtk.Template.Child() - group_patterns = Gtk.Template.Child() - # endregion - - def __init__(self, window, config, **kwargs): - super().__init__(**kwargs) - self.set_transient_for(window) - - # common variables and references - self.window = window - self.manager = window.manager - self.config = config - - self.__populate_patterns_list() - - # connect signals - self.entry_name.connect("apply", self.__save_var) - - def __save_var(self, *_args): - """ - This function save the new env var to the - bottle configuration - """ - pattern = self.entry_name.get_text() - self.manager.update_config( - config=self.config, - key="Versioning_Exclusion_Patterns", - value=self.config.Versioning_Exclusion_Patterns + [pattern], - ) - _entry = ExclusionPatternRow(self, pattern) - GLib.idle_add(self.group_patterns.add, _entry) - self.entry_name.set_text("") - - def __populate_patterns_list(self): - """ - This function populate the list of exclusion patterns - with the existing ones from the bottle configuration - """ - patterns = self.config.Versioning_Exclusion_Patterns - if len(patterns) == 0: - self.group_patterns.set_description(_("No exclusion patterns defined.")) - return - - self.group_patterns.set_description("") - for pattern in patterns: - _entry = ExclusionPatternRow(self, pattern) - GLib.idle_add(self.group_patterns.add, _entry) diff --git a/bottles/frontend/filters.py b/bottles/frontend/filters.py index 58b19690371..509bd850e0b 100644 --- a/bottles/frontend/filters.py +++ b/bottles/frontend/filters.py @@ -27,10 +27,6 @@ def add_executable_filters(dialog): __set_filter(dialog, _("Supported Executables"), ["*.exe", "*.msi"]) -def add_soundfont_filters(dialog): - __set_filter(dialog, _("Supported SoundFonts"), ["*.sf2", "*.sf3"]) - - def add_yaml_filters(dialog): # TODO: Investigate why `filter.add_mime_type(...)` does not show filter in all distributions. # Intended MIME types are: diff --git a/bottles/frontend/fsr_dialog.py b/bottles/frontend/fsr_dialog.py index e35d83d1bf1..8a08df0acba 100644 --- a/bottles/frontend/fsr_dialog.py +++ b/bottles/frontend/fsr_dialog.py @@ -18,9 +18,6 @@ from gettext import gettext as _ from gi.repository import Gtk, GLib, Adw -from bottles.backend.logger import Logger - -logging = Logger() @Gtk.Template(resource_path="/com/usebottles/bottles/fsr-dialog.ui") diff --git a/bottles/frontend/importer-row.blp b/bottles/frontend/importer-row.blp deleted file mode 100644 index 609124e57ae..00000000000 --- a/bottles/frontend/importer-row.blp +++ /dev/null @@ -1,75 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -Popover pop_actions { - styles [ - "menu", - ] - - Box { - orientation: vertical; - spacing: 3; - - $GtkModelButton btn_browse { - text: _("Browse Files"); - } - } -} - -template $ImporterRow: Adw.ActionRow { - /* Translators: A Wine prefix is a separate environment (C:\ drive) for the Wine program */ - title: _("Wine prefix name"); - - Box { - spacing: 6; - - Label label_manager { - valign: center; - label: _("Manager"); - - styles [ - "tag", - "caption", - ] - } - - Image img_lock { - visible: false; - tooltip-text: _("This Wine prefix was already imported in Bottles."); - valign: center; - icon-name: "channel-secure-symbolic"; - - styles [ - "tag", - "caption", - ] - } - - Button btn_import { - valign: center; - - Image { - icon-name: "document-save-symbolic"; - } - - styles [ - "flat", - ] - } - - Separator { - margin-top: 12; - margin-bottom: 12; - } - - MenuButton { - valign: center; - popover: pop_actions; - icon-name: "view-more-symbolic"; - - styles [ - "flat", - ] - } - } -} diff --git a/bottles/frontend/importer-view.blp b/bottles/frontend/importer-view.blp deleted file mode 100644 index 7cd80ccd7b1..00000000000 --- a/bottles/frontend/importer-view.blp +++ /dev/null @@ -1,75 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -template $ImporterView: Adw.Bin { - Box { - orientation: vertical; - - HeaderBar headerbar { - title-widget: Adw.WindowTitle window_title {}; - - [start] - Button btn_back { - tooltip-text: _("Go Back"); - icon-name: "go-previous-symbolic"; - } - - [end] - Box box_actions { - MenuButton btn_import_backup { - tooltip-text: _("Import a Bottle backup"); - popover: pop_backup; - icon-name: "document-send-symbolic"; - } - - Button btn_find_prefixes { - tooltip-text: _("Search again for prefixes"); - icon-name: "view-refresh-symbolic"; - } - } - } - - Adw.PreferencesPage { - Adw.StatusPage status_page { - vexpand: true; - icon-name: "document-save-symbolic"; - title: _("No Prefixes Found"); - description: _("No external prefixes were found. Does Bottles have access to them?\nUse the icon on the top to import a bottle from a backup."); - } - - Adw.PreferencesGroup group_prefixes { - visible: false; - - ListBox list_prefixes { - styles [ - "boxed-list", - ] - } - } - } - } -} - -Popover pop_backup { - styles [ - "menu", - ] - - Box { - orientation: vertical; - margin-top: 6; - margin-bottom: 6; - margin-start: 6; - margin-end: 6; - - $GtkModelButton btn_import_config { - tooltip-text: _("This is just the bottle configuration, it\'s perfect if you want to create a new one but without personal files."); - text: _("Configuration"); - } - - $GtkModelButton btn_import_full { - tooltip-text: _("This is the complete archive of your bottle, including personal files."); - text: _("Full Archive"); - } - } -} diff --git a/bottles/frontend/importer_row.py b/bottles/frontend/importer_row.py deleted file mode 100644 index 106bb04af5a..00000000000 --- a/bottles/frontend/importer_row.py +++ /dev/null @@ -1,82 +0,0 @@ -# importer_row.py -# -# Copyright 2025 The Bottles Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from gettext import gettext as _ - -from gi.repository import Gtk, Adw - -from bottles.backend.utils.manager import ManagerUtils -from bottles.backend.utils.threading import RunAsync -from bottles.frontend.gtk import GtkUtils - - -@Gtk.Template(resource_path="/com/usebottles/bottles/importer-row.ui") -class ImporterRow(Adw.ActionRow): - __gtype_name__ = "ImporterRow" - - # region Widgets - label_manager = Gtk.Template.Child() - btn_import = Gtk.Template.Child() - btn_browse = Gtk.Template.Child() - img_lock = Gtk.Template.Child() - - # endregion - - def __init__(self, im_manager, prefix, **kwargs): - super().__init__(**kwargs) - - # common variables and references - self.window = im_manager.window - self.import_manager = im_manager.import_manager - self.prefix = prefix - - # populate widgets - self.set_title(prefix.get("Name")) - self.label_manager.set_text(prefix.get("Manager")) - - if prefix.get("Lock"): - self.img_lock.set_visible(True) - - self.label_manager.add_css_class("tag-%s" % prefix.get("Manager").lower()) - - # connect signals - self.btn_browse.connect("clicked", self.browse_wineprefix) - self.btn_import.connect("clicked", self.import_wineprefix) - - def browse_wineprefix(self, widget): - ManagerUtils.browse_wineprefix(self.prefix) - - def import_wineprefix(self, widget): - @GtkUtils.run_in_main_loop - def set_imported(result, error=False): - self.btn_import.set_visible(result.ok) - self.img_lock.set_visible(result.ok) - - if result.ok: - self.window.show_toast( - _('"{0}" imported').format(self.prefix.get("Name")) - ) - - self.set_sensitive(True) - - self.set_sensitive(False) - - RunAsync( - self.import_manager.import_wineprefix, - callback=set_imported, - wineprefix=self.prefix, - ) diff --git a/bottles/frontend/importer_view.py b/bottles/frontend/importer_view.py deleted file mode 100644 index 4a2256f2202..00000000000 --- a/bottles/frontend/importer_view.py +++ /dev/null @@ -1,167 +0,0 @@ -# importer_view.py -# -# Copyright 2025 The Bottles Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from gettext import gettext as _ - -from gi.repository import Gtk, Adw - -from bottles.backend.managers.backup import BackupManager -from bottles.backend.utils.threading import RunAsync -from bottles.frontend.filters import add_yaml_filters, add_all_filters -from bottles.frontend.gtk import GtkUtils -from bottles.frontend.importer_row import ImporterRow - - -@Gtk.Template(resource_path="/com/usebottles/bottles/importer-view.ui") -class ImporterView(Adw.Bin): - __gtype_name__ = "ImporterView" - - # region Widgets - list_prefixes = Gtk.Template.Child() - btn_find_prefixes = Gtk.Template.Child() - btn_import_config = Gtk.Template.Child() - btn_import_full = Gtk.Template.Child() - btn_back = Gtk.Template.Child() - group_prefixes = Gtk.Template.Child() - status_page = Gtk.Template.Child() - - # endregion - - def __init__(self, window, **kwargs): - super().__init__(**kwargs) - - # common variables and references - self.window = window - self.manager = window.manager - self.import_manager = window.manager.import_manager - - # connect signals - self.btn_back.connect("clicked", self.go_back) - self.btn_find_prefixes.connect("clicked", self.__find_prefixes) - self.btn_import_full.connect("clicked", self.__import_full_bck) - self.btn_import_config.connect("clicked", self.__import_config_bck) - - def __find_prefixes(self, widget): - """ - This function remove all entries from the list_prefixes, ask the - manager to find all prefixes in the system and add them to the list - """ - - @GtkUtils.run_in_main_loop - def update(result, error=False): - widget.set_sensitive(True) - if result.ok: - wineprefixes = result.data.get("wineprefixes") - if len(wineprefixes) == 0: - return - - self.status_page.set_visible(False) - self.group_prefixes.set_visible(True) - - while self.list_prefixes.get_first_child(): - _w = self.list_prefixes.get_first_child() - self.list_prefixes.remove(_w) - - for prefix in result.data.get("wineprefixes"): - self.list_prefixes.append(ImporterRow(self, prefix)) - - widget.set_sensitive(False) - - RunAsync(self.import_manager.search_wineprefixes, callback=update) - - @GtkUtils.run_in_main_loop - def __finish(self, result, error=False): - if result.ok: - self.window.show_toast(_("Backup imported successfully")) - else: - self.window.show_toast(_("Import failed")) - - def __import_full_bck(self, *_args): - """ - This function shows a dialog to the user, from which it can choose an - archive backup to import into Bottles. It supports only .tar.gz files - as Bottles export bottles in this format. Once selected, it will - be imported. - """ - - def set_path(_dialog, response): - if response != Gtk.ResponseType.ACCEPT: - return - - self.window.show_toast(_("Importing backup…")) - RunAsync( - task_func=BackupManager.import_backup, - callback=self.__finish, - scope="full", - path=dialog.get_file().get_path(), - ) - - dialog = Gtk.FileChooserNative.new( - title=_("Select a Backup Archive"), - action=Gtk.FileChooserAction.OPEN, - parent=self.window, - accept_label=_("Import"), - ) - - filter = Gtk.FileFilter() - filter.set_name("GNU Gzip Archive") - # TODO: Investigate why `filter.add_mime_type(...)` does not show filter in all distributions. - # Intended MIME types are: - # - `application/gzip` - filter.add_pattern("*.gz") - - dialog.add_filter(filter) - add_all_filters(dialog) - dialog.set_modal(True) - dialog.connect("response", set_path) - dialog.show() - - def __import_config_bck(self, *_args): - """ - This function shows a dialog to the user, from which it can choose an - archive backup to import into Bottles. It supports only .yml files - which are the Bottles' configuration file. Once selected, it will - be imported. - """ - - def set_path(_dialog, response): - if response != Gtk.ResponseType.ACCEPT: - return - - self.window.show_toast(_("Importing backup…")) - RunAsync( - task_func=BackupManager.import_backup, - callback=self.__finish, - scope="config", - path=dialog.get_file().get_path(), - ) - - dialog = Gtk.FileChooserNative.new( - title=_("Select a Configuration File"), - action=Gtk.FileChooserAction.OPEN, - parent=self.window, - accept_label=_("Import"), - ) - - add_yaml_filters(dialog) - add_all_filters(dialog) - dialog.set_modal(True) - dialog.connect("response", set_path) - dialog.show() - - def go_back(self, *_args): - self.window.main_leaf.navigate(Adw.NavigationDirection.BACK) diff --git a/bottles/frontend/installer-dialog.blp b/bottles/frontend/installer-dialog.blp deleted file mode 100644 index a1f843d7d92..00000000000 --- a/bottles/frontend/installer-dialog.blp +++ /dev/null @@ -1,144 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -template $InstallerDialog: Adw.Window { - modal: true; - deletable: true; - default-width: 500; - default-height: 600; - - Box { - orientation: vertical; - - Adw.HeaderBar { - title-widget: Adw.WindowTitle window_title {}; - - styles [ - "flat", - ] - } - - Stack stack { - vexpand: true; - - StackPage page_init { - name: "page_init"; - - child: Box { - orientation: vertical; - spacing: 10; - valign: center; - halign: fill; - - Image img_icon { - halign: center; - } - - Adw.StatusPage status_init { - description: _("Do you want to proceed with the installation?"); - hexpand: true; - vexpand: true; - - Button btn_install { - label: _("Start Installation"); - halign: center; - - styles [ - "pill", - "suggested-action", - ] - } - } - }; - } - - StackPage page_resources { - name: "page_resources"; - - child: Adw.PreferencesPage { - Adw.PreferencesGroup group_resources { - description: _("This installer requires some local resources which cannot be provided otherwise."); - } - - Button btn_proceed { - label: _("Proceed"); - sensitive: false; - visible: false; - halign: center; - valign: center; - - styles [ - "pill", - "suggested-action", - ] - } - }; - } - - StackPage page_install { - name: "page_install"; - - child: Box { - margin-top: 10; - margin-start: 10; - margin-bottom: 10; - margin-end: 10; - orientation: vertical; - valign: center; - spacing: 5; - - Image img_icon_install { - halign: center; - margin-bottom: 2; - } - - Adw.StatusPage install_status_page { - title: "name"; - description: _("This could take a while."); - - ProgressBar progressbar { - width-request: 300; - halign: center; - margin-top: 10; - margin-bottom: 12; - show-text: true; - - styles [ - "installer", - ] - } - } - }; - } - - StackPage page_installed { - name: "page_installed"; - - child: Adw.StatusPage status_installed { - icon-name: "selection-mode-symbolic"; - title: _("Completed!"); - - Button btn_close { - label: _("Show Programs"); - halign: center; - - styles [ - "pill", - "suggested-action", - ] - } - }; - } - - StackPage page_error { - name: "page_error"; - - child: Adw.StatusPage status_error { - icon-name: "dialog-warning-symbolic"; - title: _("Installation Failed!"); - description: _("Something went wrong."); - }; - } - } - } -} diff --git a/bottles/frontend/installer-row.blp b/bottles/frontend/installer-row.blp deleted file mode 100644 index 50f530d4312..00000000000 --- a/bottles/frontend/installer-row.blp +++ /dev/null @@ -1,76 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -Popover pop_actions { - styles [ - "menu", - ] - - Box { - orientation: vertical; - margin-top: 6; - margin-bottom: 6; - margin-start: 6; - margin-end: 6; - - $GtkModelButton btn_manifest { - text: _("Show Manifest…"); - } - - $GtkModelButton btn_review { - text: _("Read Review…"); - } - - Separator {} - - $GtkModelButton btn_report { - text: _("Report a Bug…"); - } - } -} - -template $InstallerRow: Adw.ActionRow { - activatable-widget: btn_install; - title: _("Installer name"); - subtitle: _("Installer description"); - - Box { - spacing: 6; - - Label label_grade { - valign: center; - label: _("Unknown"); - - styles [ - "tag", - "caption", - ] - } - - Button btn_install { - tooltip-text: _("Install this Program"); - valign: center; - icon-name: "document-save-symbolic"; - - styles [ - "flat", - ] - } - - Separator { - margin-top: 12; - margin-bottom: 12; - } - - MenuButton { - valign: center; - popover: pop_actions; - icon-name: "view-more-symbolic"; - tooltip-text: _("Program Menu"); - - styles [ - "flat", - ] - } - } -} diff --git a/bottles/frontend/installer_dialog.py b/bottles/frontend/installer_dialog.py deleted file mode 100644 index 775c1648ff8..00000000000 --- a/bottles/frontend/installer_dialog.py +++ /dev/null @@ -1,217 +0,0 @@ -# installer_dialog.py -# -# Copyright 2025 The Bottles Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import urllib.request - -from gettext import gettext as _ -from gi.repository import Gtk, GLib, Gio, GdkPixbuf, Adw - -from bottles.backend.utils.threading import RunAsync -from bottles.frontend.gtk import GtkUtils - - -@Gtk.Template(resource_path="/com/usebottles/bottles/local-resource-row.ui") -class LocalResourceRow(Adw.ActionRow): - __gtype_name__ = "LocalResourceRow" - - # region Widgets - btn_path = Gtk.Template.Child() - - # endregion - - def __init__(self, parent, resource, **kwargs): - super().__init__(**kwargs) - - # common variables and references - self.parent = parent - self.resource = resource - - self.set_title(resource) - - # connect signals - self.btn_path.connect("clicked", self.__choose_path) - - def __choose_path(self, *_args): - """ - Open the file chooser dialog and set the path to the - selected file - """ - - def set_path(_dialog, response): - if response != Gtk.ResponseType.ACCEPT: - return - - path = dialog.get_file().get_path() - self.parent.add_resource(self.resource, path) - self.set_subtitle(path) - - dialog = Gtk.FileChooserNative.new( - title=_("Select Resource File"), - action=Gtk.FileChooserAction.OPEN, - parent=self.parent, - ) - - dialog.set_modal(True) - dialog.connect("response", set_path) - dialog.show() - - -@Gtk.Template(resource_path="/com/usebottles/bottles/installer-dialog.ui") -class InstallerDialog(Adw.Window): - __gtype_name__ = "InstallerDialog" - __sections = {} - __steps = 0 - __current_step = 0 - __local_resources = [] - __final_resources = {} - - # region widgets - stack = Gtk.Template.Child() - window_title = Gtk.Template.Child() - btn_install = Gtk.Template.Child() - btn_proceed = Gtk.Template.Child() - btn_close = Gtk.Template.Child() - status_init = Gtk.Template.Child() - status_installed = Gtk.Template.Child() - status_error = Gtk.Template.Child() - progressbar = Gtk.Template.Child() - group_resources = Gtk.Template.Child() - install_status_page = Gtk.Template.Child() - img_icon = Gtk.Template.Child() - img_icon_install = Gtk.Template.Child() - style_provider = Gtk.CssProvider() - - # endregion - - def __init__(self, window, config, installer, **kwargs): - super().__init__(**kwargs) - self.set_transient_for(window) - - self.window = window - self.manager = window.manager - self.config = config - self.installer = installer - - self.__steps_phrases = { - "deps": _("Installing Windows dependencies…"), - "params": _("Configuring the bottle…"), - "steps": _("Processing installer steps…"), - "exe": _("Installing the {}…".format(installer[1].get("Name"))), - "checks": _("Performing final checks…"), - } - - self.status_init.set_title(installer[1].get("Name")) - self.install_status_page.set_title( - _("Installing {0}…").format(installer[1].get("Name")) - ) - self.status_installed.set_description( - _("{0} is now available in the programs view.").format( - installer[1].get("Name") - ) - ) - self.__set_icon() - - self.btn_install.connect("clicked", self.__check_resources) - self.btn_proceed.connect("clicked", self.__install) - self.btn_close.connect("clicked", self.__close) - - def __set_icon(self): - try: - url = self.manager.installer_manager.get_icon_url(self.installer[0]) - if url is None: - self.img_icon.set_visible(False) - self.img_icon_install.set_visible(False) - return - - with urllib.request.urlopen(url) as res: - stream = Gio.MemoryInputStream.new_from_data(res.read(), None) - pixbuf = GdkPixbuf.Pixbuf.new_from_stream(stream, None) - self.img_icon.set_pixel_size(78) - self.img_icon.set_from_pixbuf(pixbuf) - self.img_icon_install.set_pixel_size(78) - self.img_icon_install.set_from_pixbuf(pixbuf) - except: - self.img_icon.set_visible(False) - self.img_icon_install.set_visible(False) - - def __check_resources(self, *_args): - self.__local_resources = self.manager.installer_manager.has_local_resources( - self.installer - ) - if len(self.__local_resources) == 0: - self.__install() - return - - for resource in self.__local_resources: - _entry = LocalResourceRow(self, resource) - GLib.idle_add(self.group_resources.add, _entry) - - self.btn_proceed.set_visible(True) - self.stack.set_visible_child_name("page_resources") - - def __install(self, *_args): - self.set_deletable(False) - self.stack.set_visible_child_name("page_install") - - @GtkUtils.run_in_main_loop - def set_status(result, error=False): - if result.ok: - return self.__installed() - _err = result.data.get("message", _("Installer failed with unknown error")) - self.__error(_err) - - self.set_steps(self.manager.installer_manager.count_steps(self.installer)) - - RunAsync( - task_func=self.manager.installer_manager.install, - callback=set_status, - config=self.config, - installer=self.installer, - step_fn=self.next_step, - local_resources=self.__final_resources, - ) - - def __installed(self): - self.set_deletable(False) - self.stack.set_visible_child_name("page_installed") - self.window.page_details.view_bottle.update_programs() - self.window.page_details.go_back_sidebar() - - def __error(self, error): - self.set_deletable(True) - self.status_error.set_description(error) - self.stack.set_visible_child_name("page_error") - - def next_step(self): - """Next step""" - phrase = self.__steps_phrases[self.__sections[self.__current_step]] - self.progressbar.set_text(phrase) - self.__current_step += 1 - self.progressbar.set_fraction(self.__current_step * (1 / self.__steps)) - - def set_steps(self, steps): - """Set steps""" - self.__steps = steps["total"] - self.__sections = steps["sections"] - - def add_resource(self, resource, path): - self.__final_resources[resource] = path - if len(self.__local_resources) == len(self.__final_resources): - self.btn_proceed.set_sensitive(True) - - def __close(self, *_args): - self.destroy() diff --git a/bottles/frontend/installer_row.py b/bottles/frontend/installer_row.py deleted file mode 100644 index 3e0d4906dfe..00000000000 --- a/bottles/frontend/installer_row.py +++ /dev/null @@ -1,106 +0,0 @@ -# installer_row.py -# -# Copyright 2025 The Bottles Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from gi.repository import Gtk, Adw -from gettext import gettext as _ -import webbrowser - -from bottles.frontend.generic import SourceDialog -from bottles.frontend.installer_dialog import InstallerDialog - - -@Gtk.Template(resource_path="/com/usebottles/bottles/installer-row.ui") -class InstallerRow(Adw.ActionRow): - __gtype_name__ = "InstallerRow" - - # region Widgets - btn_install = Gtk.Template.Child() - btn_review = Gtk.Template.Child() - btn_manifest = Gtk.Template.Child() - btn_report = Gtk.Template.Child() - label_grade = Gtk.Template.Child() - - # endregion - - def __init__(self, window, config, installer, **kwargs): - super().__init__(**kwargs) - - # common variables and references - self.window = window - self.manager = window.manager - self.config = config - self.installer = installer - - grade_descriptions = { - "Bronze": _( - "This application may work poorly. The installer was configured to provide the best possible experience, but expect glitches, instability and lack of working features." - ), - "Silver": _( - "This program works with noticeable glitches, but these glitches do not affect the application's functionality." - ), - "Gold": _("This program works with minor glitches."), - "Platinum": _("This program works perfectly."), - } - - name = installer[1].get("Name") - description = installer[1].get("Description") - grade = installer[1].get("Grade") - grade_description = grade_descriptions[grade] - - # populate widgets - self.set_title(name) - self.set_subtitle(description) - self.label_grade.set_text(grade) - self.label_grade.get_style_context().add_class(f"grade-{grade}") - self.set_tooltip_text(grade_description) - - # connect signals - self.btn_install.connect("clicked", self.__execute_installer) - self.btn_manifest.connect("clicked", self.__open_manifest) - self.btn_review.connect("clicked", self.__open_review) - self.btn_report.connect("clicked", self.__open_bug_report) - - def __open_manifest(self, widget): - """Open installer manifest""" - plain_manifest = self.manager.installer_manager.get_installer( - installer_name=self.installer[0], plain=True - ) - SourceDialog( - parent=self.window, - title=_("Manifest for {0}").format(self.installer[0]), - message=plain_manifest, - ).present() - - def __open_review(self, widget): - """Open review""" - plain_text = self.manager.installer_manager.get_review( - self.installer[0], parse=False - ) - SourceDialog( - parent=self.window, - title=_("Review for {0}").format(self.installer[0]), - message=plain_text, - lang="markdown", - ).present() - - @staticmethod - def __open_bug_report(widget): - """Open bug report""" - webbrowser.open("https://github.com/bottlesdevs/programs/issues") - - def __execute_installer(self, widget): - InstallerDialog(self.window, self.config, self.installer).present() diff --git a/bottles/frontend/journal-dialog.blp b/bottles/frontend/journal-dialog.blp deleted file mode 100644 index 9494e96b715..00000000000 --- a/bottles/frontend/journal-dialog.blp +++ /dev/null @@ -1,80 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -Popover pop_menu { - Box { - orientation: vertical; - spacing: 3; - - $GtkModelButton btn_all { - text: _("All messages"); - } - - $GtkModelButton btn_critical { - text: _("Critical"); - } - - $GtkModelButton btn_error { - text: _("Errors"); - } - - $GtkModelButton btn_warning { - text: _("Warnings"); - } - - $GtkModelButton btn_info { - text: _("Info"); - } - } -} - -template $JournalDialog: Adw.Window { - default-width: 800; - default-height: 600; - destroy-with-parent: true; - - Box { - orientation: vertical; - - Adw.HeaderBar { - title-widget: Adw.WindowTitle { - title: _("Journal Browser"); - }; - - [title] - Box { - SearchEntry search_entry { - placeholder-text: _("Journal Browser"); - } - - MenuButton { - focus-on-click: false; - tooltip-text: _("Change Logging Level."); - popover: pop_menu; - - Label label_filter { - label: _("All"); - } - } - - styles [ - "linked", - ] - } - } - - ScrolledWindow { - hexpand: true; - vexpand: true; - - TreeView tree_view { - reorderable: true; - hexpand: true; - vexpand: true; - - [internal-child selection] - TreeSelection {} - } - } - } -} diff --git a/bottles/frontend/journal_dialog.py b/bottles/frontend/journal_dialog.py deleted file mode 100644 index 5540bc307ee..00000000000 --- a/bottles/frontend/journal_dialog.py +++ /dev/null @@ -1,103 +0,0 @@ -# journal_dialog.py -# -# Copyright 2025 The Bottles Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from gi.repository import Gtk, Adw - -from bottles.backend.managers.journal import JournalManager, JournalSeverity - - -@Gtk.Template(resource_path="/com/usebottles/bottles/journal-dialog.ui") -class JournalDialog(Adw.Window): - __gtype_name__ = "JournalDialog" - - # region Widgets - tree_view = Gtk.Template.Child() - search_entry = Gtk.Template.Child() - btn_all = Gtk.Template.Child() - btn_critical = Gtk.Template.Child() - btn_error = Gtk.Template.Child() - btn_warning = Gtk.Template.Child() - btn_info = Gtk.Template.Child() - label_filter = Gtk.Template.Child() - - # endregion - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - self.journal = JournalManager.get().items() - self.store = Gtk.ListStore(str, str, str) - - # connect signals - self.search_entry.connect("search-changed", self.on_search_changed) - self.btn_all.connect("clicked", self.filter_results, "") - self.btn_critical.connect( - "clicked", self.filter_results, JournalSeverity.CRITICAL - ) - self.btn_error.connect("clicked", self.filter_results, JournalSeverity.ERROR) - self.btn_warning.connect( - "clicked", self.filter_results, JournalSeverity.WARNING - ) - self.btn_info.connect("clicked", self.filter_results, JournalSeverity.INFO) - - self.populate_tree_view() - - def populate_tree_view(self, query="", severity=""): - self.store.clear() - - colors = { - JournalSeverity.CRITICAL: "#db1600", - JournalSeverity.ERROR: "#db6600", - JournalSeverity.WARNING: "#dba100", - JournalSeverity.INFO: "#3283a8", - JournalSeverity.CRASH: "#db1600", - } - - for _, value in self.journal: - if query.lower() in value["message"].lower() and ( - severity == "" or severity == value["severity"] - ): - self.store.append( - [ - '{}'.format( - colors[value["severity"]], value["severity"].capitalize() - ), - value["timestamp"], - value["message"], - ] - ) - - self.tree_view.set_model(self.store) - self.tree_view.set_search_column(1) - - self.tree_view.append_column( - Gtk.TreeViewColumn("Severity", Gtk.CellRendererText(), markup=0) - ) - self.tree_view.append_column( - Gtk.TreeViewColumn("Timestamp", Gtk.CellRendererText(), text=1) - ) - self.tree_view.append_column( - Gtk.TreeViewColumn("Message", Gtk.CellRendererText(), text=2) - ) - - def on_search_changed(self, entry): - self.populate_tree_view(entry.get_text()) - - def filter_results(self, _, severity): - self.populate_tree_view(self.search_entry.get_text(), severity) - label = severity if severity != "" else "all" - self.label_filter.set_text(label.capitalize()) diff --git a/bottles/frontend/launch-options-dialog.blp b/bottles/frontend/launch-options-dialog.blp index e107cab0221..ebd35d227ec 100644 --- a/bottles/frontend/launch-options-dialog.blp +++ b/bottles/frontend/launch-options-dialog.blp @@ -140,37 +140,6 @@ template $LaunchOptionsDialog: Adw.Window { } } } - - Adw.ActionRow action_midi_soundfont { - activatable-widget: btn_midi_soundfont; - title: _("MIDI SoundFont"); - subtitle: _("Choose a custom SoundFont for MIDI playback."); - - Box { - spacing: 6; - - Button btn_midi_soundfont_reset { - tooltip-text: _("Reset to Default"); - valign: center; - visible: false; - icon-name: "edit-undo-symbolic"; - - styles [ - "flat", - ] - } - - Button btn_midi_soundfont { - tooltip-text: _("Choose a SoundFont"); - valign: center; - icon-name: "document-open-symbolic"; - - styles [ - "flat", - ] - } - } - } } Adw.PreferencesGroup { diff --git a/bottles/frontend/launch_options_dialog.py b/bottles/frontend/launch_options_dialog.py index 97c4dc9b25b..4e3e7e2123f 100644 --- a/bottles/frontend/launch_options_dialog.py +++ b/bottles/frontend/launch_options_dialog.py @@ -18,12 +18,9 @@ from gi.repository import Gtk, GLib, GObject, Adw from bottles.backend.utils.manager import ManagerUtils -from bottles.backend.logger import Logger -from bottles.frontend.filters import add_all_filters, add_soundfont_filters +import logging from gettext import gettext as _ -logging = Logger() - @Gtk.Template(resource_path="/com/usebottles/bottles/launch-options-dialog.ui") class LaunchOptionsDialog(Adw.Window): @@ -41,12 +38,9 @@ class LaunchOptionsDialog(Adw.Window): btn_post_script_reset = Gtk.Template.Child() btn_cwd = Gtk.Template.Child() btn_cwd_reset = Gtk.Template.Child() - btn_midi_soundfont = Gtk.Template.Child() - btn_midi_soundfont_reset = Gtk.Template.Child() btn_reset_defaults = Gtk.Template.Child() action_pre_script = Gtk.Template.Child() action_post_script = Gtk.Template.Child() - action_midi_soundfont = Gtk.Template.Child() switch_dxvk = Gtk.Template.Child() switch_vkd3d = Gtk.Template.Child() switch_nvapi = Gtk.Template.Child() @@ -65,7 +59,6 @@ class LaunchOptionsDialog(Adw.Window): __default_pre_script_msg = _("Choose a script which should be executed before run.") __default_post_script_msg = _("Choose a script which should be executed after run.") __default_cwd_msg = _("Choose from where start the program.") - __default_midi_soundfont_msg = _("Choose a custom SoundFont for MIDI playback.") __msg_disabled = _("{0} is disabled globally for this bottle.") __msg_override = _("This setting overrides the bottle's global setting.") @@ -111,8 +104,6 @@ def __init__(self, parent, config, program, **kwargs): self.btn_pre_script_reset.connect("clicked", self.__reset_pre_script) self.btn_post_script.connect("clicked", self.__choose_post_script) self.btn_post_script_reset.connect("clicked", self.__reset_post_script) - self.btn_midi_soundfont.connect("clicked", self.__choose_midi_soundfont) - self.btn_midi_soundfont_reset.connect("clicked", self.__reset_midi_soundfont) self.btn_cwd.connect("clicked", self.__choose_cwd) self.btn_cwd_reset.connect("clicked", self.__reset_cwd) self.btn_reset_defaults.connect("clicked", self.__reset_defaults) @@ -192,10 +183,6 @@ def __init__(self, parent, config, program, **kwargs): self.action_cwd.set_subtitle(program["folder"]) self.btn_cwd_reset.set_visible(True) - if program.get("midi_soundfont") not in ["", None]: - self.action_midi_soundfont.set_subtitle(program["midi_soundfont"]) - self.btn_midi_soundfont_reset.set_visible(True) - self.__set_disabled_switches() def __check_override(self, widget, state, action, name): @@ -355,47 +342,6 @@ def __reset_cwd(self, *_args): self.action_cwd.set_subtitle(self.__default_cwd_msg) self.btn_cwd_reset.set_visible(False) - def __choose_midi_soundfont(self, *_args): - def set_path(dialog, result): - try: - file = dialog.open_finish(result) - if file is None: - self.action_midi_soundfont.set_subtitle( - self.__default_midi_soundfont_msg - ) - return - - file_path = file.get_path() - self.program["midi_soundfont"] = file_path - self.action_midi_soundfont.set_subtitle(file_path) - self.btn_midi_soundfont_reset.set_visible(True) - - except GLib.Error as error: - # also thrown when dialog has been cancelled - if error.code == 2: - # error 2 seems to be 'dismiss' or 'cancel' - if self.program["midi_soundfont"] in (None, ""): - self.action_midi_soundfont.set_subtitle( - self.__default_midi_soundfont_msg - ) - else: - # something else happened... - logging.warning("Error selecting SoundFont file: %s" % error) - - dialog = Gtk.FileDialog.new() - dialog.set_title(_("Select MIDI SoundFont")) - dialog.set_modal(True) - - add_soundfont_filters(dialog) - add_all_filters(dialog) - - dialog.open(parent=self.window, callback=set_path) - - def __reset_midi_soundfont(self, *_args): - self.program["midi_soundfont"] = None - self.action_midi_soundfont.set_subtitle(self.__default_midi_soundfont_msg) - self.btn_midi_soundfont_reset.set_visible(False) - def __reset_defaults(self, *_args): self.switch_dxvk.set_active(self.global_dxvk) self.switch_vkd3d.set_active(self.global_vkd3d) diff --git a/bottles/frontend/library-entry.blp b/bottles/frontend/library-entry.blp deleted file mode 100644 index 9d83e522032..00000000000 --- a/bottles/frontend/library-entry.blp +++ /dev/null @@ -1,158 +0,0 @@ -using Gtk 4.0; - -template $LibraryEntry: Box { - orientation: vertical; - width-request: 128; - height-request: 348; - overflow: hidden; - - Overlay overlay { - width-request: 226; - height-request: 348; - vexpand: true; - hexpand: true; - - [overlay] - Box { - orientation: vertical; - hexpand: true; - vexpand: true; - - Picture img_cover { - visible: false; - hexpand: true; - vexpand: true; - content-fit: cover; - } - - Label label_no_cover { - halign: center; - valign: center; - hexpand: true; - vexpand: true; - label: _("No Thumbnail"); - wrap: true; - wrap-mode: word_char; - - styles [ - "dim-label", - ] - } - } - - [overlay] - Revealer revealer_run { - reveal-child: false; - transition-type: crossfade; - valign: center; - - Box { - valign: center; - halign: center; - - Button btn_run { - valign: center; - halign: center; - label: _("Launch"); - - styles [ - "osd", - "pill", - ] - } - - [overlay] - Button btn_launch_steam { - valign: center; - halign: center; - visible: false; - label: _("Launch with Steam"); - - styles [ - "osd", - "pill", - ] - } - } - } - - [overlay] - Revealer revealer_details { - reveal-child: false; - transition-type: crossfade; - valign: end; - - Box { - spacing: 6; - halign: fill; - valign: end; - vexpand: true; - margin-bottom: 10; - margin-start: 10; - margin-end: 10; - - styles [ - "osd", - "toolbar", - "library-entry-details", - ] - - Box { - orientation: vertical; - hexpand: true; - valign: center; - - Label label_name { - halign: start; - label: _("Item name"); - max-width-chars: 20; - ellipsize: middle; - - styles [ - "title", - ] - } - - Label label_bottle { - halign: start; - label: _("Bottle name"); - max-width-chars: 20; - ellipsize: middle; - - styles [ - "caption", - ] - } - } - - [end] - Box { - Button btn_remove { - halign: center; - icon-name: "user-trash-symbolic"; - tooltip-text: _("Remove from Library"); - - styles [ - "flat" - ] - } - - Button btn_stop { - visible: false; - halign: center; - icon-name: "media-playback-stop-symbolic"; - tooltip-text: _("Stop"); - - styles [ - "flat" - ] - } - } - } - } - } - - styles [ - "card", - ] -} diff --git a/bottles/frontend/library-view.blp b/bottles/frontend/library-view.blp deleted file mode 100644 index 3e3289b8832..00000000000 --- a/bottles/frontend/library-view.blp +++ /dev/null @@ -1,36 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -template $LibraryView: Adw.Bin { - Box { - orientation: vertical; - - Adw.StatusPage status_page { - vexpand: true; - hexpand: true; - icon-name: "library-symbolic"; - title: _("Library"); - description: _("Add items here from your bottle\'s program list"); - } - - ScrolledWindow scroll_window { - hexpand: true; - vexpand: true; - - FlowBox main_flow { - max-children-per-line: bind template.items_per_line; - row-spacing: 5; - column-spacing: 5; - halign: center; - valign: start; - margin-top: 5; - margin-start: 5; - margin-bottom: 5; - margin-end: 5; - homogeneous: true; - selection-mode: none; - activate-on-single-click: false; - } - } - } -} diff --git a/bottles/frontend/library_entry.py b/bottles/frontend/library_entry.py deleted file mode 100644 index 9679d98b746..00000000000 --- a/bottles/frontend/library_entry.py +++ /dev/null @@ -1,201 +0,0 @@ -# library_entry.py -# -# Copyright 2025 The Bottles Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from gettext import gettext as _ - -from gi.repository import Gtk, Gdk - -from bottles.backend.logger import Logger -from bottles.backend.managers.library import LibraryManager -from bottles.backend.managers.thumbnail import ThumbnailManager -from bottles.backend.models.result import Result -from bottles.backend.utils.threading import RunAsync -from bottles.backend.wine.executor import WineExecutor -from bottles.backend.wine.winedbg import WineDbg -from bottles.frontend.gtk import GtkUtils - -logging = Logger() - - -@Gtk.Template(resource_path="/com/usebottles/bottles/library-entry.ui") -class LibraryEntry(Gtk.Box): - __gtype_name__ = "LibraryEntry" - - # region Widgets - btn_run = Gtk.Template.Child() - btn_stop = Gtk.Template.Child() - btn_launch_steam = Gtk.Template.Child() - btn_remove = Gtk.Template.Child() - label_name = Gtk.Template.Child() - label_bottle = Gtk.Template.Child() - label_no_cover = Gtk.Template.Child() - img_cover = Gtk.Template.Child() - revealer_run = Gtk.Template.Child() - revealer_details = Gtk.Template.Child() - overlay = Gtk.Template.Child() - - # endregion - - def __init__(self, library, uuid, entry, *args, **kwargs): - super().__init__(*args, **kwargs) - self.library = library - self.window = library.window - self.manager = library.window.manager - self.name = entry["name"] - self.uuid = uuid - self.entry = entry - self.config = self.__get_config() - - # This happens when a Library entry is an "orphan" (no bottles associated) - if self.config is None: - library_manager = LibraryManager() - library_manager.remove_from_library(self.uuid) - raise Exception - - self.program = self.__get_program() - - if len(entry["name"]) >= 15: - name = entry["name"][:13] + "…" - else: - name = entry["name"] - - self.label_name.set_text(name) - self.label_bottle.set_text(entry["bottle"]["name"]) - self.label_no_cover.set_label(self.name) - - if entry.get("thumbnail"): - path = ThumbnailManager.get_path(self.config, entry["thumbnail"]) - - if path is None: - # redownloading *should* never fail as it was successfully downloaded before - logging.info("Redownloading grid image...") - library_manager = LibraryManager() - result = library_manager.download_thumbnail(self.uuid, self.config) - if result: - entry = library_manager.get_library().get(uuid) - path = ThumbnailManager.get_path(self.config, entry["thumbnail"]) - - if path is not None: - # Gtk.Picture.set_pixbuf deprecated in GTK 4.12 - texture = Gdk.Texture.new_from_filename(path) - self.img_cover.set_paintable(texture) - self.img_cover.set_visible(True) - self.label_no_cover.set_visible(False) - - motion_ctrl = Gtk.EventControllerMotion.new() - motion_ctrl.connect("enter", self.__on_motion_enter) - motion_ctrl.connect("leave", self.__on_motion_leave) - self.overlay.add_controller(motion_ctrl) - self.btn_run.connect("clicked", self.run_executable) - self.btn_launch_steam.connect("clicked", self.run_steam) - self.btn_stop.connect("clicked", self.stop_process) - self.btn_remove.connect("clicked", self.__remove_entry) - - def __get_config(self): - bottles = self.manager.local_bottles - if self.entry["bottle"]["name"] in bottles: - return bottles[self.entry["bottle"]["name"]] - parent = self.get_parent() - if parent: - parent.remove(self) # TODO: Remove from list - - def __get_program(self): - programs = self.manager.get_programs(self.config) - programs = [ - p - for p in programs - if p["id"] == self.entry["id"] or p["name"] == self.entry["name"] - ] - if len(programs) == 0: - return None # TODO: remove entry from library - return programs[0] - - @GtkUtils.run_in_main_loop - def __reset_buttons(self, result: Result | bool = None, error=False): - match result: - case Result(): - status = result.status - case bool(): - status = result - case _: - logging.error( - f"result should be Result or bool, but it was {type(result)}" - ) - status = False - - self.btn_remove.set_visible(status) - self.btn_stop.set_visible(not status) - self.btn_run.set_visible(status) - - def __is_alive(self): - winedbg = WineDbg(self.config) - - @GtkUtils.run_in_main_loop - def set_watcher(result=False, error=False): - nonlocal winedbg - self.__reset_buttons() - - RunAsync( - winedbg.wait_for_process, - callback=self.__reset_buttons, - name=self.program["executable"], - timeout=5, - ) - - RunAsync( - winedbg.is_process_alive, - callback=set_watcher, - name=self.program["executable"], - ) - - def __remove_entry(self, *args): - self.library.remove_entry(self) - - def run_executable(self, widget, with_terminal=False): - self.window.show_toast(_('Launching "{0}"…').format(self.program["name"])) - RunAsync( - WineExecutor.run_program, - callback=self.__reset_buttons, - config=self.config, - program=self.program, - ) - self.__reset_buttons() - - def run_steam(self, widget): - self.manager.steam_manager.launch_app(self.config.CompatData) - - def stop_process(self, widget): - self.window.show_toast(_('Stopping "{0}"…').format(self.program["name"])) - winedbg = WineDbg(self.config) - winedbg.kill_process(name=self.program["executable"]) - self.__reset_buttons(True) - - def __on_motion_enter(self, *args): - self.revealer_run.set_reveal_child(True) - self.revealer_details.set_reveal_child(True) - - def __on_motion_leave(self, *args): - self.revealer_run.set_reveal_child(False) - self.revealer_details.set_reveal_child(False) - - # hide() and show() are essentialy workarounds to avoid keeping - # the empty space of the hidden entry in the GtkFlowBox - def hide(self): - self.get_parent().set_visible(False) - - def show(self): - self.get_parent().set_visible(True) diff --git a/bottles/frontend/library_view.py b/bottles/frontend/library_view.py deleted file mode 100644 index c3cba16347b..00000000000 --- a/bottles/frontend/library_view.py +++ /dev/null @@ -1,90 +0,0 @@ -# library_view.py -# -# Copyright 2025 The Bottles Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import contextlib -from gettext import gettext as _ - -from gi.repository import Gtk, Adw, GObject - -from bottles.backend.managers.library import LibraryManager -from bottles.frontend.gtk import GtkUtils -from bottles.frontend.library_entry import LibraryEntry - - -@Gtk.Template(resource_path="/com/usebottles/bottles/library-view.ui") -class LibraryView(Adw.Bin): - __gtype_name__ = "LibraryView" - - # region Widgets - scroll_window = Gtk.Template.Child() - main_flow = Gtk.Template.Child() - status_page = Gtk.Template.Child() - style_provider = Gtk.CssProvider() - # endregion - - items_per_line = GObject.property(type=int, default=0) # type: ignore - - def __init__(self, window, **kwargs): - super().__init__(**kwargs) - self.window = window - self.css = b"" - self.update() - - def update(self): - library_manager = LibraryManager() - entries = library_manager.get_library() - - while self.main_flow.get_first_child() is not None: - self.main_flow.remove(self.main_flow.get_first_child()) - - self.status_page.set_visible(len(entries) == 0) - self.scroll_window.set_visible(not len(entries) == 0) - - self.items_per_line = len(entries) - - for u, e in entries.items(): - # We suppress exceptions so that it doesn't continue if the init fails - with contextlib.suppress(Exception): - entry = LibraryEntry(self, u, e) - self.main_flow.append(entry) - - def remove_entry(self, entry): - @GtkUtils.run_in_main_loop - def undo_callback(*args): - self.items_per_line += 1 - entry.show() - - @GtkUtils.run_in_main_loop - def dismissed_callback(*args): - self.__delete_entry(entry) - - entry.hide() - self.items_per_line -= 1 - self.window.show_toast( - message=_('"{0}" removed from the library.').format(entry.name), - timeout=5, - action_label=_("Undo"), - action_callback=undo_callback, - dismissed_callback=dismissed_callback, - ) - - def __delete_entry(self, entry): - library_manager = LibraryManager() - library_manager.remove_from_library(entry.uuid) - - def go_back(self, widget=False): - self.window.main_leaf.navigate(Adw.NavigationDirection.BACK) diff --git a/bottles/frontend/loading-view.blp b/bottles/frontend/loading-view.blp deleted file mode 100644 index 5967a8d55e9..00000000000 --- a/bottles/frontend/loading-view.blp +++ /dev/null @@ -1,45 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -template $LoadingView: Adw.Bin { - WindowHandle { - hexpand: true; - vexpand: true; - - Box { - orientation: vertical; - - Adw.StatusPage loading_status_page { - title: _("Starting up…"); - hexpand: true; - vexpand: true; - } - - Button btn_go_offline { - margin-bottom: 20; - valign: center; - halign: center; - label: _("Continue Offline"); - - styles [ - "destructive-action", - "pill", - ] - } - - Label label_fetched { - styles [ - "dim-label", - ] - } - - Label label_downloading { - margin-bottom: 20; - - styles [ - "dim-label", - ] - } - } - } -} diff --git a/bottles/frontend/loading_view.py b/bottles/frontend/loading_view.py deleted file mode 100644 index 330f26ac469..00000000000 --- a/bottles/frontend/loading_view.py +++ /dev/null @@ -1,57 +0,0 @@ -# loading_view.py -# -# Copyright 2025 The Bottles Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from gettext import gettext as _ - -from gi.repository import Gtk, Adw - -from bottles.backend.models.result import Result -from bottles.backend.state import SignalManager, Signals -from bottles.frontend.gtk import GtkUtils -from bottles.frontend.params import APP_ID - - -@Gtk.Template(resource_path="/com/usebottles/bottles/loading-view.ui") -class LoadingView(Adw.Bin): - __gtype_name__ = "LoadingView" - __fetched = 0 - - # region widgets - label_fetched = Gtk.Template.Child() - label_downloading = Gtk.Template.Child() - btn_go_offline = Gtk.Template.Child() - loading_status_page = Gtk.Template.Child() - # endregion - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.loading_status_page.set_icon_name(APP_ID) - self.btn_go_offline.connect("clicked", self.go_offline) - - @GtkUtils.run_in_main_loop - def add_fetched(self, res: Result): - total: int = res.data - self.__fetched += 1 - self.label_downloading.set_text( - _("Downloading ~{0} of packages…").format("20kb") - ) - self.label_fetched.set_text( - _("Fetched {0} of {1} packages").format(self.__fetched, total) - ) - - def go_offline(self, _widget): - SignalManager.send(Signals.ForceStopNetworking, Result(status=True)) diff --git a/bottles/frontend/main.py b/bottles/frontend/main.py index 35855da8726..c56df8f59dc 100644 --- a/bottles/frontend/main.py +++ b/bottles/frontend/main.py @@ -15,15 +15,15 @@ # along with this program. If not, see . # +import os import sys import gi -import gettext -import locale import webbrowser -from os import path +from gettext import gettext as _ -from bottles.backend.logger import Logger +import logging from bottles.backend.health import HealthChecker +from bottles.backend.models.config import BottleConfig from bottles.frontend.params import ( APP_ID, APP_MAJOR_VERSION, @@ -40,50 +40,16 @@ # ruff: noqa: E402 from gi.repository import Gio, GLib, GObject, Adw # type: ignore from bottles.frontend.window import BottlesWindow +from bottles.backend.bottle import Bottle from bottles.frontend.preferences import PreferencesWindow -logging = Logger() - -# region Translations -""" -This code snippet searches for and uploads translations to different -directories, depending on your production or development environment. -The function _() can be used to create and retrieve translations. -""" -share_dir = path.join(sys.prefix, "share") -base_dir = "." - -if getattr(sys, "frozen", False): - base_dir = path.dirname(sys.executable) - share_dir = path.join(base_dir, "share") -elif sys.argv[0]: - exec_dir = path.dirname(path.realpath(sys.argv[0])) - base_dir = path.dirname(exec_dir) - share_dir = path.join(base_dir, "share") - - if not path.exists(share_dir): - share_dir = base_dir - -locale_dir = path.join(share_dir, "locale") - -if not path.exists(locale_dir): # development - locale_dir = path.join(base_dir, "build", "mo") - -locale.bindtextdomain("bottles", locale_dir) -locale.textdomain("bottles") -gettext.bindtextdomain("bottles", locale_dir) -gettext.textdomain("bottles") -_ = gettext.gettext - - -# endregion - - class Bottles(Adw.Application): arg_exe = None arg_bottle = None dark_provider = None + local_bottles: dict[str, BottleConfig] = {} + bottles_config_dir = os.path.join(GLib.get_user_data_dir(), "bottles", "bottles") def __init__(self): super().__init__( @@ -94,13 +60,14 @@ def __init__(self): ) self.__create_action("quit", self.__quit, ["q", "w"]) self.__create_action("about", self.__show_about_dialog) - self.__create_action("import", self.__show_importer_view, ["i"]) self.__create_action("preferences", self.__show_preferences, ["comma"]) self.__create_action("help", self.__help, ["F1"]) self.__create_action("new", self.__new_bottle, ["n"]) self.__register_arguments() + self.local_bottles = Bottle.generate_local_bottles_list(self.bottles_config_dir) + def __register_arguments(self): """ This function registers the command line arguments. @@ -254,7 +221,7 @@ def do_activate(self): Adw.Application.do_activate(self) win = self.props.active_window if not win: - win = BottlesWindow(application=self, arg_bottle=self.arg_bottle) + win = BottlesWindow(application=self) self.win = win win.present() @@ -281,16 +248,6 @@ def __help(action=None, param=None): ) webbrowser.open_new_tab("https://docs.usebottles.com") - def __refresh(self, action=None, param=None): - """ - This function refresh the user bottle list. - It is used by the [Ctrl+R] shortcut. - """ - logging.info( - _("[Refresh] request received."), - ) - self.win.manager.update_bottles() - def __show_preferences(self, *args): preferences_window = PreferencesWindow(self.win) preferences_window.present() @@ -298,9 +255,6 @@ def __show_preferences(self, *args): def __new_bottle(self, *args): self.win.show_add_view() - def __show_importer_view(self, widget=False, *args): - self.win.main_leaf.set_visible_child(self.win.page_importer) - def __show_about_dialog(self, *_args): developers = [ "Mirko Brombin https://github.com/mirkobrombin", diff --git a/bottles/frontend/mangohud_dialog.py b/bottles/frontend/mangohud_dialog.py index 7690c2ac539..4f35dfaa263 100644 --- a/bottles/frontend/mangohud_dialog.py +++ b/bottles/frontend/mangohud_dialog.py @@ -16,9 +16,6 @@ # along with this program. If not, see . from gi.repository import Gtk, GLib, Adw -from bottles.backend.logger import Logger - -logging = Logger() @Gtk.Template(resource_path="/com/usebottles/bottles/mangohud-dialog.ui") diff --git a/bottles/frontend/meson.build b/bottles/frontend/meson.build index 5bf84dd5d2c..0dcbce59083 100644 --- a/bottles/frontend/meson.build +++ b/bottles/frontend/meson.build @@ -15,26 +15,19 @@ blueprints = custom_target('blueprints', 'dependency-entry-row.blp', 'bottle-details-page.blp', 'details-dependencies-view.blp', - 'details-installers-view.blp', 'details-task-manager-view.blp', - 'details-versioning-page.blp', 'bottle-details-view.blp', 'bottle-picker-dialog.blp', 'crash-report-dialog.blp', 'dependencies-check-dialog.blp', 'dll-overrides-dialog.blp', 'drives-dialog.blp', - 'duplicate-dialog.blp', 'environment-variables-dialog.blp', - 'exclusion-patterns-dialog.blp', 'gamescope-dialog.blp', - 'installer-dialog.blp', - 'journal-dialog.blp', 'launch-options-dialog.blp', 'proton-alert-dialog.blp', 'rename-program-dialog.blp', 'sandbox-dialog.blp', - 'upgrade-versioning-dialog.blp', 'vkbasalt-dialog.blp', 'display-dialog.blp', 'vmtouch-dialog.blp', @@ -43,21 +36,13 @@ blueprints = custom_target('blueprints', 'dll-override-entry.blp', 'drive-entry.blp', 'env-var-entry.blp', - 'exclusion-pattern-row.blp', - 'importer-row.blp', - 'importer-view.blp', - 'installer-row.blp', - 'library-entry.blp', - 'library-view.blp', 'bottle-row.blp', 'bottles-list-view.blp', - 'loading-view.blp', 'local-resource-row.blp', 'new-bottle-dialog.blp', 'onboard-dialog.blp', 'preferences.blp', 'program-row.blp', - 'state-row.blp', 'task-row.blp', 'window.blp', 'details-preferences-page.blp', @@ -74,15 +59,6 @@ gnome.compile_resources('bottles', install_dir: pkgdatadir, ) -configure_file( - input: 'cli.py', - output: 'bottles-cli', - configuration: conf, - install: true, - install_dir: get_option('bindir'), - install_mode: ['rwxr-xr-x'] -) - configure_file( input: 'bottles.py', output: 'bottles', @@ -108,28 +84,18 @@ bottles_sources = [ 'sh.py', 'new_bottle_dialog.py', 'bottles_list_view.py', - 'library_view.py', 'bottle_details_view.py', 'preferences.py', - 'importer_view.py', - 'loading_view.py', 'bottle_details_page.py', - 'details_installers_view.py', 'details_dependencies_view.py', 'details_preferences_page.py', - 'details_versioning_page.py', 'details_task_manager_view.py', 'dependency_entry_row.py', 'executable.py', - 'importer_row.py', - 'installer_row.py', 'program_row.py', - 'state_row.py', 'component_entry_row.py', - 'library_entry.py', 'crash_report_dialog.py', 'dll_overrides_dialog.py', - 'duplicate_dialog.py', 'environment_variables_dialog.py', 'generic.py', 'launch_options_dialog.py', @@ -142,14 +108,10 @@ bottles_sources = [ 'mangohud_dialog.py', 'display_dialog.py', 'generic_cli.py', - 'journal_dialog.py', 'bottle_picker_dialog.py', 'proton_alert_dialog.py', 'sandbox_dialog.py', - 'installer_dialog.py', 'dependencies_check_dialog.py', - 'exclusion_patterns_dialog.py', - 'upgrade_versioning_dialog.py', 'vmtouch_dialog.py', 'window.py', params_file, diff --git a/bottles/frontend/new-bottle-dialog.blp b/bottles/frontend/new-bottle-dialog.blp index d13cc8e8949..d15cb858842 100644 --- a/bottles/frontend/new-bottle-dialog.blp +++ b/bottles/frontend/new-bottle-dialog.blp @@ -69,7 +69,7 @@ template $NewBottleDialog: Adw.Dialog { $CheckRow application { title: _("_Application"); - environment: "application"; + environment: "Application"; subtitle: _("Optimized for productivity software"); icon-name: "applications-engineering-symbolic"; use-underline: true; @@ -78,7 +78,7 @@ template $NewBottleDialog: Adw.Dialog { $CheckRow { title: _("_Gaming"); - environment: "gaming"; + environment: "Gaming"; subtitle: _("Optimized for games, game engines, and 3D apps"); icon-name: "input-gaming-symbolic"; use-underline: true; @@ -87,7 +87,7 @@ template $NewBottleDialog: Adw.Dialog { $CheckRow custom { title: _("C_ustom"); - environment: "custom"; + environment: "Custom"; subtitle: _("A clean state intended for specific use cases"); icon-name: "applications-science-symbolic"; use-underline: true; diff --git a/bottles/frontend/new_bottle_dialog.py b/bottles/frontend/new_bottle_dialog.py index 7cb47c9dbfb..34e46cdd276 100644 --- a/bottles/frontend/new_bottle_dialog.py +++ b/bottles/frontend/new_bottle_dialog.py @@ -15,6 +15,9 @@ # along with this program. If not, see . # +import os +import subprocess + from gettext import gettext as _ from typing import Any from gi.repository import Gtk, Adw, Pango, Gio, Xdp, GObject, GLib @@ -84,13 +87,34 @@ def __init__(self, **kwargs: Any) -> None: return self.app = self.window.get_application() - self.manager = self.window.manager self.new_bottle_config = BottleConfig() + self.available_runners = [] + self.available_dxvk_versions = [] self.env_recipe_path = None self.custom_path = "" - self.runner = None self.default_string = _("(Default)") + try: + wine_version = subprocess.check_output(["wine", "--version"], text=True) + wine_version = "sys-" + wine_version.split("\n")[0].split(" ")[0] + self.available_runners.append(wine_version) + except FileNotFoundError: + pass + + self.available_dxvk_versions = self.__get_available_versions_from_component( + "dxvk" + ) + self.available_nvapi_versions = self.__get_available_versions_from_component( + "nvapi" + ) + self.available_vkd3d_versions = self.__get_available_versions_from_component( + "vkd3d" + ) + self.available_runners = ( + self.available_runners + + self.__get_available_versions_from_component("runners") + ) + self.arch = {"win64": "64-bit", "win32": "32-bit"} # connect signals @@ -112,16 +136,28 @@ def __init__(self, **kwargs: Any) -> None: # Populate widgets self.label_choose_env.set_label(self.default_string) self.label_choose_path.set_label(self.default_string) - self.str_list_runner.splice(0, 0, self.manager.runners_available) + self.str_list_runner.splice(0, 0, self.available_runners) self.str_list_arch.splice(0, 0, list(self.arch.values())) self.selected_environment = ( self.environment_list_box.get_first_child().environment ) + def __get_available_versions_from_component(self, component: str) -> list[str]: + component_dir = os.path.join(GLib.get_user_data_dir(), "bottles", component) + return os.listdir(component_dir) + + def __get_path(self) -> str: + if self.custom_path: + return self.custom_path + else: + return os.path.join( + GLib.get_user_data_dir(), "bottles", self.entry_name.get_text() + ) + def __check_validity(self, *_args: Any) -> tuple[bool, bool]: is_empty = self.entry_name.get_text() == "" - is_duplicate = self.entry_name.get_text() in self.manager.local_bottles + is_duplicate = self.entry_name.get_text() in self.app.local_bottles return (is_empty, is_duplicate) def __check_entry_name(self, *_args: Any) -> None: @@ -202,24 +238,33 @@ def set_path(dialog, result): def create_bottle(self, *_args: Any) -> None: """Starts creating the bottle.""" # set widgets states - self.set_can_close(False) + # self.set_can_close(False) TODO: UNCOMMENT self.stack_create.set_visible_child_name("page_creating") - self.runner = self.manager.runners_available[self.combo_runner.get_selected()] - - RunAsync( - task_func=self.manager.create_bottle, - callback=self.finish, - name=self.entry_name.get_text(), - path=self.custom_path, - environment=self.selected_environment, - runner=self.runner, - arch=list(self.arch)[self.combo_arch.get_selected()], - dxvk=self.manager.dxvk_available[0], - fn_logger=self.update_output, - custom_environment=self.env_recipe_path, + config = BottleConfig( + Name=self.entry_name.get_text(), + Arch=list(self.arch)[self.combo_arch.get_selected()], + Runner=self.available_runners[self.combo_runner.get_selected()], + Custom_Path=bool(self.custom_path), + Path=os.path.join(self.custom_path, self.entry_name.get_text()), + Environment=self.selected_environment, ) + print(config) + + # RunAsync( + # task_func=self.manager.create_bottle, + # callback=self.finish, + # name=self.entry_name.get_text(), + # path=self.custom_path, + # environment=self.selected_environment, + # runner=runner, + # arch=list(self.arch)[self.combo_arch.get_selected()], + # dxvk=self.available_dxvk_versions[0], + # fn_logger=self.update_output, + # custom_environment=self.env_recipe_path, + # ) + @GtkUtils.run_in_main_loop def update_output(self, text: str) -> None: """ diff --git a/bottles/frontend/preferences.blp b/bottles/frontend/preferences.blp index c1fcf649f00..c90ca549d61 100644 --- a/bottles/frontend/preferences.blp +++ b/bottles/frontend/preferences.blp @@ -49,16 +49,6 @@ template $PreferencesWindow: Adw.PreferencesWindow { } } - Adw.ActionRow { - title: _("Temp Files"); - subtitle: _("Clean temp files when Bottles launches?"); - activatable-widget: switch_temp; - - Switch switch_temp { - valign: center; - } - } - Adw.ActionRow { title: _("Close Bottles After Starting a Program"); subtitle: _("Close Bottles after starting a program from the file manager."); @@ -103,26 +93,6 @@ template $PreferencesWindow: Adw.PreferencesWindow { valign: center; } } - - Adw.ActionRow { - title: _("List Epic Games in Programs List"); - subtitle: _("Requires Epic Games Store installed in the bottle."); - activatable-widget: switch_epic_games; - - Switch switch_epic_games { - valign: center; - } - } - - Adw.ActionRow { - title: _("List Ubisoft Games in Programs List"); - subtitle: _("Requires Ubisoft Connect installed in the bottle."); - activatable-widget: switch_ubisoft_connect; - - Switch switch_ubisoft_connect { - valign: center; - } - } } Adw.PreferencesGroup { diff --git a/bottles/frontend/preferences.py b/bottles/frontend/preferences.py index 3fe92492773..64a1a288504 100644 --- a/bottles/frontend/preferences.py +++ b/bottles/frontend/preferences.py @@ -22,7 +22,7 @@ from gi.repository import Gtk, Adw, Gio, GLib -from bottles.backend.managers.data import DataManager, UserDataKeys +from bottles.backend.globals import Paths from bottles.backend.state import EventManager, Events from bottles.backend.utils.threading import RunAsync from bottles.backend.utils.generic import sort_by_version @@ -47,15 +47,12 @@ class PreferencesWindow(Adw.PreferencesWindow): switch_theme = Gtk.Template.Child() switch_notifications = Gtk.Template.Child() switch_force_offline = Gtk.Template.Child() - switch_temp = Gtk.Template.Child() switch_release_candidate = Gtk.Template.Child() switch_steam = Gtk.Template.Child() switch_sandbox = Gtk.Template.Child() switch_auto_close = Gtk.Template.Child() switch_update_date = Gtk.Template.Child() switch_steam_programs = Gtk.Template.Child() - switch_epic_games = Gtk.Template.Child() - switch_ubisoft_connect = Gtk.Template.Child() list_runners = Gtk.Template.Child() list_dxvk = Gtk.Template.Child() list_vkd3d = Gtk.Template.Child() @@ -78,16 +75,8 @@ def __init__(self, window, **kwargs): self.window = window self.settings = window.settings self.manager = window.manager - self.data = DataManager() self.style_manager = Adw.StyleManager.get_default() - self.current_bottles_path = self.data.get(UserDataKeys.CustomBottlesPath) - if self.current_bottles_path: - self.label_bottles_path.set_label( - os.path.basename(self.current_bottles_path) - ) - self.btn_bottles_path_reset.set_visible(True) - # bind widgets self.settings.bind( "dark-theme", self.switch_theme, "active", Gio.SettingsBindFlags.DEFAULT @@ -104,9 +93,6 @@ def __init__(self, window, **kwargs): "active", Gio.SettingsBindFlags.DEFAULT, ) - self.settings.bind( - "temp", self.switch_temp, "active", Gio.SettingsBindFlags.DEFAULT - ) # Connect RC signal to another func self.settings.bind( "release-candidate", @@ -144,18 +130,6 @@ def __init__(self, window, **kwargs): "active", Gio.SettingsBindFlags.DEFAULT, ) - self.settings.bind( - "epic-games", - self.switch_epic_games, - "active", - Gio.SettingsBindFlags.DEFAULT, - ) - self.settings.bind( - "ubisoft-connect", - self.switch_ubisoft_connect, - "active", - Gio.SettingsBindFlags.DEFAULT, - ) # setup loading screens self.installers_stack.set_visible_child_name("installers_loading") @@ -163,10 +137,6 @@ def __init__(self, window, **kwargs): self.dlls_stack.set_visible_child_name("dlls_loading") self.dlls_spinner.start() - if not self.manager.utils_conn.status: - self.installers_stack.set_visible_child_name("installers_offline") - self.dlls_stack.set_visible_child_name("dlls_offline") - RunAsync(self.ui_update) # connect signals @@ -177,13 +147,6 @@ def __init__(self, window, **kwargs): self.btn_bottles_path_reset.connect("clicked", self.__reset_bottles_path) self.btn_steam_proton_doc.connect("clicked", self.__open_steam_proton_doc) - if not self.manager.steam_manager.is_steam_supported: - self.switch_steam.set_sensitive(False) - self.action_steam_proton.set_tooltip_text( - _("Steam was not found or Bottles does not have enough permissions.") - ) - self.btn_steam_proton_doc.set_visible(True) - if not self.style_manager.get_system_supports_color_schemes(): self.row_theme.set_visible(True) @@ -195,16 +158,15 @@ def empty_list(self): self.__registry = [] def ui_update(self): - if self.manager.utils_conn.status: - EventManager.wait(Events.ComponentsOrganizing) - GLib.idle_add(self.empty_list) - GLib.idle_add(self.populate_runners_list) - GLib.idle_add(self.populate_dxvk_list) - GLib.idle_add(self.populate_vkd3d_list) - GLib.idle_add(self.populate_nvapi_list) - GLib.idle_add(self.populate_latencyflex_list) + EventManager.wait(Events.ComponentsOrganizing) + GLib.idle_add(self.empty_list) + GLib.idle_add(self.populate_runners_list) + GLib.idle_add(self.populate_dxvk_list) + GLib.idle_add(self.populate_vkd3d_list) + GLib.idle_add(self.populate_nvapi_list) + GLib.idle_add(self.populate_latencyflex_list) - GLib.idle_add(self.dlls_stack.set_visible_child_name, "dlls_list") + GLib.idle_add(self.dlls_stack.set_visible_child_name, "dlls_list") def __toggle_night(self, widget, state): if self.settings.get_boolean("dark-theme"): @@ -230,7 +192,6 @@ def set_path(_dialog, response): path = dialog.get_file().get_path() - self.data.set(UserDataKeys.CustomBottlesPath, path) self.label_bottles_path.set_label(os.path.basename(path)) self.btn_bottles_path_reset.set_visible(True) self.prompt_restart() @@ -251,28 +212,9 @@ def handle_restart(self, widget, response_id): self.window.proper_close() widget.destroy() - def prompt_restart(self): - if self.current_bottles_path != self.data.get(UserDataKeys.CustomBottlesPath): - dialog = Adw.MessageDialog.new( - self.window, - _("Relaunch Bottles?"), - _( - "Bottles will need to be relaunched to use this directory.\n\nBe sure to close every program launched from Bottles before relaunching Bottles, as not doing so can cause data loss, corruption and programs to malfunction." - ), - ) - dialog.add_response("dismiss", _("_Cancel")) - dialog.add_response("restart", _("_Relaunch")) - dialog.set_response_appearance( - "restart", Adw.ResponseAppearance.DESTRUCTIVE - ) - dialog.connect("response", self.handle_restart) - dialog.present() - def __reset_bottles_path(self, widget): - self.data.remove(UserDataKeys.CustomBottlesPath) self.btn_bottles_path_reset.set_visible(False) self.label_bottles_path.set_label(_("(Default)")) - self.prompt_restart() def __display_unstable_candidate(self, component=["", {"Channel": "unstable"}]): return self.window.settings.get_boolean("release-candidate") or component[1][ diff --git a/bottles/frontend/program-row.blp b/bottles/frontend/program-row.blp index 7c51a6e95bf..055543e00da 100644 --- a/bottles/frontend/program-row.blp +++ b/bottles/frontend/program-row.blp @@ -39,10 +39,6 @@ Popover pop_actions { text: _("Change Launch Options…"); } - $GtkModelButton btn_add_library { - text: _("Add to Library"); - } - $GtkModelButton btn_add_entry { text: _("Add Desktop Entry"); } diff --git a/bottles/frontend/program_row.py b/bottles/frontend/program_row.py index d73f272608c..d411594035d 100644 --- a/bottles/frontend/program_row.py +++ b/bottles/frontend/program_row.py @@ -20,8 +20,6 @@ from gi.repository import Gtk, Adw -from bottles.backend.managers.library import LibraryManager -from bottles.backend.managers.steam import SteamManager from bottles.backend.models.result import Result from bottles.backend.utils.manager import ManagerUtils from bottles.backend.utils.threading import RunAsync @@ -43,7 +41,6 @@ class ProgramRow(Adw.ActionRow): btn_run = Gtk.Template.Child() btn_stop = Gtk.Template.Child() btn_launch_options = Gtk.Template.Child() - btn_launch_steam = Gtk.Template.Child() btn_uninstall = Gtk.Template.Child() btn_remove = Gtk.Template.Child() btn_hide = Gtk.Template.Child() @@ -52,7 +49,6 @@ class ProgramRow(Adw.ActionRow): btn_browse = Gtk.Template.Child() btn_add_steam = Gtk.Template.Child() btn_add_entry = Gtk.Template.Child() - btn_add_library = Gtk.Template.Child() btn_launch_terminal = Gtk.Template.Child() pop_actions = Gtk.Template.Child() @@ -72,16 +68,7 @@ def __init__( self.set_title(self.program["name"]) - if is_steam: - self.set_subtitle("Steam") - for w in [self.btn_run, self.btn_stop, self.btn_menu]: - w.set_visible(False) - w.set_sensitive(False) - self.btn_launch_steam.set_visible(True) - self.btn_launch_steam.set_sensitive(True) - self.set_activatable_widget(self.btn_launch_steam) - else: - self.executable = program.get("executable", "") + self.executable = program.get("executable", "") if program.get("removed"): self.add_css_class("removed") @@ -92,21 +79,12 @@ def __init__( self.btn_hide.set_visible(not program.get("removed")) self.btn_unhide.set_visible(program.get("removed")) - if self.manager.steam_manager.is_steam_supported: - self.btn_add_steam.set_visible(True) - - library_manager = LibraryManager() - for _uuid, entry in library_manager.get_library().items(): - if entry.get("id") == program.get("id"): - self.btn_add_library.set_visible(False) - external_programs = [] for v in self.config.External_Programs.values(): external_programs.append(v["name"]) """Signal connections""" self.btn_run.connect("clicked", self.run_executable) - self.btn_launch_steam.connect("clicked", self.run_steam) self.btn_launch_terminal.connect("clicked", self.run_executable, True) self.btn_stop.connect("clicked", self.stop_process) self.btn_launch_options.connect("clicked", self.show_launch_options_view) @@ -116,8 +94,6 @@ def __init__( self.btn_rename.connect("clicked", self.rename_program) self.btn_browse.connect("clicked", self.browse_program_folder) self.btn_add_entry.connect("clicked", self.add_entry) - self.btn_add_library.connect("clicked", self.add_to_library) - self.btn_add_steam.connect("clicked", self.add_to_steam) self.btn_remove.connect("clicked", self.remove_program) if not program.get("removed") and not is_steam and check_boot: @@ -180,13 +156,6 @@ def _run(): RunAsync(_run, callback=self.__reset_buttons) self.__reset_buttons() - def run_steam(self, _widget): - self.manager.steam_manager.launch_app(self.config.CompatData) - self.window.show_toast( - _('Launching "{0}" with Steam…').format(self.program["name"]) - ) - self.pop_actions.popdown() # workaround #1640 - def stop_process(self, widget): self.window.show_toast(_('Stopping "{0}"…').format(self.program["name"])) winedbg = WineDbg(self.config) @@ -252,28 +221,11 @@ def func(new_name): scope="External_Programs", ) - def async_work(): - library_manager = LibraryManager() - entries = library_manager.get_library() - - for uuid, entry in entries.items(): - if entry.get("id") == self.program["id"]: - entries[uuid]["name"] = new_name - library_manager.download_thumbnail(uuid, self.config) - break - - library_manager.__library = entries - library_manager.save_library() - - @GtkUtils.run_in_main_loop - def ui_update(_result, _error): - self.window.page_library.update() - self.window.show_toast( - _('"{0}" renamed to "{1}"').format(old_name, new_name) - ) - self.update_programs() - - RunAsync(async_work, callback=ui_update) + self.window.page_library.update() + self.window.show_toast( + _('"{0}" renamed to "{1}"').format(old_name, new_name) + ) + self.update_programs() dialog = RenameProgramDialog( self.window, on_save=func, name=self.program["name"] @@ -307,49 +259,3 @@ def update(result, _error=False): "path": self.program["path"], }, ) - - def add_to_library(self, _widget): - def update(_result, _error=False): - self.window.update_library() - self.window.show_toast( - _('"{0}" added to your library').format(self.program["name"]) - ) - - def add_to_library(): - self.save_program() # we need to store it in the bottle configuration to keep the reference - library_manager = LibraryManager() - library_manager.add_to_library( - { - "bottle": {"name": self.config.Name, "path": self.config.Path}, - "name": self.program["name"], - "id": str(self.program["id"]), - "icon": ManagerUtils.extract_icon( - self.config, self.program["name"], self.program["path"] - ), - }, - self.config, - ) - - self.btn_add_library.set_visible(False) - RunAsync(add_to_library, update) - - def add_to_steam(self, _widget): - def update(result, _error=False): - if result.ok: - self.window.show_toast( - _('"{0}" added to your Steam library').format(self.program["name"]) - ) - else: - self.window.show_toast( - _('"{0}" failed adding to your Steam library').format( - self.program["name"] - ) - ) - - steam_manager = SteamManager(self.config) - RunAsync( - steam_manager.add_shortcut, - update, - program_name=self.program["name"], - program_path=self.program["path"], - ) diff --git a/bottles/frontend/state-row.blp b/bottles/frontend/state-row.blp deleted file mode 100644 index 8358e50282e..00000000000 --- a/bottles/frontend/state-row.blp +++ /dev/null @@ -1,24 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -template $StateRow: Adw.ActionRow { - activatable-widget: btn_restore; - - /* Translators: id as identification */ - title: _("State id"); - subtitle: _("State comment"); - - Spinner spinner { - visible: false; - } - - Button btn_restore { - tooltip-text: _("Restore this Snapshot"); - valign: center; - icon-name: "document-open-recent-symbolic"; - - styles [ - "flat", - ] - } -} diff --git a/bottles/frontend/state_row.py b/bottles/frontend/state_row.py deleted file mode 100644 index 35e1f654a33..00000000000 --- a/bottles/frontend/state_row.py +++ /dev/null @@ -1,128 +0,0 @@ -# state_row.py -# -# Copyright 2025 The Bottles Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from datetime import datetime -from gettext import gettext as _ - -from gi.repository import Gtk, Adw - -from bottles.backend.utils.threading import RunAsync -from bottles.frontend.gtk import GtkUtils - - -@Gtk.Template(resource_path="/com/usebottles/bottles/state-row.ui") -class StateRow(Adw.ActionRow): - __gtype_name__ = "StateRow" - - # region Widgets - btn_restore = Gtk.Template.Child() - spinner = Gtk.Template.Child() - - # endregion - - def __init__(self, parent, config, state, active, **kwargs): - super().__init__(**kwargs) - - # common variables and references - self.parent = parent - self.window = parent.window - self.manager = parent.window.manager - self.queue = parent.window.page_details.queue - self.state = state - - if config.Versioning: - self.state_name = "#{} - {}".format( - state[0], - datetime.strptime( - state[1]["Creation_Date"], "%Y-%m-%d %H:%M:%S.%f" - ).strftime("%d %B %Y, %H:%M"), - ) - - self.set_subtitle(self.state[1]["Comment"]) - if state[0] == config.State: - self.add_css_class("current-state") - else: - self.state_name = "{} - {}".format( - state[0], - datetime.fromtimestamp(state[1]["timestamp"]).strftime( - "%d %B %Y, %H:%M" - ), - ) - self.set_subtitle(state[1]["message"]) - if active: - self.add_css_class("current-state") - - self.set_title(self.state_name) - self.config = config - self.versioning_manager = self.manager.versioning_manager - - # connect signals - self.btn_restore.connect("clicked", self.set_state) - - def set_state(self, widget): - """ - Set the bottle state to this one. - """ - - def handle_response(dialog, response_id): - if response_id == "ok": - self.queue.add_task() - self.parent.set_sensitive(False) - self.spinner.show() - self.spinner.start() - - def _after(): - self.window.page_details.view_versioning.update(None, self.config) - self.manager.update_bottles() - - RunAsync( - task_func=self.versioning_manager.set_state, - callback=self.set_completed, - config=self.config, - state_id=self.state[0], - after=_after, - ) - dialog.destroy() - - dialog = Adw.MessageDialog.new( - self.window, - _("Are you sure you want to restore this state?"), - _( - "Restoring this state will overwrite the current configuration and cannot be undone." - ), - ) - dialog.add_response("cancel", _("_Cancel")) - dialog.add_response("ok", _("_Restore")) - dialog.set_response_appearance("ok", Adw.ResponseAppearance.SUGGESTED) - dialog.connect("response", handle_response) - dialog.present() - - @GtkUtils.run_in_main_loop - def set_completed(self, result, error=False): - """ - Set completed status to the widget. - """ - if not self.config.Versioning and result.message: - self.window.show_toast(result.message) - self.spinner.stop() - self.spinner.hide() - self.btn_restore.set_visible(False) - self.parent.set_sensitive(True) - self.queue.end_task() - self.manager.update_bottles() - config = self.manager.local_bottles[self.config.Path] - self.window.page_details.set_config(config) diff --git a/bottles/frontend/upgrade-versioning-dialog.blp b/bottles/frontend/upgrade-versioning-dialog.blp deleted file mode 100644 index 2b4909ab07c..00000000000 --- a/bottles/frontend/upgrade-versioning-dialog.blp +++ /dev/null @@ -1,131 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -template $UpgradeVersioningDialog: Adw.Window { - modal: true; - default-width: 500; - default-height: 400; - destroy-with-parent: true; - - Box { - orientation: vertical; - - Adw.HeaderBar { - show-end-title-buttons: false; - - title-widget: Adw.WindowTitle { - title: _("Upgrade Needed"); - }; - - Button btn_cancel { - label: _("_Cancel"); - use-underline: true; - action-name: "window.close"; - } - - ShortcutController { - scope: managed; - - Shortcut { - trigger: "Escape"; - action: "action(window.close)"; - } - } - - [end] - Button btn_proceed { - label: _("Continue"); - - styles [ - "suggested-action", - ] - } - - [end] - Button btn_upgrade { - label: _("Launch upgrade"); - visible: false; - - styles [ - "suggested-action", - ] - } - - styles [ - "flat", - ] - } - - Stack stack_switcher { - StackPage { - name: "page_status"; - - child: Adw.StatusPage status_page { - icon-name: "preferences-system-time-symbolic"; - title: _("New Versioning System"); - vexpand: true; - hexpand: true; - description: _("The new bottle versioning system has landed."); - }; - } - - StackPage { - name: "page_info"; - - child: Label { - margin-top: 10; - margin-start: 10; - margin-end: 10; - wrap: true; - label: _("Bottles has a whole new Versioning System that is not backwards compatible.\n\nTo continue using versioning we need to re-initialize the bottle repository. This will not delete data from your bottle but will delete all existing snapshots and create a new one.\n\nIf you need to go back to a previous snapshot before continuing, close this window and restore the snapshot, then reopen the bottle to show this window again.\n\nThe old system will be discontinued in one of the next releases."); - }; - } - - StackPage { - name: "page_upgrading"; - - child: Box page_upgrading { - margin-top: 24; - margin-bottom: 24; - orientation: vertical; - vexpand: true; - hexpand: true; - - Label { - halign: center; - margin-top: 12; - margin-bottom: 12; - label: _("Re-initializing Repository…"); - - styles [ - "title-1", - ] - } - - Label { - margin-bottom: 6; - label: _("This could take a while."); - } - - ProgressBar progressbar { - width-request: 300; - halign: center; - margin-top: 24; - margin-bottom: 12; - } - }; - } - - StackPage { - name: "page_finish"; - - child: Adw.StatusPage page_finish { - vexpand: true; - hexpand: true; - icon-name: "object-select-symbolic"; - title: _("Done! Please restart Bottles."); - }; - } - } - } -} diff --git a/bottles/frontend/upgrade_versioning_dialog.py b/bottles/frontend/upgrade_versioning_dialog.py deleted file mode 100644 index cb5b2d8d34c..00000000000 --- a/bottles/frontend/upgrade_versioning_dialog.py +++ /dev/null @@ -1,81 +0,0 @@ -# upgrade_versioning_dialog.py -# -# Copyright 2025 The Bottles Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, in version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import time -from gi.repository import Gtk, Adw - -from bottles.backend.utils.threading import RunAsync - - -@Gtk.Template(resource_path="/com/usebottles/bottles/upgrade-versioning-dialog.ui") -class UpgradeVersioningDialog(Adw.Window): - __gtype_name__ = "UpgradeVersioningDialog" - - # region Widgets - btn_cancel = Gtk.Template.Child() - btn_proceed = Gtk.Template.Child() - btn_upgrade = Gtk.Template.Child() - stack_switcher = Gtk.Template.Child() - progressbar = Gtk.Template.Child() - - # endregion - - def __init__(self, parent, **kwargs): - super().__init__(**kwargs) - self.set_transient_for(parent.window) - - # common variables and references - self.parent = parent - self.config = parent.config - - # connect signals - self.btn_upgrade.connect("clicked", self.__upgrade) - self.btn_proceed.connect("clicked", self.__proceed) - - def __upgrade(self, widget): - """ - This function take the new bottle name from the entry - and create a new duplicate of the bottle. It also change the - stack_switcher page when the process is finished. - """ - self.stack_switcher.set_visible_child_name("page_upgrading") - self.btn_upgrade.set_visible(False) - self.btn_cancel.set_visible(False) - self.btn_cancel.set_label("Close") - - RunAsync(self.pulse) - RunAsync( - self.parent.manager.versioning_manager.update_system, - self.finish, - self.config, - ) - - def __proceed(self, widget): - self.stack_switcher.set_visible_child_name("page_info") - self.btn_proceed.set_visible(False) - self.btn_upgrade.set_visible(True) - - def finish(self, result, error=False): - self.btn_cancel.set_visible(True) - self.parent.manager.update_bottles() - self.stack_switcher.set_visible_child_name("page_finish") - - def pulse(self): - # This function update the progress bar every half second. - while True: - time.sleep(0.5) - self.progressbar.pulse() diff --git a/bottles/frontend/vkbasalt_dialog.py b/bottles/frontend/vkbasalt_dialog.py index f3f77c66454..c79c5112c00 100644 --- a/bottles/frontend/vkbasalt_dialog.py +++ b/bottles/frontend/vkbasalt_dialog.py @@ -29,9 +29,7 @@ from gi.repository import Gtk, GLib, Adw from vkbasalt.lib import parse, ParseConfig # type: ignore [import-untyped] from bottles.backend.utils.manager import ManagerUtils -from bottles.backend.logger import Logger - -logging = Logger() +import logging class VkBasaltSettings: diff --git a/bottles/frontend/window.blp b/bottles/frontend/window.blp index cc3bf7155f2..3c517936a91 100644 --- a/bottles/frontend/window.blp +++ b/bottles/frontend/window.blp @@ -75,13 +75,6 @@ template $BottlesWindow: Adw.ApplicationWindow { } menu primary_menu { - section { - item { - label: _("_Import…"); - action: "app.import"; - } - } - section { item { label: _("_Preferences"); diff --git a/bottles/frontend/window.py b/bottles/frontend/window.py index c627e8de9b2..d898262e89d 100644 --- a/bottles/frontend/window.py +++ b/bottles/frontend/window.py @@ -24,30 +24,20 @@ from bottles.backend.globals import Paths from bottles.backend.health import HealthChecker -from bottles.backend.logger import Logger -from bottles.backend.managers.data import UserDataKeys -from bottles.backend.managers.manager import Manager +import logging from bottles.backend.models.config import BottleConfig from bottles.backend.models.result import Result -from bottles.backend.state import SignalManager, Signals, Notification -from bottles.backend.utils.connection import ConnectionUtils from bottles.backend.utils.threading import RunAsync -from bottles.frontend.operation import TaskSyncer from bottles.frontend.params import APP_ID, BASE_ID, PROFILE from bottles.frontend.gtk import GtkUtils from bottles.frontend.bottle_details_view import BottleDetailsView -from bottles.frontend.importer_view import ImporterView -from bottles.frontend.library_view import LibraryView from bottles.frontend.bottles_list_view import BottlesListView -from bottles.frontend.loading_view import LoadingView from bottles.frontend.new_bottle_dialog import NewBottleDialog from bottles.frontend.preferences import PreferencesWindow from bottles.frontend.crash_report_dialog import CrashReportDialog from bottles.frontend.dependencies_check_dialog import DependenciesCheckDialog from bottles.frontend.onboard_dialog import OnboardDialog -logging = Logger() - @Gtk.Template(resource_path="/com/usebottles/bottles/window.ui") class BottlesWindow(Adw.ApplicationWindow): @@ -72,22 +62,19 @@ class BottlesWindow(Adw.ApplicationWindow): settings = Gio.Settings.new(BASE_ID) argument_executed = False - def __init__(self, arg_bottle, **kwargs): + def __init__(self, **kwargs): width = self.settings.get_int("window-width") height = self.settings.get_int("window-height") super().__init__(**kwargs, default_width=width, default_height=height) - self.utils_conn = ConnectionUtils( - force_offline=self.settings.get_boolean("force-offline") - ) self.manager = None - self.arg_bottle = arg_bottle self.app = kwargs.get("application") self.set_icon_name(APP_ID) if PROFILE == "development": self.add_css_class("devel") + logging.getLogger().setLevel(logging.DEBUG) self.btn_donate.add_css_class("donate") @@ -125,13 +112,6 @@ def response(dialog, response, *args): logging.error("https://usebottles.com/download/") return - # Loading view - self.page_loading = LoadingView() - - # Populate stack - self.stack_main.add_named( - child=self.page_loading, name="page_loading" - ).set_visible(False) self.headerbar.add_css_class("flat") # Signal connections @@ -141,160 +121,55 @@ def response(dialog, response, *args): "https://usebottles.com/funding/", ) self.btn_add.connect("clicked", self.show_add_view) - self.btn_noconnection.connect("clicked", self.check_for_connection) self.stack_main.connect("notify::visible-child", self.__on_page_changed) - # backend signal handlers - self.task_syncer = TaskSyncer(self) - SignalManager.connect(Signals.TaskAdded, self.task_syncer.task_added_handler) - SignalManager.connect( - Signals.TaskRemoved, self.task_syncer.task_removed_handler - ) - SignalManager.connect( - Signals.TaskUpdated, self.task_syncer.task_updated_handler - ) - SignalManager.connect( - Signals.NetworkStatusChanged, self.network_changed_handler + # Pages + self.page_details = BottleDetailsView(self) + self.page_list = BottlesListView() + + self.main_leaf.append(self.page_details) + + self.main_leaf.get_page(self.page_details).set_navigatable(False) + + self.stack_main.add_titled( + child=self.page_list, name="page_list", title=_("Bottles") + ).set_icon_name(f"{APP_ID}-symbolic") + + self.page_list.search_bar.set_key_capture_widget(self) + self.btn_search.bind_property( + "active", + self.page_list.search_bar, + "search-mode-enabled", + GObject.BindingFlags.BIDIRECTIONAL, ) - SignalManager.connect(Signals.GNotification, self.g_notification_handler) - SignalManager.connect(Signals.GShowUri, self.g_show_uri_handler) - self.__on_start() - logging.info( - "Bottles Started!", + if ( + self.stack_main.get_child_by_name(self.settings.get_string("startup-view")) + is None + ): + self.stack_main.set_visible_child_name("page_list") + + self.settings.bind( + "startup-view", + self.stack_main, + "visible-child-name", + Gio.SettingsBindFlags.DEFAULT, ) + self.lock_ui(False) + self.headerbar.get_style_context().remove_class("flat") + @Gtk.Template.Callback() def on_close_request(self, *args): self.settings.set_int("window-width", self.get_width()) self.settings.set_int("window-height", self.get_height()) - # region Backend signal handlers - def network_changed_handler(self, res: Result): - GLib.idle_add(self.btn_noconnection.set_visible, not res.status) - - def g_notification_handler(self, res: Result): - """handle backend notification request""" - notify: Notification = res.data - self.send_notification(title=notify.title, text=notify.text, image=notify.image) - - def g_show_uri_handler(self, res: Result): - """handle backend show_uri request""" - uri: str = res.data - Gtk.show_uri(self, uri, Gdk.CURRENT_TIME) - # endregion - def update_library(self): - GLib.idle_add(self.page_library.update) - def title(self, title, subtitle: str = ""): self.view_switcher_title.set_title(title) self.view_switcher_title.set_subtitle(subtitle) - def check_for_connection(self, status): - """ - This method checks if the client has an internet connection. - If true, the manager checks will be performed, unlocking all the - features locked for no internet connection. - """ - if self.utils_conn.check_connection(): - self.manager.checks(install_latest=False, first_run=True) - - def __on_start(self): - """ - This method is called before the window is shown. This check if there - is at least one local runner installed. If not, the user will be - prompted with the onboard dialog. - """ - - @GtkUtils.run_in_main_loop - def set_manager(result: Manager, error=None): - self.manager = result - - tmp_runners = [ - x for x in self.manager.runners_available if not x.startswith("sys-") - ] - if len(tmp_runners) == 0: - self.show_onboard_view() - - # Pages - self.page_details = BottleDetailsView(self) - self.page_list = BottlesListView(self, arg_bottle=self.arg_bottle) - self.page_importer = ImporterView(self) - self.page_library = LibraryView(self) - - self.main_leaf.append(self.page_details) - self.main_leaf.append(self.page_importer) - - self.main_leaf.get_page(self.page_details).set_navigatable(False) - self.main_leaf.get_page(self.page_importer).set_navigatable(False) - - self.stack_main.add_titled( - child=self.page_list, name="page_list", title=_("Bottles") - ).set_icon_name(f"{APP_ID}-symbolic") - self.stack_main.add_titled( - child=self.page_library, name="page_library", title=_("Library") - ).set_icon_name("library-symbolic") - - self.page_list.search_bar.set_key_capture_widget(self) - self.btn_search.bind_property( - "active", - self.page_list.search_bar, - "search-mode-enabled", - GObject.BindingFlags.BIDIRECTIONAL, - ) - - if ( - self.stack_main.get_child_by_name( - self.settings.get_string("startup-view") - ) - is None - ): - self.stack_main.set_visible_child_name("page_list") - - self.settings.bind( - "startup-view", - self.stack_main, - "visible-child-name", - Gio.SettingsBindFlags.DEFAULT, - ) - - self.lock_ui(False) - self.headerbar.get_style_context().remove_class("flat") - - user_defined_bottles_path = self.manager.data_mgr.get( - UserDataKeys.CustomBottlesPath - ) - if user_defined_bottles_path and Paths.bottles != user_defined_bottles_path: - dialog = Adw.MessageDialog.new( - self, - _("Custom Bottles Path not Found"), - _( - "Falling back to default path. No bottles from the given path will be listed." - ), - ) - dialog.add_response("cancel", _("_Dismiss")) - dialog.present() - - def get_manager(): - if self.utils_conn.check_connection(): - SignalManager.connect( - Signals.RepositoryFetched, self.page_loading.add_fetched - ) - - # do not redo connection if aborted connection - mng = Manager( - g_settings=self.settings, - check_connection=self.utils_conn.aborted_connections == 0, - ) - return mng - - self.show_loading_view() - RunAsync(get_manager, callback=set_manager) - - self.check_crash_log() - def send_notification(self, title, text, image="", ignore_user=False): """ This method is used to send a notification to the user using @@ -318,10 +193,6 @@ def show_details_view(self, widget=False, config: BottleConfig | None = None): self.main_leaf.set_visible_child(self.page_details) self.page_details.set_config(config or BottleConfig()) - def show_loading_view(self, widget=False): - self.lock_ui() - self.stack_main.set_visible_child_name("page_loading") - def show_onboard_view(self, widget=False): onboard_window = OnboardDialog(self) onboard_window.present() @@ -333,9 +204,6 @@ def show_add_view(self, widget=False): def show_list_view(self, widget=False): self.stack_main.set_visible_child_name("page_list") - def show_importer_view(self, widget=False): - self.main_leaf.set_visible_child(self.page_importer) - def show_prefs_view(self, widget=False, view=0): preferences_window = PreferencesWindow(self) preferences_window.present() diff --git a/bottles/tests/__init__.py b/bottles/tests/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/bottles/tests/backend/__init__.py b/bottles/tests/backend/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/bottles/tests/backend/manager/__init__.py b/bottles/tests/backend/manager/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/bottles/tests/backend/manager/test_manager.py b/bottles/tests/backend/manager/test_manager.py deleted file mode 100644 index 7651141c78a..00000000000 --- a/bottles/tests/backend/manager/test_manager.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Core Manager tests""" - -from bottles.backend.managers.manager import Manager -from bottles.backend.utils.gsettings_stub import GSettingsStub - - -def test_manager_is_singleton(): - assert Manager(is_cli=True) is Manager( - is_cli=True - ), "Manager should be singleton object" - assert Manager(is_cli=True) is Manager( - g_settings=GSettingsStub(), is_cli=True - ), "Manager should be singleton even with different argument" - - -def test_manager_default_gsettings_stub(): - assert Manager().settings.get_boolean("anything") is False diff --git a/bottles/tests/backend/state/__init__.py b/bottles/tests/backend/state/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/bottles/tests/backend/state/test_events.py b/bottles/tests/backend/state/test_events.py deleted file mode 100644 index c216d8ac20a..00000000000 --- a/bottles/tests/backend/state/test_events.py +++ /dev/null @@ -1,145 +0,0 @@ -"""EventManager tests""" - -import time -from enum import Enum -from threading import Thread -import pytest - -from bottles.backend.state import EventManager - - -class Events(Enum): - SimpleEvent = "simple.event" - WaitAfterDone = "wait_after_done.event" - SetResetEvent = "set_reset.event" - WaitSingleton = "wait_singleton.event" - DoneSingleton = "done_singleton.event" - CorrectFlagDone = "correct_flag_done.event" - - -def approx_time(start, target): - epsilon = 0.010 # 5 ms window - variation = time.time() - start - target - result = -epsilon / 2 <= variation <= epsilon / 2 - if not result: - print(f"Start: {start}") - print(f"End: {variation + start + target}") - print(f"Variation: {variation}") - return result - - -def test_simple_event(): - start_time = time.time() - - def t1_func(): - EventManager.wait(Events.SimpleEvent) - - t1 = Thread(target=t1_func) - t1.start() - - time.sleep(0.2) - EventManager.done(Events.SimpleEvent) - - t1.join() - assert approx_time(start_time, 0.2) - - -def test_wait_after_done_event(): - start_time = time.time() - EventManager.done(Events.WaitAfterDone) - - EventManager.wait(Events.WaitAfterDone) - assert approx_time(start_time, 0) - - -@pytest.mark.filterwarnings("error") -def test_set_reset(): - start_time = time.time() - - def t1_func(): - start_time_t1 = time.time() - EventManager.wait(Events.SetResetEvent) - assert approx_time(start_time_t1, 0.1) - - def t2_func(): - start_time_t1 = time.time() - EventManager.wait(Events.SetResetEvent) - assert approx_time(start_time_t1, 0) - - t1 = Thread(target=t1_func) - t1.start() - - time.sleep(0.1) - EventManager.done(Events.SetResetEvent) - - # Assert wait for 0.1s - t1.join() - - t2 = Thread(target=t2_func) - t2.start() - # Assert wait for 0s - t2.join() - - time.sleep(0.1) - - EventManager.reset(Events.SetResetEvent) - - t1 = Thread(target=t1_func) - t1.start() - - time.sleep(0.1) - EventManager.done(Events.SetResetEvent) - - # Assert wait for 0.1s - t1.join() - assert approx_time(start_time, 0.3) - - -def test_event_singleton_wait(): - EventManager._EVENTS = {} - - def wait_thread(): - EventManager.wait(Events.WaitSingleton) - - def wait_thread_by_value(): - EventManager.wait(Events("wait_singleton.event")) - - t1 = Thread(target=wait_thread) - t1.start() - - t2 = Thread(target=wait_thread) - t2.start() - - t3 = Thread(target=wait_thread_by_value) - t3.start() - - assert len(EventManager._EVENTS) == 1 - - EventManager.done(Events.WaitSingleton) - t1.join() - t2.join() - t3.join() - - -def test_event_singleton_done_reset(): - EventManager._EVENTS = {} - - EventManager.done(Events.DoneSingleton) - EventManager.done(Events.DoneSingleton) - assert len(EventManager._EVENTS) == 1 - - EventManager.reset(Events.DoneSingleton) - assert len(EventManager._EVENTS) == 1 - - EventManager.reset(Events.DoneSingleton) - assert len(EventManager._EVENTS) == 1 - - -def test_correct_internal_flag(): - EventManager.done(Events.CorrectFlagDone) - - assert EventManager._EVENTS[Events.CorrectFlagDone].is_set() - - EventManager.reset(Events.CorrectFlagDone) - - assert not EventManager._EVENTS[Events.CorrectFlagDone].is_set() diff --git a/bottles/tests/backend/utils/__init__.py b/bottles/tests/backend/utils/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/bottles/tests/backend/utils/test_generic.py b/bottles/tests/backend/utils/test_generic.py deleted file mode 100644 index aebd901816b..00000000000 --- a/bottles/tests/backend/utils/test_generic.py +++ /dev/null @@ -1,25 +0,0 @@ -import pytest - -from bottles.backend.utils.generic import detect_encoding - - -# CP932 is superset of Shift-JIS, which is default codec for Japanese in Windows -# GBK is default codec for Chinese in Windows -@pytest.mark.parametrize( - "text, hint, codec", - [ - ("Hello, world!", None, "ascii"), - (" ", None, "ascii"), - ("Привет, мир!", None, "windows-1251"), - ("こんにちは、世界!", "ja_JP", "cp932"), - ("こんにちは、世界!", "ja_JP.utf-8", "utf-8"), - ("你好,世界!", "zh_CN", "gbk"), - ("你好,世界!", "zh_CN.UTF-8", "utf-8"), - ("你好,世界!", "zh_CN.invalid_fallback", "gbk"), - ("", None, "utf-8"), - ], -) -def test_detect_encoding(text: str, hint: str | None, codec: str | None): - text_bytes = text.encode(codec) - guess = detect_encoding(text_bytes, hint) - assert guess.lower() == codec.lower() diff --git a/data/com.usebottles.bottles.gschema.xml b/data/com.usebottles.bottles.gschema.xml index f8167f30868..687a06a10d1 100644 --- a/data/com.usebottles.bottles.gschema.xml +++ b/data/com.usebottles.bottles.gschema.xml @@ -26,16 +26,6 @@ Steam apps listing Toggle steam apps listing. - - true - Epic Games listing - Toggle epic games listing. - - - true - Ubisoft Connect listing - Toggle ubisoft connect listing. - 880 Window width