diff --git a/build-aux/share/portproton/scripts/functions_helper b/build-aux/share/portproton/scripts/functions_helper index f0eb6d02..dbec6c9e 100755 --- a/build-aux/share/portproton/scripts/functions_helper +++ b/build-aux/share/portproton/scripts/functions_helper @@ -1190,6 +1190,10 @@ check_user_conf () { # export DXVK_HUD=full # export GALLIUM_HUD=fps export PW_GPU_USE="disabled" +# Optional defaults for executables without matching db values: +# export PW_DEFAULT_WINE_USE="WINE_LG" +# export PW_DEFAULT_PREFIX_NAME="DEFAULT" +# export PW_DEFAULT_VULKAN_USE="2" EOF else sed -i \ @@ -1903,6 +1907,10 @@ check_dirs_and_files_in_pfx () { } pw_init_db () { + [[ -n "${PW_DEFAULT_WINE_USE:-}" ]] && export PW_WINE_USE="${PW_DEFAULT_WINE_USE}" + [[ -n "${PW_DEFAULT_PREFIX_NAME:-}" ]] && export PW_PREFIX_NAME="${PW_DEFAULT_PREFIX_NAME}" + [[ -n "${PW_DEFAULT_VULKAN_USE:-}" ]] && export PW_VULKAN_USE="${PW_DEFAULT_VULKAN_USE}" + if echo "$LSPCI_VGA" | grep -i -q 'nvidia.*rtx' ; then export PW_USE_NVAPI_AND_DLSS="1" export PW_USE_RAY_TRACING="1" @@ -2228,15 +2236,18 @@ edit_user_conf_from_gui () { elif [[ $proxy_mod_db =~ ^(Enabled|Enable)$ ]] ; then proxy_mod_db=enabled fi + proxy_mod_db=$(echo "$proxy_mod_db" | sed 's/\\/\\\\\\\\/g; s/"/\\"/g') + proxy_mod_db_escaped=$(printf '%s\n' "$proxy_mod_db" | sed 's/[\/&]/\\&/g') + if ! grep "export ${mod_db}=" "${USER_CONF}" &>/dev/null then echo "export ${mod_db}=\"$proxy_mod_db\"" >> "${USER_CONF}" elif grep "export ${mod_db}=" "${USER_CONF}" | grep -v "#" &>/dev/null then if [[ "$(grep "export ${mod_db}=" "${USER_CONF}" | grep -v "#")" != "export ${mod_db}=\"$proxy_mod_db\"" ]] - then sed -ri "s/^export ${mod_db}=.*/export ${mod_db}=\"$proxy_mod_db\"/" "${USER_CONF}" + then sed -ri "s|^export ${mod_db}=.*|export ${mod_db}=\"${proxy_mod_db_escaped}\"|" "${USER_CONF}" fi elif [[ "$(grep "export ${mod_db}=" "${USER_CONF}")" != "export ${mod_db}=$proxy_mod_db" ]] - then sed -ri "s/.*export ${mod_db}=.*/export ${mod_db}=\"$proxy_mod_db\"/g" "${USER_CONF}" + then sed -ri "s|.*export ${mod_db}=.*|export ${mod_db}=\"${proxy_mod_db_escaped}\"|g" "${USER_CONF}" fi done return 0 diff --git a/build-aux/share/portproton/scripts/start.sh b/build-aux/share/portproton/scripts/start.sh index 913c2889..546cd7bb 100755 --- a/build-aux/share/portproton/scripts/start.sh +++ b/build-aux/share/portproton/scripts/start.sh @@ -396,6 +396,10 @@ Usage examples: done < <(grep -E '^export ' "$USER_CONF" 2>/dev/null | sed -E 's/[[:space:]]*#.*$//' | sed '/^[[:space:]]*$/d') fi + [[ -n "${PW_DEFAULT_WINE_USE:-}" && -z "${all_vars[PW_WINE_USE]+x}" ]] && all_vars["PW_WINE_USE"]="$PW_DEFAULT_WINE_USE" + [[ -n "${PW_DEFAULT_PREFIX_NAME:-}" && -z "${all_vars[PW_PREFIX_NAME]+x}" ]] && all_vars["PW_PREFIX_NAME"]="$PW_DEFAULT_PREFIX_NAME" + [[ -n "${PW_DEFAULT_VULKAN_USE:-}" && -z "${all_vars[PW_VULKAN_USE]+x}" ]] && all_vars["PW_VULKAN_USE"]="$PW_DEFAULT_VULKAN_USE" + while IFS='=' read -r key val; do key="${key#export }" val="${val#\"}" diff --git a/portprotonqt/dialogs/settings_dialog.py b/portprotonqt/dialogs/settings_dialog.py index e8abe5e6..e8c1cd98 100644 --- a/portprotonqt/dialogs/settings_dialog.py +++ b/portprotonqt/dialogs/settings_dialog.py @@ -2,9 +2,7 @@ import os import re -import shutil import subprocess -from pathlib import Path from typing import cast, TYPE_CHECKING from PySide6.QtCore import Qt, QObject, QEvent, QProcess, QTimer, QUrl @@ -46,12 +44,13 @@ from portprotonqt.logger import get_logger from portprotonqt.preloader import Preloader from portprotonqt.settings_manager import ( ADVANCED_SETTING_KEYS, + get_available_prefix_options, + get_available_wine_options, get_advanced_settings, get_toggle_settings, read_lg_dist_versions_from_var, ) from portprotonqt.theme_manager import ThemeManager -from portprotonqt.version_utils import include_pinned_prefixes, version_sort_key from portprotonqt.virtual_keyboard import VirtualKeyboard logger = get_logger(__name__) @@ -142,42 +141,18 @@ class ExeSettingsDialog(DraggableDialog, MangoHudSettingsMixin, GamescopeSetting self.lg_dist_aliases = {} self.prefix_options = [] if self.portproton_path: - dist_dir = os.path.join(self.portproton_path, "data", 'dist') - if os.path.exists(dist_dir): - self.dist_options = sorted( - [f for f in os.listdir(dist_dir) if os.path.isdir(os.path.join(dist_dir, f))], - key=version_sort_key - ) scripts_path = get_portproton_scripts_path() if scripts_path: var_path = os.path.join(scripts_path, "var") self.lg_dist_aliases = read_lg_dist_versions_from_var(var_path) - for version in self.lg_dist_aliases.values(): - if version not in self.dist_options: - self.dist_options.append(version) - from portprotonqt.steam_api import get_steam_proton_versions - from portprotonqt.steam_api.utils import _is_steam_proton_dir - for version in get_steam_proton_versions(): - if version not in self.dist_options: - self.dist_options.append(version) - if self.game_source == "steam": - self.dist_options = [ - version for version in self.dist_options - if _is_steam_proton_dir( - Path(version) if os.path.isabs(version) else Path(dist_dir, version) - ) - ] - self.dist_options.sort(key=version_sort_key) + system_wine_label = "" if self.game_source == "steam" else _('System WINE') + self.dist_options = get_available_wine_options( + self.portproton_path, system_wine_label, self.game_source == "steam" + ) prefixes_dir = os.path.join(self.portproton_path, 'prefixes') - prefixes = [] if os.path.exists(prefixes_dir): _normalize_prefix_directories(prefixes_dir) - prefixes = [f for f in os.listdir(prefixes_dir) if os.path.isdir(os.path.join(prefixes_dir, f))] - self.prefix_options = include_pinned_prefixes(prefixes) - - if self.game_source != "steam" and shutil.which('wine'): - if _('System WINE') not in self.dist_options: - self.dist_options.append(_('System WINE')) + self.prefix_options = get_available_prefix_options(self.portproton_path) self.current_settings = {} self.value_widgets = {} diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index 25496cbe..cb1accbe 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -59,7 +59,7 @@ from portprotonqt.cli import ( ) from portprotonqt.tray_manager import restart_application_process -from portprotonqt.version_utils import include_pinned_prefixes, version_sort_key +from portprotonqt.version_utils import version_sort_key from portprotonqt.localization import _, get_metadata_language, read_metadata_translations from portprotonqt.downloader import Downloader from portprotonqt.tray_manager import TrayManager @@ -72,6 +72,7 @@ from portprotonqt.scripts_utils.prefix_backup import BACKUP_EXTENSION, is_legacy from portprotonqt.tabs.control_hints import MainWindowControlHintsMixin from portprotonqt.tabs.system_tab import MainWindowSystemTabMixin from portprotonqt.tabs.workers import MainWindowWorkersMixin +from portprotonqt.settings_manager import get_available_prefix_options, get_available_wine_options from PySide6.QtWidgets import (QLineEdit, QMainWindow, QWidget, QVBoxLayout, QLabel, QHBoxLayout, QStackedWidget, QComboBox, QDialog, QFormLayout, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy, QGridLayout, QScrollArea, QScroller, QSlider, QFrame) @@ -168,6 +169,48 @@ class MainWindow(MainWindowControlHintsMixin, MainWindowSystemTabMixin, MainWind logger.warning("Failed to read LG versions from %s: %s", var_path, exc) return versions + def _get_default_exe_wine_options(self) -> list[str]: + if not self.portproton_location: + return [] + return get_available_wine_options(self.portproton_location, "USE_SYSTEM_WINE") + + def _get_var_default_setting(self, key: str, fallback: str) -> str: + scripts_path = get_portproton_scripts_path() + if not scripts_path: + return fallback + + var_path = os.path.join(scripts_path, "var") + if not os.path.exists(var_path): + return fallback + + try: + with open(var_path, encoding="utf-8") as var_file: + for line in var_file: + match = re.match(rf'^\s*check_variables\s+{key}\s+["\']?([^"\']+)', line) + if match: + return match.group(1) + except OSError as exc: + logger.warning("Failed to read default %s from %s: %s", key, var_path, exc) + return fallback + + def _get_default_exe_prefix_options(self) -> list[str]: + if not self.portproton_location: + return [] + + prefixes_path = os.path.join(self.portproton_location, "data", "prefixes") + if os.path.exists(prefixes_path): + self._normalize_prefix_directories(prefixes_path) + return get_available_prefix_options(self.portproton_location) + + def _set_combo_current_data(self, combo: QComboBox, value: str) -> None: + for index in range(combo.count()): + if combo.itemData(index) == value: + combo.setCurrentIndex(index) + return + if value: + combo.addItem(value, value) + combo.setCurrentIndex(combo.count() - 1) + def __init__(self, app_name: str, version: str, launch_exe: str | None = None, resolution: tuple[int, int] | None = None, show_system_tab: bool = False): super().__init__() self.theme_manager = ThemeManager() @@ -1840,11 +1883,7 @@ class MainWindow(MainWindowControlHintsMixin, MainWindowSystemTabMixin, MainWind self.wineCombo.setCurrentIndex(0) formLayout.addRow(self.wineTitleLabel, self.wineCombo) - prefixes = [ - d for d in os.listdir(prefixes_path) - if os.path.isdir(os.path.join(prefixes_path, d)) - ] if os.path.exists(prefixes_path) else [] - self.prefixes = include_pinned_prefixes(prefixes) + self.prefixes = get_available_prefix_options(self.portproton_location) self.prefixCombo = QComboBox() self.prefixCombo.view().window().setWindowFlags( Qt.WindowType.Popup | Qt.WindowType.FramelessWindowHint @@ -2196,12 +2235,7 @@ class MainWindow(MainWindowControlHintsMixin, MainWindowSystemTabMixin, MainWind if os.path.exists(prefixes_path): self._normalize_prefix_directories(prefixes_path) - # Update the prefixes list with sorting - prefixes = [ - d for d in os.listdir(prefixes_path) - if os.path.isdir(os.path.join(prefixes_path, d)) - ] if os.path.exists(prefixes_path) else [] - self.prefixes = include_pinned_prefixes(prefixes) + self.prefixes = get_available_prefix_options(self.portproton_location) self.prefixCombo.clear() self.prefixCombo.addItems(self.prefixes) if normalized_current_prefix: @@ -2689,7 +2723,99 @@ class MainWindow(MainWindowControlHintsMixin, MainWindowSystemTabMixin, MainWind downloadForm.addRow(economy_mode_layout) downloadForm.addRow(self.downloadMirrorTitle, self.downloadMirrorCombo) - # 4. Hardware Settings Section + # 4. Default Exe Settings Section + exeDefaultsFrame, exeDefaultsForm = create_section(_("Default Exe Settings"), self.theme) + exeDefaultsForm.setRowWrapPolicy(QFormLayout.RowWrapPolicy.DontWrapRows) + scrollLayout.addWidget(exeDefaultsFrame) + + self.defaultExeWineCombo = QComboBox() + self.defaultExeWineCombo.view().window().setWindowFlags( + Qt.WindowType.Popup | Qt.WindowType.FramelessWindowHint + ) + self.defaultExeWineCombo.view().window().setAttribute( + Qt.WidgetAttribute.WA_TranslucentBackground + ) + self.defaultExeWineCombo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + self.defaultExeWineCombo.setStyleSheet(self.theme.COMBOBOX_STYLE + self.theme.SCROLL_STYLE) + self.defaultExeWineCombo.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + default_wine_from_var = self._get_var_default_setting("PW_WINE_USE", "PROTON_LG") + self.defaultExeWineCombo.addItem(default_wine_from_var, "") + for wine_version in self._get_default_exe_wine_options(): + if wine_version == default_wine_from_var: + continue + display_name = _("System WINE") if wine_version == "USE_SYSTEM_WINE" else wine_version + self.defaultExeWineCombo.addItem(display_name, wine_version) + self._set_combo_current_data( + self.defaultExeWineCombo, get_user_conf_setting('PW_DEFAULT_WINE_USE') or "" + ) + self.defaultExeWineTitle = QLabel(_("Default WINE/Proton:")) + self.defaultExeWineTitle.setStyleSheet(self.theme.SETTINGS_TITLE_STYLE) + self.defaultExeWineTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus) + exeDefaultsForm.addRow(self.defaultExeWineTitle, self.defaultExeWineCombo) + + self.defaultExePrefixCombo = QComboBox() + self.defaultExePrefixCombo.view().window().setWindowFlags( + Qt.WindowType.Popup | Qt.WindowType.FramelessWindowHint + ) + self.defaultExePrefixCombo.view().window().setAttribute( + Qt.WidgetAttribute.WA_TranslucentBackground + ) + self.defaultExePrefixCombo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + self.defaultExePrefixCombo.setStyleSheet(self.theme.COMBOBOX_STYLE + self.theme.SCROLL_STYLE) + self.defaultExePrefixCombo.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + default_prefix_from_var = self._get_var_default_setting("PW_PREFIX_NAME", "DEFAULT") + self.defaultExePrefixCombo.addItem(default_prefix_from_var, "") + for prefix_name in self._get_default_exe_prefix_options(): + if prefix_name != default_prefix_from_var: + self.defaultExePrefixCombo.addItem(prefix_name) + self.defaultExePrefixCombo.setEditable(True) + self.defaultExePrefixCombo.setInsertPolicy(QComboBox.InsertPolicy.NoInsert) + prefix_line_edit = self.defaultExePrefixCombo.lineEdit() + if prefix_line_edit is not None: + prefix_line_edit.setPlaceholderText(_("Enter prefix name")) + default_prefix = get_user_conf_setting('PW_DEFAULT_PREFIX_NAME') + if default_prefix: + self.defaultExePrefixCombo.setCurrentText(default_prefix) + self.defaultExePrefixTitle = QLabel(_("Default prefix:")) + self.defaultExePrefixTitle.setStyleSheet(self.theme.SETTINGS_TITLE_STYLE) + self.defaultExePrefixTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus) + exeDefaultsForm.addRow(self.defaultExePrefixTitle, self.defaultExePrefixCombo) + + self.defaultExeVulkanCombo = QComboBox() + self.defaultExeVulkanCombo.view().window().setWindowFlags( + Qt.WindowType.Popup | Qt.WindowType.FramelessWindowHint + ) + self.defaultExeVulkanCombo.view().window().setAttribute( + Qt.WidgetAttribute.WA_TranslucentBackground + ) + self.defaultExeVulkanCombo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + self.defaultExeVulkanCombo.setStyleSheet(self.theme.COMBOBOX_STYLE + self.theme.SCROLL_STYLE) + self.defaultExeVulkanCombo.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + default_vulkan_from_var = self._get_var_default_setting("PW_VULKAN_USE", "6") + vulkan_options = [ + (_("Newest"), "6"), + (_("Stable"), "2"), + ("Sarek", "1"), + ("WINED3D - OpenGL", "0"), + ] + for vulkan_name, vulkan_value in vulkan_options: + if vulkan_value == default_vulkan_from_var: + self.defaultExeVulkanCombo.addItem(vulkan_name, "") + break + if self.defaultExeVulkanCombo.count() == 0: + self.defaultExeVulkanCombo.addItem(default_vulkan_from_var, "") + for vulkan_name, vulkan_value in vulkan_options: + if vulkan_value != default_vulkan_from_var: + self.defaultExeVulkanCombo.addItem(vulkan_name, vulkan_value) + self._set_combo_current_data( + self.defaultExeVulkanCombo, get_user_conf_setting('PW_DEFAULT_VULKAN_USE') or "" + ) + self.defaultExeVulkanTitle = QLabel(_("Default 3D API:")) + self.defaultExeVulkanTitle.setStyleSheet(self.theme.SETTINGS_TITLE_STYLE) + self.defaultExeVulkanTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus) + exeDefaultsForm.addRow(self.defaultExeVulkanTitle, self.defaultExeVulkanCombo) + + # 5. Hardware Settings Section hwFrame, hwForm = create_section(_("Hardware Settings"), self.theme) hwForm.setRowWrapPolicy(QFormLayout.RowWrapPolicy.DontWrapRows) scrollLayout.addWidget(hwFrame) @@ -2723,7 +2849,7 @@ class MainWindow(MainWindowControlHintsMixin, MainWindowSystemTabMixin, MainWind self.gpuTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus) hwForm.addRow(self.gpuTitle, self.gpuCombo) - # 5. Proxy Settings Section + # 6. Proxy Settings Section proxyFrame, proxyForm = create_section(_("Proxy Settings"), self.theme) proxyForm.setRowWrapPolicy(QFormLayout.RowWrapPolicy.DontWrapRows) scrollLayout.addWidget(proxyFrame) @@ -2923,6 +3049,16 @@ class MainWindow(MainWindowControlHintsMixin, MainWindowSystemTabMixin, MainWind else: ui_config.get_disable_runtime_download() + default_wine = self.defaultExeWineCombo.currentData() + set_user_conf_setting('PW_DEFAULT_WINE_USE', str(default_wine or "")) + default_prefix = self.defaultExePrefixCombo.currentText().strip() + if default_prefix == self.defaultExePrefixCombo.itemText(0): + default_prefix = "" + default_prefix = re.sub(r"[ \t]", "_", default_prefix).upper() if default_prefix else "" + set_user_conf_setting('PW_DEFAULT_PREFIX_NAME', default_prefix) + default_vulkan = self.defaultExeVulkanCombo.currentData() + set_user_conf_setting('PW_DEFAULT_VULKAN_USE', str(default_vulkan or "")) + set_user_conf_setting('MIRROR', self.downloadMirrorCombo.currentText()) # Save GPU selection to user.conf (only if the combo box exists) diff --git a/portprotonqt/portproton_api.py b/portprotonqt/portproton_api.py index 370b0576..12f3328d 100644 --- a/portprotonqt/portproton_api.py +++ b/portprotonqt/portproton_api.py @@ -932,12 +932,12 @@ def get_user_conf_setting(variable_name): clean_lines = [] for line in result.stdout.splitlines(): - stripped_line = line.strip() + stripped_line = re.sub(r'\x1b\[[0-9;]*[A-Za-z]', '', line).strip() if not stripped_line: continue if any(char in stripped_line for char in "█░▄▀╔╗╚╝║"): continue - if stripped_line.startswith(("Warning:", "Info:")): + if re.match(r'^\[?[0-9;]*m?\s*(Info|Warning|Error|OK):', stripped_line): continue if "must use subscript when assigning associative array" in stripped_line: continue diff --git a/portprotonqt/settings_manager.py b/portprotonqt/settings_manager.py index 839e5d2a..68481348 100644 --- a/portprotonqt/settings_manager.py +++ b/portprotonqt/settings_manager.py @@ -1,7 +1,11 @@ import os +import shutil import subprocess +from pathlib import Path from portprotonqt.logger import get_logger +from portprotonqt.version_utils import include_pinned_prefixes +from portprotonqt.version_utils import version_sort_key logger = get_logger(__name__) LG_WINE_ALIASES = ( @@ -40,6 +44,58 @@ def resolve_lg_wine_alias(wine_version: str, env_vars: dict[str, str]) -> str: return wine_version +def get_available_wine_options( + portproton_path: str, + system_wine_label: str = "", + steam_only: bool = False +) -> list[str]: + dist_dir = os.path.join(portproton_path, "data", "dist") + options = [] + if os.path.exists(dist_dir): + options = sorted( + [f for f in os.listdir(dist_dir) if os.path.isdir(os.path.join(dist_dir, f))], + key=version_sort_key + ) + + from portprotonqt.config import get_portproton_scripts_path + scripts_path = get_portproton_scripts_path() + if scripts_path: + var_path = os.path.join(scripts_path, "var") + for version in read_lg_dist_versions_from_var(var_path).values(): + if version not in options: + options.append(version) + + from portprotonqt.steam_api import get_steam_proton_versions + from portprotonqt.steam_api.utils import _is_steam_proton_dir + for version in get_steam_proton_versions(): + if version not in options: + options.append(version) + + if steam_only: + options = [ + version for version in options + if _is_steam_proton_dir( + Path(version) if os.path.isabs(version) else Path(dist_dir, version) + ) + ] + elif system_wine_label and shutil.which('wine') and system_wine_label not in options: + options.append(system_wine_label) + + options.sort(key=version_sort_key) + return options + + +def get_available_prefix_options(portproton_path: str) -> list[str]: + prefixes_dir = os.path.join(portproton_path, "data", "prefixes") + prefixes = [] + if os.path.exists(prefixes_dir): + prefixes = [ + name for name in os.listdir(prefixes_dir) + if os.path.isdir(os.path.join(prefixes_dir, name)) + ] + return include_pinned_prefixes(prefixes) + + def get_available_locale_options() -> list[str]: """Get locale options based on locales available in the system.""" target_locales = ("ru_RU", "en_US", "zh_CN", "ja_JP", "ko_KR")