Skip to content

Commit

Permalink
Prompt for Steam directory if multiple found
Browse files Browse the repository at this point in the history
Prompt the user for the correct Steam installation directory if multiple
installations are found and `STEAM_DIR` isn't set.

The previous behavior is confusing when using the application from
desktop, and most users can't be expected to change the environment
variable manually in this case.

Fixes #181
  • Loading branch information
Matoking committed Nov 17, 2022
1 parent fd93e45 commit 490faf5
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 83 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).


## [Unreleased]
### Added
- Prompt the user for a Steam installation if multiple installations are found

### Fixed
- Detect XDG user directory permissions in Flatpak environment

Expand Down
16 changes: 11 additions & 5 deletions src/protontricks/cli/launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
from pathlib import Path
from subprocess import run

from ..gui import prompt_filesystem_access, select_steam_app_with_gui
from ..steam import find_steam_path, get_steam_apps, get_steam_lib_paths
from ..gui import (prompt_filesystem_access, select_steam_app_with_gui,
select_steam_installation)
from ..steam import (find_steam_installations, find_steam_path, get_steam_apps,
get_steam_lib_paths)
from .main import main as cli_main
from .util import (CustomArgumentParser, cli_error_handler, enable_logging,
exit_with_error)
Expand Down Expand Up @@ -109,10 +111,14 @@ def exit_(error):
executable_path = Path(args.executable).resolve()

# 1. Find Steam path
steam_path, steam_root = find_steam_path()
if not steam_path:
steam_installations = find_steam_installations()
if not steam_installations:
exit_("Steam installation directory could not be found.")

steam_path, steam_root = select_steam_installation(steam_installations)
if not steam_path:
exit_("No Steam installation was selected.")

# 2. Find any Steam library folders
steam_lib_paths = get_steam_lib_paths(steam_path)

Expand Down Expand Up @@ -182,7 +188,7 @@ def exit_(error):
logger.info(
"Calling `protontricks` with the command: %s", cli_args
)
cli_main(cli_args)
cli_main(cli_args, steam_path=steam_path, steam_root=steam_root)


if __name__ == "__main__":
Expand Down
22 changes: 17 additions & 5 deletions src/protontricks/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
from .. import __version__
from ..flatpak import (FLATPAK_BWRAP_COMPATIBLE_VERSION,
get_running_flatpak_version)
from ..gui import prompt_filesystem_access, select_steam_app_with_gui
from ..gui import (prompt_filesystem_access, select_steam_app_with_gui,
select_steam_installation)
from ..steam import (find_legacy_steam_runtime_path, find_proton_app,
find_steam_path, get_steam_apps, get_steam_lib_paths)
find_steam_installations, get_steam_apps,
get_steam_lib_paths)
from ..util import run_command
from ..winetricks import get_winetricks_path
from .util import (CustomArgumentParser, cli_error_handler, enable_logging,
Expand All @@ -31,7 +33,7 @@ def cli(args=None):


@cli_error_handler
def main(args=None):
def main(args=None, steam_path=None, steam_root=None):
"""
'protontricks' script entrypoint
"""
Expand Down Expand Up @@ -188,9 +190,19 @@ def exit_(error):
use_bwrap = False

# 1. Find Steam path
steam_path, steam_root = find_steam_path()
# We can skip the Steam installation detection if the CLI entrypoint
# has already been provided the path as a keyword argument.
# This is the case when this entrypoint is being called by
# 'protontricks-launch'. This prevents us from asking the user for
# the Steam installation twice.
if not steam_path:
exit_("Steam installation directory could not be found.")
steam_installations = find_steam_installations()
if not steam_installations:
exit_("Steam installation directory could not be found.")

steam_path, steam_root = select_steam_installation(steam_installations)
if not steam_path:
exit_("No Steam installation was selected.")

# 2. Find the pre-installed legacy Steam Runtime if enabled
legacy_steam_runtime_path = None
Expand Down
158 changes: 114 additions & 44 deletions src/protontricks/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

__all__ = (
"LocaleError", "get_gui_provider", "select_steam_app_with_gui",
"show_text_dialog", "prompt_filesystem_access"
"select_steam_installation", "show_text_dialog", "prompt_filesystem_access"
)

logger = logging.getLogger("protontricks")
Expand Down Expand Up @@ -77,6 +77,42 @@ def _get_appid2icon(steam_apps, steam_path):
return appid2icon


def _run_gui(args, input_=None, strip_nonascii=False):
"""
Run YAD/Zenity with the given args.
If 'strip_nonascii' is True, strip non-ASCII characters to workaround
environments that can't handle all characters
"""
if strip_nonascii:
# Convert to bytes and back to strings while stripping
# non-ASCII characters
args = [
arg.encode("ascii", "ignore").decode("ascii") for arg in args
]
if input_:
input_ = input_.encode("ascii", "ignore").decode("ascii")

if input_:
input_ = input_.encode("utf-8")

try:
return run(
args, input=input_, check=True, stdout=PIPE, stderr=PIPE,
)
except CalledProcessError as exc:
if exc.returncode == 255 and not strip_nonascii:
# User has weird locale settings. Log a warning and
# rerun the command while stripping non-ASCII characters.
logger.warning(
"Your system locale is incapable of displaying all "
"characters. Some app names may not show up correctly. "
"Please use an UTF-8 locale to avoid this warning."
)
return _run_gui(args, strip_nonascii=True)

raise

def show_text_dialog(
title,
text,
Expand Down Expand Up @@ -130,6 +166,82 @@ def _get_zenity_args():
return process.returncode == 0


def select_steam_installation(steam_installations):
"""
Prompt the user to select a Steam installation if more than one
installation is available
Return the selected (steam_path, steam_root) installation, or None
if the user picked nothing
"""
def _get_yad_args():
return [
"yad", "--list", "--no-headers", "--center",
"--window-icon", "wine",
# Disabling markup means we won't have to escape special characters
"--no-markup",
"--width", "600", "--height", "400",
"--text", "Select Steam installation",
"--title", "Protontricks",
"--column", "Path"
]

def _get_zenity_args():
return [
"zenity", "--list", "--hide-header",
"--width", "600",
"--height", "400",
"--text", "Select Steam installation",
"--title", "Protontricks",
"--column", "Path"
]

if len(steam_installations) == 1:
return steam_installations[0]

gui_provider = get_gui_provider()

cmd_input = []

for i, installation in enumerate(steam_installations):
steam_path, steam_root = installation

is_flatpak = (
str(steam_path).endswith(
"/com.valvesoftware.Steam/.local/share/Steam"
)
)
install_type = "Flatpak" if is_flatpak else "Native"

cmd_input.append(f"{i+1}: {install_type} - {steam_path}")

cmd_input = "\n".join(cmd_input)

if gui_provider == "yad":
args = _get_yad_args()
elif gui_provider == "zenity":
args = _get_zenity_args()

try:
result = _run_gui(args, input_=cmd_input)
choice = result.stdout
except CalledProcessError as exc:
if exc.returncode in (1, 252):
# YAD returns 252 when dialog is closed by pressing Esc
# No installation was selected
choice = b""
else:
raise RuntimeError("{} returned an error".format(gui_provider))

if choice in (b"", b" \n"):
return None, None

choice = choice.decode("utf-8").split(":")[0]
choice = int(choice) - 1

return steam_installations[choice]


def select_steam_app_with_gui(steam_apps, steam_path, title=None):
"""
Prompt the user to select a Proton-enabled Steam app from
Expand Down Expand Up @@ -162,37 +274,6 @@ def _get_zenity_args():
"--column", "Steam app"
]

def run_gui(args, input_=None, strip_nonascii=False):
"""
Run YAD/Zenity with the given args.
If 'strip_nonascii' is True, strip non-ASCII characters to workaround
environments that can't handle all characters
"""
if strip_nonascii:
# Convert to bytes and back to strings while stripping
# non-ASCII characters
args = [
arg.encode("ascii", "ignore").decode("ascii") for arg in args
]
if input_:
input_ = input_.encode("ascii", "ignore").decode("ascii")

if input_:
input_ = input_.encode("utf-8")

try:
return run(
args, input=input_, check=True, stdout=PIPE, stderr=PIPE,
)
except CalledProcessError as exc:
if exc.returncode == 255:
# User locale incapable of handling all characters in the
# command
raise LocaleError()

raise

if not title:
title = "Select Steam app"

Expand Down Expand Up @@ -223,18 +304,7 @@ def run_gui(args, input_=None, strip_nonascii=False):
cmd_input = "\n".join(cmd_input)

try:
try:
result = run_gui(args, input_=cmd_input)
except LocaleError:
# User has weird locale settings. Log a warning and
# run the command while stripping non-ASCII characters.
logger.warning(
"Your system locale is incapable of displaying all "
"characters. Some app names may not show up correctly. "
"Please use an UTF-8 locale to avoid this warning."
)
result = run_gui(args, strip_nonascii=True)

result = _run_gui(args, input_=cmd_input)
choice = result.stdout
except CalledProcessError as exc:
# TODO: Remove this hack once the bug has been fixed upstream
Expand Down
50 changes: 33 additions & 17 deletions src/protontricks/steam.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@
import string
import struct
import zlib
from pathlib import Path
from collections import OrderedDict
from pathlib import Path

import vdf

from .flatpak import is_flatpak_sandbox
from .util import lower_dict

__all__ = (
"COMMON_STEAM_DIRS", "SteamApp", "find_steam_path",
"find_legacy_steam_runtime_path", "iter_appinfo_sections",
"get_appinfo_sections", "get_tool_appid", "find_steam_compat_tool_app",
"find_appid_proton_prefix", "find_proton_app", "get_steam_lib_paths",
"get_compat_tool_dirs", "get_custom_compat_tool_installations_in_dir",
"COMMON_STEAM_DIRS", "SteamApp", "find_steam_installations",
"find_steam_path", "find_legacy_steam_runtime_path",
"iter_appinfo_sections", "get_appinfo_sections", "get_tool_appid",
"find_steam_compat_tool_app", "find_appid_proton_prefix",
"find_proton_app", "get_steam_lib_paths", "get_compat_tool_dirs",
"get_custom_compat_tool_installations_in_dir",
"get_custom_compat_tool_installations", "find_current_steamid3",
"get_appid_from_shortcut", "get_custom_windows_shortcuts",
"get_steam_apps", "is_steam_deck"
Expand Down Expand Up @@ -279,14 +280,10 @@ def _get_required_tool_appid(path):
return None


def find_steam_path():
def find_steam_installations():
"""
Try to discover default Steam dir using common locations and return the
first one that matches
Return (steam_path, steam_root), where steam_path points to
"~/.steam/steam" (contains "appcache", "config" and "steamapps")
and "~/.steam/root" (contains "ubuntu12_32" and "compatibilitytools.d")
Find all Steam installations and return them as a list of (steam_path, steam_root)
tuples
"""
def has_steamapps_dir(path):
"""
Expand Down Expand Up @@ -315,14 +312,16 @@ def has_runtime_dir(path):
"Found a valid Steam installation at %s.", steam_path
)

return steam_path, steam_path
return [
(Path(steam_path), Path(steam_path))
]

logger.error(
"$STEAM_DIR was provided but didn't point to a valid Steam "
"installation."
)

return None, None
return []

# Track the found Steam directory candidates using an ordered dict,
# ensuring that any duplicates are eliminated and that we pick the first
Expand Down Expand Up @@ -355,6 +354,23 @@ def has_runtime_dir(path):
for steam_path, _ in candidates.keys():
logger.info("Found Steam directory at %s", steam_path)

return [
[Path(steam_path), Path(steam_root)]
for steam_path, steam_root in candidates.keys()
]


def find_steam_path():
"""
Try to discover default Steam dir using common locations and return the
first one that matches
Return (steam_path, steam_root), where steam_path points to
"~/.steam/steam" (contains "appcache", "config" and "steamapps")
and "~/.steam/root" (contains "ubuntu12_32" and "compatibilitytools.d")
"""
candidates = find_steam_installations()

if len(candidates) > 1:
logger.warning(
"Found multiple Steam directories. If you want to select a "
Expand All @@ -363,11 +379,11 @@ def has_runtime_dir(path):
)
logger.warning("$ STEAM_DIR=<path> protontricks <appid> <command>")
logger.warning("The following Steam directories were found:")
for steam_path, _ in candidates.keys():
for steam_path, _ in candidates:
logger.warning(" %s", str(steam_path))

try:
steam_path, steam_root = list(candidates.keys())[0]
steam_path, steam_root = candidates[0]
logger.info(
"Using Steam directory at %s. You can also define Steam directory "
"manually using $STEAM_DIR", str(steam_path)
Expand Down
Loading

0 comments on commit 490faf5

Please sign in to comment.