#!/usr/bin/env python3 import requests import gi import tarfile import shutil import gettext gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gio, GLib from faugus.language_config import * from faugus.dark_theme import * IS_FLATPAK = 'FLATPAK_ID' in os.environ or os.path.exists('/.flatpak-info') if IS_FLATPAK: faugus_png = PathManager.get_icon('io.github.Faugus.faugus-launcher.png') STEAM_COMPATIBILITY_PATH = Path(os.path.expanduser("~/.local/share/Steam/compatibilitytools.d")) else: faugus_png = PathManager.get_icon('faugus-launcher.png') 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') try: translation = gettext.translation('faugus-proton-manager', localedir=LOCALE_DIR, languages=[lang]) translation.install() _ = translation.gettext except FileNotFoundError: gettext.install('faugus-proton-manager', localedir=LOCALE_DIR) _ = gettext.gettext class ConfigManager: def __init__(self): self.default_config = { 'language': lang, } self.config = {} self.load_config() def load_config(self): if os.path.isfile(config_file_dir): with open(config_file_dir, 'r') as f: for line in f.read().splitlines(): if '=' in line: key, value = line.split('=', 1) self.config[key.strip()] = value.strip().strip('"') updated = False for key, default_value in self.default_config.items(): if key not in self.config: self.config[key] = default_value updated = True if updated or not os.path.isfile(config_file_dir): self.save_config() def save_config(self): if not os.path.exists(faugus_launcher_dir): os.makedirs(faugus_launcher_dir) with open(config_file_dir, 'w') as f: for key, value in self.config.items(): f.write(f'{key}={value}\n') class ProtonDownloader(Gtk.Dialog): def __init__(self): super().__init__(title=_("Faugus Proton Manager")) self.set_resizable(False) self.set_modal(True) self.set_icon_from_file(faugus_png) frame = Gtk.Frame() frame.set_margin_start(10) frame.set_margin_end(10) frame.set_margin_top(10) frame.set_margin_bottom(10) self.content_area = self.get_content_area() self.content_area.set_border_width(0) self.content_area.set_halign(Gtk.Align.CENTER) self.content_area.set_valign(Gtk.Align.CENTER) self.content_area.set_vexpand(True) self.content_area.set_hexpand(True) self.content_area.add(frame) self.progress_label = Gtk.Label(label="") self.progress_label.set_margin_start(10) self.progress_label.set_margin_end(10) self.progress_label.set_margin_bottom(10) self.content_area.add(self.progress_label) self.progress_bar = Gtk.ProgressBar() self.progress_bar.set_margin_start(10) self.progress_bar.set_margin_end(10) self.progress_bar.set_margin_bottom(10) self.content_area.add(self.progress_bar) 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) # 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) 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) # 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() self.get_releases() self.show_all() self.progress_bar.set_visible(False) self.progress_label.set_visible(False) def load_config(self): cfg = ConfigManager() self.language = cfg.config.get('language', '') 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 = [] seen_tags = set() while True: response = requests.get(url, params={"page": page, "per_page": 100}) if response.status_code == 200: page_releases = response.json() if not page_releases: break releases.extend(page_releases) page += 1 else: break for release in releases: tag_name = release["tag_name"] if tag_name in seen_tags: continue seen_tags.add(tag_name) if "GloriousEggroll" in url: 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) < (9, 1): continue except Exception: continue elif "Etaash-mathamsetty" in url: if not tag_name.startswith("EM-"): continue assets = release.get("assets", []) has_valid_asset = any( asset["name"].endswith((".tar.gz", ".tar.xz")) for asset in assets ) if not has_valid_asset: continue 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) grid.attach(label, 0, row_index, 1, 1) 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) def get_installed_path(self, tag_name): tag_lower = tag_name.lower() if STEAM_COMPATIBILITY_PATH.exists(): 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): 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) else: self.progress_bar.set_visible(True) self.progress_label.set_visible(True) self.on_download_clicked(widget, release) def disable_all_buttons(self): 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 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", ".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): button.set_label(_("Downloading...")) display_tag_name = f"proton-{tag_name}" if tag_name.startswith("EM-") else tag_name self.progress_label.set_text(_("Downloading %s...") % display_tag_name) self.progress_label.set_visible(True) self.progress_bar.set_visible(True) self.progress_bar.set_fraction(0) button.set_sensitive(False) while Gtk.events_pending(): Gtk.main_iteration_do(False) if not os.path.exists(STEAM_COMPATIBILITY_PATH): os.makedirs(STEAM_COMPATIBILITY_PATH) response = requests.get(url, stream=True) total_size = int(response.headers.get("content-length", 0)) downloaded_size = 0 tar_file_path = os.path.join(os.getcwd(), filename) with open(tar_file_path, "wb") as file: for data in response.iter_content(1024): file.write(data) downloaded_size += len(data) progress = downloaded_size / total_size if total_size > 0 else 0 self.progress_bar.set_fraction(progress) self.progress_bar.set_text(f"{int(progress * 100)}%") while Gtk.events_pending(): Gtk.main_iteration_do(False) file.flush() os.fsync(file.fileno()) 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...")) display_tag_name = f"proton-{tag_name}" if tag_name.startswith("EM-") else tag_name self.progress_label.set_text(_("Extracting %s...") % display_tag_name) self.progress_bar.set_fraction(0) self.progress_bar.set_text("0%") while Gtk.events_pending(): Gtk.main_iteration_do(False) mode = 'r:xz' if tar_file_path.endswith('.tar.xz') else 'r:gz' try: with tarfile.open(tar_file_path, mode) as tar: total_members = len(tar.getmembers()) extracted_members = 0 temp_dir = os.path.join(STEAM_COMPATIBILITY_PATH, f"temp_{tag_name}") os.makedirs(temp_dir, exist_ok=True) for member in tar.getmembers(): tar.extract(member, path=temp_dir, filter="fully_trusted") extracted_members += 1 progress = extracted_members / total_members self.progress_bar.set_fraction(progress) self.progress_bar.set_text(f"{int(progress * 100)}%") while Gtk.events_pending(): Gtk.main_iteration_do(False) 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) self.update_button(button, _("Remove")) self.progress_bar.set_visible(False) self.progress_label.set_visible(False) except Exception as e: print(f"Error during extraction: {e}") self.progress_label.set_text(_("Error during extraction")) self.update_button(button, _("Download")) finally: self.enable_all_buttons() button.set_sensitive(True) def on_remove_clicked(self, widget, release): 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: pass def main(): apply_dark_theme() win = ProtonDownloader() win.connect("destroy", Gtk.main_quit) Gtk.main() if __name__ == "__main__": main()