875 lines
31 KiB
Python
875 lines
31 KiB
Python
#!/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
|
|
|
|
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}'
|
|
|
|
@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
|
|
|
|
IS_FLATPAK = 'FLATPAK_ID' in os.environ or os.path.exists('/.flatpak-info')
|
|
if IS_FLATPAK:
|
|
share_dir = os.path.expanduser('~/.local/share')
|
|
faugus_png = PathManager.get_icon('io.github.Faugus.faugus-launcher.png')
|
|
else:
|
|
share_dir = PathManager.user_data()
|
|
faugus_png = PathManager.get_icon('faugus-launcher.png')
|
|
|
|
umu_run = PathManager.user_data('faugus-launcher/umu-run')
|
|
config_file_dir = PathManager.user_config('faugus-launcher/config.ini')
|
|
envar_dir = PathManager.user_config('faugus-launcher/envar.txt')
|
|
games_dir = PathManager.user_config('faugus-launcher/games.json')
|
|
faugus_launcher_dir = PathManager.user_config('faugus-launcher')
|
|
faugus_components = PathManager.find_binary('faugus-components')
|
|
faugus_proton_downloader = PathManager.find_binary('faugus-proton-downloader')
|
|
prefixes_dir = str(Path.home() / 'Faugus')
|
|
logs_dir = PathManager.user_config('faugus-launcher/logs')
|
|
faugus_notification = PathManager.system_data('faugus-launcher/faugus-notification.ogg')
|
|
eac_dir = f'PROTON_EAC_RUNTIME={PathManager.user_config("faugus-launcher/components/eac")}'
|
|
be_dir = f'PROTON_BATTLEYE_RUNTIME={PathManager.user_config("faugus-launcher/components/be")}'
|
|
|
|
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',
|
|
localedir=LOCALE_DIR,
|
|
languages=[lang] if lang else ['en_US']
|
|
)
|
|
translation.install()
|
|
globals()['_'] = translation.gettext
|
|
except FileNotFoundError:
|
|
gettext.install('faugus-run', localedir=LOCALE_DIR)
|
|
globals()['_'] = gettext.gettext
|
|
|
|
class ConfigManager:
|
|
def __init__(self):
|
|
self.default_config = {
|
|
'close-onlaunch': 'False',
|
|
'default-prefix': prefixes_dir,
|
|
'mangohud': 'False',
|
|
'gamemode': 'False',
|
|
'disable-hidraw': 'False',
|
|
'default-runner': 'GE-Proton',
|
|
'lossless-location': 'False',
|
|
'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,
|
|
}
|
|
|
|
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')
|
|
|
|
class FaugusRun:
|
|
def __init__(self, message):
|
|
self.message = message
|
|
self.process = None
|
|
self.warning_dialog = None
|
|
self.log_window = None
|
|
self.text_view = None
|
|
self.load_config()
|
|
|
|
def show_error_dialog(self, protonpath):
|
|
dialog = Gtk.Dialog(title="Faugus Launcher")
|
|
dialog.set_resizable(False)
|
|
dialog.set_icon_from_file(faugus_png)
|
|
subprocess.Popen(["canberra-gtk-play", "-f", faugus_notification])
|
|
|
|
label = Gtk.Label()
|
|
label.set_label(_("%s was not found.") % protonpath)
|
|
label.set_halign(Gtk.Align.CENTER)
|
|
|
|
label2 = Gtk.Label()
|
|
label2.set_label(_("Please install it or use another Proton version."))
|
|
label2.set_halign(Gtk.Align.CENTER)
|
|
|
|
button_yes = Gtk.Button(label=_("Ok"))
|
|
button_yes.set_size_request(150, -1)
|
|
button_yes.connect("clicked", lambda w: dialog.response(Gtk.ResponseType.OK))
|
|
|
|
content_area = dialog.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.show_all()
|
|
dialog.run()
|
|
dialog.destroy()
|
|
Gtk.main_quit()
|
|
sys.exit()
|
|
|
|
def update_protonpath(self, message):
|
|
versions = [
|
|
d for d in os.listdir(compatibility_dir)
|
|
if os.path.isdir(os.path.join(compatibility_dir, d)) and d.startswith("proton-EM-")
|
|
]
|
|
|
|
if not versions:
|
|
return message
|
|
|
|
def version_key(v):
|
|
version_str = v.replace("proton-EM-", "")
|
|
|
|
if version_str.isdigit():
|
|
return (int(version_str), "")
|
|
|
|
match = re.match(r"^(\d+)([A-Za-z]*)$", version_str)
|
|
if match:
|
|
num_part = int(match.group(1))
|
|
alpha_part = match.group(2)
|
|
return (num_part, alpha_part)
|
|
|
|
return (0, version_str)
|
|
|
|
versions.sort(key=version_key, reverse=True)
|
|
latest_version = versions[0]
|
|
updated_message = re.sub(r'PROTONPATH=Proton-EM\b', f'PROTONPATH={latest_version}', message)
|
|
return updated_message
|
|
|
|
def update_test(self):
|
|
if "Proton-EM" in self.message:
|
|
self.message = self.update_protonpath(self.message)
|
|
|
|
def start_process(self, command):
|
|
protonpath = next((part.split('=')[1] for part in self.message.split() if part.startswith("PROTONPATH=")), None)
|
|
if protonpath and protonpath != "GE-Proton" and protonpath != "Proton-EM":
|
|
protonpath_path = Path(share_dir) / 'Steam/compatibilitytools.d' / protonpath
|
|
if not protonpath_path.is_dir():
|
|
self.close_warning_dialog()
|
|
self.show_error_dialog(protonpath)
|
|
|
|
if self.default_runner == "UMU-Proton Latest":
|
|
self.default_runner = ""
|
|
if self.default_runner == "GE-Proton Latest (default)":
|
|
self.default_runner = "GE-Proton"
|
|
if self.default_runner == "Proton-EM Latest":
|
|
self.default_runner = "Proton-EM"
|
|
|
|
self.discrete_gpu = "DRI_PRIME=1"
|
|
if not self.discrete_gpu:
|
|
self.discrete_gpu = "DRI_PRIME=0"
|
|
if self.discrete_gpu:
|
|
self.discrete_gpu = "DRI_PRIME=1"
|
|
if self.discrete_gpu == None:
|
|
self.discrete_gpu = "DRI_PRIME=1"
|
|
|
|
if "WINEPREFIX" not in self.message:
|
|
if self.default_runner:
|
|
if "PROTONPATH" not in self.message:
|
|
if "UMU_NO_PROTON" not in self.message:
|
|
self.message = f'WINEPREFIX="{self.default_prefix}/default" PROTONPATH={self.default_runner} {self.message}'
|
|
else:
|
|
self.message = f'WINEPREFIX="{self.default_prefix}/default" {self.message}'
|
|
else:
|
|
self.message = f'WINEPREFIX="{self.default_prefix}/default" {self.message}'
|
|
if "gamemoderun" in self.message:
|
|
self.set_ld_preload()
|
|
self.message = f'LD_PRELOAD={self.ld_preload} {self.message}'
|
|
|
|
if not "winetricks-gui" in self.message:
|
|
for part in self.message.split():
|
|
if part.startswith("GAMEID="):
|
|
game_id = part.split("=")[1]
|
|
if "umu" not in game_id:
|
|
self.message = f'PROTONFIXES_DISABLE=1 {self.message}'
|
|
break
|
|
|
|
if "proton-cachyos" in self.message:
|
|
if "slr" not in self.message:
|
|
self.message = f'UMU_NO_RUNTIME=1 {self.message}'
|
|
|
|
if self.wayland_driver:
|
|
self.message = f'PROTON_ENABLE_WAYLAND=1 {self.message}'
|
|
if self.enable_hdr:
|
|
self.message = f'PROTON_ENABLE_HDR=1 {self.message}'
|
|
if self.enable_wow64:
|
|
self.message = f'PROTON_USE_WOW64=1 {self.message}'
|
|
if "LSFG_LEGACY" in self.message:
|
|
if self.lossless_location:
|
|
self.message = f'LSFG_DLL_PATH="{self.lossless_location}" {self.message}'
|
|
|
|
if self.enable_logging:
|
|
match = re.search(r"FAUGUS_LOG=(?:'([^']*)'|\"([^\"]*)\"|(\S+))", self.message)
|
|
if match:
|
|
self.game_title = next(g for g in match.groups() if g).split("/")[-1]
|
|
|
|
self.load_env_from_file(envar_dir)
|
|
self.run_processes_sequentially()
|
|
|
|
def load_env_from_file(self, filename=envar_dir):
|
|
try:
|
|
with open(filename, "r", encoding="utf-8") as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line or "=" not in line:
|
|
continue
|
|
key, value = line.split("=", 1)
|
|
key, value = key.strip(), value.strip()
|
|
os.environ[key] = value
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
def run_processes_sequentially(self):
|
|
if "UMU_NO_PROTON" not in self.message:
|
|
if self.enable_logging:
|
|
self.message = f'UMU_LOG=1 PROTON_LOG_DIR={logs_dir}/{self.game_title} PROTON_LOG=1 {self.message}'
|
|
|
|
if "Proton-EM" in self.message:
|
|
self.process = subprocess.Popen(
|
|
[PathManager.find_binary("bash"), "-c", f"{faugus_proton_downloader}"],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
bufsize=8192,
|
|
text=True
|
|
)
|
|
|
|
self.stdout_watch_id = GLib.io_add_watch(
|
|
self.process.stdout,
|
|
GLib.PRIORITY_LOW,
|
|
GLib.IO_IN,
|
|
self.on_output
|
|
)
|
|
self.stderr_watch_id = GLib.io_add_watch(
|
|
self.process.stderr,
|
|
GLib.PRIORITY_LOW,
|
|
GLib.IO_IN,
|
|
self.on_output
|
|
)
|
|
|
|
GLib.child_watch_add(
|
|
GLib.PRIORITY_DEFAULT,
|
|
self.process.pid,
|
|
self.on_proton_downloader_finished
|
|
)
|
|
else:
|
|
self.execute_final_command()
|
|
|
|
def on_proton_downloader_finished(self, pid, status):
|
|
if hasattr(self, 'stdout_watch_id'):
|
|
GLib.source_remove(self.stdout_watch_id)
|
|
if hasattr(self, 'stderr_watch_id'):
|
|
GLib.source_remove(self.stderr_watch_id)
|
|
self.update_test()
|
|
|
|
self.execute_final_command()
|
|
|
|
def execute_final_command(self):
|
|
if "UMU_NO_PROTON" not in self.message:
|
|
cmd = f"{faugus_components}; {self.discrete_gpu} {eac_dir} {be_dir} {self.message}"
|
|
else:
|
|
cmd = f"{self.discrete_gpu} {eac_dir} {be_dir} {self.message}"
|
|
|
|
self.process = subprocess.Popen(
|
|
[PathManager.find_binary("bash"), "-c", cmd],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
bufsize=8192,
|
|
text=True
|
|
)
|
|
|
|
self.stdout_watch_id = GLib.io_add_watch(
|
|
self.process.stdout,
|
|
GLib.PRIORITY_LOW,
|
|
GLib.IO_IN,
|
|
self.on_output
|
|
)
|
|
self.stderr_watch_id = GLib.io_add_watch(
|
|
self.process.stderr,
|
|
GLib.PRIORITY_LOW,
|
|
GLib.IO_IN,
|
|
self.on_output
|
|
)
|
|
|
|
GLib.child_watch_add(
|
|
GLib.PRIORITY_DEFAULT,
|
|
self.process.pid,
|
|
self.on_process_exit
|
|
)
|
|
|
|
def set_ld_preload(self):
|
|
lib_paths = [
|
|
PathManager.find_library('libgamemode.so.0'),
|
|
PathManager.find_library('libgamemodeauto.so.0')
|
|
]
|
|
|
|
ld_preload_paths = [path for path in lib_paths if path]
|
|
self.ld_preload = ":".join(ld_preload_paths)
|
|
|
|
def load_config(self):
|
|
cfg = ConfigManager()
|
|
|
|
self.discrete_gpu = cfg.config.get('discrete-gpu', 'False') == 'True'
|
|
self.splash_disable = cfg.config.get('splash-disable', 'False') == 'True'
|
|
self.default_runner = cfg.config.get('default-runner', '')
|
|
self.lossless_location = cfg.config.get('lossless-location', '')
|
|
self.default_prefix = cfg.config.get('default-prefix', '')
|
|
self.enable_logging = cfg.config.get('enable-logging', 'False') == 'True'
|
|
self.wayland_driver = cfg.config.get('wayland-driver', 'False') == 'True'
|
|
self.enable_hdr = cfg.config.get('enable-hdr', 'False') == 'True'
|
|
self.enable_wow64 = cfg.config.get('enable-wow64', 'False') == 'True'
|
|
self.language = cfg.config.get('language', '')
|
|
|
|
def show_warning_dialog(self):
|
|
self.warning_dialog = Gtk.Window(title="Faugus Launcher")
|
|
self.warning_dialog.set_decorated(False)
|
|
self.warning_dialog.set_resizable(False)
|
|
self.warning_dialog.set_default_size(280, -1)
|
|
self.warning_dialog.set_icon_from_file(faugus_png)
|
|
|
|
frame = Gtk.Frame()
|
|
frame.set_label_align(0.5, 0.5)
|
|
frame.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
|
|
|
|
grid = Gtk.Grid()
|
|
grid.set_halign(Gtk.Align.CENTER)
|
|
grid.set_valign(Gtk.Align.CENTER)
|
|
frame.add(grid)
|
|
|
|
image_path = faugus_png
|
|
pixbuf = GdkPixbuf.Pixbuf.new_from_file(image_path)
|
|
|
|
pixbuf = pixbuf.scale_simple(75, 75, GdkPixbuf.InterpType.BILINEAR)
|
|
|
|
image = Gtk.Image.new_from_pixbuf(pixbuf)
|
|
image.set_margin_top(20)
|
|
image.set_margin_start(20)
|
|
image.set_margin_end(20)
|
|
image.set_margin_bottom(20)
|
|
grid.attach(image, 0, 0, 1, 1)
|
|
|
|
protonpath = next((part.split('=')[1] for part in self.message.split() if part.startswith("PROTONPATH=")), None)
|
|
if protonpath == "Using UMU-Proton":
|
|
protonpath = "UMU-Proton Latest"
|
|
if not protonpath:
|
|
if "UMU_NO_PROTON" in self.message:
|
|
protonpath = "Linux Native"
|
|
else:
|
|
protonpath = "Using UMU-Proton Latest"
|
|
else:
|
|
protonpath = _("Using %s") % protonpath
|
|
print(protonpath)
|
|
|
|
self.label = Gtk.Label(label=protonpath)
|
|
self.label.set_margin_start(20)
|
|
self.label.set_margin_end(20)
|
|
|
|
self.label2 = Gtk.Label()
|
|
self.label2.set_margin_bottom(20)
|
|
self.label2.set_margin_start(20)
|
|
self.label2.set_margin_end(20)
|
|
|
|
grid.attach(self.label, 0, 1, 1, 1)
|
|
grid.attach(self.label2, 0, 2, 1, 1)
|
|
|
|
self.warning_dialog.add(frame)
|
|
|
|
if not self.splash_disable:
|
|
self.warning_dialog.show_all()
|
|
|
|
def show_log_window(self):
|
|
self.log_window = Gtk.Window(title="Winetricks Logs")
|
|
self.log_window.set_default_size(600, 400)
|
|
self.log_window.set_icon_from_file(faugus_png)
|
|
|
|
scrolled_window = Gtk.ScrolledWindow()
|
|
scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
|
|
|
self.text_view = Gtk.TextView()
|
|
self.text_view.set_editable(False)
|
|
scrolled_window.add(self.text_view)
|
|
self.log_window.add(scrolled_window)
|
|
|
|
self.log_window.connect("delete-event", self.on_log_window_delete_event)
|
|
self.log_window.show_all()
|
|
|
|
def on_output(self, source, condition):
|
|
if self.enable_logging:
|
|
log_dir = f"{logs_dir}/{self.game_title}"
|
|
if not os.path.exists(log_dir):
|
|
os.makedirs(log_dir)
|
|
|
|
if not hasattr(self, "_log_file_cleaned"):
|
|
with open(f"{log_dir}/umu.log", "w") as log_file:
|
|
log_file.write("")
|
|
self._log_file_cleaned = True
|
|
|
|
def remove_ansi_escape(text):
|
|
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
return ansi_escape.sub('', text)
|
|
|
|
if line := source.readline():
|
|
clean_line = remove_ansi_escape(line).strip()
|
|
|
|
if self.enable_logging:
|
|
with open(f"{log_dir}/umu.log", "a") as log_file:
|
|
log_file.write(clean_line + "\n")
|
|
log_file.flush()
|
|
|
|
self.check_game_output(clean_line)
|
|
|
|
if "libgamemode.so.0" in clean_line or "libgamemodeauto.so.0" in clean_line or "libgamemode.so" in clean_line:
|
|
return True
|
|
|
|
if "winetricks" in self.message:
|
|
self.append_to_text_view(clean_line)
|
|
else:
|
|
print(line, end='')
|
|
|
|
return True
|
|
|
|
def check_game_output(self, clean_line):
|
|
if "Downloading" in clean_line or "Updating BattlEye..." in clean_line or "Updating Easy Anti-Cheat..." in clean_line or "Updating UMU-Launcher..." in clean_line:
|
|
self.warning_dialog.show_all()
|
|
if "Updating UMU-Launcher..." in clean_line:
|
|
self.label.set_text(_("Updating UMU-Launcher..."))
|
|
if "UMU-Launcher is up to date." in clean_line:
|
|
self.label.set_text(_("UMU-Launcher is up to date"))
|
|
if "Updating BattlEye..." in clean_line:
|
|
self.label.set_text(_("Updating BattlEye..."))
|
|
if "Updating Easy Anti-Cheat..." in clean_line:
|
|
self.label.set_text(_("Updating Easy Anti-Cheat..."))
|
|
if "Components are up to date." in clean_line:
|
|
self.label.set_text(_("Components are up to date"))
|
|
if "Downloading GE-Proton" in clean_line:
|
|
self.label.set_text(_("Downloading GE-Proton..."))
|
|
if "Downloading UMU-Proton" in clean_line:
|
|
self.label.set_text(_("Downloading UMU-Proton..."))
|
|
if "Downloading steamrt3 (latest)" in clean_line:
|
|
self.label2.set_text(_("Downloading Steam Runtime..."))
|
|
if "SteamLinuxRuntime_sniper.tar.xz" in clean_line:
|
|
self.label2.set_text(_("Extracting Steam Runtime..."))
|
|
if "Extracting GE-Proton" in clean_line:
|
|
self.label.set_text(_("Extracting GE-Proton..."))
|
|
if "Extracting UMU-Proton" in clean_line:
|
|
self.label.set_text(_("Extracting UMU-Proton..."))
|
|
if "GE-Proton is up to date" in clean_line:
|
|
self.label.set_text(_("GE-Proton is up to date"))
|
|
if "UMU-Proton is up to date" in clean_line:
|
|
self.label.set_text(_("UMU-Proton is up to date"))
|
|
if "steamrt3 is up to date" in clean_line:
|
|
self.label2.set_text(_("Steam Runtime is up to date"))
|
|
if "->" in clean_line and "GE-Proton" in clean_line:
|
|
self.label.set_text(_("GE-Proton is up to date"))
|
|
if "->" in clean_line and "UMU-Proton" in clean_line:
|
|
self.label.set_text(_("UMU-Proton is up to date"))
|
|
if "mtree is OK" in clean_line:
|
|
self.label2.set_text(_("Steam Runtime is up to date"))
|
|
if "Downloading proton-EM" in clean_line:
|
|
self.label.set_text(_("Downloading Proton-EM..."))
|
|
if "Extracting archive" in clean_line:
|
|
self.label.set_text(_("Extracting Proton-EM..."))
|
|
if "Proton installed successfully" in clean_line:
|
|
self.label.set_text(_("Proton-EM is up to date"))
|
|
|
|
if (
|
|
"fsync:" in clean_line
|
|
or "NTSync" in clean_line
|
|
or "Using winetricks" in clean_line
|
|
or "steamrt3 is up to date" in clean_line
|
|
or "Command exited with status: 0" in clean_line
|
|
or "SingleInstance" in clean_line
|
|
or "mtree is OK" in clean_line
|
|
or "Skipping fix execution" in clean_line
|
|
or "Selected GPU" in clean_line
|
|
):
|
|
GLib.timeout_add_seconds(0, self.close_warning_dialog)
|
|
|
|
def append_to_text_view(self, clean_line):
|
|
if self.text_view:
|
|
|
|
if any(keyword in clean_line for keyword in
|
|
{"zenity", "Gtk-WARNING", "Gtk-Message", "pixbuf"}) or not clean_line:
|
|
return
|
|
|
|
buffer = self.text_view.get_buffer()
|
|
end_iter = buffer.get_end_iter()
|
|
buffer.insert(end_iter, clean_line + "\n")
|
|
adj = self.text_view.get_parent().get_vadjustment()
|
|
adj.set_value(adj.get_upper() - adj.get_page_size())
|
|
|
|
def close_warning_dialog(self):
|
|
if self.warning_dialog:
|
|
self.warning_dialog.destroy()
|
|
self.warning_dialog = None
|
|
|
|
def close_log_window(self):
|
|
if self.log_window:
|
|
self.log_window.destroy()
|
|
self.log_window = None
|
|
|
|
def on_log_window_delete_event(self, widget, event):
|
|
return True
|
|
|
|
def show_exit_warning(self):
|
|
parts = self.message.split()
|
|
if parts:
|
|
last_part = parts[-1].strip('"')
|
|
|
|
if last_part.endswith(".reg"):
|
|
dialog = Gtk.Dialog(title="Faugus Launcher", modal=True)
|
|
dialog.set_resizable(False)
|
|
dialog.set_icon_from_file(faugus_png)
|
|
subprocess.Popen(["canberra-gtk-play", "-i", "dialog-information"])
|
|
|
|
label = Gtk.Label()
|
|
label.set_label(_("The keys and values were successfully added to the registry."))
|
|
label.set_halign(Gtk.Align.CENTER)
|
|
|
|
button_yes = Gtk.Button(label=_("Ok"))
|
|
button_yes.set_size_request(150, -1)
|
|
button_yes.connect("clicked", lambda w: dialog.response(Gtk.ResponseType.OK))
|
|
|
|
content_area = dialog.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_bottom.pack_start(button_yes, True, True, 0)
|
|
|
|
content_area.add(box_top)
|
|
content_area.add(box_bottom)
|
|
|
|
dialog.show_all()
|
|
dialog.run()
|
|
dialog.destroy()
|
|
Gtk.main_quit()
|
|
sys.exit()
|
|
|
|
def on_process_exit(self, pid, condition):
|
|
if self.process.poll() is not None:
|
|
GLib.idle_add(self.close_warning_dialog)
|
|
GLib.idle_add(self.close_log_window)
|
|
GLib.idle_add(self.show_exit_warning)
|
|
GLib.idle_add(Gtk.main_quit)
|
|
return False
|
|
|
|
|
|
def handle_command(message, command=None):
|
|
updater = FaugusRun(message)
|
|
updater.show_warning_dialog()
|
|
if command == "winetricks":
|
|
updater.show_log_window()
|
|
|
|
def run_process():
|
|
updater.start_process(command)
|
|
|
|
process_thread = Thread(target=run_process)
|
|
|
|
def start_thread():
|
|
process_thread.start()
|
|
|
|
GLib.idle_add(start_thread)
|
|
Gtk.main()
|
|
|
|
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", "")
|
|
prefix = game.get("prefix", "")
|
|
launch_arguments = game.get("launch_arguments", "")
|
|
game_arguments = game.get("game_arguments", "")
|
|
protonfix = game.get("protonfix", "")
|
|
runner = game.get("runner", "")
|
|
addapp_bat = game.get("addapp_bat", "")
|
|
mangohud = game.get("mangohud", "")
|
|
gamemode = game.get("gamemode", "")
|
|
disable_hidraw = game.get("disable_hidraw", "")
|
|
addapp_checkbox = game.get("addapp_checkbox", "")
|
|
lossless_enabled = game.get("lossless_enabled", "")
|
|
lossless_multiplier = game.get("lossless_multiplier", "")
|
|
lossless_flow = game.get("lossless_flow", "")
|
|
lossless_performance = game.get("lossless_performance", "")
|
|
lossless_hdr = game.get("lossless_hdr", "")
|
|
|
|
if lossless_performance:
|
|
lossless_performance = 1
|
|
else:
|
|
lossless_performance = 0
|
|
if lossless_hdr:
|
|
lossless_hdr = 1
|
|
else:
|
|
lossless_hdr = 0
|
|
|
|
command_parts = []
|
|
|
|
if gameid:
|
|
command_parts.append(f"FAUGUS_LOG='{gameid}'")
|
|
if mangohud:
|
|
command_parts.append(mangohud)
|
|
if disable_hidraw:
|
|
command_parts.append(disable_hidraw)
|
|
if runner != "Linux-Native" and prefix:
|
|
command_parts.append(f"WINEPREFIX='{prefix}'")
|
|
if protonfix:
|
|
command_parts.append(f"GAMEID={protonfix}")
|
|
else:
|
|
command_parts.append(f"GAMEID={game['gameid']}")
|
|
if runner:
|
|
if runner == "Linux-Native":
|
|
command_parts.append('UMU_NO_PROTON=1')
|
|
else:
|
|
command_parts.append(f"PROTONPATH={runner}")
|
|
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}")
|
|
if lossless_performance:
|
|
command_parts.append(f"LSFG_PERFORMANCE_MODE={lossless_performance}")
|
|
if lossless_hdr:
|
|
command_parts.append(f"LSFG_HDR_MODE={lossless_hdr}")
|
|
|
|
command_parts.append(f"'{umu_run}'")
|
|
|
|
if addapp_checkbox == "addapp_enabled":
|
|
command_parts.append(f"'{addapp_bat}'")
|
|
else:
|
|
command_parts.append(f"'{path}'")
|
|
|
|
if game_arguments:
|
|
command_parts.append(game_arguments)
|
|
|
|
return " ".join(command_parts)
|
|
|
|
def load_game_from_json(gameid):
|
|
if not os.path.exists(games_dir):
|
|
return None
|
|
|
|
try:
|
|
with open(games_dir, "r", encoding="utf-8") as f:
|
|
games = json.load(f)
|
|
except json.JSONDecodeError:
|
|
return None
|
|
|
|
for game in games:
|
|
if game.get("gameid") == gameid:
|
|
return game
|
|
|
|
return None
|
|
|
|
def main():
|
|
apply_dark_theme()
|
|
|
|
parser = argparse.ArgumentParser(description=None)
|
|
parser.add_argument("message", nargs='?')
|
|
parser.add_argument("command", nargs='?', default=None)
|
|
parser.add_argument("--game")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.game:
|
|
game = load_game_from_json(args.game)
|
|
if not game:
|
|
return
|
|
|
|
launch_options = build_launch_command(game)
|
|
handle_command(launch_options, None)
|
|
else:
|
|
handle_command(args.message, args.command)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|