From 2cc29fcf63f08cf1e245f2fa7c2ad802c1f0f347 Mon Sep 17 00:00:00 2001 From: Faugus Date: Sat, 3 Jan 2026 18:20:31 -0300 Subject: [PATCH] Some code restructuring --- ...Faugus.faugus-launcher.shortcut.desktop.in | 2 +- faugus_components.py => faugus/components.py | 0 faugus/config_manager.py | 72 ++ faugus/dark_theme.py | 28 + faugus/language_config.py | 38 + faugus/meson.build | 10 + faugus/path_manager.py | 45 + .../proton_downloader.py | 0 faugus/shortcut.py | 993 ++++++++++++++++++ faugus_launcher.py | 973 +---------------- faugus_proton_manager.py | 90 +- faugus_run.py | 127 +-- meson.build | 5 +- 13 files changed, 1202 insertions(+), 1181 deletions(-) rename faugus_components.py => faugus/components.py (100%) create mode 100644 faugus/config_manager.py create mode 100644 faugus/dark_theme.py create mode 100644 faugus/language_config.py create mode 100644 faugus/meson.build create mode 100644 faugus/path_manager.py rename faugus_proton_downloader.py => faugus/proton_downloader.py (100%) create mode 100755 faugus/shortcut.py diff --git a/data/io.github.Faugus.faugus-launcher.shortcut.desktop.in b/data/io.github.Faugus.faugus-launcher.shortcut.desktop.in index eb29eff..e0bca56 100644 --- a/data/io.github.Faugus.faugus-launcher.shortcut.desktop.in +++ b/data/io.github.Faugus.faugus-launcher.shortcut.desktop.in @@ -1,7 +1,7 @@ [Desktop Entry] Type=Application Name=Faugus Create Shortcut -Exec=faugus-launcher --shortcut %f +Exec=python -m faugus.shortcut %f MimeType=application/x-ms-dos-executable;application/x-msi;application/x-ms-shortcut;application/x-bat;text/x-ms-regedit Icon=@ICON@ NoDisplay=true diff --git a/faugus_components.py b/faugus/components.py similarity index 100% rename from faugus_components.py rename to faugus/components.py diff --git a/faugus/config_manager.py b/faugus/config_manager.py new file mode 100644 index 0000000..4f8dfec --- /dev/null +++ b/faugus/config_manager.py @@ -0,0 +1,72 @@ +from faugus.language_config import * + +faugus_launcher_dir = PathManager.user_config('faugus-launcher') +prefixes_dir = str(Path.home() / 'Faugus') + +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', + 'lossless-location': '', + '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', + 'enable-wow64': 'False', + 'language': lang, + 'logging-warning': 'False', + 'show-hidden': 'False', + } + + 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) + key = key.strip() + value = value.strip().strip('"') + self.config[key] = value + + 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(): + if key in ['default-prefix', 'default-runner']: + f.write(f'{key}="{value}"\n') + else: + f.write(f'{key}={value}\n') + + def save_with_values(self, *args): + keys = list(self.default_config.keys()) + for key, value in zip(keys, args): + self.config[key] = str(value) + self.save_config() \ No newline at end of file diff --git a/faugus/dark_theme.py b/faugus/dark_theme.py new file mode 100644 index 0000000..21aa9a3 --- /dev/null +++ b/faugus/dark_theme.py @@ -0,0 +1,28 @@ +import os +from gi.repository import Gtk, Gio, GLib +IS_FLATPAK = 'FLATPAK_ID' in os.environ or os.path.exists('/.flatpak-info') + +def apply_dark_theme(): + if IS_FLATPAK: + if (os.environ.get("XDG_CURRENT_DESKTOP")) == "KDE": + Gtk.Settings.get_default().set_property("gtk-theme-name", "Breeze") + try: + proxy = Gio.DBusProxy.new_sync( + Gio.bus_get_sync(Gio.BusType.SESSION, None), 0, None, + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Settings", None) + is_dark = proxy.call_sync( + "Read", GLib.Variant("(ss)", ("org.freedesktop.appearance", "color-scheme")), + 0, -1, None).unpack()[0] == 1 + except: + is_dark = False + Gtk.Settings.get_default().set_property("gtk-application-prefer-dark-theme", is_dark) + else: + desktop_env = Gio.Settings.new("org.gnome.desktop.interface") + try: + is_dark_theme = desktop_env.get_string("color-scheme") == "prefer-dark" + except Exception: + is_dark_theme = "-dark" in desktop_env.get_string("gtk-theme") + if is_dark_theme: + Gtk.Settings.get_default().set_property("gtk-application-prefer-dark-theme", True) \ No newline at end of file diff --git a/faugus/language_config.py b/faugus/language_config.py new file mode 100644 index 0000000..1b0e157 --- /dev/null +++ b/faugus/language_config.py @@ -0,0 +1,38 @@ +import locale + +from faugus.path_manager import * + +config_file_dir = PathManager.user_config('faugus-launcher/config.ini') + +def get_system_locale(): + lang = os.environ.get('LANG') or os.environ.get('LC_MESSAGES') + if lang: + return lang.split('.')[0] + + try: + loc = locale.getdefaultlocale()[0] + if loc: + return loc + except Exception: + pass + + return 'en_US' + +def get_language_from_config(): + if os.path.exists(config_file_dir): + with open(config_file_dir, 'r') as f: + for line in f: + line = line.strip() + if line.startswith('language='): + return line.split('=', 1)[1].strip() + return None + +lang = get_language_from_config() +if not lang: + lang = get_system_locale() + +LOCALE_DIR = ( + PathManager.system_data('locale') + if os.path.isdir(PathManager.system_data('locale')) + else os.path.join(os.path.dirname(__file__), 'locale') +) \ No newline at end of file diff --git a/faugus/meson.build b/faugus/meson.build new file mode 100644 index 0000000..7d2bf75 --- /dev/null +++ b/faugus/meson.build @@ -0,0 +1,10 @@ +py.install_sources( + 'components.py', + 'proton_downloader.py', + 'shortcut.py', + 'config_manager.py', + 'path_manager.py', + 'language_config.py', + 'dark_theme.py', + subdir: 'faugus', +) diff --git a/faugus/path_manager.py b/faugus/path_manager.py new file mode 100644 index 0000000..37e8b34 --- /dev/null +++ b/faugus/path_manager.py @@ -0,0 +1,45 @@ +#!/usr/bin/python3 + +import os +from pathlib import Path + +class PathManager: + @staticmethod + def system_data(*relative_paths): + xdg_data_dirs = os.getenv('XDG_DATA_DIRS', '/usr/local/share:/usr/share').split(':') + for data_dir in xdg_data_dirs: + path = Path(data_dir).joinpath(*relative_paths) + if path.exists(): + return str(path) + return str(Path(xdg_data_dirs[0]).joinpath(*relative_paths)) + + @staticmethod + def user_data(*relative_paths): + xdg_data_home = Path(os.getenv('XDG_DATA_HOME', Path.home() / '.local/share')) + return str(xdg_data_home.joinpath(*relative_paths)) + + @staticmethod + def user_config(*relative_paths): + xdg_config_home = Path(os.getenv('XDG_CONFIG_HOME', Path.home() / '.config')) + return str(xdg_config_home.joinpath(*relative_paths)) + + @staticmethod + def find_binary(binary_name): + paths = os.getenv('PATH', '').split(':') + for path in paths: + binary_path = Path(path) / binary_name + if binary_path.exists(): + return str(binary_path) + return f'/usr/bin/{binary_name}' # Fallback + + @staticmethod + def get_icon(icon_name): + icon_paths = [ + PathManager.user_data('icons', icon_name), + PathManager.system_data('icons/hicolor/256x256/apps', icon_name), + PathManager.system_data('icons', icon_name) + ] + for path in icon_paths: + if Path(path).exists(): + return path + return icon_paths[-1] # Fallback \ No newline at end of file diff --git a/faugus_proton_downloader.py b/faugus/proton_downloader.py similarity index 100% rename from faugus_proton_downloader.py rename to faugus/proton_downloader.py diff --git a/faugus/shortcut.py b/faugus/shortcut.py new file mode 100755 index 0000000..ac506ba --- /dev/null +++ b/faugus/shortcut.py @@ -0,0 +1,993 @@ +#!/usr/bin/python3 + +import gi +import sys +import gettext +import shutil +import subprocess +import re +import webbrowser +import locale + +gi.require_version("Gtk", "3.0") +gi.require_version('Gdk', '3.0') + +from gi.repository import Gtk, Gdk, GdkPixbuf, GLib +from PIL import Image +from faugus.path_manager import * +from faugus.dark_theme import * + +IS_FLATPAK = 'FLATPAK_ID' in os.environ or os.path.exists('/.flatpak-info') +if IS_FLATPAK: + app_dir = str(Path.home() / '.local/share/applications') + faugus_png = PathManager.get_icon('io.github.Faugus.faugus-launcher.png') + lsfgvk_path = Path("/usr/lib/extensions/vulkan/lsfgvk/lib/liblsfg-vk.so") + lsfgvk_path = lsfgvk_path if lsfgvk_path.exists() else Path(os.path.expanduser('~/.local/lib/liblsfg-vk.so')) +else: + app_dir = PathManager.user_data('applications') + faugus_png = PathManager.get_icon('faugus-launcher.png') + lsfgvk_possible_paths = [ + Path("/usr/lib/liblsfg-vk.so"), + Path("/usr/lib64/liblsfg-vk.so"), + Path(os.path.expanduser('~/.local/lib/liblsfg-vk.so')) + ] + lsfgvk_path = next((p for p in lsfgvk_possible_paths if p.exists()), lsfgvk_possible_paths[-1]) +icons_dir = PathManager.user_config('faugus-launcher/icons') +config_file_dir = PathManager.user_config('faugus-launcher/config.ini') +prefixes_dir = str(Path.home() / 'Faugus') +mangohud_dir = PathManager.find_binary('mangohud') +gamemoderun = PathManager.find_binary('gamemoderun') +umu_run = PathManager.user_data('faugus-launcher/umu-run') +faugus_run = PathManager.find_binary('faugus-run') +faugus_launcher_dir = PathManager.user_config('faugus-launcher') +faugus_notification = PathManager.system_data('faugus-launcher/faugus-notification.ogg') + +def get_desktop_dir(): + try: + desktop_dir = subprocess.check_output(['xdg-user-dir', 'DESKTOP'], text=True).strip() + return desktop_dir + except (FileNotFoundError, subprocess.CalledProcessError): + print("xdg-user-dir not found or failed; falling back to ~/Desktop") + return str(Path.home() / 'Desktop') + +desktop_dir = get_desktop_dir() + +def find_lossless_dll(): + possible_common_locations = [ + Path.home() / '.local' / 'share' / 'Steam' / 'steamapps' / 'common', + Path.home() / '.steam' / 'steam' / 'steamapps' / 'common', + Path.home() / '.steam' / 'root' / 'steamapps' / 'common', + Path.home() / 'SteamLibrary' / 'steamapps' / 'common', + Path(os.path.expanduser('~/.var/app/com.valvesoftware.Steam/.steam/steamapps/common/')) + ] + + for location in possible_common_locations: + dll_candidate = location / 'Lossless Scaling' / 'Lossless.dll' + if dll_candidate.exists(): + return str(dll_candidate) + + return "" + +def get_system_locale(): + lang = os.environ.get('LANG') or os.environ.get('LC_MESSAGES') + if lang: + return lang.split('.')[0] + + try: + loc = locale.getdefaultlocale()[0] + if loc: + return loc + except Exception: + pass + + return 'en_US' + +def get_language_from_config(): + if os.path.exists(config_file_dir): + with open(config_file_dir, 'r') as f: + for line in f: + line = line.strip() + if line.startswith('language='): + return line.split('=', 1)[1].strip() + return None + +lang = get_language_from_config() +if not lang: + lang = get_system_locale() + +LOCALE_DIR = ( + PathManager.system_data('locale') + if os.path.isdir(PathManager.system_data('locale')) + else os.path.join(os.path.dirname(__file__), 'locale') +) + +try: + translation = gettext.translation( + 'faugus-launcher', + localedir=LOCALE_DIR, + languages=[lang] if lang else ['en_US'] + ) + translation.install() + globals()['_'] = translation.gettext +except FileNotFoundError: + gettext.install('faugus-launcher', localedir=LOCALE_DIR) + globals()['_'] = gettext.gettext + +def format_title(title): + title = title.strip().lower() + title = re.sub(r"['’]", "", title) + title = re.sub(r"[^a-z0-9]+", "-", title) + title = title.strip("-") + return title + +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', + 'lossless-location': '', + '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', + 'enable-wow64': 'False', + 'language': lang, + 'logging-warning': 'False', + 'show-hidden': 'False', + } + + 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) + key = key.strip() + value = value.strip().strip('"') + self.config[key] = value + + 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(): + if key in ['default-prefix', 'default-runner']: + f.write(f'{key}="{value}"\n') + else: + f.write(f'{key}={value}\n') + + def save_with_values(self, *args): + keys = list(self.default_config.keys()) + for key, value in zip(keys, args): + self.config[key] = str(value) + self.save_config() + +class CreateShortcut(Gtk.Window): + def __init__(self, file_path): + super().__init__(title="Faugus Launcher") + self.file_path = file_path + self.set_resizable(False) + self.set_icon_from_file(faugus_png) + + game_title = os.path.basename(file_path) + self.set_title(game_title) + print(self.file_path) + + self.icon_directory = f"{icons_dir}/icon_temp/" + + if not os.path.exists(self.icon_directory): + os.makedirs(self.icon_directory) + + self.icons_path = icons_dir + self.icon_extracted = os.path.expanduser(f'{self.icons_path}/icon_temp/icon.ico') + self.icon_converted = os.path.expanduser(f'{self.icons_path}/icon_temp/icon.png') + self.icon_temp = f'{self.icons_path}/icon_temp.ico' + + self.default_prefix = "" + + self.lossless_enabled = False + self.lossless_multiplier = 1 + self.lossless_flow = 100 + self.lossless_performance = False + self.lossless_hdr = False + + self.label_title = Gtk.Label(label=_("Title")) + self.label_title.set_halign(Gtk.Align.START) + self.entry_title = Gtk.Entry() + self.entry_title.connect("changed", self.on_entry_changed, self.entry_title) + self.entry_title.set_tooltip_text(_("Game Title")) + + self.label_protonfix = Gtk.Label(label="Protonfix") + self.label_protonfix.set_halign(Gtk.Align.START) + self.entry_protonfix = Gtk.Entry() + self.entry_protonfix.set_tooltip_text("UMU ID") + self.button_search_protonfix = Gtk.Button() + self.button_search_protonfix.set_image( + Gtk.Image.new_from_icon_name("system-search-symbolic", Gtk.IconSize.BUTTON)) + self.button_search_protonfix.connect("clicked", self.on_button_search_protonfix_clicked) + self.button_search_protonfix.set_size_request(50, -1) + + self.label_launch_arguments = Gtk.Label(label=_("Launch Arguments")) + self.label_launch_arguments.set_halign(Gtk.Align.START) + self.entry_launch_arguments = Gtk.Entry() + self.entry_launch_arguments.set_tooltip_text(_("e.g.: PROTON_USE_WINED3D=1 gamescope -W 2560 -H 1440")) + + self.label_game_arguments = Gtk.Label(label=_("Game Arguments")) + self.label_game_arguments.set_halign(Gtk.Align.START) + self.entry_game_arguments = Gtk.Entry() + self.entry_game_arguments.set_tooltip_text(_("e.g.: -d3d11 -fullscreen")) + + self.button_lossless = Gtk.Button(label=_("Lossless Scaling Frame Generation")) + self.button_lossless.connect("clicked", self.on_button_lossless_clicked) + + self.label_addapp = Gtk.Label(label=_("Additional Application")) + self.label_addapp.set_halign(Gtk.Align.START) + self.entry_addapp = Gtk.Entry() + self.entry_addapp.set_tooltip_text(_("/path/to/the/app")) + self.button_search_addapp = Gtk.Button() + self.button_search_addapp.set_image(Gtk.Image.new_from_icon_name("system-search-symbolic", Gtk.IconSize.BUTTON)) + self.button_search_addapp.connect("clicked", self.on_button_search_addapp_clicked) + self.button_search_addapp.set_size_request(50, -1) + + self.button_shortcut_icon = Gtk.Button() + self.button_shortcut_icon.set_size_request(120, -1) + self.button_shortcut_icon.set_tooltip_text(_("Select an icon for the shortcut")) + self.button_shortcut_icon.connect("clicked", self.on_button_shortcut_icon_clicked) + + self.checkbox_mangohud = Gtk.CheckButton(label="MangoHud") + self.checkbox_mangohud.set_tooltip_text( + _("Shows an overlay for monitoring FPS, temperatures, CPU/GPU load and more.")) + self.checkbox_gamemode = Gtk.CheckButton(label="GameMode") + self.checkbox_gamemode.set_tooltip_text(_("Tweaks your system to improve performance.")) + self.checkbox_disable_hidraw = Gtk.CheckButton(label=_("Disable Hidraw")) + self.checkbox_disable_hidraw.set_tooltip_text( + _("May fix controller issues with some games. Only works with GE-Proton10 or Proton-EM-10.")) + + # Button Cancel + self.button_cancel = Gtk.Button(label=_("Cancel")) + self.button_cancel.connect("clicked", self.on_cancel_clicked) + self.button_cancel.set_size_request(150, -1) + + # Button Ok + self.button_ok = Gtk.Button(label=_("Ok")) + self.button_ok.connect("clicked", self.on_ok_clicked) + self.button_ok.set_size_request(150, -1) + + css_provider = Gtk.CssProvider() + css = """ + .entry { + border-color: Red; + } + """ + css_provider.load_from_data(css.encode('utf-8')) + Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), css_provider, + Gtk.STYLE_PROVIDER_PRIORITY_USER) + + self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + self.box.set_margin_start(0) + self.box.set_margin_end(0) + self.box.set_margin_top(0) + self.box.set_margin_bottom(0) + + frame = Gtk.Frame() + frame.set_margin_start(10) + frame.set_margin_end(10) + frame.set_margin_top(10) + frame.set_margin_bottom(10) + + self.grid_title = Gtk.Grid() + self.grid_title.set_row_spacing(10) + self.grid_title.set_column_spacing(10) + self.grid_title.set_margin_start(10) + self.grid_title.set_margin_end(10) + self.grid_title.set_margin_top(10) + + self.grid_protonfix = Gtk.Grid() + self.grid_protonfix.set_row_spacing(10) + self.grid_protonfix.set_column_spacing(10) + self.grid_protonfix.set_margin_start(10) + self.grid_protonfix.set_margin_end(10) + self.grid_protonfix.set_margin_top(10) + + self.grid_launch_arguments = Gtk.Grid() + self.grid_launch_arguments.set_row_spacing(10) + self.grid_launch_arguments.set_column_spacing(10) + self.grid_launch_arguments.set_margin_start(10) + self.grid_launch_arguments.set_margin_end(10) + self.grid_launch_arguments.set_margin_top(10) + + self.grid_game_arguments = Gtk.Grid() + self.grid_game_arguments.set_row_spacing(10) + self.grid_game_arguments.set_column_spacing(10) + self.grid_game_arguments.set_margin_start(10) + self.grid_game_arguments.set_margin_end(10) + self.grid_game_arguments.set_margin_top(10) + + self.grid_lossless = Gtk.Grid() + self.grid_lossless.set_row_spacing(10) + self.grid_lossless.set_column_spacing(10) + self.grid_lossless.set_margin_start(10) + self.grid_lossless.set_margin_end(10) + self.grid_lossless.set_margin_top(10) + + self.grid_addapp = Gtk.Grid() + self.grid_addapp.set_row_spacing(10) + self.grid_addapp.set_column_spacing(10) + self.grid_addapp.set_margin_start(10) + self.grid_addapp.set_margin_end(10) + self.grid_addapp.set_margin_top(10) + + self.grid_title.attach(self.label_title, 0, 0, 4, 1) + self.grid_title.attach(self.entry_title, 0, 1, 4, 1) + self.entry_title.set_hexpand(True) + + self.grid_protonfix.attach(self.label_protonfix, 0, 0, 1, 1) + self.grid_protonfix.attach(self.entry_protonfix, 0, 1, 3, 1) + self.entry_protonfix.set_hexpand(True) + self.grid_protonfix.attach(self.button_search_protonfix, 3, 1, 1, 1) + + self.grid_launch_arguments.attach(self.label_launch_arguments, 0, 0, 4, 1) + self.grid_launch_arguments.attach(self.entry_launch_arguments, 0, 1, 4, 1) + self.entry_launch_arguments.set_hexpand(True) + + self.grid_game_arguments.attach(self.label_game_arguments, 0, 0, 4, 1) + self.grid_game_arguments.attach(self.entry_game_arguments, 0, 1, 4, 1) + self.entry_game_arguments.set_hexpand(True) + + self.grid_addapp.attach(self.label_addapp, 0, 0, 1, 1) + self.grid_addapp.attach(self.entry_addapp, 0, 1, 3, 1) + self.entry_addapp.set_hexpand(True) + self.grid_addapp.attach(self.button_search_addapp, 3, 1, 1, 1) + + self.grid_lossless.attach(self.button_lossless, 0, 0, 1, 1) + self.button_lossless.set_hexpand(True) + + self.grid_tools = Gtk.Grid(orientation=Gtk.Orientation.VERTICAL) + self.grid_tools.set_row_spacing(10) + self.grid_tools.set_column_spacing(10) + self.grid_tools.set_margin_start(10) + self.grid_tools.set_margin_end(10) + self.grid_tools.set_margin_top(10) + self.grid_tools.set_margin_bottom(10) + + self.grid_shortcut_icon = Gtk.Grid(orientation=Gtk.Orientation.VERTICAL) + self.grid_shortcut_icon.set_row_spacing(10) + self.grid_shortcut_icon.set_column_spacing(10) + self.grid_shortcut_icon.set_margin_start(10) + self.grid_shortcut_icon.set_margin_end(10) + self.grid_shortcut_icon.set_margin_top(10) + self.grid_shortcut_icon.set_margin_bottom(10) + + self.grid_tools.add(self.checkbox_mangohud) + self.grid_tools.add(self.checkbox_gamemode) + self.grid_tools.add(self.checkbox_disable_hidraw) + + self.grid_shortcut_icon.add(self.button_shortcut_icon) + self.grid_shortcut_icon.set_valign(Gtk.Align.CENTER) + + self.box_tools = Gtk.Box() + self.box_tools.pack_start(self.grid_tools, False, False, 0) + self.box_tools.pack_end(self.grid_shortcut_icon, False, False, 0) + + bottom_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + bottom_box.set_margin_start(10) + bottom_box.set_margin_end(10) + # botton_box.set_margin_top(10) + bottom_box.set_margin_bottom(10) + + self.button_cancel.set_hexpand(True) + self.button_ok.set_hexpand(True) + + bottom_box.pack_start(self.button_cancel, True, True, 0) + bottom_box.pack_start(self.button_ok, True, True, 0) + + self.main_grid = Gtk.Grid(orientation=Gtk.Orientation.VERTICAL) + self.main_grid.add(self.grid_title) + self.main_grid.add(self.grid_protonfix) + self.main_grid.add(self.grid_launch_arguments) + self.main_grid.add(self.grid_game_arguments) + self.main_grid.add(self.grid_addapp) + self.main_grid.add(self.grid_lossless) + self.main_grid.add(self.box_tools) + + self.load_config() + + self.mangohud_enabled = os.path.exists(mangohud_dir) + if not self.mangohud_enabled: + self.checkbox_mangohud.set_sensitive(False) + self.checkbox_mangohud.set_active(False) + self.checkbox_mangohud.set_tooltip_text( + _("Shows an overlay for monitoring FPS, temperatures, CPU/GPU load and more. NOT INSTALLED.")) + + self.gamemode_enabled = os.path.exists(gamemoderun) or os.path.exists("/usr/games/gamemoderun") + if not self.gamemode_enabled: + self.checkbox_gamemode.set_sensitive(False) + self.checkbox_gamemode.set_active(False) + self.checkbox_gamemode.set_tooltip_text(_("Tweaks your system to improve performance. NOT INSTALLED.")) + + lossless_dll_path = find_lossless_dll() + if os.path.exists(lsfgvk_path): + if lossless_dll_path or os.path.exists(self.lossless_location): + self.button_lossless.set_sensitive(True) + else: + self.button_lossless.set_sensitive(False) + self.button_lossless.set_tooltip_text(_("Lossless.dll NOT FOUND. If it's installed, go to Faugus Launcher's settings and set the location.")) + else: + self.button_lossless.set_sensitive(False) + self.button_lossless.set_tooltip_text(_("Lossless Scaling Vulkan Layer NOT INSTALLED.")) + + frame.add(self.main_grid) + self.box.add(frame) + self.box.add(bottom_box) + self.add(self.box) + + if not os.path.exists(self.icon_directory): + os.makedirs(self.icon_directory) + + try: + # Attempt to extract the icon + command = f'icoextract "{file_path}" "{self.icon_extracted}"' + result = subprocess.run(command, shell=True, text=True, capture_output=True) + + # Check if there was an error in executing the command + if result.returncode != 0: + if "NoIconsAvailableError" in result.stderr or "PEFormatError" in result.stderr: + print("The file does not contain icons.") + self.button_shortcut_icon.set_image(self.set_image_shortcut_icon()) + else: + print(f"Error extracting icon: {result.stderr}") + else: + # Convert the extracted icon to PNG + command_magick = shutil.which("magick") or shutil.which("convert") + os.system(f'{command_magick} "{self.icon_extracted}" "{self.icon_converted}"') + if os.path.isfile(self.icon_extracted): + os.remove(self.icon_extracted) + + largest_image = self.find_largest_resolution(self.icon_directory) + shutil.move(largest_image, os.path.expanduser(self.icon_temp)) + + pixbuf = GdkPixbuf.Pixbuf.new_from_file(self.icon_temp) + scaled_pixbuf = pixbuf.scale_simple(50, 50, GdkPixbuf.InterpType.BILINEAR) + image = Gtk.Image.new_from_file(self.icon_temp) + image.set_from_pixbuf(scaled_pixbuf) + + self.button_shortcut_icon.set_image(image) + + except Exception as e: + print(f"An error occurred: {e}") + + shutil.rmtree(self.icon_directory) + + # Connect the destroy signal to Gtk.main_quit + self.connect("destroy", Gtk.main_quit) + + def on_button_lossless_clicked(self, widget): + dialog = Gtk.Dialog(title=_("Lossless Scaling Frame Generation"), parent=self, flags=0) + dialog.set_resizable(False) + dialog.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) + + grid = Gtk.Grid() + grid.set_row_spacing(10) + grid.set_column_spacing(10) + grid.set_margin_top(10) + grid.set_margin_bottom(10) + grid.set_margin_start(10) + grid.set_margin_end(10) + + enabled = val if (val := getattr(self, "lossless_enabled", False)) != "" else False + multiplier = val if (val := getattr(self, "lossless_multiplier", 1)) != "" else 1 + flow = val if (val := getattr(self, "lossless_flow", 100)) != "" else 100 + performance = val if (val := getattr(self, "lossless_performance", False)) != "" else False + hdr = val if (val := getattr(self, "lossless_hdr", False)) != "" else False + + checkbox_enable = Gtk.CheckButton(label="Enable") + checkbox_enable.set_active(enabled) + checkbox_enable.set_halign(Gtk.Align.START) + + label_multiplier = Gtk.Label(label=_("Multiplier")) + label_multiplier.set_halign(Gtk.Align.START) + spin_multiplier = Gtk.SpinButton() + spin_multiplier.set_adjustment(Gtk.Adjustment(value=multiplier, lower=1, upper=20, step_increment=1)) + spin_multiplier.set_numeric(True) + spin_multiplier.set_tooltip_text(_("Multiply the FPS.")) + + label_flow = Gtk.Label(label=_("Flow Scale")) + label_flow.set_halign(Gtk.Align.START) + adjustment = Gtk.Adjustment(value=flow, lower=25, upper=100, step_increment=1) + scale_flow = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL, adjustment=adjustment) + scale_flow.set_digits(0) + scale_flow.set_hexpand(True) + scale_flow.set_value_pos(Gtk.PositionType.RIGHT) + scale_flow.set_tooltip_text(_("Lower the internal motion estimation resolution.")) + + checkbox_performance = Gtk.CheckButton(label=_("Performance Mode")) + checkbox_performance.set_tooltip_text(_("Massively improve performance at the cost of quality.")) + checkbox_performance.set_active(performance) + + checkbox_hdr = Gtk.CheckButton(label=_("HDR Mode")) + checkbox_hdr.set_tooltip_text(_("Enable special HDR-only behavior.")) + checkbox_hdr.set_active(hdr) + + def on_enable_toggled(cb): + active = cb.get_active() + label_multiplier.set_sensitive(active) + spin_multiplier.set_sensitive(active) + label_flow.set_sensitive(active) + scale_flow.set_sensitive(active) + checkbox_performance.set_sensitive(active) + checkbox_hdr.set_sensitive(active) + + checkbox_enable.connect("toggled", on_enable_toggled) + on_enable_toggled(checkbox_enable) + + grid.attach(checkbox_enable, 0, 0, 1, 1) + grid.attach(label_multiplier, 0, 1, 1, 1) + grid.attach(spin_multiplier, 0, 2, 1, 1) + grid.attach(label_flow, 0, 3, 1, 1) + grid.attach(scale_flow, 0, 4, 1, 1) + grid.attach(checkbox_performance, 0, 5, 1, 1) + grid.attach(checkbox_hdr, 0, 6, 1, 1) + + frame.add(grid) + + button_cancel = Gtk.Button(label=_("Cancel")) + button_cancel.set_size_request(150, -1) + button_cancel.set_hexpand(True) + button_cancel.connect("clicked", lambda b: dialog.response(Gtk.ResponseType.CANCEL)) + + button_ok = Gtk.Button(label=_("Ok")) + button_ok.set_size_request(150, -1) + button_ok.set_hexpand(True) + button_ok.connect("clicked", lambda b: dialog.response(Gtk.ResponseType.OK)) + + bottom_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + bottom_box.set_margin_start(10) + bottom_box.set_margin_end(10) + bottom_box.set_margin_bottom(10) + bottom_box.pack_start(button_cancel, True, True, 0) + bottom_box.pack_start(button_ok, True, True, 0) + + content_area = dialog.get_content_area() + content_area.pack_start(frame, True, True, 0) + content_area.pack_start(bottom_box, False, False, 0) + + dialog.show_all() + response = dialog.run() + + if response == Gtk.ResponseType.OK: + self.lossless_enabled = checkbox_enable.get_active() + self.lossless_multiplier = spin_multiplier.get_value_as_int() + self.lossless_flow = scale_flow.get_value() + self.lossless_performance = checkbox_performance.get_active() + self.lossless_hdr = checkbox_hdr.get_active() + + dialog.destroy() + return response + + def on_button_search_addapp_clicked(self, widget): + filechooser = Gtk.FileChooserNative( + title=_("Select an additional application"), + transient_for=self, + action=Gtk.FileChooserAction.OPEN, + accept_label=_("Open"), + cancel_label=_("Cancel"), + ) + + filechooser.set_current_folder(os.path.expanduser("~/")) + + windows_filter = Gtk.FileFilter() + windows_filter.set_name(_("Windows files")) + windows_filter.add_pattern("*.exe") + windows_filter.add_pattern("*.msi") + windows_filter.add_pattern("*.bat") + windows_filter.add_pattern("*.lnk") + windows_filter.add_pattern("*.reg") + + all_files_filter = Gtk.FileFilter() + all_files_filter.set_name(_("All files")) + all_files_filter.add_pattern("*") + + filechooser.add_filter(windows_filter) + filechooser.add_filter(all_files_filter) + filechooser.set_filter(windows_filter) + + response = filechooser.run() + + if response == Gtk.ResponseType.ACCEPT: + self.entry_addapp.set_text(filechooser.get_filename()) + + filechooser.destroy() + + def find_largest_resolution(self, directory): + largest_image = None + largest_resolution = (0, 0) # (width, height) + + # Define a set of valid image extensions + valid_image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff'} + + for file_name in os.listdir(directory): + file_path = os.path.join(directory, file_name) + if os.path.isfile(file_path): + # Check if the file has a valid image extension + if os.path.splitext(file_name)[1].lower() in valid_image_extensions: + try: + with Image.open(file_path) as img: + width, height = img.size + if width * height > largest_resolution[0] * largest_resolution[1]: + largest_resolution = (width, height) + largest_image = file_path + except IOError: + print(f"Unable to open {file_path}") + + return largest_image + + def on_button_search_protonfix_clicked(self, widget): + webbrowser.open("https://umu.openwinecomponents.org/") + + def load_config(self): + cfg = ConfigManager() + + self.default_prefix = cfg.config.get('default-prefix', '').strip('"') + mangohud = cfg.config.get('mangohud', 'False') == 'True' + gamemode = cfg.config.get('gamemode', 'False') == 'True' + disable_hidraw = cfg.config.get('disable-hidraw', 'False') == 'True' + self.default_runner = cfg.config.get('default-runner', '').strip('"') + self.lossless_location = cfg.config.get('lossless-location', '') + + self.checkbox_mangohud.set_active(mangohud) + self.checkbox_gamemode.set_active(gamemode) + self.checkbox_disable_hidraw.set_active(disable_hidraw) + + def on_cancel_clicked(self, widget): + if os.path.isfile(self.icon_temp): + os.remove(self.icon_temp) + if os.path.isdir(self.icon_directory): + shutil.rmtree(self.icon_directory) + self.destroy() + + def on_ok_clicked(self, widget): + + validation_result = self.validate_fields() + if not validation_result: + self.set_sensitive(True) + return + + title = self.entry_title.get_text() + title_formatted = format_title(title) + + addapp = self.entry_addapp.get_text() + addapp_bat = f"{os.path.dirname(self.file_path)}/faugus-{title_formatted}.bat" + + if self.entry_addapp.get_text(): + with open(addapp_bat, "w") as bat_file: + bat_file.write(f'start "" "z:{addapp}"\n') + bat_file.write(f'start "" "z:{self.file_path}"\n') + + if os.path.isfile(os.path.expanduser(self.icon_temp)): + os.rename(os.path.expanduser(self.icon_temp), f'{self.icons_path}/{title_formatted}.ico') + + # Check if the icon file exists + new_icon_path = f"{icons_dir}/{title_formatted}.ico" + if not os.path.exists(new_icon_path): + new_icon_path = faugus_png + + protonfix = self.entry_protonfix.get_text() + launch_arguments = self.entry_launch_arguments.get_text() + game_arguments = self.entry_game_arguments.get_text() + lossless_enabled = self.lossless_enabled + lossless_multiplier = self.lossless_multiplier + lossless_flow = self.lossless_flow + lossless_performance = self.lossless_performance + lossless_hdr = self.lossless_hdr + + mangohud = "MANGOHUD=1" if self.checkbox_mangohud.get_active() else "" + gamemode = "gamemoderun" if self.checkbox_gamemode.get_active() else "" + disable_hidraw = "PROTON_DISABLE_HIDRAW=1" if self.checkbox_disable_hidraw.get_active() else "" + + # Get the directory containing the executable + game_directory = os.path.dirname(self.file_path) + + command_parts = [] + + # Add command parts if they are not empty + if mangohud: + command_parts.append(mangohud) + if disable_hidraw: + command_parts.append(disable_hidraw) + + # command_parts.append(f'WINEPREFIX={self.default_prefix}/default') + + if protonfix: + command_parts.append(f'GAMEID={protonfix}') + else: + command_parts.append(f'GAMEID={title_formatted}') + + if gamemode: + command_parts.append(gamemode) + if launch_arguments: + command_parts.append(launch_arguments) + if lossless_enabled: + command_parts.append("LSFG_LEGACY=1") + if lossless_multiplier: + command_parts.append(f"LSFG_MULTIPLIER={lossless_multiplier}") + if lossless_flow: + command_parts.append(f"LSFG_FLOW_SCALE={lossless_flow/100}") + if lossless_performance: + command_parts.append(f"LSFG_PERFORMANCE_MODE={1 if lossless_performance == 'true' else 0}") + if lossless_hdr: + command_parts.append(f"LSFG_HDR_MODE={1 if lossless_hdr == 'true' else 0}") + + # Add the fixed command and remaining arguments + command_parts.append(f"'{umu_run}'") + if self.entry_addapp.get_text(): + command_parts.append(f"'{addapp_bat}'") + elif self.file_path: + command_parts.append(f"'{self.file_path}'") + if game_arguments: + command_parts.append(f"{game_arguments}") + + # Join all parts into a single command + command = ' '.join(command_parts) + + # Create a .desktop file + if IS_FLATPAK: + desktop_file_content = ( + f'[Desktop Entry]\n' + f'Name={title}\n' + f'Exec=flatpak run --command={faugus_run} io.github.Faugus.faugus-launcher "{command}"\n' + f'Icon={new_icon_path}\n' + f'Type=Application\n' + f'Categories=Game;\n' + f'Path={game_directory}\n' + ) + else: + desktop_file_content = ( + f'[Desktop Entry]\n' + f'Name={title}\n' + f'Exec={faugus_run} "{command}"\n' + f'Icon={new_icon_path}\n' + f'Type=Application\n' + f'Categories=Game;\n' + f'Path={game_directory}\n' + ) + + # Check if the destination directory exists and create if it doesn't + applications_directory = app_dir + if not os.path.exists(applications_directory): + os.makedirs(applications_directory) + + desktop_directory = desktop_dir + if not os.path.exists(desktop_directory): + os.makedirs(desktop_directory) + + applications_shortcut_path = f"{app_dir}/{title_formatted}.desktop" + + with open(applications_shortcut_path, 'w') as desktop_file: + desktop_file.write(desktop_file_content) + + # Make the .desktop file executable + os.chmod(applications_shortcut_path, 0o755) + + # Copy the shortcut to Desktop + desktop_shortcut_path = f"{desktop_dir}/{title_formatted}.desktop" + shutil.copyfile(applications_shortcut_path, desktop_shortcut_path) + os.chmod(desktop_shortcut_path, 0o755) + + if os.path.isfile(self.icon_temp): + os.remove(self.icon_temp) + if os.path.isdir(self.icon_directory): + shutil.rmtree(self.icon_directory) + self.destroy() + + def on_entry_changed(self, widget, entry): + if entry.get_text(): + entry.get_style_context().remove_class("entry") + + def set_image_shortcut_icon(self): + image_path = faugus_png + + pixbuf = GdkPixbuf.Pixbuf.new_from_file(image_path) + scaled_pixbuf = pixbuf.scale_simple(50, 50, GdkPixbuf.InterpType.BILINEAR) + + image = Gtk.Image.new_from_pixbuf(scaled_pixbuf) + return image + + def on_button_shortcut_icon_clicked(self, widget): + self.set_sensitive(False) + + path = self.file_path + + if not os.path.exists(self.icon_directory): + os.makedirs(self.icon_directory) + + try: + command = f'icoextract "{path}" "{self.icon_extracted}"' + result = subprocess.run(command, shell=True, text=True, capture_output=True) + + if result.returncode != 0: + if "NoIconsAvailableError" in result.stderr or "PEFormatError" in result.stderr: + print("The file does not contain icons.") + self.button_shortcut_icon.set_image(self.set_image_shortcut_icon()) + else: + print(f"Error extracting icon: {result.stderr}") + else: + command_magick = shutil.which("magick") or shutil.which("convert") + os.system(f'{command_magick} "{self.icon_extracted}" "{self.icon_converted}"') + if os.path.isfile(self.icon_extracted): + os.remove(self.icon_extracted) + + except Exception as e: + print(f"An error occurred: {e}") + + def is_valid_image(file_path): + try: + pixbuf = GdkPixbuf.Pixbuf.new_from_file(file_path) + return pixbuf is not None + except Exception: + return False + + filechooser = Gtk.FileChooserNative.new( + _("Select an icon for the shortcut"), + self, + Gtk.FileChooserAction.OPEN, + _("Open"), + _("Cancel") + ) + + filter_ico = Gtk.FileFilter() + filter_ico.set_name(_("Image files")) + filter_ico.add_pattern("*.png") + filter_ico.add_pattern("*.jpg") + filter_ico.add_pattern("*.jpeg") + filter_ico.add_pattern("*.bmp") + filter_ico.add_pattern("*.gif") + filter_ico.add_pattern("*.svg") + filter_ico.add_pattern("*.ico") + filechooser.add_filter(filter_ico) + + filechooser.set_current_folder(self.icon_directory) + + response = filechooser.run() + if response == Gtk.ResponseType.ACCEPT: + file_path = filechooser.get_filename() + if not file_path or not is_valid_image(file_path): + dialog_image = Gtk.Dialog(title="Faugus Launcher", transient_for=self, modal=True) + dialog_image.set_resizable(False) + dialog_image.set_icon_from_file(faugus_png) + subprocess.Popen(["canberra-gtk-play", "-f", faugus_notification]) + + label = Gtk.Label() + label.set_label(_("The selected file is not a valid image.")) + label.set_halign(Gtk.Align.CENTER) + + label2 = Gtk.Label() + label2.set_label(_("Please choose another one.")) + label2.set_halign(Gtk.Align.CENTER) + + button_yes = Gtk.Button(label=_("Ok")) + button_yes.set_size_request(150, -1) + button_yes.connect("clicked", lambda x: dialog_image.response(Gtk.ResponseType.YES)) + + content_area = dialog_image.get_content_area() + content_area.set_border_width(0) + content_area.set_halign(Gtk.Align.CENTER) + content_area.set_valign(Gtk.Align.CENTER) + content_area.set_vexpand(True) + content_area.set_hexpand(True) + + box_top = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + box_top.set_margin_start(20) + box_top.set_margin_end(20) + box_top.set_margin_top(20) + box_top.set_margin_bottom(20) + + box_bottom = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + box_bottom.set_margin_start(10) + box_bottom.set_margin_end(10) + box_bottom.set_margin_bottom(10) + + box_top.pack_start(label, True, True, 0) + box_top.pack_start(label2, True, True, 0) + box_bottom.pack_start(button_yes, True, True, 0) + + content_area.add(box_top) + content_area.add(box_bottom) + + dialog_image.show_all() + dialog_image.run() + dialog_image.destroy() + else: + shutil.copyfile(file_path, self.icon_temp) + pixbuf = GdkPixbuf.Pixbuf.new_from_file(self.icon_temp) + scaled_pixbuf = pixbuf.scale_simple(50, 50, GdkPixbuf.InterpType.BILINEAR) + image = Gtk.Image.new_from_file(self.icon_temp) + image.set_from_pixbuf(scaled_pixbuf) + self.button_shortcut_icon.set_image(image) + + filechooser.destroy() + + if os.path.isdir(self.icon_directory): + shutil.rmtree(self.icon_directory) + self.set_sensitive(True) + + def update_preview(self, dialog): + if file_path := dialog.get_preview_filename(): + try: + pixbuf = GdkPixbuf.Pixbuf.new_from_file(file_path) + max_width = 400 + max_height = 400 + width = pixbuf.get_width() + height = pixbuf.get_height() + + if width > max_width or height > max_height: + ratio = min(max_width / width, max_height / height) + new_width = int(width * ratio) + new_height = int(height * ratio) + pixbuf = pixbuf.scale_simple(new_width, new_height, GdkPixbuf.InterpType.BILINEAR) + + image = Gtk.Image.new_from_pixbuf(pixbuf) + dialog.set_preview_widget(image) + dialog.set_preview_widget_active(True) + dialog.get_preview_widget().set_size_request(max_width, max_height) + except GLib.Error: + dialog.set_preview_widget_active(False) + else: + dialog.set_preview_widget_active(False) + + def validate_fields(self): + + title = self.entry_title.get_text() + + self.entry_title.get_style_context().remove_class("entry") + + if not title: + self.entry_title.get_style_context().add_class("entry") + return False + + return True + +def main(): + os.environ["GTK_USE_PORTAL"] = "1" + apply_dark_theme() + exec_path = sys.argv[1] + + win = CreateShortcut(exec_path) + win.connect("destroy", Gtk.main_quit) + win.show_all() + Gtk.main() + + +if __name__ == "__main__": + main() diff --git a/faugus_launcher.py b/faugus_launcher.py index de8b3ce..46d266f 100644 --- a/faugus_launcher.py +++ b/faugus_launcher.py @@ -1,7 +1,6 @@ #!/usr/bin/python3 import json -import os import re import shutil import socket @@ -14,11 +13,8 @@ import gi import psutil import requests import vdf -import tarfile -import gettext -import locale import signal -from pathlib import Path +import gettext gi.require_version('Gtk', '3.0') gi.require_version('Gdk', '3.0') @@ -27,47 +23,8 @@ gi.require_version('AyatanaAppIndicator3', '0.1') from gi.repository import Gtk, Gdk, GdkPixbuf, GLib, AyatanaAppIndicator3, Gio, Pango from PIL import Image from filelock import FileLock, Timeout - -class PathManager: - @staticmethod - def system_data(*relative_paths): - xdg_data_dirs = os.getenv('XDG_DATA_DIRS', '/usr/local/share:/usr/share').split(':') - for data_dir in xdg_data_dirs: - path = Path(data_dir).joinpath(*relative_paths) - if path.exists(): - return str(path) - return str(Path(xdg_data_dirs[0]).joinpath(*relative_paths)) - - @staticmethod - def user_data(*relative_paths): - xdg_data_home = Path(os.getenv('XDG_DATA_HOME', Path.home() / '.local/share')) - return str(xdg_data_home.joinpath(*relative_paths)) - - @staticmethod - def user_config(*relative_paths): - xdg_config_home = Path(os.getenv('XDG_CONFIG_HOME', Path.home() / '.config')) - return str(xdg_config_home.joinpath(*relative_paths)) - - @staticmethod - def find_binary(binary_name): - paths = os.getenv('PATH', '').split(':') - for path in paths: - binary_path = Path(path) / binary_name - if binary_path.exists(): - return str(binary_path) - return f'/usr/bin/{binary_name}' # Fallback - - @staticmethod - def get_icon(icon_name): - icon_paths = [ - PathManager.user_data('icons', icon_name), - PathManager.system_data('icons/hicolor/256x256/apps', icon_name), - PathManager.system_data('icons', icon_name) - ] - for path in icon_paths: - if Path(path).exists(): - return path - return icon_paths[-1] # Fallback +from faugus.config_manager import * +from faugus.dark_theme import * VERSION = "1.11.8" print(f"Faugus Launcher {VERSION}") @@ -194,39 +151,6 @@ def get_desktop_dir(): desktop_dir = get_desktop_dir() -def get_system_locale(): - lang = os.environ.get('LANG') or os.environ.get('LC_MESSAGES') - if lang: - return lang.split('.')[0] - - try: - loc = locale.getdefaultlocale()[0] - if loc: - return loc - except Exception: - pass - - return 'en_US' - -def get_language_from_config(): - if os.path.exists(config_file_dir): - with open(config_file_dir, 'r') as f: - for line in f: - line = line.strip() - if line.startswith('language='): - return line.split('=', 1)[1].strip() - return None - -lang = get_language_from_config() -if not lang: - lang = get_system_locale() - -LOCALE_DIR = ( - PathManager.system_data('locale') - if os.path.isdir(PathManager.system_data('locale')) - else os.path.join(os.path.dirname(__file__), 'locale') -) - try: translation = gettext.translation( 'faugus-launcher', @@ -246,74 +170,6 @@ def format_title(title): title = title.strip("-") return title -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', - 'lossless-location': '', - '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', - 'enable-wow64': 'False', - 'language': lang, - 'logging-warning': 'False', - 'show-hidden': 'False', - } - - 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) - key = key.strip() - value = value.strip().strip('"') - self.config[key] = value - - 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(): - if key in ['default-prefix', 'default-runner']: - f.write(f'{key}="{value}"\n') - else: - f.write(f'{key}={value}\n') - - def save_with_values(self, *args): - keys = list(self.default_config.keys()) - for key, value in zip(keys, args): - self.config[key] = str(value) - self.save_config() - class Main(Gtk.Window): def __init__(self): # Initialize the main window with title and default size @@ -6495,797 +6351,6 @@ class AddGame(Gtk.Dialog): return True - -class CreateShortcut(Gtk.Window): - def __init__(self, file_path): - super().__init__(title="Faugus Launcher") - self.file_path = file_path - self.set_resizable(False) - self.set_icon_from_file(faugus_png) - - game_title = os.path.basename(file_path) - self.set_title(game_title) - print(self.file_path) - - self.icon_directory = f"{icons_dir}/icon_temp/" - - if not os.path.exists(self.icon_directory): - os.makedirs(self.icon_directory) - - self.icons_path = icons_dir - self.icon_extracted = os.path.expanduser(f'{self.icons_path}/icon_temp/icon.ico') - self.icon_converted = os.path.expanduser(f'{self.icons_path}/icon_temp/icon.png') - self.icon_temp = f'{self.icons_path}/icon_temp.ico' - - self.default_prefix = "" - - self.lossless_enabled = False - self.lossless_multiplier = 1 - self.lossless_flow = 100 - self.lossless_performance = False - self.lossless_hdr = False - - self.label_title = Gtk.Label(label=_("Title")) - self.label_title.set_halign(Gtk.Align.START) - self.entry_title = Gtk.Entry() - self.entry_title.connect("changed", self.on_entry_changed, self.entry_title) - self.entry_title.set_tooltip_text(_("Game Title")) - - self.label_protonfix = Gtk.Label(label="Protonfix") - self.label_protonfix.set_halign(Gtk.Align.START) - self.entry_protonfix = Gtk.Entry() - self.entry_protonfix.set_tooltip_text("UMU ID") - self.button_search_protonfix = Gtk.Button() - self.button_search_protonfix.set_image( - Gtk.Image.new_from_icon_name("system-search-symbolic", Gtk.IconSize.BUTTON)) - self.button_search_protonfix.connect("clicked", self.on_button_search_protonfix_clicked) - self.button_search_protonfix.set_size_request(50, -1) - - self.label_launch_arguments = Gtk.Label(label=_("Launch Arguments")) - self.label_launch_arguments.set_halign(Gtk.Align.START) - self.entry_launch_arguments = Gtk.Entry() - self.entry_launch_arguments.set_tooltip_text(_("e.g.: PROTON_USE_WINED3D=1 gamescope -W 2560 -H 1440")) - - self.label_game_arguments = Gtk.Label(label=_("Game Arguments")) - self.label_game_arguments.set_halign(Gtk.Align.START) - self.entry_game_arguments = Gtk.Entry() - self.entry_game_arguments.set_tooltip_text(_("e.g.: -d3d11 -fullscreen")) - - self.button_lossless = Gtk.Button(label=_("Lossless Scaling Frame Generation")) - self.button_lossless.connect("clicked", self.on_button_lossless_clicked) - - self.label_addapp = Gtk.Label(label=_("Additional Application")) - self.label_addapp.set_halign(Gtk.Align.START) - self.entry_addapp = Gtk.Entry() - self.entry_addapp.set_tooltip_text(_("/path/to/the/app")) - self.button_search_addapp = Gtk.Button() - self.button_search_addapp.set_image(Gtk.Image.new_from_icon_name("system-search-symbolic", Gtk.IconSize.BUTTON)) - self.button_search_addapp.connect("clicked", self.on_button_search_addapp_clicked) - self.button_search_addapp.set_size_request(50, -1) - - self.button_shortcut_icon = Gtk.Button() - self.button_shortcut_icon.set_size_request(120, -1) - self.button_shortcut_icon.set_tooltip_text(_("Select an icon for the shortcut")) - self.button_shortcut_icon.connect("clicked", self.on_button_shortcut_icon_clicked) - - self.checkbox_mangohud = Gtk.CheckButton(label="MangoHud") - self.checkbox_mangohud.set_tooltip_text( - _("Shows an overlay for monitoring FPS, temperatures, CPU/GPU load and more.")) - self.checkbox_gamemode = Gtk.CheckButton(label="GameMode") - self.checkbox_gamemode.set_tooltip_text(_("Tweaks your system to improve performance.")) - self.checkbox_disable_hidraw = Gtk.CheckButton(label=_("Disable Hidraw")) - self.checkbox_disable_hidraw.set_tooltip_text( - _("May fix controller issues with some games. Only works with GE-Proton10 or Proton-EM-10.")) - - # Button Cancel - self.button_cancel = Gtk.Button(label=_("Cancel")) - self.button_cancel.connect("clicked", self.on_cancel_clicked) - self.button_cancel.set_size_request(150, -1) - - # Button Ok - self.button_ok = Gtk.Button(label=_("Ok")) - self.button_ok.connect("clicked", self.on_ok_clicked) - self.button_ok.set_size_request(150, -1) - - css_provider = Gtk.CssProvider() - css = """ - .entry { - border-color: Red; - } - """ - css_provider.load_from_data(css.encode('utf-8')) - Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), css_provider, - Gtk.STYLE_PROVIDER_PRIORITY_USER) - - self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - self.box.set_margin_start(0) - self.box.set_margin_end(0) - self.box.set_margin_top(0) - self.box.set_margin_bottom(0) - - frame = Gtk.Frame() - frame.set_margin_start(10) - frame.set_margin_end(10) - frame.set_margin_top(10) - frame.set_margin_bottom(10) - - self.grid_title = Gtk.Grid() - self.grid_title.set_row_spacing(10) - self.grid_title.set_column_spacing(10) - self.grid_title.set_margin_start(10) - self.grid_title.set_margin_end(10) - self.grid_title.set_margin_top(10) - - self.grid_protonfix = Gtk.Grid() - self.grid_protonfix.set_row_spacing(10) - self.grid_protonfix.set_column_spacing(10) - self.grid_protonfix.set_margin_start(10) - self.grid_protonfix.set_margin_end(10) - self.grid_protonfix.set_margin_top(10) - - self.grid_launch_arguments = Gtk.Grid() - self.grid_launch_arguments.set_row_spacing(10) - self.grid_launch_arguments.set_column_spacing(10) - self.grid_launch_arguments.set_margin_start(10) - self.grid_launch_arguments.set_margin_end(10) - self.grid_launch_arguments.set_margin_top(10) - - self.grid_game_arguments = Gtk.Grid() - self.grid_game_arguments.set_row_spacing(10) - self.grid_game_arguments.set_column_spacing(10) - self.grid_game_arguments.set_margin_start(10) - self.grid_game_arguments.set_margin_end(10) - self.grid_game_arguments.set_margin_top(10) - - self.grid_lossless = Gtk.Grid() - self.grid_lossless.set_row_spacing(10) - self.grid_lossless.set_column_spacing(10) - self.grid_lossless.set_margin_start(10) - self.grid_lossless.set_margin_end(10) - self.grid_lossless.set_margin_top(10) - - self.grid_addapp = Gtk.Grid() - self.grid_addapp.set_row_spacing(10) - self.grid_addapp.set_column_spacing(10) - self.grid_addapp.set_margin_start(10) - self.grid_addapp.set_margin_end(10) - self.grid_addapp.set_margin_top(10) - - self.grid_title.attach(self.label_title, 0, 0, 4, 1) - self.grid_title.attach(self.entry_title, 0, 1, 4, 1) - self.entry_title.set_hexpand(True) - - self.grid_protonfix.attach(self.label_protonfix, 0, 0, 1, 1) - self.grid_protonfix.attach(self.entry_protonfix, 0, 1, 3, 1) - self.entry_protonfix.set_hexpand(True) - self.grid_protonfix.attach(self.button_search_protonfix, 3, 1, 1, 1) - - self.grid_launch_arguments.attach(self.label_launch_arguments, 0, 0, 4, 1) - self.grid_launch_arguments.attach(self.entry_launch_arguments, 0, 1, 4, 1) - self.entry_launch_arguments.set_hexpand(True) - - self.grid_game_arguments.attach(self.label_game_arguments, 0, 0, 4, 1) - self.grid_game_arguments.attach(self.entry_game_arguments, 0, 1, 4, 1) - self.entry_game_arguments.set_hexpand(True) - - self.grid_addapp.attach(self.label_addapp, 0, 0, 1, 1) - self.grid_addapp.attach(self.entry_addapp, 0, 1, 3, 1) - self.entry_addapp.set_hexpand(True) - self.grid_addapp.attach(self.button_search_addapp, 3, 1, 1, 1) - - self.grid_lossless.attach(self.button_lossless, 0, 0, 1, 1) - self.button_lossless.set_hexpand(True) - - self.grid_tools = Gtk.Grid(orientation=Gtk.Orientation.VERTICAL) - self.grid_tools.set_row_spacing(10) - self.grid_tools.set_column_spacing(10) - self.grid_tools.set_margin_start(10) - self.grid_tools.set_margin_end(10) - self.grid_tools.set_margin_top(10) - self.grid_tools.set_margin_bottom(10) - - self.grid_shortcut_icon = Gtk.Grid(orientation=Gtk.Orientation.VERTICAL) - self.grid_shortcut_icon.set_row_spacing(10) - self.grid_shortcut_icon.set_column_spacing(10) - self.grid_shortcut_icon.set_margin_start(10) - self.grid_shortcut_icon.set_margin_end(10) - self.grid_shortcut_icon.set_margin_top(10) - self.grid_shortcut_icon.set_margin_bottom(10) - - self.grid_tools.add(self.checkbox_mangohud) - self.grid_tools.add(self.checkbox_gamemode) - self.grid_tools.add(self.checkbox_disable_hidraw) - - self.grid_shortcut_icon.add(self.button_shortcut_icon) - self.grid_shortcut_icon.set_valign(Gtk.Align.CENTER) - - self.box_tools = Gtk.Box() - self.box_tools.pack_start(self.grid_tools, False, False, 0) - self.box_tools.pack_end(self.grid_shortcut_icon, False, False, 0) - - bottom_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) - bottom_box.set_margin_start(10) - bottom_box.set_margin_end(10) - # botton_box.set_margin_top(10) - bottom_box.set_margin_bottom(10) - - self.button_cancel.set_hexpand(True) - self.button_ok.set_hexpand(True) - - bottom_box.pack_start(self.button_cancel, True, True, 0) - bottom_box.pack_start(self.button_ok, True, True, 0) - - self.main_grid = Gtk.Grid(orientation=Gtk.Orientation.VERTICAL) - self.main_grid.add(self.grid_title) - self.main_grid.add(self.grid_protonfix) - self.main_grid.add(self.grid_launch_arguments) - self.main_grid.add(self.grid_game_arguments) - self.main_grid.add(self.grid_addapp) - self.main_grid.add(self.grid_lossless) - self.main_grid.add(self.box_tools) - - self.load_config() - - self.mangohud_enabled = os.path.exists(mangohud_dir) - if not self.mangohud_enabled: - self.checkbox_mangohud.set_sensitive(False) - self.checkbox_mangohud.set_active(False) - self.checkbox_mangohud.set_tooltip_text( - _("Shows an overlay for monitoring FPS, temperatures, CPU/GPU load and more. NOT INSTALLED.")) - - self.gamemode_enabled = os.path.exists(gamemoderun) or os.path.exists("/usr/games/gamemoderun") - if not self.gamemode_enabled: - self.checkbox_gamemode.set_sensitive(False) - self.checkbox_gamemode.set_active(False) - self.checkbox_gamemode.set_tooltip_text(_("Tweaks your system to improve performance. NOT INSTALLED.")) - - lossless_dll_path = find_lossless_dll() - if os.path.exists(lsfgvk_path): - if lossless_dll_path or os.path.exists(self.lossless_location): - self.button_lossless.set_sensitive(True) - else: - self.button_lossless.set_sensitive(False) - self.button_lossless.set_tooltip_text(_("Lossless.dll NOT FOUND. If it's installed, go to Faugus Launcher's settings and set the location.")) - else: - self.button_lossless.set_sensitive(False) - self.button_lossless.set_tooltip_text(_("Lossless Scaling Vulkan Layer NOT INSTALLED.")) - - frame.add(self.main_grid) - self.box.add(frame) - self.box.add(bottom_box) - self.add(self.box) - - if not os.path.exists(self.icon_directory): - os.makedirs(self.icon_directory) - - try: - # Attempt to extract the icon - command = f'icoextract "{file_path}" "{self.icon_extracted}"' - result = subprocess.run(command, shell=True, text=True, capture_output=True) - - # Check if there was an error in executing the command - if result.returncode != 0: - if "NoIconsAvailableError" in result.stderr or "PEFormatError" in result.stderr: - print("The file does not contain icons.") - self.button_shortcut_icon.set_image(self.set_image_shortcut_icon()) - else: - print(f"Error extracting icon: {result.stderr}") - else: - # Convert the extracted icon to PNG - command_magick = shutil.which("magick") or shutil.which("convert") - os.system(f'{command_magick} "{self.icon_extracted}" "{self.icon_converted}"') - if os.path.isfile(self.icon_extracted): - os.remove(self.icon_extracted) - - largest_image = self.find_largest_resolution(self.icon_directory) - shutil.move(largest_image, os.path.expanduser(self.icon_temp)) - - pixbuf = GdkPixbuf.Pixbuf.new_from_file(self.icon_temp) - scaled_pixbuf = pixbuf.scale_simple(50, 50, GdkPixbuf.InterpType.BILINEAR) - image = Gtk.Image.new_from_file(self.icon_temp) - image.set_from_pixbuf(scaled_pixbuf) - - self.button_shortcut_icon.set_image(image) - - except Exception as e: - print(f"An error occurred: {e}") - - shutil.rmtree(self.icon_directory) - - # Connect the destroy signal to Gtk.main_quit - self.connect("destroy", Gtk.main_quit) - - def on_button_lossless_clicked(self, widget): - dialog = Gtk.Dialog(title=_("Lossless Scaling Frame Generation"), parent=self, flags=0) - dialog.set_resizable(False) - dialog.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) - - grid = Gtk.Grid() - grid.set_row_spacing(10) - grid.set_column_spacing(10) - grid.set_margin_top(10) - grid.set_margin_bottom(10) - grid.set_margin_start(10) - grid.set_margin_end(10) - - enabled = val if (val := getattr(self, "lossless_enabled", False)) != "" else False - multiplier = val if (val := getattr(self, "lossless_multiplier", 1)) != "" else 1 - flow = val if (val := getattr(self, "lossless_flow", 100)) != "" else 100 - performance = val if (val := getattr(self, "lossless_performance", False)) != "" else False - hdr = val if (val := getattr(self, "lossless_hdr", False)) != "" else False - - checkbox_enable = Gtk.CheckButton(label="Enable") - checkbox_enable.set_active(enabled) - checkbox_enable.set_halign(Gtk.Align.START) - - label_multiplier = Gtk.Label(label=_("Multiplier")) - label_multiplier.set_halign(Gtk.Align.START) - spin_multiplier = Gtk.SpinButton() - spin_multiplier.set_adjustment(Gtk.Adjustment(value=multiplier, lower=1, upper=20, step_increment=1)) - spin_multiplier.set_numeric(True) - spin_multiplier.set_tooltip_text(_("Multiply the FPS.")) - - label_flow = Gtk.Label(label=_("Flow Scale")) - label_flow.set_halign(Gtk.Align.START) - adjustment = Gtk.Adjustment(value=flow, lower=25, upper=100, step_increment=1) - scale_flow = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL, adjustment=adjustment) - scale_flow.set_digits(0) - scale_flow.set_hexpand(True) - scale_flow.set_value_pos(Gtk.PositionType.RIGHT) - scale_flow.set_tooltip_text(_("Lower the internal motion estimation resolution.")) - - checkbox_performance = Gtk.CheckButton(label=_("Performance Mode")) - checkbox_performance.set_tooltip_text(_("Massively improve performance at the cost of quality.")) - checkbox_performance.set_active(performance) - - checkbox_hdr = Gtk.CheckButton(label=_("HDR Mode")) - checkbox_hdr.set_tooltip_text(_("Enable special HDR-only behavior.")) - checkbox_hdr.set_active(hdr) - - def on_enable_toggled(cb): - active = cb.get_active() - label_multiplier.set_sensitive(active) - spin_multiplier.set_sensitive(active) - label_flow.set_sensitive(active) - scale_flow.set_sensitive(active) - checkbox_performance.set_sensitive(active) - checkbox_hdr.set_sensitive(active) - - checkbox_enable.connect("toggled", on_enable_toggled) - on_enable_toggled(checkbox_enable) - - grid.attach(checkbox_enable, 0, 0, 1, 1) - grid.attach(label_multiplier, 0, 1, 1, 1) - grid.attach(spin_multiplier, 0, 2, 1, 1) - grid.attach(label_flow, 0, 3, 1, 1) - grid.attach(scale_flow, 0, 4, 1, 1) - grid.attach(checkbox_performance, 0, 5, 1, 1) - grid.attach(checkbox_hdr, 0, 6, 1, 1) - - frame.add(grid) - - button_cancel = Gtk.Button(label=_("Cancel")) - button_cancel.set_size_request(150, -1) - button_cancel.set_hexpand(True) - button_cancel.connect("clicked", lambda b: dialog.response(Gtk.ResponseType.CANCEL)) - - button_ok = Gtk.Button(label=_("Ok")) - button_ok.set_size_request(150, -1) - button_ok.set_hexpand(True) - button_ok.connect("clicked", lambda b: dialog.response(Gtk.ResponseType.OK)) - - bottom_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) - bottom_box.set_margin_start(10) - bottom_box.set_margin_end(10) - bottom_box.set_margin_bottom(10) - bottom_box.pack_start(button_cancel, True, True, 0) - bottom_box.pack_start(button_ok, True, True, 0) - - content_area = dialog.get_content_area() - content_area.pack_start(frame, True, True, 0) - content_area.pack_start(bottom_box, False, False, 0) - - dialog.show_all() - response = dialog.run() - - if response == Gtk.ResponseType.OK: - self.lossless_enabled = checkbox_enable.get_active() - self.lossless_multiplier = spin_multiplier.get_value_as_int() - self.lossless_flow = scale_flow.get_value() - self.lossless_performance = checkbox_performance.get_active() - self.lossless_hdr = checkbox_hdr.get_active() - - dialog.destroy() - return response - - def on_button_search_addapp_clicked(self, widget): - filechooser = Gtk.FileChooserNative( - title=_("Select an additional application"), - transient_for=self, - action=Gtk.FileChooserAction.OPEN, - accept_label=_("Open"), - cancel_label=_("Cancel"), - ) - - filechooser.set_current_folder(os.path.expanduser("~/")) - - windows_filter = Gtk.FileFilter() - windows_filter.set_name(_("Windows files")) - windows_filter.add_pattern("*.exe") - windows_filter.add_pattern("*.msi") - windows_filter.add_pattern("*.bat") - windows_filter.add_pattern("*.lnk") - windows_filter.add_pattern("*.reg") - - all_files_filter = Gtk.FileFilter() - all_files_filter.set_name(_("All files")) - all_files_filter.add_pattern("*") - - filechooser.add_filter(windows_filter) - filechooser.add_filter(all_files_filter) - filechooser.set_filter(windows_filter) - - response = filechooser.run() - - if response == Gtk.ResponseType.ACCEPT: - self.entry_addapp.set_text(filechooser.get_filename()) - - filechooser.destroy() - - def find_largest_resolution(self, directory): - largest_image = None - largest_resolution = (0, 0) # (width, height) - - # Define a set of valid image extensions - valid_image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff'} - - for file_name in os.listdir(directory): - file_path = os.path.join(directory, file_name) - if os.path.isfile(file_path): - # Check if the file has a valid image extension - if os.path.splitext(file_name)[1].lower() in valid_image_extensions: - try: - with Image.open(file_path) as img: - width, height = img.size - if width * height > largest_resolution[0] * largest_resolution[1]: - largest_resolution = (width, height) - largest_image = file_path - except IOError: - print(f"Unable to open {file_path}") - - return largest_image - - def on_button_search_protonfix_clicked(self, widget): - webbrowser.open("https://umu.openwinecomponents.org/") - - def load_config(self): - cfg = ConfigManager() - - self.default_prefix = cfg.config.get('default-prefix', '').strip('"') - mangohud = cfg.config.get('mangohud', 'False') == 'True' - gamemode = cfg.config.get('gamemode', 'False') == 'True' - disable_hidraw = cfg.config.get('disable-hidraw', 'False') == 'True' - self.default_runner = cfg.config.get('default-runner', '').strip('"') - self.lossless_location = cfg.config.get('lossless-location', '') - - self.checkbox_mangohud.set_active(mangohud) - self.checkbox_gamemode.set_active(gamemode) - self.checkbox_disable_hidraw.set_active(disable_hidraw) - - def on_cancel_clicked(self, widget): - if os.path.isfile(self.icon_temp): - os.remove(self.icon_temp) - if os.path.isdir(self.icon_directory): - shutil.rmtree(self.icon_directory) - self.destroy() - - def on_ok_clicked(self, widget): - - validation_result = self.validate_fields() - if not validation_result: - self.set_sensitive(True) - return - - title = self.entry_title.get_text() - title_formatted = format_title(title) - - addapp = self.entry_addapp.get_text() - addapp_bat = f"{os.path.dirname(self.file_path)}/faugus-{title_formatted}.bat" - - if self.entry_addapp.get_text(): - with open(addapp_bat, "w") as bat_file: - bat_file.write(f'start "" "z:{addapp}"\n') - bat_file.write(f'start "" "z:{self.file_path}"\n') - - if os.path.isfile(os.path.expanduser(self.icon_temp)): - os.rename(os.path.expanduser(self.icon_temp), f'{self.icons_path}/{title_formatted}.ico') - - # Check if the icon file exists - new_icon_path = f"{icons_dir}/{title_formatted}.ico" - if not os.path.exists(new_icon_path): - new_icon_path = faugus_png - - protonfix = self.entry_protonfix.get_text() - launch_arguments = self.entry_launch_arguments.get_text() - game_arguments = self.entry_game_arguments.get_text() - lossless_enabled = self.lossless_enabled - lossless_multiplier = self.lossless_multiplier - lossless_flow = self.lossless_flow - lossless_performance = self.lossless_performance - lossless_hdr = self.lossless_hdr - - mangohud = "MANGOHUD=1" if self.checkbox_mangohud.get_active() else "" - gamemode = "gamemoderun" if self.checkbox_gamemode.get_active() else "" - disable_hidraw = "PROTON_DISABLE_HIDRAW=1" if self.checkbox_disable_hidraw.get_active() else "" - - # Get the directory containing the executable - game_directory = os.path.dirname(self.file_path) - - command_parts = [] - - # Add command parts if they are not empty - if mangohud: - command_parts.append(mangohud) - if disable_hidraw: - command_parts.append(disable_hidraw) - - # command_parts.append(f'WINEPREFIX={self.default_prefix}/default') - - if protonfix: - command_parts.append(f'GAMEID={protonfix}') - else: - command_parts.append(f'GAMEID={title_formatted}') - - if gamemode: - command_parts.append(gamemode) - if launch_arguments: - command_parts.append(launch_arguments) - if lossless_enabled: - command_parts.append("LSFG_LEGACY=1") - if lossless_multiplier: - command_parts.append(f"LSFG_MULTIPLIER={lossless_multiplier}") - if lossless_flow: - command_parts.append(f"LSFG_FLOW_SCALE={lossless_flow/100}") - if lossless_performance: - command_parts.append(f"LSFG_PERFORMANCE_MODE={1 if lossless_performance == 'true' else 0}") - if lossless_hdr: - command_parts.append(f"LSFG_HDR_MODE={1 if lossless_hdr == 'true' else 0}") - - # Add the fixed command and remaining arguments - command_parts.append(f"'{umu_run}'") - if self.entry_addapp.get_text(): - command_parts.append(f"'{addapp_bat}'") - elif self.file_path: - command_parts.append(f"'{self.file_path}'") - if game_arguments: - command_parts.append(f"{game_arguments}") - - # Join all parts into a single command - command = ' '.join(command_parts) - - # Create a .desktop file - if IS_FLATPAK: - desktop_file_content = ( - f'[Desktop Entry]\n' - f'Name={title}\n' - f'Exec=flatpak run --command={faugus_run} io.github.Faugus.faugus-launcher "{command}"\n' - f'Icon={new_icon_path}\n' - f'Type=Application\n' - f'Categories=Game;\n' - f'Path={game_directory}\n' - ) - else: - desktop_file_content = ( - f'[Desktop Entry]\n' - f'Name={title}\n' - f'Exec={faugus_run} "{command}"\n' - f'Icon={new_icon_path}\n' - f'Type=Application\n' - f'Categories=Game;\n' - f'Path={game_directory}\n' - ) - - # Check if the destination directory exists and create if it doesn't - applications_directory = app_dir - if not os.path.exists(applications_directory): - os.makedirs(applications_directory) - - desktop_directory = desktop_dir - if not os.path.exists(desktop_directory): - os.makedirs(desktop_directory) - - applications_shortcut_path = f"{app_dir}/{title_formatted}.desktop" - - with open(applications_shortcut_path, 'w') as desktop_file: - desktop_file.write(desktop_file_content) - - # Make the .desktop file executable - os.chmod(applications_shortcut_path, 0o755) - - # Copy the shortcut to Desktop - desktop_shortcut_path = f"{desktop_dir}/{title_formatted}.desktop" - shutil.copyfile(applications_shortcut_path, desktop_shortcut_path) - os.chmod(desktop_shortcut_path, 0o755) - - if os.path.isfile(self.icon_temp): - os.remove(self.icon_temp) - if os.path.isdir(self.icon_directory): - shutil.rmtree(self.icon_directory) - self.destroy() - - def on_entry_changed(self, widget, entry): - if entry.get_text(): - entry.get_style_context().remove_class("entry") - - def set_image_shortcut_icon(self): - image_path = faugus_png - - pixbuf = GdkPixbuf.Pixbuf.new_from_file(image_path) - scaled_pixbuf = pixbuf.scale_simple(50, 50, GdkPixbuf.InterpType.BILINEAR) - - image = Gtk.Image.new_from_pixbuf(scaled_pixbuf) - return image - - def on_button_shortcut_icon_clicked(self, widget): - self.set_sensitive(False) - - path = self.file_path - - if not os.path.exists(self.icon_directory): - os.makedirs(self.icon_directory) - - try: - command = f'icoextract "{path}" "{self.icon_extracted}"' - result = subprocess.run(command, shell=True, text=True, capture_output=True) - - if result.returncode != 0: - if "NoIconsAvailableError" in result.stderr or "PEFormatError" in result.stderr: - print("The file does not contain icons.") - self.button_shortcut_icon.set_image(self.set_image_shortcut_icon()) - else: - print(f"Error extracting icon: {result.stderr}") - else: - command_magick = shutil.which("magick") or shutil.which("convert") - os.system(f'{command_magick} "{self.icon_extracted}" "{self.icon_converted}"') - if os.path.isfile(self.icon_extracted): - os.remove(self.icon_extracted) - - except Exception as e: - print(f"An error occurred: {e}") - - def is_valid_image(file_path): - try: - pixbuf = GdkPixbuf.Pixbuf.new_from_file(file_path) - return pixbuf is not None - except Exception: - return False - - filechooser = Gtk.FileChooserNative.new( - _("Select an icon for the shortcut"), - self, - Gtk.FileChooserAction.OPEN, - _("Open"), - _("Cancel") - ) - - filter_ico = Gtk.FileFilter() - filter_ico.set_name(_("Image files")) - filter_ico.add_pattern("*.png") - filter_ico.add_pattern("*.jpg") - filter_ico.add_pattern("*.jpeg") - filter_ico.add_pattern("*.bmp") - filter_ico.add_pattern("*.gif") - filter_ico.add_pattern("*.svg") - filter_ico.add_pattern("*.ico") - filechooser.add_filter(filter_ico) - - filechooser.set_current_folder(self.icon_directory) - - response = filechooser.run() - if response == Gtk.ResponseType.ACCEPT: - file_path = filechooser.get_filename() - if not file_path or not is_valid_image(file_path): - dialog_image = Gtk.Dialog(title="Faugus Launcher", transient_for=self, modal=True) - dialog_image.set_resizable(False) - dialog_image.set_icon_from_file(faugus_png) - subprocess.Popen(["canberra-gtk-play", "-f", faugus_notification]) - - label = Gtk.Label() - label.set_label(_("The selected file is not a valid image.")) - label.set_halign(Gtk.Align.CENTER) - - label2 = Gtk.Label() - label2.set_label(_("Please choose another one.")) - label2.set_halign(Gtk.Align.CENTER) - - button_yes = Gtk.Button(label=_("Ok")) - button_yes.set_size_request(150, -1) - button_yes.connect("clicked", lambda x: dialog_image.response(Gtk.ResponseType.YES)) - - content_area = dialog_image.get_content_area() - content_area.set_border_width(0) - content_area.set_halign(Gtk.Align.CENTER) - content_area.set_valign(Gtk.Align.CENTER) - content_area.set_vexpand(True) - content_area.set_hexpand(True) - - box_top = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) - box_top.set_margin_start(20) - box_top.set_margin_end(20) - box_top.set_margin_top(20) - box_top.set_margin_bottom(20) - - box_bottom = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) - box_bottom.set_margin_start(10) - box_bottom.set_margin_end(10) - box_bottom.set_margin_bottom(10) - - box_top.pack_start(label, True, True, 0) - box_top.pack_start(label2, True, True, 0) - box_bottom.pack_start(button_yes, True, True, 0) - - content_area.add(box_top) - content_area.add(box_bottom) - - dialog_image.show_all() - dialog_image.run() - dialog_image.destroy() - else: - shutil.copyfile(file_path, self.icon_temp) - pixbuf = GdkPixbuf.Pixbuf.new_from_file(self.icon_temp) - scaled_pixbuf = pixbuf.scale_simple(50, 50, GdkPixbuf.InterpType.BILINEAR) - image = Gtk.Image.new_from_file(self.icon_temp) - image.set_from_pixbuf(scaled_pixbuf) - self.button_shortcut_icon.set_image(image) - - filechooser.destroy() - - if os.path.isdir(self.icon_directory): - shutil.rmtree(self.icon_directory) - self.set_sensitive(True) - - def update_preview(self, dialog): - if file_path := dialog.get_preview_filename(): - try: - pixbuf = GdkPixbuf.Pixbuf.new_from_file(file_path) - max_width = 400 - max_height = 400 - width = pixbuf.get_width() - height = pixbuf.get_height() - - if width > max_width or height > max_height: - ratio = min(max_width / width, max_height / height) - new_width = int(width * ratio) - new_height = int(height * ratio) - pixbuf = pixbuf.scale_simple(new_width, new_height, GdkPixbuf.InterpType.BILINEAR) - - image = Gtk.Image.new_from_pixbuf(pixbuf) - dialog.set_preview_widget(image) - dialog.set_preview_widget_active(True) - dialog.get_preview_widget().set_size_request(max_width, max_height) - except GLib.Error: - dialog.set_preview_widget_active(False) - else: - dialog.set_preview_widget_active(False) - - def validate_fields(self): - - title = self.entry_title.get_text() - - self.entry_title.get_style_context().remove_class("entry") - - if not title: - self.entry_title.get_style_context().add_class("entry") - return False - - return True - def run_file(file_path): cfg = ConfigManager() @@ -7350,31 +6415,6 @@ def run_file(file_path): # Run the command in the directory of the file subprocess.run([faugus_run_path, command], cwd=file_dir) -def apply_dark_theme(): - if IS_FLATPAK: - if (os.environ.get("XDG_CURRENT_DESKTOP")) == "KDE": - Gtk.Settings.get_default().set_property("gtk-theme-name", "Breeze") - try: - proxy = Gio.DBusProxy.new_sync( - Gio.bus_get_sync(Gio.BusType.SESSION, None), 0, None, - "org.freedesktop.portal.Desktop", - "/org/freedesktop/portal/desktop", - "org.freedesktop.portal.Settings", None) - is_dark = proxy.call_sync( - "Read", GLib.Variant("(ss)", ("org.freedesktop.appearance", "color-scheme")), - 0, -1, None).unpack()[0] == 1 - except: - is_dark = False - Gtk.Settings.get_default().set_property("gtk-application-prefer-dark-theme", is_dark) - else: - desktop_env = Gio.Settings.new("org.gnome.desktop.interface") - try: - is_dark_theme = desktop_env.get_string("color-scheme") == "prefer-dark" - except Exception: - is_dark_theme = "-dark" in desktop_env.get_string("gtk-theme") - if is_dark_theme: - Gtk.Settings.get_default().set_property("gtk-application-prefer-dark-theme", True) - def update_games_file(): if not os.path.exists(games_json): return @@ -7411,19 +6451,12 @@ def faugus_launcher(): app.connect("destroy", app.on_destroy) Gtk.main() - elif len(sys.argv) == 3 and sys.argv[1] == "--shortcut": - app = CreateShortcut(sys.argv[2]) - app.show_all() - Gtk.main() - else: print("Invalid arguments") def main(): if len(sys.argv) == 2 and sys.argv[1] != "--hide": run_file(sys.argv[1]) - elif len(sys.argv) == 3 and sys.argv[1] == "--shortcut": - faugus_launcher() else: try: with lock: diff --git a/faugus_proton_manager.py b/faugus_proton_manager.py index aeb4349..6ee70df 100644 --- a/faugus_proton_manager.py +++ b/faugus_proton_manager.py @@ -2,47 +2,15 @@ import requests import gi -import os import tarfile import shutil import gettext -import locale -from pathlib import Path gi.require_version("Gtk", "3.0") + from gi.repository import Gtk, Gio, GLib - -class PathManager: - @staticmethod - def system_data(*relative_paths): - xdg_data_dirs = os.getenv('XDG_DATA_DIRS', '/usr/local/share:/usr/share').split(':') - for data_dir in xdg_data_dirs: - path = Path(data_dir).joinpath(*relative_paths) - if path.exists(): - return str(path) - return str(Path(xdg_data_dirs[0]).joinpath(*relative_paths)) - - @staticmethod - def user_data(*relative_paths): - xdg_data_home = Path(os.getenv('XDG_DATA_HOME', Path.home() / '.local/share')) - return str(xdg_data_home.joinpath(*relative_paths)) - - @staticmethod - def user_config(*relative_paths): - xdg_config_home = Path(os.getenv('XDG_CONFIG_HOME', Path.home() / '.config')) - return str(xdg_config_home.joinpath(*relative_paths)) - - @staticmethod - def get_icon(icon_name): - icon_paths = [ - PathManager.user_data('icons', icon_name), - PathManager.system_data('icons/hicolor/256x256/apps', icon_name), - PathManager.system_data('icons', icon_name) - ] - for path in icon_paths: - if Path(path).exists(): - return path - return icon_paths[-1] +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: @@ -55,33 +23,6 @@ else: config_file_dir = PathManager.user_config('faugus-launcher/config.ini') faugus_launcher_dir = PathManager.user_config('faugus-launcher') -def get_system_locale(): - lang = os.environ.get('LANG') or os.environ.get('LC_MESSAGES') - if lang: - return lang.split('.')[0] - - try: - return locale.getdefaultlocale()[0] or 'en_US' - except Exception: - return 'en_US' - -def get_language_from_config(): - if os.path.exists(config_file_dir): - with open(config_file_dir, 'r') as f: - for line in f: - line = line.strip() - if line.startswith('language='): - return line.split('=', 1)[1].strip() - return None - -lang = get_language_from_config() or get_system_locale() - -LOCALE_DIR = ( - PathManager.system_data('locale') - if os.path.isdir(PathManager.system_data('locale')) - else os.path.join(os.path.dirname(__file__), 'locale') -) - try: translation = gettext.translation('faugus-proton-manager', localedir=LOCALE_DIR, languages=[lang]) translation.install() @@ -444,31 +385,6 @@ class ProtonDownloader(Gtk.Dialog): except Exception: pass -def apply_dark_theme(): - if IS_FLATPAK: - if (os.environ.get("XDG_CURRENT_DESKTOP")) == "KDE": - Gtk.Settings.get_default().set_property("gtk-theme-name", "Breeze") - try: - proxy = Gio.DBusProxy.new_sync( - Gio.bus_get_sync(Gio.BusType.SESSION, None), 0, None, - "org.freedesktop.portal.Desktop", - "/org/freedesktop/portal/desktop", - "org.freedesktop.portal.Settings", None) - is_dark = proxy.call_sync( - "Read", GLib.Variant("(ss)", ("org.freedesktop.appearance", "color-scheme")), - 0, -1, None).unpack()[0] == 1 - except: - is_dark = False - Gtk.Settings.get_default().set_property("gtk-application-prefer-dark-theme", is_dark) - else: - desktop_env = Gio.Settings.new("org.gnome.desktop.interface") - try: - is_dark_theme = desktop_env.get_string("color-scheme") == "prefer-dark" - except Exception: - is_dark_theme = "-dark" in desktop_env.get_string("gtk-theme") - if is_dark_theme: - Gtk.Settings.get_default().set_property("gtk-application-prefer-dark-theme", True) - def main(): apply_dark_theme() win = ProtonDownloader() diff --git a/faugus_run.py b/faugus_run.py index 1540e80..c695c56 100644 --- a/faugus_run.py +++ b/faugus_run.py @@ -1,74 +1,21 @@ #!/usr/bin/env python3 import gi -gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib, GdkPixbuf, Gio -from threading import Thread -from pathlib import Path import sys import subprocess import argparse import re -import os -import gettext -import locale import json import time import shlex +import gettext -class PathManager: - @staticmethod - def system_data(*relative_paths): - xdg_data_dirs = os.getenv('XDG_DATA_DIRS', '/usr/local/share:/usr/share').split(':') - for data_dir in xdg_data_dirs: - path = Path(data_dir).joinpath(*relative_paths) - if path.exists(): - return str(path) - return str(Path(xdg_data_dirs[0]).joinpath(*relative_paths)) +gi.require_version("Gtk", "3.0") - @staticmethod - def user_data(*relative_paths): - xdg_data_home = Path(os.getenv('XDG_DATA_HOME', Path.home() / '.local/share')) - return str(xdg_data_home.joinpath(*relative_paths)) - - @staticmethod - def user_config(*relative_paths): - xdg_config_home = Path(os.getenv('XDG_CONFIG_HOME', Path.home() / '.config')) - return str(xdg_config_home.joinpath(*relative_paths)) - - @staticmethod - def find_binary(binary_name): - paths = os.getenv('PATH', '').split(':') - for path in paths: - binary_path = Path(path) / binary_name - if binary_path.exists(): - return str(binary_path) - return f'/usr/bin/{binary_name}' - - @staticmethod - def get_icon(icon_name): - icon_paths = [ - PathManager.user_data('icons', icon_name), - PathManager.system_data('icons/hicolor/256x256/apps', icon_name), - PathManager.system_data('icons', icon_name) - ] - for path in icon_paths: - if Path(path).exists(): - return path - return icon_paths[-1] - - @staticmethod - def find_library(lib_name): - lib_paths = [ - Path("/usr/lib") / lib_name, - Path("/usr/lib32") / lib_name, - Path("/usr/lib/x86_64-linux-gnu") / lib_name, - Path("/usr/lib64") / lib_name - ] - for path in lib_paths: - if path.exists(): - return str(path) - return None +from gi.repository import Gtk, GLib, GdkPixbuf, Gio +from threading import Thread +from faugus.config_manager import * +from faugus.dark_theme import * IS_FLATPAK = 'FLATPAK_ID' in os.environ or os.path.exists('/.flatpak-info') if IS_FLATPAK: @@ -95,39 +42,6 @@ proton_cachyos = PathManager.system_data('steam/compatibilitytools.d/proton-cach compatibility_dir = os.path.expanduser("~/.local/share/Steam/compatibilitytools.d") os.makedirs(compatibility_dir, exist_ok=True) -def get_system_locale(): - lang = os.environ.get('LANG') or os.environ.get('LC_MESSAGES') - if lang: - return lang.split('.')[0] - - try: - loc = locale.getdefaultlocale()[0] - if loc: - return loc - except Exception: - pass - - return 'en_US' - -def get_language_from_config(): - if os.path.exists(config_file_dir): - with open(config_file_dir, 'r') as f: - for line in f: - line = line.strip() - if line.startswith('language='): - return line.split('=', 1)[1].strip() - return None - -lang = get_language_from_config() -if not lang: - lang = get_system_locale() - -LOCALE_DIR = ( - PathManager.system_data('locale') - if os.path.isdir(PathManager.system_data('locale')) - else os.path.join(os.path.dirname(__file__), 'locale') -) - try: translation = gettext.translation( 'faugus-run', @@ -383,7 +297,7 @@ class FaugusRun: if "Proton-EM" in self.message: self.process = subprocess.Popen( - [PathManager.find_binary("bash"), "-c", f"{faugus_proton_downloader}"], + [sys.executable, "-m", "faugus.proton_downloader"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=8192, @@ -424,7 +338,7 @@ class FaugusRun: 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}" + cmd = f"{sys.executable} -m faugus.components; {self.discrete_gpu} {eac_dir} {be_dir} {self.message}" else: cmd = f"{self.discrete_gpu} {eac_dir} {be_dir} {self.message}" @@ -754,31 +668,6 @@ def handle_command(message, command=None): process_thread.join() sys.exit(0) -def apply_dark_theme(): - if IS_FLATPAK: - if (os.environ.get("XDG_CURRENT_DESKTOP")) == "KDE": - Gtk.Settings.get_default().set_property("gtk-theme-name", "Breeze") - try: - proxy = Gio.DBusProxy.new_sync( - Gio.bus_get_sync(Gio.BusType.SESSION, None), 0, None, - "org.freedesktop.portal.Desktop", - "/org/freedesktop/portal/desktop", - "org.freedesktop.portal.Settings", None) - is_dark = proxy.call_sync( - "Read", GLib.Variant("(ss)", ("org.freedesktop.appearance", "color-scheme")), - 0, -1, None).unpack()[0] == 1 - except: - is_dark = False - Gtk.Settings.get_default().set_property("gtk-application-prefer-dark-theme", is_dark) - else: - desktop_env = Gio.Settings.new("org.gnome.desktop.interface") - try: - is_dark_theme = desktop_env.get_string("color-scheme") == "prefer-dark" - except Exception: - is_dark_theme = "-dark" in desktop_env.get_string("gtk-theme") - if is_dark_theme: - Gtk.Settings.get_default().set_property("gtk-application-prefer-dark-theme", True) - def build_launch_command(game): gameid = game.get("gameid", "") path = game.get("path", "") diff --git a/meson.build b/meson.build index 5b34c10..9f93a29 100644 --- a/meson.build +++ b/meson.build @@ -13,19 +13,16 @@ gnome = import('gnome') subdir('assets') subdir('languages') subdir('data') +subdir('faugus') install_data( 'faugus_launcher.py', 'faugus_run.py', 'faugus_proton_manager.py', - 'faugus_components.py', - 'faugus_proton_downloader.py', rename: [ 'faugus-launcher', 'faugus-run', 'faugus-proton-manager', - 'faugus-components', - 'faugus-proton-downloader', ], install_mode: 'rwxr-xr-x', install_dir: get_option('bindir'),