Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,22 @@ function findPythonCmd(projectRoot: string): string {
return "python3";
}

const EXTRA_PATH_DIRS = [
"/usr/bin",
"/usr/local/bin",
"/var/usrlocal/bin",
"/usr/lib/extensions/vulkan/MangoHud/bin",
"/usr/lib64/extensions/vulkan/MangoHud/bin",
"/run/current-system/sw/bin", // NixOS
].join(":");

function getPythonCommand(): { cmd: string; args: string[]; env: NodeJS.ProcessEnv } {
const env = { ...process.env };
// Immutable distros (Bazzite, SteamOS, Fedora Atomic) and AppImage
// wrappers can strip PATH entries. Ensure common locations are present.
if (env.PATH && !env.PATH.includes("/var/usrlocal/bin")) {
env.PATH = `${env.PATH}:${EXTRA_PATH_DIRS}`;
}

if (isDev) {
const projectRoot = path.resolve(__dirname, "..", "..");
Expand Down
2 changes: 1 addition & 1 deletion electron/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "protonshift",
"version": "0.8.8",
"version": "0.8.9",
"description": "Linux game configuration toolkit",
"main": "dist/main.js",
"scripts": {
Expand Down
3 changes: 2 additions & 1 deletion electron/renderer/src/app/mangohud/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,9 @@ export default function MangoHudPage() {
<p className="text-xs text-text-secondary font-medium">Install via your package manager:</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{[
{ distro: "Bazzite / SteamOS", cmd: "Pre-installed — restart app or check PATH" },
{ distro: "Ubuntu / Pop!_OS / Mint", cmd: "sudo apt install mangohud" },
{ distro: "Fedora", cmd: "sudo dnf install mangohud" },
{ distro: "Fedora / Fedora Atomic", cmd: "sudo dnf install mangohud" },
{ distro: "Arch / Manjaro / EndeavourOS", cmd: "sudo pacman -S mangohud" },
{ distro: "openSUSE", cmd: "sudo zypper install mangohud" },
{ distro: "Flatpak (Steam)", cmd: "flatpak install flathub org.freedesktop.Platform.VulkanLayer.MangoHud" },
Expand Down
3 changes: 2 additions & 1 deletion electron/renderer/src/components/gamescope-builder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,9 @@ export function GamescopeBuilder({ onInsert }: GamescopeBuilderProps) {
<p className="text-xs text-text-secondary font-medium">Install via your package manager:</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-1.5">
{[
{ distro: "Bazzite / SteamOS", cmd: "Pre-installed — restart app or check PATH" },
{ distro: "Arch / Manjaro / EndeavourOS", cmd: "sudo pacman -S gamescope" },
{ distro: "Fedora", cmd: "sudo dnf install gamescope" },
{ distro: "Fedora / Fedora Atomic", cmd: "sudo dnf install gamescope" },
{ distro: "openSUSE", cmd: "sudo zypper install gamescope" },
{ distro: "NixOS", cmd: "nix-env -iA nixpkgs.gamescope" },
{ distro: "Flatpak (Steam)", cmd: "Bundled with Steam Flatpak" },
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "protonshift"
version = "0.8.8"
version = "0.8.9"
description = "Linux game configuration toolkit: GPU, launch options, Proton, env vars"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
18 changes: 11 additions & 7 deletions src/game_setup_hub/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@

import os
import re
import shutil
import subprocess
from dataclasses import dataclass

from game_setup_hub.tool_check import find_tool


@dataclass
class MonitorInfo:
Expand All @@ -33,11 +34,12 @@ def get_session_type() -> str:

def _parse_xrandr() -> list[MonitorInfo]:
"""Parse xrandr output for connected monitors."""
if not shutil.which("xrandr"):
xrandr = find_tool("xrandr")
if not xrandr:
return []
try:
result = subprocess.run(
["xrandr", "--current"],
[xrandr, "--current"],
capture_output=True,
text=True,
timeout=5,
Expand Down Expand Up @@ -93,11 +95,12 @@ def _parse_xrandr() -> list[MonitorInfo]:

def _parse_wlr_randr() -> list[MonitorInfo]:
"""Parse wlr-randr output for Wayland monitors."""
if not shutil.which("wlr-randr"):
wlr_randr = find_tool("wlr-randr")
if not wlr_randr:
return []
try:
result = subprocess.run(
["wlr-randr"],
[wlr_randr],
capture_output=True,
text=True,
timeout=5,
Expand Down Expand Up @@ -158,9 +161,10 @@ def get_monitors() -> list[MonitorInfo]:

def set_resolution(monitor: str, width: int, height: int, refresh: float = 0) -> bool:
"""Set resolution for a monitor using xrandr. Returns True on success."""
if not shutil.which("xrandr"):
xrandr = find_tool("xrandr")
if not xrandr:
return False
cmd = ["xrandr", "--output", monitor, "--mode", f"{width}x{height}"]
cmd = [xrandr, "--output", monitor, "--mode", f"{width}x{height}"]
if refresh > 0:
cmd.extend(["--rate", str(refresh)])
try:
Expand Down
5 changes: 3 additions & 2 deletions src/game_setup_hub/gamescope.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

from __future__ import annotations

import shutil
from dataclasses import dataclass

from game_setup_hub.tool_check import is_tool_available


@dataclass
class GamescopeOptions:
Expand All @@ -25,7 +26,7 @@ class GamescopeOptions:

def is_gamescope_available() -> bool:
"""Check if gamescope is installed."""
return shutil.which("gamescope") is not None
return is_tool_available("gamescope")


def build_gamescope_cmd(opts: GamescopeOptions) -> str:
Expand Down
5 changes: 3 additions & 2 deletions src/game_setup_hub/mangohud.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

from __future__ import annotations

import shutil
from pathlib import Path

from game_setup_hub.tool_check import is_tool_available

MANGOHUD_CONFIG_DIR = Path.home() / ".config" / "MangoHud"
MANGOHUD_GLOBAL_CONF = MANGOHUD_CONFIG_DIR / "MangoHud.conf"

Expand Down Expand Up @@ -166,7 +167,7 @@

def is_mangohud_available() -> bool:
"""Check if MangoHud is installed."""
return shutil.which("mangohud") is not None
return is_tool_available("mangohud")


def read_mangohud_config(path: Path | None = None) -> dict[str, str]:
Expand Down
19 changes: 5 additions & 14 deletions src/game_setup_hub/presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

from __future__ import annotations

import shutil
from dataclasses import dataclass

from game_setup_hub.tool_check import is_tool_available


@dataclass
class LaunchPreset:
Expand All @@ -19,20 +20,10 @@ class LaunchPreset:
def is_installed(self) -> bool:
"""Check if the tool/feature is available."""
if self.value == "gamemoderun":
return _check_gamemode()
return is_tool_available("gamemoderun")
if "MANGOHUD" in self.value:
return _check_mangohud()
return True # No check for env vars, Proton Log, NVIDIA dGPU


def _check_gamemode() -> bool:
"""Check if gamemode is installed."""
return shutil.which("gamemoderun") is not None


def _check_mangohud() -> bool:
"""Check if MangoHud is installed."""
return shutil.which("mangohud") is not None
return is_tool_available("mangohud")
return True


LAUNCH_PRESETS: list[LaunchPreset] = [
Expand Down
10 changes: 6 additions & 4 deletions src/game_setup_hub/protontricks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

from __future__ import annotations

import shutil
import subprocess

from game_setup_hub.tool_check import find_tool


PROTONTRICKS_FLATPAK = "com.github.Matoking.protontricks"

Expand All @@ -20,7 +21,7 @@

def is_protontricks_available() -> bool:
"""Check if Protontricks is available (native or Flatpak)."""
if shutil.which("protontricks"):
if find_tool("protontricks"):
return True
try:
r = subprocess.run(
Expand All @@ -39,8 +40,9 @@ def get_protontricks_cmd(app_id: str, verb: str | None = None) -> list[str] | No
Returns [cmd, ...args] or None if not available.
If verb is None, opens GUI (--gui).
"""
if shutil.which("protontricks"):
cmd = ["protontricks", app_id]
pt = find_tool("protontricks")
if pt:
cmd = [pt, app_id]
if verb:
cmd.append(verb)
else:
Expand Down
93 changes: 93 additions & 0 deletions src/game_setup_hub/tool_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""Unified tool detection with fallback paths for immutable distros.

On Bazzite, SteamOS, Fedora Atomic (Kinoite/Silverblue), NixOS, and
Flatpak-installed tools, binaries often live outside the default PATH
that shutil.which() searches. This module provides a single
``find_tool()`` helper that first tries the normal PATH lookup, then
probes well-known fallback locations.
"""

from __future__ import annotations

import os
import shutil
from functools import lru_cache
from pathlib import Path

_EXTRA_BIN_DIRS: tuple[str, ...] = (
"/usr/bin",
"/usr/local/bin",
"/usr/games",
"/var/usrlocal/bin",
# Flatpak host extensions / SDK merge dirs
"/usr/lib/extensions/vulkan/MangoHud/bin",
"/usr/lib64/extensions/vulkan/MangoHud/bin",
# NixOS profile links
os.path.expanduser("~/.nix-profile/bin"),
"/run/current-system/sw/bin",
# Homebrew / Linuxbrew
"/home/linuxbrew/.linuxbrew/bin",
)

_TOOL_SPECIFIC_PATHS: dict[str, tuple[str, ...]] = {
"gamescope": (
"/usr/bin/gamescope",
"/usr/local/bin/gamescope",
"/usr/lib/extensions/gamescope/bin/gamescope",
),
"mangohud": (
"/usr/bin/mangohud",
"/usr/local/bin/mangohud",
"/usr/lib/extensions/vulkan/MangoHud/bin/mangohud",
"/usr/lib64/extensions/vulkan/MangoHud/bin/mangohud",
),
"gamemoderun": (
"/usr/bin/gamemoderun",
"/usr/local/bin/gamemoderun",
"/usr/lib/extensions/gamemode/bin/gamemoderun",
),
"protontricks": (
"/usr/bin/protontricks",
"/usr/local/bin/protontricks",
os.path.expanduser("~/.local/bin/protontricks"),
),
"xrandr": (
"/usr/bin/xrandr",
"/usr/local/bin/xrandr",
),
"wlr-randr": (
"/usr/bin/wlr-randr",
"/usr/local/bin/wlr-randr",
),
}


@lru_cache(maxsize=32)
def find_tool(name: str) -> str | None:
"""Locate a tool binary, returning the absolute path or ``None``.

1. ``shutil.which(name)`` — uses the current process PATH.
2. ``shutil.which(name, path=...)`` — searches extra bin directories
common on immutable/atomic distros.
3. Direct file existence checks for tool-specific known paths.
"""
found = shutil.which(name)
if found:
return found

extra_path = os.pathsep.join(_EXTRA_BIN_DIRS)
found = shutil.which(name, path=extra_path)
if found:
return found

for candidate in _TOOL_SPECIFIC_PATHS.get(name, ()):
p = Path(candidate)
if p.is_file() and os.access(p, os.X_OK):
return str(p)

return None


def is_tool_available(name: str) -> bool:
"""Return True if *name* can be found anywhere on the system."""
return find_tool(name) is not None
Loading