Added support to Proton-EM

This commit is contained in:
Faugus
2025-06-26 17:46:37 -03:00
committed by GitHub
parent 12a324d21d
commit e92f9b83e2
4 changed files with 420 additions and 174 deletions

View File

@@ -290,7 +290,6 @@ class Main(Gtk.Window):
self.current_prefix = None
self.games = []
self.flowbox_child = None
self.play_button_locked = False
self.updated_steam_id = None
self.game_running = False
@@ -299,6 +298,7 @@ class Main(Gtk.Window):
self.double_click_time_threshold = 500
self.processos = {}
self.button_locked = {}
self.working_directory = faugus_launcher_dir
os.chdir(self.working_directory)
@@ -442,6 +442,8 @@ class Main(Gtk.Window):
for title in to_remove:
del processos[title]
updated = True
if title in self.button_locked:
del self.button_locked[title]
if updated:
with open(running_games, "w") as f:
@@ -458,8 +460,7 @@ class Main(Gtk.Window):
game_label = hbox.get_children()[1]
selected_title = game_label.get_text()
if self.play_button_locked == False:
self.on_item_selected(self.flowbox, selected_child)
self.on_item_selected(self.flowbox, selected_child)
return True
@@ -1437,7 +1438,11 @@ class Main(Gtk.Window):
Gtk.Image.new_from_icon_name("faugus-play-symbolic", Gtk.IconSize.BUTTON))
else:
processos = self.load_processes_from_file()
if title in processos:
if title in self.button_locked:
self.menu_item_play.set_sensitive(False)
self.button_play.set_sensitive(False)
self.button_play.set_image(Gtk.Image.new_from_icon_name("faugus-stop-symbolic", Gtk.IconSize.BUTTON))
elif title in processos:
self.menu_item_play.set_sensitive(True)
self.button_play.set_sensitive(True)
self.button_play.set_image(Gtk.Image.new_from_icon_name("faugus-stop-symbolic", Gtk.IconSize.BUTTON))
@@ -1446,6 +1451,7 @@ class Main(Gtk.Window):
self.button_play.set_sensitive(True)
self.button_play.set_image(
Gtk.Image.new_from_icon_name("faugus-play-symbolic", Gtk.IconSize.BUTTON))
else:
self.menu_item_edit.set_sensitive(False)
self.menu_item_delete.set_sensitive(False)
@@ -1494,6 +1500,8 @@ class Main(Gtk.Window):
default_runner = ""
if default_runner == "GE-Proton Latest (default)":
default_runner = "GE-Proton"
if default_runner == "Proton-EM Latest":
default_runner = "Proton-EM"
# Handle dialog response
if response_id == Gtk.ResponseType.OK:
@@ -1603,21 +1611,22 @@ class Main(Gtk.Window):
title = game_label.get_text()
processos = self.load_processes_from_file()
self.play_button_locked = True
self.button_locked[title] = True
if title in processos:
data = processos[title]
pid = data.get("main")
if pid:
parent = psutil.Process(pid)
children = parent.children(recursive=True)
for child in children:
child.terminate()
for key in ("umu", "main"):
pid = data.get(key)
if pid:
try:
proc = psutil.Process(pid)
for child in proc.children(recursive=True):
child.terminate()
proc.terminate()
except psutil.NoSuchProcess:
continue
parent.terminate()
self.play_button_locked = False
return
# Find the selected game object
@@ -1727,6 +1736,7 @@ class Main(Gtk.Window):
def find_pid(self, game):
try:
# faugus-run é o processo main, que é um python3
parent = psutil.Process(self.processo.pid)
all_descendants = parent.children(recursive=True)
except psutil.NoSuchProcess:
@@ -1737,28 +1747,28 @@ class Main(Gtk.Window):
for proc in all_descendants:
try:
name = os.path.splitext(proc.name())[0].lower()
if name == "umu-run" or name == "python3":
if name == "umu-run":
umu_run_pid = proc.pid
break
except psutil.NoSuchProcess:
continue
if umu_run_pid:
self.save_process_to_file(game.title, umu_run_pid)
self.menu_item_play.set_sensitive(True)
self.button_play.set_sensitive(True)
self.button_play.set_image(Gtk.Image.new_from_icon_name("faugus-stop-symbolic", Gtk.IconSize.BUTTON))
self.play_button_locked = False
return True
# Salva apenas o processo python3 (main) e o umu-run (se existir)
self.save_process_to_file(
game.title,
main_pid=self.processo.pid,
umu_pid=umu_run_pid
)
self.menu_item_play.set_sensitive(True)
self.button_play.set_sensitive(True)
self.button_play.set_image(Gtk.Image.new_from_icon_name("faugus-play-symbolic", Gtk.IconSize.BUTTON))
self.button_play.set_image(Gtk.Image.new_from_icon_name("faugus-stop-symbolic", Gtk.IconSize.BUTTON))
if game.title in self.button_locked:
del self.button_locked[game.title]
return False
return True
def save_process_to_file(self, title, processo_pid, _unused=None):
def save_process_to_file(self, title, main_pid, umu_pid=None):
os.makedirs(os.path.dirname(running_games), exist_ok=True)
try:
@@ -1767,7 +1777,10 @@ class Main(Gtk.Window):
except (FileNotFoundError, json.JSONDecodeError):
processos = {}
processos[title] = {"main": processo_pid}
processos[title] = {
"main": main_pid,
"umu": umu_pid
}
with open(running_games, "w") as f:
json.dump(processos, f, indent=2)
@@ -1801,6 +1814,7 @@ class Main(Gtk.Window):
done
""", shell=True)
self.game_running = False
self.button_locked.clear()
def on_button_add_clicked(self, widget):
file_path = ""
@@ -1836,6 +1850,8 @@ class Main(Gtk.Window):
game_runner = "GE-Proton Latest (default)"
if game.runner == "":
game_runner = "UMU-Proton Latest"
if game.runner == "Proton-EM":
game_runner = "Proton-EM Latest"
if game_runner == "Linux-Native":
edit_game_dialog.combo_box_launcher.set_active(1)
@@ -2134,6 +2150,8 @@ class Main(Gtk.Window):
runner = ""
if runner == "GE-Proton Latest (default)":
runner = "GE-Proton"
if runner == "Proton-EM Latest":
runner = "Proton-EM"
if add_game_dialog.combo_box_launcher.get_active() == 1:
runner = "Linux-Native"
@@ -2519,6 +2537,8 @@ class Main(Gtk.Window):
game.runner = ""
if game.runner == "GE-Proton Latest (default)":
game.runner = "GE-Proton"
if game.runner == "Proton-EM Latest":
game.runner = "Proton-EM"
if edit_game_dialog.combo_box_launcher.get_active() == 1:
game.runner = "Linux-Native"
@@ -3146,7 +3166,7 @@ class Settings(Gtk.Dialog):
self.label_runner.set_halign(Gtk.Align.START)
self.combo_box_runner = Gtk.ComboBoxText()
self.button_proton_manager = Gtk.Button(label=_("GE-Proton Manager"))
self.button_proton_manager = Gtk.Button(label=_("Proton Manager"))
self.button_proton_manager.connect("clicked", self.on_button_proton_manager_clicked)
self.label_miscellaneous = Gtk.Label(label=_("Miscellaneous"))
@@ -3547,6 +3567,8 @@ class Settings(Gtk.Dialog):
default_runner = ""
if default_runner == "GE-Proton Latest (default)":
default_runner = "GE-Proton"
if default_runner == "Proton-EM Latest":
default_runner = "Proton-EM"
config = ConfigManager()
config.save_with_values(checkbox_state, default_prefix, mangohud_state, gamemode_state, disable_hidraw_state,
@@ -3581,6 +3603,7 @@ class Settings(Gtk.Dialog):
# List of default entries
self.combo_box_runner.append_text("GE-Proton Latest (default)")
self.combo_box_runner.append_text("UMU-Proton Latest")
self.combo_box_runner.append_text("Proton-EM Latest")
# Path to the directory containing the folders
if IS_FLATPAK:
@@ -3657,6 +3680,8 @@ class Settings(Gtk.Dialog):
default_runner = ""
if default_runner == "GE-Proton Latest (default)":
default_runner = "GE-Proton"
if default_runner == "Proton-EM Latest":
default_runner = "Proton-EM"
config = ConfigManager()
config.save_with_values(checkbox_state, default_prefix, mangohud_state, gamemode_state, disable_hidraw_state,
@@ -3817,6 +3842,8 @@ class Settings(Gtk.Dialog):
default_runner = ""
if default_runner == "GE-Proton Latest (default)":
default_runner = "GE-Proton"
if default_runner == "Proton-EM Latest":
default_runner = "Proton-EM"
config = ConfigManager()
config.save_with_values(checkbox_state, default_prefix, mangohud_state, gamemode_state, disable_hidraw_state,
@@ -3904,6 +3931,8 @@ class Settings(Gtk.Dialog):
default_runner = ""
if default_runner == "GE-Proton Latest (default)":
default_runner = "GE-Proton"
if default_runner == "Proton-EM Latest":
default_runner = "Proton-EM"
config = ConfigManager()
config.save_with_values(checkbox_state, default_prefix, mangohud_state, gamemode_state, disable_hidraw_state,
@@ -4949,6 +4978,8 @@ class AddGame(Gtk.Dialog):
self.default_runner = "UMU-Proton Latest"
if self.default_runner == "GE-Proton":
self.default_runner = "GE-Proton Latest (default)"
if self.default_runner == "Proton-EM":
self.default_runner = "Proton-EM Latest"
for i, row in enumerate(model):
if row[0] == self.default_runner:
@@ -5406,6 +5437,7 @@ class AddGame(Gtk.Dialog):
# List of default entries
self.combo_box_runner.append_text("GE-Proton Latest (default)")
self.combo_box_runner.append_text("UMU-Proton Latest")
self.combo_box_runner.append_text("Proton-EM Latest")
# Path to the directory containing the folders
if IS_FLATPAK:
@@ -5543,6 +5575,8 @@ class AddGame(Gtk.Dialog):
runner = ""
if runner == "GE-Proton Latest (default)":
runner = "GE-Proton"
if runner == "Proton-EM Latest":
runner = "Proton-EM"
command_parts = []
@@ -5864,6 +5898,8 @@ class AddGame(Gtk.Dialog):
runner = ""
if runner == "GE-Proton Latest (default)":
runner = "GE-Proton"
if runner == "Proton-EM Latest":
runner = "Proton-EM"
command_parts = []
@@ -5919,6 +5955,8 @@ class AddGame(Gtk.Dialog):
runner = ""
if runner == "GE-Proton Latest (default)":
runner = "GE-Proton"
if runner == "Proton-EM Latest":
runner = "Proton-EM"
command_parts = []
@@ -6909,6 +6947,8 @@ def run_file(file_path):
default_runner = ""
if default_runner == "GE-Proton Latest (default)":
default_runner = "GE-Proton"
if default_runner == "Proton-EM Latest":
default_runner = "Proton-EM"
command_parts = []

View File

@@ -0,0 +1,71 @@
#!/usr/bin/python3
import os
import requests
import tarfile
import re
from pathlib import Path
import threading
STEAM_COMPAT_DIR = Path.home() / ".local/share/Steam/compatibilitytools.d"
GITHUB_API_URL = "https://api.github.com/repos/Etaash-mathamsetty/Proton/releases/latest"
DOWNLOAD_BASE_URL = "https://github.com/Etaash-mathamsetty/Proton/releases/download"
def get_latest_release_tag():
response = requests.get(GITHUB_API_URL)
if response.status_code == 200:
return response.json()["tag_name"]
else:
print("Failed to access GitHub API:", response.status_code, flush=True)
return None
def get_installed_proton_versions():
if not STEAM_COMPAT_DIR.exists():
return []
return sorted([
entry.name.replace("proton-", "")
for entry in STEAM_COMPAT_DIR.iterdir()
if entry.is_dir() and re.match(r"proton-EM-\d+\.\d+-\d+", entry.name)
])
def download_and_extract(version_tag):
tar_name = f"proton-{version_tag}.tar.xz"
url = f"{DOWNLOAD_BASE_URL}/{version_tag}/{tar_name}"
tar_path = STEAM_COMPAT_DIR / tar_name
print(f"Downloading {tar_name}...", flush=True)
response = requests.get(url, stream=True)
if response.status_code == 200:
with open(tar_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
else:
print("Failed to download:", response.status_code)
return
print("Extracting archive...", flush=True)
with tarfile.open(tar_path, "r:xz") as tar:
tar.extractall(path=STEAM_COMPAT_DIR)
os.remove(tar_path)
print("Proton installed successfully.", flush=True)
def main():
latest_version = get_latest_release_tag()
if not latest_version:
return
installed_versions = get_installed_proton_versions()
print("Latest available version:", latest_version, flush=True)
print("Installed versions:", ", ".join(installed_versions) or "none", flush=True)
if latest_version not in installed_versions:
thread = threading.Thread(target=download_and_extract, args=(latest_version,))
thread.start()
thread.join()
else:
print("The latest version is already installed.", flush=True)
if __name__ == "__main__":
main()

View File

@@ -5,7 +5,6 @@ import gi
import os
import tarfile
import shutil
import sys
import gettext
import locale
from pathlib import Path
@@ -43,9 +42,7 @@ class PathManager:
for path in icon_paths:
if Path(path).exists():
return path
return icon_paths[-1] # Fallback
GITHUB_API_URL = "https://api.github.com/repos/GloriousEggroll/proton-ge-custom/releases"
return icon_paths[-1]
IS_FLATPAK = 'FLATPAK_ID' in os.environ or os.path.exists('/.flatpak-info')
if IS_FLATPAK:
@@ -53,11 +50,10 @@ if IS_FLATPAK:
STEAM_COMPATIBILITY_PATH = Path(os.path.expanduser("~/.local/share/Steam/compatibilitytools.d"))
else:
faugus_png = PathManager.get_icon('faugus-launcher.png')
STEAM_COMPATIBILITY_PATH = PathManager.user_data("Steam/compatibilitytools.d")
STEAM_COMPATIBILITY_PATH = Path(PathManager.user_data("Steam/compatibilitytools.d"))
config_file_dir = PathManager.user_config('faugus-launcher/config.ini')
faugus_launcher_dir = PathManager.user_config('faugus-launcher')
prefixes_dir = PathManager.user_config('faugus-launcher/prefixes')
def get_system_locale():
lang = os.environ.get('LANG') or os.environ.get('LC_MESSAGES')
@@ -65,13 +61,9 @@ def get_system_locale():
return lang.split('.')[0]
try:
loc = locale.getdefaultlocale()[0]
if loc:
return loc
return locale.getdefaultlocale()[0] or 'en_US'
except Exception:
pass
return 'en_US'
return 'en_US'
def get_language_from_config():
if os.path.exists(config_file_dir):
@@ -82,9 +74,7 @@ def get_language_from_config():
return line.split('=', 1)[1].strip()
return None
lang = get_language_from_config()
if not lang:
lang = get_system_locale()
lang = get_language_from_config() or get_system_locale()
LOCALE_DIR = (
PathManager.system_data('locale')
@@ -93,42 +83,18 @@ LOCALE_DIR = (
)
try:
translation = gettext.translation(
'faugus-proton-manager',
localedir=LOCALE_DIR,
languages=[lang] if lang else ['en_US']
)
translation = gettext.translation('faugus-proton-manager', localedir=LOCALE_DIR, languages=[lang])
translation.install()
globals()['_'] = translation.gettext
_ = translation.gettext
except FileNotFoundError:
gettext.install('faugus-proton-manager', localedir=LOCALE_DIR)
globals()['_'] = gettext.gettext
_ = gettext.gettext
class ConfigManager:
def __init__(self):
self.default_config = {
'close-onlaunch': 'False',
'default-prefix': prefixes_dir,
'mangohud': 'False',
'gamemode': 'False',
'disable-hidraw': 'False',
'default-runner': 'GE-Proton',
'discrete-gpu': 'False',
'splash-disable': 'False',
'system-tray': 'False',
'start-boot': 'False',
'mono-icon': 'False',
'interface-mode': 'List',
'start-maximized': 'False',
'start-fullscreen': 'False',
'show-labels': 'False',
'smaller-banners': 'False',
'enable-logging': 'False',
'wayland-driver': 'False',
'enable-hdr': 'False',
'language': lang,
}
self.config = {}
self.load_config()
@@ -138,9 +104,7 @@ class ConfigManager:
for line in f.read().splitlines():
if '=' in line:
key, value = line.split('=', 1)
key = key.strip()
value = value.strip().strip('"')
self.config[key] = value
self.config[key.strip()] = value.strip().strip('"')
updated = False
for key, default_value in self.default_config.items():
@@ -157,14 +121,11 @@ class ConfigManager:
with open(config_file_dir, 'w') as f:
for key, value in self.config.items():
if key in ['default-prefix', 'default-runner']:
f.write(f'{key}="{value}"\n')
else:
f.write(f'{key}={value}\n')
f.write(f'{key}={value}\n')
class ProtonDownloader(Gtk.Dialog):
def __init__(self):
super().__init__(title=_("Faugus GE-Proton Manager"))
super().__init__(title=_("Faugus Proton Manager"))
self.set_resizable(False)
self.set_modal(True)
self.set_icon_from_file(faugus_png)
@@ -195,28 +156,58 @@ class ProtonDownloader(Gtk.Dialog):
self.progress_bar.set_margin_bottom(10)
self.content_area.add(self.progress_bar)
# Scrolled window to hold the Grid
self.scrolled_window = Gtk.ScrolledWindow()
self.scrolled_window.set_size_request(400, 400)
self.scrolled_window.set_margin_start(10)
self.scrolled_window.set_margin_end(10)
self.scrolled_window.set_margin_top(10)
self.scrolled_window.set_margin_bottom(10)
self.notebook = Gtk.Notebook()
self.notebook.set_halign(Gtk.Align.FILL)
self.notebook.set_valign(Gtk.Align.FILL)
self.notebook.set_vexpand(True)
self.notebook.set_hexpand(True)
frame.add(self.notebook)
# Grid for releases
self.grid = Gtk.Grid()
self.scrolled_window.add(self.grid)
# Tab 1: GE-Proton
self.grid_ge = Gtk.Grid()
self.grid_ge.set_hexpand(True)
self.grid_ge.set_row_spacing(5)
self.grid_ge.set_column_spacing(10)
scroll_ge = Gtk.ScrolledWindow()
scroll_ge.set_size_request(400, 400)
scroll_ge.set_margin_top(10)
scroll_ge.set_margin_bottom(10)
scroll_ge.set_margin_start(10)
scroll_ge.set_margin_end(10)
scroll_ge.add(self.grid_ge)
frame.add(self.scrolled_window)
tab_box_ge = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
tab_label_ge = Gtk.Label(label="GE-Proton")
tab_label_ge.set_width_chars(15)
tab_label_ge.set_xalign(0.5)
tab_box_ge.pack_start(tab_label_ge, True, True, 0)
tab_box_ge.set_hexpand(True)
tab_box_ge.show_all()
self.notebook.append_page(scroll_ge, tab_box_ge)
# Set row and column spacing
self.grid.set_row_spacing(5)
self.grid.set_column_spacing(10)
# Tab 2: Proton-EM
self.grid_em = Gtk.Grid()
self.grid_em.set_hexpand(True)
self.grid_em.set_row_spacing(5)
self.grid_em.set_column_spacing(10)
scroll_em = Gtk.ScrolledWindow()
scroll_em.set_size_request(400, 400)
scroll_em.set_margin_top(10)
scroll_em.set_margin_bottom(10)
scroll_em.set_margin_start(10)
scroll_em.set_margin_end(10)
scroll_em.add(self.grid_em)
tab_box_em = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
tab_label_em = Gtk.Label(label="Proton-EM")
tab_label_em.set_width_chars(15)
tab_label_em.set_xalign(0.5)
tab_box_em.pack_start(tab_label_em, True, True, 0)
tab_box_em.set_hexpand(True)
tab_box_em.show_all()
self.notebook.append_page(scroll_em, tab_box_em)
self.load_config()
# Fetch and populate releases in the Grid
self.releases = []
self.get_releases()
self.show_all()
self.progress_bar.set_visible(False)
@@ -226,50 +217,105 @@ class ProtonDownloader(Gtk.Dialog):
cfg = ConfigManager()
self.language = cfg.config.get('language', '')
def filter_releases(self):
filtered_releases = []
for release in self.releases:
filtered_releases.append(release)
if release["tag_name"] == "GE-Proton8-1":
break
return filtered_releases
def get_releases(self):
self.fetch_releases_from_url(
"https://api.github.com/repos/GloriousEggroll/proton-ge-custom/releases",
self.grid_ge
)
self.fetch_releases_from_url(
"https://api.github.com/repos/Etaash-mathamsetty/Proton/releases",
self.grid_em
)
def fetch_releases_from_url(self, url, grid):
page = 1
releases = []
while True:
response = requests.get(GITHUB_API_URL, params={"page": page, "per_page": 100})
response = requests.get(url, params={"page": page, "per_page": 100})
if response.status_code == 200:
releases = response.json()
if not releases:
page_releases = response.json()
if not page_releases:
break
self.releases.extend(releases)
releases.extend(page_releases)
page += 1
else:
print(_("Error fetching releases:"), response.status_code)
break
self.releases = self.filter_releases()
for release in releases:
tag_name = release["tag_name"]
for release in self.releases:
self.add_release_to_grid(release)
if "GloriousEggroll" in url:
# Esperado: GE-Proton8-1
if not tag_name.startswith("GE-Proton"):
continue
try:
version_str = tag_name.replace("GE-Proton", "")
major, minor = map(int, version_str.split("-"))
if (major, minor) < (8, 1):
continue
except Exception:
continue
def add_release_to_grid(self, release):
row_index = len(self.grid.get_children()) // 2
elif "Etaash-mathamsetty" in url:
# Esperado: EM-10.0-4
if not tag_name.startswith("EM-"):
continue
try:
version_str = tag_name.replace("EM-", "")
part1, part2 = version_str.split("-")
major, minor = map(int, part1.split("."))
patch = int(part2)
if (major, minor, patch) < (10, 0, 4):
continue
except Exception:
continue
label = Gtk.Label(label=release["tag_name"], xalign=0)
self.add_release_to_grid(release, grid)
def add_release_to_grid(self, release, grid):
tag_name = release["tag_name"]
display_tag_name = f"proton-{tag_name}" if tag_name.startswith("EM-") else tag_name
row_index = len(grid.get_children()) // 2
label = Gtk.Label(label=display_tag_name, xalign=0)
label.set_halign(Gtk.Align.START)
label.set_hexpand(True)
self.grid.attach(label, 0, row_index, 1, 1)
grid.attach(label, 0, row_index, 1, 1)
version_path = os.path.join(STEAM_COMPATIBILITY_PATH, release["tag_name"])
button = Gtk.Button(label=_("Remove") if os.path.exists(version_path) else _("Download"))
version_path = self.get_installed_path(display_tag_name)
is_installed = os.path.exists(version_path)
button = Gtk.Button(label=_("Remove") if is_installed else _("Download"))
button.connect("clicked", self.on_button_clicked, release)
button.set_size_request(120, -1)
grid.attach(button, 1, row_index, 1, 1)
self.grid.attach(button, 1, row_index, 1, 1)
def get_installed_path(self, tag_name):
if not STEAM_COMPATIBILITY_PATH.exists():
return None
tag_lower = tag_name.lower()
for folder in STEAM_COMPATIBILITY_PATH.iterdir():
folder_name_lower = folder.name.lower()
if folder_name_lower.endswith(tag_lower):
return folder
if tag_lower.startswith("proton-"):
if folder_name_lower.endswith(tag_lower[len("proton-"):]):
return folder
return STEAM_COMPATIBILITY_PATH / tag_name
def update_button(self, button, new_label):
button.set_label(new_label)
button.set_sensitive(True)
def on_button_clicked(self, widget, release):
version_path = os.path.join(STEAM_COMPATIBILITY_PATH, release["tag_name"])
tag_name = release["tag_name"]
if tag_name.startswith("EM-"):
tag_name = f"proton-{tag_name}"
version_path = self.get_installed_path(tag_name)
if os.path.exists(version_path):
self.on_remove_clicked(widget, release)
@@ -279,29 +325,33 @@ class ProtonDownloader(Gtk.Dialog):
self.on_download_clicked(widget, release)
def disable_all_buttons(self):
for child in self.grid.get_children():
if isinstance(child, Gtk.Button):
child.set_sensitive(False)
for grid in (self.grid_ge, self.grid_em):
for child in grid.get_children():
if isinstance(child, Gtk.Button):
child.set_sensitive(False)
def enable_all_buttons(self):
for child in self.grid.get_children():
if isinstance(child, Gtk.Button):
child.set_sensitive(True)
for grid in (self.grid_ge, self.grid_em):
for child in grid.get_children():
if isinstance(child, Gtk.Button):
child.set_sensitive(True)
def on_download_clicked(self, widget, release):
self.disable_all_buttons()
for asset in release["assets"]:
if asset["name"].endswith(".tar.gz"):
download_url = asset["browser_download_url"]
self.download_and_extract(download_url, asset["name"], release["tag_name"], widget)
if asset["name"].endswith((".tar.gz", ".tar.xz")):
self.download_and_extract(
asset["browser_download_url"],
asset["name"],
release["tag_name"],
widget
)
break
def download_and_extract(self, url, filename, tag_name, button):
dialog = Gtk.Dialog(title=_("Downloading"), parent=self, modal=True)
dialog.set_resizable(False)
button.set_label(_("Downloading..."))
self.progress_label.set_text(_("Downloading {tag}...").format(tag=tag_name))
display_tag_name = f"proton-{tag_name}" if tag_name.startswith("EM-") else tag_name
self.progress_label.set_text(_("Downloading {tag}...").format(tag=display_tag_name))
button.set_sensitive(False)
if not os.path.exists(STEAM_COMPATIBILITY_PATH):
@@ -321,16 +371,35 @@ class ProtonDownloader(Gtk.Dialog):
self.progress_bar.set_text(f"{int(progress * 100)}%")
Gtk.main_iteration_do(False)
dialog.destroy()
self.extract_tar_and_update_button(tar_file_path, tag_name, button)
def extract_tar_and_update_button(self, tar_file_path, tag_name, button):
button.set_label(_("Extracting..."))
self.progress_label.set_text(_("Extracting {tag}...").format(tag=tag_name))
display_tag_name = f"proton-{tag_name}" if tag_name.startswith("EM-") else tag_name
self.progress_label.set_text(_("Extracting {tag}...").format(tag=display_tag_name))
Gtk.main_iteration_do(False)
self.extract_tar(tar_file_path, STEAM_COMPATIBILITY_PATH, self.progress_bar)
mode = 'r:xz' if tar_file_path.endswith('.tar.xz') else 'r:gz'
with tarfile.open(tar_file_path, mode) as tar:
temp_dir = os.path.join(STEAM_COMPATIBILITY_PATH, f"temp_{tag_name}")
os.makedirs(temp_dir, exist_ok=True)
tar.extractall(path=temp_dir)
extracted_dir = None
for item in os.listdir(temp_dir):
item_path = os.path.join(temp_dir, item)
if os.path.isdir(item_path):
extracted_dir = item_path
break
if extracted_dir:
final_dir = os.path.join(STEAM_COMPATIBILITY_PATH, os.path.basename(extracted_dir))
if os.path.exists(final_dir):
shutil.rmtree(final_dir)
shutil.move(extracted_dir, STEAM_COMPATIBILITY_PATH)
shutil.rmtree(temp_dir)
os.remove(tar_file_path)
@@ -340,31 +409,14 @@ class ProtonDownloader(Gtk.Dialog):
self.enable_all_buttons()
button.set_sensitive(True)
def extract_tar(self, tar_file_path, extract_to, progress_bar):
try:
with tarfile.open(tar_file_path, "r:gz") as tar:
members = tar.getmembers()
total_members = len(members)
for index, member in enumerate(members, start=1):
tar.extract(member, path=extract_to)
progress = index / total_members
progress_bar.set_fraction(progress)
progress_bar.set_text(_("Extracting... {percent}%").format(percent=int(progress * 100)))
Gtk.main_iteration_do(False)
except Exception as e:
print(_("Failed to extract {tar_file_path}: {error}").format(tar_file_path=tar_file_path, error=e))
def on_remove_clicked(self, widget, release):
version_path = os.path.join(STEAM_COMPATIBILITY_PATH, release["tag_name"])
if os.path.exists(version_path):
version_path = self.get_installed_path(release["tag_name"])
if version_path and os.path.exists(version_path):
try:
shutil.rmtree(version_path)
self.update_button(widget, _("Download"))
except Exception as e:
print(_("Failed to remove {version_path}: {error}").format(version_path=version_path, error=e))
def update_button(self, button, new_label):
button.set_label(new_label)
except Exception:
pass
def apply_dark_theme():
desktop_env = Gio.Settings.new("org.gnome.desktop.interface")

View File

@@ -78,6 +78,7 @@ else:
config_file_dir = PathManager.user_config('faugus-launcher/config.ini')
faugus_launcher_dir = PathManager.user_config('faugus-launcher')
faugus_components = PathManager.find_binary('faugus-components')
faugus_proton_downloader = PathManager.find_binary('faugus-proton-downloader')
prefixes_dir = str(Path.home() / 'Faugus')
logs_dir = PathManager.user_config('faugus-launcher/logs')
faugus_notification = PathManager.system_data('faugus-launcher/faugus-notification.ogg')
@@ -245,9 +246,29 @@ class FaugusRun:
Gtk.main_quit()
sys.exit()
def update_protonpath(self, message):
compatibility_dir = os.path.expanduser("~/.local/share/Steam/compatibilitytools.d")
versions = [
d for d in os.listdir(compatibility_dir)
if os.path.isdir(os.path.join(compatibility_dir, d)) and d.startswith("proton-EM-")
]
if not versions:
return message
versions.sort(key=lambda v: [int(x) for x in re.findall(r'\d+', v)], reverse=True)
latest_version = versions[0]
updated_message = re.sub(r'PROTONPATH=Proton-EM\b', f'PROTONPATH={latest_version}', message)
return updated_message
def update_test(self):
if "Proton-EM" in self.message:
self.message = self.update_protonpath(self.message)
def start_process(self, command):
protonpath = next((part.split('=')[1] for part in self.message.split() if part.startswith("PROTONPATH=")), None)
if protonpath and protonpath != "GE-Proton":
if protonpath and protonpath != "GE-Proton" and protonpath != "Proton-EM":
protonpath_path = Path(share_dir) / 'Steam/compatibilitytools.d' / protonpath
if not protonpath_path.is_dir():
self.close_warning_dialog()
@@ -257,14 +278,16 @@ class FaugusRun:
self.default_runner = ""
if self.default_runner == "GE-Proton Latest (default)":
self.default_runner = "GE-Proton"
if self.default_runner == "Proton-EM Latest":
self.default_runner = "Proton-EM"
discrete_gpu = "DRI_PRIME=1"
self.discrete_gpu = "DRI_PRIME=1"
if not self.discrete_gpu:
discrete_gpu = "DRI_PRIME=0"
self.discrete_gpu = "DRI_PRIME=0"
if self.discrete_gpu:
discrete_gpu = "DRI_PRIME=1"
self.discrete_gpu = "DRI_PRIME=1"
if self.discrete_gpu == None:
discrete_gpu = "DRI_PRIME=1"
self.discrete_gpu = "DRI_PRIME=1"
if "WINEPREFIX" not in self.message:
if self.default_runner:
@@ -300,29 +323,83 @@ class FaugusRun:
if match:
self.game_title = match.group(1).split("/")[-1]
if "UMU_NO_PROTON" not in self.message:
self.run_processes_sequentially()
def run_processes_sequentially(self):
if "UMU_NO_PROTON" not in self.message and "Proton-EM" in self.message:
if self.enable_logging:
self.message = f'UMU_LOG=1 PROTON_LOG_DIR={logs_dir}/{self.game_title} PROTON_LOG=1 {self.message}'
self.process = subprocess.Popen(
[PathManager.find_binary("bash"), "-c", f"{faugus_components}; {discrete_gpu} {eac_dir} {be_dir} {self.message}"],
[PathManager.find_binary("bash"), "-c", f"{faugus_proton_downloader}"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
bufsize=8192,
text=True
)
self.stdout_watch_id = GLib.io_add_watch(
self.process.stdout,
GLib.PRIORITY_LOW,
GLib.IO_IN,
self.on_output
)
self.stderr_watch_id = GLib.io_add_watch(
self.process.stderr,
GLib.PRIORITY_LOW,
GLib.IO_IN,
self.on_output
)
GLib.child_watch_add(
GLib.PRIORITY_DEFAULT,
self.process.pid,
self.on_proton_downloader_finished
)
else:
self.process = subprocess.Popen(
[PathManager.find_binary("bash"), "-c", f"{discrete_gpu} {eac_dir} {be_dir} {self.message}"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
self.execute_final_command()
print(self.message)
def on_proton_downloader_finished(self, pid, status):
if hasattr(self, 'stdout_watch_id'):
GLib.source_remove(self.stdout_watch_id)
if hasattr(self, 'stderr_watch_id'):
GLib.source_remove(self.stderr_watch_id)
self.update_test()
GLib.io_add_watch(self.process.stdout, GLib.IO_IN, self.on_output)
GLib.io_add_watch(self.process.stderr, GLib.IO_IN, self.on_output)
self.execute_final_command()
GLib.child_watch_add(self.process.pid, self.on_process_exit)
def execute_final_command(self):
if "UMU_NO_PROTON" not in self.message:
cmd = f"{faugus_components}; {self.discrete_gpu} {eac_dir} {be_dir} {self.message}"
else:
cmd = f"{self.discrete_gpu} {eac_dir} {be_dir} {self.message}"
self.process = subprocess.Popen(
[PathManager.find_binary("bash"), "-c", cmd],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
bufsize=8192,
text=True
)
self.stdout_watch_id = GLib.io_add_watch(
self.process.stdout,
GLib.PRIORITY_LOW,
GLib.IO_IN,
self.on_output
)
self.stderr_watch_id = GLib.io_add_watch(
self.process.stderr,
GLib.PRIORITY_LOW,
GLib.IO_IN,
self.on_output
)
GLib.child_watch_add(
GLib.PRIORITY_DEFAULT,
self.process.pid,
self.on_process_exit
)
def set_ld_preload(self):
lib_paths = [
@@ -486,6 +563,12 @@ class FaugusRun:
self.label.set_text(_("UMU-Proton is up to date"))
if "mtree is OK" in clean_line:
self.label2.set_text(_("Steam Runtime is up to date"))
if "Downloading proton-EM" in clean_line:
self.label.set_text(_("Downloading Proton-EM..."))
if "Extracting archive" in clean_line:
self.label.set_text(_("Extracting Proton-EM..."))
if "Proton installed successfully" in clean_line:
self.label.set_text(_("Proton-EM is up to date"))
if "UMU_NO_PROTON" in self.message:
if "steamrt3 is up to date" in clean_line or "mtree is OK" in clean_line: