Files
faugus-launcher/faugus_launcher.py
2026-03-01 11:43:52 +01:00

6830 lines
278 KiB
Python

#!/usr/bin/python3
import json
import re
import shutil
import socket
import subprocess
import sys
import threading
import urllib.request
import webbrowser
import gi
import psutil
import requests
import vdf
import signal
import gettext
import unicodedata
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
gi.require_version('AyatanaAppIndicator3', '0.1')
from gi.repository import Gtk, Gdk, GdkPixbuf, GLib, AyatanaAppIndicator3, Gio, Pango
from PIL import Image
from faugus.config_manager import *
from faugus.dark_theme import *
from faugus.shortcut_utils import build_desktop_entry, get_shortcut_icon_path
from faugus.steam_setup import *
VERSION = "1.15.1"
IS_FLATPAK = 'FLATPAK_ID' in os.environ or os.path.exists('/.flatpak-info')
faugus_banner = PathManager.system_data('faugus-launcher/faugus-banner.png')
faugus_notification = PathManager.system_data('faugus-launcher/faugus-notification.ogg')
faugus_launcher_dir = PathManager.user_config('faugus-launcher')
prefixes_dir = str(Path.home() / 'Faugus')
logs_dir = PathManager.user_config('faugus-launcher/logs')
icons_dir = PathManager.user_config('faugus-launcher/icons')
banners_dir = PathManager.user_config('faugus-launcher/banners')
config_file_dir = PathManager.user_config('faugus-launcher/config.ini')
envar_dir = PathManager.user_config('faugus-launcher/envar.txt')
share_dir = PathManager.user_data()
backup_dir = PathManager.user_config("faugus-launcher/games-backup")
faugus_mono_icon = PathManager.get_icon('faugus-mono.svg')
proton_cachyos = PathManager.system_data('steam/compatibilitytools.d/proton-cachyos-slr/')
if IS_FLATPAK:
app_dir = str(Path.home() / '.local/share/applications')
faugus_png = PathManager.get_icon('io.github.Faugus.faugus-launcher.png')
tray_icon = 'io.github.Faugus.faugus-launcher'
mono_dest = Path(os.path.expanduser('~/.local/share/faugus-launcher/faugus-mono.svg'))
mono_dest.parent.mkdir(parents=True, exist_ok=True)
if not mono_dest.exists():
shutil.copy(faugus_mono_icon, mono_dest)
faugus_mono_icon = os.path.expanduser('~/.local/share/faugus-launcher/faugus-mono.svg')
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')
tray_icon = 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])
epic_icon = PathManager.get_icon('faugus-epic-games.png')
battle_icon = PathManager.get_icon('faugus-battlenet.png')
ubisoft_icon = PathManager.get_icon('faugus-ubisoft-connect.png')
ea_icon = PathManager.get_icon('faugus-ea.png')
rockstar_icon = PathManager.get_icon('faugus-rockstar.png')
faugus_run = PathManager.find_binary('faugus-run')
faugus_proton_manager = PathManager.find_binary('faugus-proton-manager')
umu_run = PathManager.user_data('faugus-launcher/umu-run')
mangohud_dir = PathManager.find_binary('mangohud')
gamemoderun = PathManager.find_binary('gamemoderun')
games_json = PathManager.user_config('faugus-launcher/games.json')
latest_games = PathManager.user_config('faugus-launcher/latest-games.txt')
faugus_launcher_share_dir = PathManager.user_data('faugus-launcher')
faugus_temp = str(Path.home() / 'faugus_temp')
running_games = PathManager.user_data('faugus-launcher/running_games.json')
faugus_backup = False
os.makedirs(faugus_launcher_share_dir, exist_ok=True)
os.makedirs(faugus_launcher_dir, exist_ok=True)
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()
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"[^a-z0-9 ]+", "", title)
title = re.sub(r"\s+", "-", title)
return title
def convert_runner(runner):
if runner == "Proton-GE Latest":
return "GE-Proton Latest (default)"
if runner == "GE-Proton Latest (default)":
return "Proton-GE Latest"
if runner == "UMU-Proton Latest":
return ""
if runner == "":
return "UMU-Proton Latest"
return runner
def _validate_text(entry):
text = entry.get_text()
for i, c in enumerate(text):
if c.isalpha() and "LATIN" not in unicodedata.name(c, ""):
new_text = text[:i] + text[i+1:]
entry.set_text(new_text)
entry.set_position(i)
break
class FaugusApp(Gtk.Application):
def __init__(self):
super().__init__(
application_id="io.github.Faugus.faugus-launcher",
flags=Gio.ApplicationFlags.HANDLES_OPEN
)
self.window = None
def do_startup(self):
Gtk.Application.do_startup(self)
os.environ["GTK_USE_PORTAL"] = "1"
apply_dark_theme()
def do_activate(self):
if not self.window:
self.window = Main(self)
self.window.present()
def do_open(self, files, n_files, hint):
self.do_activate()
for file in files:
run_file(file.get_path())
class Main(Gtk.ApplicationWindow):
def __init__(self, app):
super().__init__(application=app, title="Faugus Launcher")
self.set_icon_from_file(faugus_png)
self.connect("delete-event", self.on_close)
print(f"Faugus Launcher {VERSION}")
self.start_maximized = False
self.start_fullscreen = False
self.fullscreen_activated = False
self.system_tray = False
self.indicator = False
self.start_boot = False
self.mono_icon = False
self.current_prefix = None
self.games = []
self.flowbox_child = None
self.updated_steam_id = None
self.game_running = False
self.last_click_time = 0
self.last_clicked_item = None
self.double_click_time_threshold = 400
self.processos = {}
self.button_locked = {}
self.working_directory = faugus_launcher_dir
os.chdir(self.working_directory)
self.provider = Gtk.CssProvider()
self.provider.load_from_data(b"""
.hbox-favorite {
background-color: alpha(@theme_selected_bg_color, 0.25);
}
.hbox-normal {
background-color: alpha(@theme_base_color, 0.5);
}
""")
Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), self.provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
self.load_config()
self.context_menu = Gtk.Menu()
self.menu_item_title = Gtk.MenuItem(label="")
self.menu_item_title.set_sensitive(False)
self.context_menu.append(self.menu_item_title)
self.menu_item_playtime = Gtk.MenuItem(label="")
self.menu_item_playtime.set_sensitive(False)
self.context_menu.append(self.menu_item_playtime)
self.context_menu.append(Gtk.SeparatorMenuItem())
self.menu_item_play = Gtk.MenuItem(label=_("Play"))
self.menu_item_play.connect("activate", self.on_context_menu_play)
self.context_menu.append(self.menu_item_play)
self.menu_item_edit = Gtk.MenuItem(label=_("Edit"))
self.menu_item_edit.connect("activate", self.on_context_menu_edit)
self.context_menu.append(self.menu_item_edit)
self.menu_item_delete = Gtk.MenuItem(label=_("Delete"))
self.menu_item_delete.connect("activate", self.on_context_menu_delete)
self.context_menu.append(self.menu_item_delete)
self.menu_item_duplicate = Gtk.MenuItem(label=_("Duplicate"))
self.menu_item_duplicate.connect("activate", self.on_context_menu_duplicate)
self.context_menu.append(self.menu_item_duplicate)
self.menu_item_hide = Gtk.MenuItem(label=_("Hide"))
self.menu_item_hide.connect("activate", self.on_context_menu_hide)
self.context_menu.append(self.menu_item_hide)
self.menu_item_favorite = Gtk.MenuItem(label=_("Add to favorites"))
self.menu_item_favorite.connect("activate", self.on_context_menu_favorite)
self.context_menu.append(self.menu_item_favorite)
self.menu_item_game = Gtk.MenuItem(label=_("Open game location"))
self.menu_item_game.connect("activate", self.on_context_menu_game)
self.context_menu.append(self.menu_item_game)
self.menu_item_prefix = Gtk.MenuItem(label=_("Open prefix location"))
self.menu_item_prefix.connect("activate", self.on_context_menu_prefix)
self.context_menu.append(self.menu_item_prefix)
self.menu_show_logs = Gtk.MenuItem(label=_("Show logs"))
self.menu_show_logs.connect("activate", self.on_context_show_logs)
self.context_menu.append(self.menu_show_logs)
self.context_menu.show_all()
if self.interface_mode == "List":
self.small_interface()
if self.interface_mode == "Blocks":
if self.start_maximized:
self.maximize()
if self.start_fullscreen:
self.fullscreen()
self.fullscreen_activated = True
self.big_interface()
if self.interface_mode == "Banners":
if self.start_maximized:
self.maximize()
if self.start_fullscreen:
self.fullscreen()
self.fullscreen_activated = True
self.big_interface()
if not self.interface_mode:
self.interface_mode = "List"
self.small_interface()
self.flowbox.connect("button-press-event", self.on_item_right_click)
self.load_tray_icon()
if IS_FLATPAK:
signal.signal(signal.SIGCHLD, self.on_child_process_closed)
else:
GLib.timeout_add_seconds(1, self.check_running_processes)
def load_tray_icon(self):
if not self.system_tray:
if self.indicator:
try:
self.indicator.set_status(AyatanaAppIndicator3.IndicatorStatus.PASSIVE)
except:
pass
self.indicator = None
return
menu = Gtk.Menu()
if os.path.exists(latest_games):
with open(latest_games, "r") as f:
for line in f:
name = line.strip()
if name:
item = Gtk.MenuItem(label=name)
item.connect("activate", self.on_game_selected, name)
menu.append(item)
if menu.get_children():
menu.append(Gtk.SeparatorMenuItem())
restore_item = Gtk.MenuItem(label=_("Open Faugus Launcher"))
restore_item.connect("activate", self.restore_window)
menu.append(restore_item)
quit_item = Gtk.MenuItem(label=_("Quit"))
quit_item.connect("activate", self.on_quit)
menu.append(quit_item)
menu.show_all()
if self.indicator:
try:
old = self.indicator
self.indicator = None
old.set_status(AyatanaAppIndicator3.IndicatorStatus.PASSIVE)
del old
except:
pass
while Gtk.events_pending():
Gtk.main_iteration()
if not hasattr(self, "_tray_counter"):
self._tray_counter = 0
self._tray_counter += 1
unique_id = f"faugus-launcher-{self._tray_counter}"
icon = faugus_mono_icon if self.mono_icon else tray_icon
self.indicator = AyatanaAppIndicator3.Indicator.new(
unique_id, icon, AyatanaAppIndicator3.IndicatorCategory.APPLICATION_STATUS
)
self.indicator.set_menu(menu)
self.indicator.set_icon_full(icon, "Faugus Launcher")
self.indicator.set_status(AyatanaAppIndicator3.IndicatorStatus.ACTIVE)
self.indicator.set_icon_theme_path("")
def on_close(self, *_):
if self.system_tray:
self.hide()
return True
if self.indicator:
self.indicator.set_status(
AyatanaAppIndicator3.IndicatorStatus.PASSIVE
)
self.get_application().quit()
return False
def restore_window(self, *_):
self.show_all()
if self.interface_mode != "List":
if self.fullscreen_activated:
self.fullscreen_activated = True
self.grid_corner.set_visible(True)
self.grid_left.set_margin_start(70)
else:
self.fullscreen_activated = False
self.grid_corner.set_visible(False)
self.grid_left.set_margin_start(0)
self.present()
def on_quit(self, *_):
if self.indicator:
self.indicator.set_status(
AyatanaAppIndicator3.IndicatorStatus.PASSIVE
)
self.get_application().quit()
def select_first_child(self):
if self.flowbox.get_children():
self.flowbox.select_child(self.flowbox.get_children()[0])
self.on_item_selected(self.flowbox, self.flowbox.get_children()[0])
def on_child_process_closed(self, signum, frame):
for title, processo in list(self.processos.items()):
retcode = processo.poll()
if retcode is not None:
del self.processos[title]
selected_child = None
for child in self.flowbox.get_children():
if child.get_state_flags() & Gtk.StateFlags.SELECTED:
selected_child = child
break
if selected_child:
hbox = selected_child.get_children()[0]
game_label = hbox.get_children()[1]
selected_title = game_label.get_text()
if selected_title not in self.processos:
self.menu_item_play.set_sensitive(True)
self.button_play.set_sensitive(True)
self.button_play.set_image(
Gtk.Image.new_from_icon_name("faugus-play-symbolic", Gtk.IconSize.BUTTON))
else:
self.menu_item_play.set_sensitive(False)
self.button_play.set_sensitive(False)
self.button_play.set_image(
Gtk.Image.new_from_icon_name("faugus-stop-symbolic", Gtk.IconSize.BUTTON))
def check_running_processes(self):
processos = self.load_processes_from_file()
updated = False
to_remove = []
for title, data in processos.items():
pid_main = data.get("faugus-run") or data.get("main")
try:
proc = psutil.Process(pid_main)
if proc.status() == psutil.STATUS_ZOMBIE:
to_remove.append(title)
except psutil.NoSuchProcess:
to_remove.append(title)
except Exception:
to_remove.append(title)
for title in to_remove:
del processos[title]
updated = True
if title in self.button_locked:
del self.button_locked[title]
if updated:
with open(running_games, "w") as f:
json.dump(processos, f, indent=2)
selected_child = None
for child in self.flowbox.get_children():
if child.get_state_flags() & Gtk.StateFlags.SELECTED:
selected_child = child
break
if selected_child:
hbox = selected_child.get_children()[0]
game_label = hbox.get_children()[1]
selected_title = game_label.get_text()
self.on_item_selected(self.flowbox, selected_child)
return True
def load_processes_from_file(self):
if os.path.exists(running_games):
try:
with open(running_games, "r") as f:
return json.load(f)
except json.JSONDecodeError:
return {}
return {}
def small_interface(self):
self.set_default_size(-1, 610)
self.set_resizable(False)
self.big_interface_active = False
# Create main box and its components
self.box_main = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.box_top = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.box_bottom = Gtk.Box()
# Create buttons for adding, editing, and deleting games
self.button_add = Gtk.Button()
self.button_add.connect("clicked", self.on_button_add_clicked)
self.button_add.set_can_focus(False)
self.button_add.set_size_request(50, 50)
self.button_add.set_image(Gtk.Image.new_from_icon_name("faugus-add-symbolic", Gtk.IconSize.BUTTON))
self.button_add.set_margin_top(10)
self.button_add.set_margin_start(10)
self.button_add.set_margin_bottom(10)
# Create button for killing processes
button_kill = Gtk.Button()
button_kill.connect("clicked", self.on_button_kill_clicked)
button_kill.set_can_focus(False)
button_kill.set_tooltip_text(_("Force close all running games"))
button_kill.set_size_request(50, 50)
button_kill.set_image(Gtk.Image.new_from_icon_name("faugus-kill-symbolic", Gtk.IconSize.BUTTON))
button_kill.set_margin_top(10)
button_kill.set_margin_end(10)
button_kill.set_margin_bottom(10)
# Create button for settings
button_settings = Gtk.Button()
button_settings.connect("clicked", self.on_button_settings_clicked)
button_settings.set_can_focus(False)
button_settings.set_size_request(50, 50)
button_settings.set_image(Gtk.Image.new_from_icon_name("faugus-settings-symbolic", Gtk.IconSize.BUTTON))
button_settings.set_margin_top(10)
button_settings.set_margin_start(10)
button_settings.set_margin_bottom(10)
# Create button for launching games
self.button_play = Gtk.Button()
self.button_play.connect("clicked", self.on_button_play_clicked)
self.button_play.set_can_focus(False)
self.button_play.set_size_request(50, 50)
self.button_play.set_image(Gtk.Image.new_from_icon_name("faugus-play-symbolic", Gtk.IconSize.BUTTON))
self.button_play.set_margin_top(10)
self.button_play.set_margin_end(10)
self.button_play.set_margin_bottom(10)
self.entry_search = Gtk.Entry()
self.entry_search.set_placeholder_text(_("Search..."))
self.entry_search.connect("changed", self.on_search_changed)
self.entry_search.set_size_request(170, 50)
self.entry_search.set_margin_top(10)
self.entry_search.set_margin_start(10)
self.entry_search.set_margin_bottom(10)
self.entry_search.set_margin_end(10)
# Create scrolled window for game list
scroll_box = Gtk.ScrolledWindow()
scroll_box.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
scroll_box.set_margin_start(10)
scroll_box.set_margin_top(10)
scroll_box.set_margin_end(10)
self.flowbox = Gtk.FlowBox()
self.flowbox.set_selection_mode(Gtk.SelectionMode.SINGLE)
self.flowbox.set_halign(Gtk.Align.FILL)
self.flowbox.set_valign(Gtk.Align.START)
self.flowbox.connect('child-activated', self.on_item_selected)
self.flowbox.connect('button-release-event', self.on_item_release_event)
def sort_games(child1, child2, _):
g1 = child1.game
g2 = child2.game
if g1.favorite and not g2.favorite:
return -1
if not g1.favorite and g2.favorite:
return 1
return (g1.title > g2.title) - (g1.title < g2.title)
self.flowbox.set_sort_func(sort_games, None)
scroll_box.add(self.flowbox)
self.load_games()
# Pack left and scrolled box into the top box
self.box_top.pack_start(scroll_box, True, True, 0)
# Pack buttons and other components into the bottom box
self.box_bottom.pack_start(self.button_add, False, False, 0)
self.box_bottom.pack_start(button_settings, False, False, 0)
self.box_bottom.pack_start(self.entry_search, True, True, 0)
self.box_bottom.pack_end(self.button_play, False, False, 0)
self.box_bottom.pack_end(button_kill, False, False, 0)
# Pack top and bottom boxes into the main box
self.box_main.pack_start(self.box_top, True, True, 0)
self.box_main.pack_end(self.box_bottom, False, True, 0)
self.add(self.box_main)
self.menu_item_edit.set_sensitive(False)
self.menu_item_delete.set_sensitive(False)
self.menu_item_play.set_sensitive(False)
self.button_play.set_sensitive(False)
self.select_first_child()
self.connect("key-press-event", self.on_key_press_event)
self.show_all()
def big_interface(self):
self.set_default_size(1280, 720)
self.set_resizable(True)
self.big_interface_active = True
# Create main box and its components
self.box_main = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.box_top = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
self.box_bottom = Gtk.Box()
# Create buttons for adding, editing, and deleting games
self.button_add = Gtk.Button()
self.button_add.connect("clicked", self.on_button_add_clicked)
self.button_add.set_can_focus(False)
self.button_add.set_size_request(50, 50)
self.button_add.set_image(Gtk.Image.new_from_icon_name("faugus-add-symbolic", Gtk.IconSize.BUTTON))
self.button_add.set_margin_top(10)
self.button_add.set_margin_start(10)
self.button_add.set_margin_bottom(10)
# Create button for killing processes
button_kill = Gtk.Button()
button_kill.connect("clicked", self.on_button_kill_clicked)
button_kill.set_can_focus(False)
button_kill.set_tooltip_text(_("Force close all running games"))
button_kill.set_size_request(50, 50)
button_kill.set_image(Gtk.Image.new_from_icon_name("faugus-kill-symbolic", Gtk.IconSize.BUTTON))
button_kill.set_margin_top(10)
button_kill.set_margin_bottom(10)
# Create button for exiting
button_bye = Gtk.Button()
button_bye.connect("clicked", self.on_button_bye_clicked)
button_bye.set_can_focus(False)
button_bye.set_size_request(50, 50)
button_bye.set_image(Gtk.Image.new_from_icon_name("faugus-exit-symbolic", Gtk.IconSize.BUTTON))
button_bye.set_margin_start(10)
button_bye.set_margin_top(10)
button_bye.set_margin_bottom(10)
button_bye.set_margin_end(10)
# Create button for settings
button_settings = Gtk.Button()
button_settings.connect("clicked", self.on_button_settings_clicked)
button_settings.set_can_focus(False)
button_settings.set_size_request(50, 50)
button_settings.set_image(Gtk.Image.new_from_icon_name("faugus-settings-symbolic", Gtk.IconSize.BUTTON))
button_settings.set_margin_top(10)
button_settings.set_margin_start(10)
button_settings.set_margin_bottom(10)
# Create button for launching games
self.button_play = Gtk.Button()
self.button_play.connect("clicked", self.on_button_play_clicked)
self.button_play.set_can_focus(False)
self.button_play.set_size_request(50, 50)
self.button_play.set_image(Gtk.Image.new_from_icon_name("faugus-play-symbolic", Gtk.IconSize.BUTTON))
self.button_play.set_margin_top(10)
self.button_play.set_margin_start(10)
self.button_play.set_margin_end(10)
self.button_play.set_margin_bottom(10)
self.entry_search = Gtk.Entry()
self.entry_search.set_placeholder_text(_("Search..."))
self.entry_search.connect("changed", self.on_search_changed)
self.entry_search.set_size_request(170, 50)
self.entry_search.set_margin_top(10)
self.entry_search.set_margin_start(10)
self.entry_search.set_margin_bottom(10)
self.entry_search.set_margin_end(10)
self.grid_left = Gtk.Grid()
self.grid_left.set_hexpand(True)
self.grid_left.set_halign(Gtk.Align.END)
self.grid_left.add(self.button_add)
self.grid_left.add(button_settings)
grid_middle = Gtk.Grid()
grid_middle.add(self.entry_search)
grid_right = Gtk.Grid()
grid_right.set_hexpand(True)
grid_right.set_halign(Gtk.Align.START)
grid_right.add(button_kill)
grid_right.add(self.button_play)
self.grid_corner = Gtk.Grid()
self.grid_corner.add(button_bye)
# Create scrolled window for game list
scroll_box = Gtk.ScrolledWindow()
scroll_box.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
scroll_box.set_margin_top(10)
scroll_box.set_margin_end(10)
scroll_box.set_margin_start(10)
scroll_box.set_margin_bottom(10)
self.flowbox = Gtk.FlowBox()
self.flowbox.set_selection_mode(Gtk.SelectionMode.SINGLE)
self.flowbox.set_halign(Gtk.Align.CENTER)
self.flowbox.set_valign(Gtk.Align.CENTER)
self.flowbox.set_min_children_per_line(2)
self.flowbox.set_max_children_per_line(20)
self.flowbox.connect('child-activated', self.on_item_selected)
self.flowbox.connect('button-release-event', self.on_item_release_event)
def sort_games(child1, child2, _):
g1 = child1.game
g2 = child2.game
if g1.favorite and not g2.favorite:
return -1
if not g1.favorite and g2.favorite:
return 1
return (g1.title > g2.title) - (g1.title < g2.title)
self.flowbox.set_sort_func(sort_games, None)
scroll_box.add(self.flowbox)
self.load_games()
self.box_top.pack_start(scroll_box, True, True, 0)
self.box_bottom.pack_start(self.grid_left, True, True, 0)
self.box_bottom.pack_start(grid_middle, False, False, 0)
self.box_bottom.pack_start(grid_right, True, True, 0)
self.box_bottom.pack_end(self.grid_corner, False, False, 0)
self.box_main.pack_start(self.box_top, True, True, 0)
self.box_main.pack_end(self.box_bottom, False, True, 0)
self.add(self.box_main)
self.menu_item_edit.set_sensitive(False)
self.menu_item_delete.set_sensitive(False)
self.menu_item_play.set_sensitive(False)
self.button_play.set_sensitive(False)
self.select_first_child()
self.connect("key-press-event", self.on_key_press_event)
self.show_all()
if self.start_fullscreen:
self.fullscreen_activated = True
self.grid_corner.set_visible(True)
self.grid_left.set_margin_start(70)
else:
self.fullscreen_activated = False
self.grid_corner.set_visible(False)
self.grid_left.set_margin_start(0)
def on_destroy(self, *args):
self.get_application().quit()
def on_button_bye_clicked(self, widget):
menu = Gtk.Menu()
shutdown_item = Gtk.MenuItem(label=_("Shut down"))
reboot_item = Gtk.MenuItem(label=_("Reboot"))
close_item = Gtk.MenuItem(label=_("Close"))
shutdown_item.connect("activate", self.on_shutdown)
reboot_item.connect("activate", self.on_reboot)
close_item.connect("activate", self.on_close_fullscreen)
menu.append(shutdown_item)
menu.append(reboot_item)
menu.append(close_item)
menu.show_all()
menu.attach_to_widget(self, None)
menu.popup(None, None, None, None, 0, Gtk.get_current_event_time())
def on_shutdown(self, widget):
subprocess.run(["pkexec", "shutdown", "-h", "now"])
def on_reboot(self, widget):
subprocess.run(["pkexec", "reboot"])
def on_close_fullscreen(self, widget):
self.get_application().quit()
def on_item_right_click(self, widget, event):
if event.button == Gdk.BUTTON_SECONDARY:
item = self.get_item_at_event(event)
if item:
self.flowbox.emit('child-activated', item)
self.flowbox.select_child(item)
selected_children = self.flowbox.get_selected_children()
selected_child = selected_children[0]
game = selected_child.game
title = game.title
self.menu_item_title.get_child().set_text(title)
with open(games_json, "r") as f:
data = json.load(f)
for item in data:
if isinstance(item, dict) and item.get("gameid") == game.gameid:
game.playtime = item.get("playtime", 0)
formatted = self.format_playtime(game.playtime)
if not formatted:
self.menu_item_playtime.hide()
else:
self.menu_item_playtime.show()
self.menu_item_playtime.get_child().set_text(formatted)
break
if game.protonfix:
match = re.search(r"umu-(\d+)", game.protonfix)
if match:
log_id = match.group(1)
else:
log_id = "0"
self.log_file_path = f"{logs_dir}/{game.gameid}/steam-{log_id}.log"
else:
self.log_file_path = f"{logs_dir}/{game.gameid}/steam-0.log"
self.umu_log_file_path = f"{logs_dir}/{game.gameid}/umu.log"
if self.enable_logging:
self.menu_show_logs.set_visible(True)
if os.path.exists(self.log_file_path):
self.menu_show_logs.set_sensitive(True)
self.current_title = title
else:
self.menu_show_logs.set_sensitive(False)
else:
self.menu_show_logs.set_visible(False)
if game.hidden:
self.menu_item_hide.get_child().set_text(_("Remove from hidden"))
else:
self.menu_item_hide.get_child().set_text(_("Hide"))
if game.favorite:
self.menu_item_favorite.get_child().set_text(_("Remove from favorites"))
else:
self.menu_item_favorite.get_child().set_text(_("Add to favorites"))
if game.runner == "Steam":
self.menu_item_duplicate.set_visible(False)
self.menu_item_game.set_visible(False)
self.menu_item_prefix.set_visible(False)
self.menu_show_logs.set_visible(False)
elif game.runner == "Linux-Native":
self.menu_item_duplicate.set_visible(True)
self.menu_item_game.set_visible(True)
self.menu_item_prefix.set_visible(False)
self.menu_show_logs.set_visible(False)
else:
self.menu_item_duplicate.set_visible(True)
self.menu_item_game.set_visible(True)
self.menu_item_prefix.set_visible(True)
processos = self.load_processes_from_file()
if title in processos:
self.menu_item_play.get_child().set_text(_("Stop"))
else:
self.menu_item_play.get_child().set_text(_("Play"))
if os.path.dirname(game.path):
self.menu_item_game.set_sensitive(True)
self.current_game = os.path.dirname(game.path)
else:
self.menu_item_game.set_sensitive(False)
self.current_game = None
if os.path.isdir(game.prefix):
self.menu_item_prefix.set_sensitive(True)
self.current_prefix = game.prefix
else:
self.menu_item_prefix.set_sensitive(False)
self.current_prefix = None
self.context_menu.popup_at_pointer(event)
def format_playtime(self, seconds):
if not seconds:
return None
try:
seconds = int(seconds)
except (ValueError, TypeError):
seconds = 0
hours = seconds // 3600
minutes = (seconds % 3600) // 60
if hours == 0 and minutes == 0:
return None
txt_hour = _("hour")
txt_hours = _("hours")
txt_minute = _("minute")
txt_minutes = _("minutes")
parts = []
if hours > 0:
word = txt_hour if hours == 1 else txt_hours
parts.append(f"{hours} {word}")
if minutes > 0:
word = txt_minute if minutes == 1 else txt_minutes
parts.append(f"{minutes} {word}")
return " ".join(parts)
def on_context_menu_play(self, menu_item):
selected_item = self.flowbox.get_selected_children()[0]
self.on_button_play_clicked(selected_item)
def on_context_menu_edit(self, menu_item):
selected_item = self.flowbox.get_selected_children()[0]
self.on_button_edit_clicked(selected_item)
def on_context_menu_delete(self, menu_item):
selected_item = self.flowbox.get_selected_children()[0]
self.on_button_delete_clicked(selected_item)
def on_context_menu_duplicate(self, menu_item):
selected_item = self.flowbox.get_selected_children()[0]
self.on_duplicate_clicked(selected_item)
def on_context_menu_hide(self, menu_item):
selected = self.flowbox.get_selected_children()
if not selected:
return
game = selected[0].game
if not game:
return
try:
with open(games_json, "r", encoding="utf-8") as f:
data = json.load(f)
for item in data:
if item.get("gameid") == game.gameid:
item["hidden"] = not item.get("hidden", False)
game.hidden = item["hidden"]
break
with open(games_json, "w", encoding="utf-8") as f:
json.dump(data, f, indent=4, ensure_ascii=False)
except Exception as e:
return
self.update_list()
self.select_first_child()
def on_context_menu_favorite(self, menu_item):
selected = self.flowbox.get_selected_children()
if not selected:
return
game = selected[0].game
if not game:
return
selected_gameid = game.gameid
try:
with open(games_json, "r", encoding="utf-8") as f:
data = json.load(f)
for item in data:
if item.get("gameid") == game.gameid:
item["favorite"] = not item.get("favorite", False)
game.favorite = item["favorite"]
break
with open(games_json, "w", encoding="utf-8") as f:
json.dump(data, f, indent=4, ensure_ascii=False)
except Exception as e:
return
self.update_list()
for child in self.flowbox.get_children():
if hasattr(child, "game") and child.game.gameid == selected_gameid:
self.flowbox.select_child(child)
self.flowbox.set_focus_child(child)
break
def on_context_menu_game(self, menu_item):
subprocess.run(["xdg-open", self.current_game], check=True)
def on_context_menu_prefix(self, menu_item):
subprocess.run(["xdg-open", self.current_prefix], check=True)
def on_context_show_logs(self, menu_item):
selected_item = self.flowbox.get_selected_children()[0]
self.on_show_logs_clicked(selected_item)
def on_show_logs_clicked(self, widget):
dialog = Gtk.Dialog(title=_("%s Logs") % self.current_title, parent=self, modal=True)
dialog.set_icon_from_file(faugus_png)
dialog.set_default_size(1280, 720)
scrolled_window1 = Gtk.ScrolledWindow()
scrolled_window1.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
text_view1 = Gtk.TextView()
text_view1.set_editable(False)
text_buffer1 = text_view1.get_buffer()
with open(self.log_file_path, "r") as log_file:
text_buffer1.set_text(log_file.read())
scrolled_window1.add(text_view1)
scrolled_window2 = Gtk.ScrolledWindow()
scrolled_window2.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
text_view2 = Gtk.TextView()
text_view2.set_editable(False)
text_buffer2 = text_view2.get_buffer()
with open(self.umu_log_file_path, "r") as log_file:
text_buffer2.set_text(log_file.read())
scrolled_window2.add(text_view2)
def copy_to_clipboard(button):
current_page = notebook.get_current_page()
if current_page == 0: # Tab 1: Proton
start_iter, end_iter = text_buffer1.get_bounds()
text_to_copy = text_buffer1.get_text(start_iter, end_iter, False)
elif current_page == 1: # Tab 2: UMU-Launcher
start_iter, end_iter = text_buffer2.get_bounds()
text_to_copy = text_buffer2.get_text(start_iter, end_iter, False)
else:
text_to_copy = ""
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
clipboard.set_text(text_to_copy, -1)
clipboard.store()
def open_location(button):
subprocess.run(["xdg-open", os.path.dirname(self.log_file_path)], check=True)
button_copy_clipboard = Gtk.Button(label=_("Copy to clipboard"))
button_copy_clipboard.set_size_request(150, -1)
button_copy_clipboard.connect("clicked", copy_to_clipboard)
button_open_location = Gtk.Button(label=_("Open file location"))
button_open_location.set_size_request(150, -1)
button_open_location.connect("clicked", open_location)
notebook = Gtk.Notebook()
notebook.set_margin_start(10)
notebook.set_margin_end(10)
notebook.set_margin_top(10)
notebook.set_margin_bottom(10)
notebook.set_halign(Gtk.Align.FILL)
notebook.set_valign(Gtk.Align.FILL)
notebook.set_vexpand(True)
notebook.set_hexpand(True)
tab_box1 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
tab_label1 = Gtk.Label(label="Proton")
tab_label1.set_width_chars(15)
tab_label1.set_xalign(0.5)
tab_box1.pack_start(tab_label1, True, True, 0)
tab_box1.set_hexpand(True)
tab_box2 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
tab_label2 = Gtk.Label(label="UMU-Launcher")
tab_label2.set_width_chars(15)
tab_label2.set_xalign(0.5)
tab_box2.pack_start(tab_label2, True, True, 0)
tab_box2.set_hexpand(True)
notebook.append_page(scrolled_window1, tab_box1)
notebook.append_page(scrolled_window2, tab_box2)
content_area = dialog.get_content_area()
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_bottom.pack_start(button_copy_clipboard, True, True, 0)
box_bottom.pack_start(button_open_location, True, True, 0)
content_area.add(notebook)
content_area.add(box_bottom)
tab_box1.show_all()
tab_box2.show_all()
dialog.show_all()
dialog.run()
dialog.destroy()
def on_duplicate_clicked(self, widget):
selected_children = self.flowbox.get_selected_children()
selected_child = selected_children[0]
game = selected_child.game
title = game.title
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
)
duplicate_dialog = DuplicateDialog(self, title)
while True:
response = duplicate_dialog.run()
if response != Gtk.ResponseType.OK:
break
new_title = duplicate_dialog.entry_title.get_text().strip()
if not new_title:
duplicate_dialog.entry_title.get_style_context().add_class("entry")
continue
if any(new_title == g.title for g in self.games):
duplicate_dialog.show_warning_dialog(
duplicate_dialog,
_("%s already exists.") % new_title
)
continue
title_formatted_old = format_title(game.title)
title_formatted = format_title(new_title)
icon = f"{icons_dir}/{title_formatted_old}.ico"
banner = game.banner
new_icon = f"{icons_dir}/{title_formatted}.ico"
new_banner = f"{banners_dir}/{title_formatted}.png"
new_addapp_bat = f"{os.path.dirname(game.path)}/faugus-{title_formatted}.bat"
if os.path.exists(icon):
shutil.copyfile(icon, new_icon)
if os.path.exists(banner):
shutil.copyfile(banner, new_banner)
if os.path.exists(game.addapp_bat):
shutil.copyfile(game.addapp_bat, new_addapp_bat)
game.title = new_title
game.banner = new_banner
game.addapp_bat = new_addapp_bat
game_info = {
"gameid": title_formatted,
"title": game.title,
"path": game.path,
"prefix": game.prefix,
"launch_arguments": game.launch_arguments,
"game_arguments": game.game_arguments,
"mangohud": game.mangohud,
"gamemode": game.gamemode,
"disable_hidraw": game.disable_hidraw,
"protonfix": game.protonfix,
"runner": game.runner,
"addapp_checkbox": game.addapp_checkbox,
"addapp": game.addapp,
"addapp_bat": game.addapp_bat,
"banner": game.banner,
"lossless_enabled": game.lossless_enabled,
"lossless_multiplier": game.lossless_multiplier,
"lossless_flow": game.lossless_flow,
"lossless_performance": game.lossless_performance,
"lossless_hdr": game.lossless_hdr,
"playtime": game.playtime,
"hidden": game.hidden,
"prevent_sleep": game.prevent_sleep,
"favorite": game.favorite,
}
games = []
if os.path.exists("games.json"):
try:
with open("games.json", "r", encoding="utf-8") as file:
games = json.load(file)
except json.JSONDecodeError as e:
print(f"Error reading the JSON file: {e}")
games.append(game_info)
with open("games.json", "w", encoding="utf-8") as file:
json.dump(games, file, ensure_ascii=False, indent=4)
self.games.append(game)
self.add_item_list(game)
self.update_list()
self.select_game_by_title(new_title)
break
duplicate_dialog.destroy()
def on_item_release_event(self, widget, event):
if event.button == Gdk.BUTTON_PRIMARY:
current_time = event.time
current_item = self.get_item_at_event(event)
if current_item:
self.flowbox.select_child(current_item)
if current_item == self.last_clicked_item and current_time - self.last_click_time < self.double_click_time_threshold:
self.on_item_double_click(current_item)
self.last_clicked_item = current_item
self.last_click_time = current_time
def get_item_at_event(self, event):
x, y = event.x, event.y
return self.flowbox.get_child_at_pos(x, y)
def on_item_double_click(self, item):
selected_children = self.flowbox.get_selected_children()
if not selected_children:
return
selected_child = selected_children[0]
hbox = selected_child.get_child()
game_label = hbox.get_children()[1]
title = game_label.get_text()
current_focus = self.get_focus()
if IS_FLATPAK:
if title not in self.processos:
self.on_button_play_clicked(item)
else:
self.running_dialog(title)
else:
processos = self.load_processes_from_file()
if title not in processos:
self.on_button_play_clicked(selected_child)
else:
self.running_dialog(title)
def on_key_press_event(self, widget, event):
selected_children = self.flowbox.get_selected_children()
if event.keyval == Gdk.KEY_h and event.state & Gdk.ModifierType.CONTROL_MASK:
try:
with open(config_file_dir, "r", encoding="utf-8") as f:
lines = f.readlines()
new_lines = []
found = False
for line in lines:
stripped = line.strip()
if stripped.lower().startswith("show-hidden"):
sep_index = line.find("=")
if sep_index != -1:
left = line[:sep_index]
right = line[sep_index + 1:].strip()
if right.lower() == "true":
new_value = "False"
else:
new_value = "True"
new_lines.append(f"{left}={new_value}\n")
found = True
else:
new_lines.append(line)
else:
new_lines.append(line)
if not found:
new_lines.append("show-hidden=True\n")
with open(config_file_dir, "w", encoding="utf-8") as f:
f.writelines(new_lines)
self.load_config()
self.update_list()
self.button_play.set_sensitive(False)
self.select_first_child()
return True
except Exception as e:
return False
if not selected_children:
return
selected_child = selected_children[0]
hbox = selected_child.get_child()
game_label = hbox.get_children()[1]
title = game_label.get_text()
current_focus = self.get_focus()
if event.keyval in (Gdk.KEY_Up, Gdk.KEY_Down, Gdk.KEY_Left, Gdk.KEY_Right):
if current_focus not in self.flowbox.get_children():
selected_child.grab_focus()
if self.interface_mode != "List":
if event.keyval == Gdk.KEY_Return and event.state & Gdk.ModifierType.MOD1_MASK:
if self.get_window().get_state() & Gdk.WindowState.FULLSCREEN:
self.fullscreen_activated = False
self.unfullscreen()
self.grid_corner.set_visible(False)
self.grid_left.set_margin_start(0)
else:
self.fullscreen_activated = True
self.fullscreen()
self.grid_corner.set_visible(True)
self.grid_left.set_margin_start(70)
return True
if IS_FLATPAK:
if event.keyval == Gdk.KEY_Return:
if title not in self.processos:
widget = self.button_play
self.on_button_play_clicked(selected_child)
else:
self.running_dialog(title)
elif event.keyval == Gdk.KEY_Delete:
self.on_button_delete_clicked(selected_child)
else:
if event.keyval == Gdk.KEY_Return:
processos = self.load_processes_from_file()
if title not in processos:
self.on_button_play_clicked(selected_child)
else:
self.running_dialog(title)
elif event.keyval == Gdk.KEY_Delete:
self.on_button_delete_clicked(selected_child)
if event.string:
if event.string.isprintable():
self.entry_search.grab_focus()
current_text = self.entry_search.get_text()
new_text = current_text + event.string
self.entry_search.set_text(new_text)
self.entry_search.set_position(len(new_text))
elif event.keyval == Gdk.KEY_BackSpace:
self.entry_search.grab_focus()
current_text = self.entry_search.get_text()
new_text = current_text[:-1]
self.entry_search.set_text(new_text)
self.entry_search.set_position(len(new_text))
return True
return False
def running_dialog(self, title):
dialog = Gtk.Dialog(title="Faugus Launcher", parent=self, modal=True)
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 is already running.") % title)
label.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.response(Gtk.ResponseType.YES))
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()
def load_config(self):
cfg = ConfigManager()
self.system_tray = cfg.config.get('system-tray', 'False') == 'True'
self.start_boot = cfg.config.get('start-boot', 'False') == 'True'
self.mono_icon = cfg.config.get('mono-icon', 'False') == 'True'
self.close_on_launch = cfg.config.get('close-onlaunch', 'False') == 'True'
self.start_maximized = cfg.config.get('start-maximized', 'False') == 'True'
self.interface_mode = cfg.config.get('interface-mode', '').strip('"')
self.start_fullscreen = cfg.config.get('start-fullscreen', 'False') == 'True'
self.show_labels = cfg.config.get('show-labels', 'False') == 'True'
self.smaller_banners = cfg.config.get('smaller-banners', 'False') == 'True'
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', '')
self.show_hidden = cfg.config.get('show-hidden', 'False') == 'True'
def on_game_selected(self, widget, game_name):
# Find the game in the FlowBox by name and select it
self.flowbox.unselect_all()
for child in self.flowbox.get_children():
hbox = child.get_children()[0] # Assuming HBox structure
game_label = hbox.get_children()[1] # The label should be the second item in HBox
title = game_label.get_text()
if game_label.get_text() == game_name:
# Select this item in FlowBox
self.flowbox.select_child(child)
break
# Call the function to run the selected game
processos = self.load_processes_from_file()
if title not in processos:
self.on_button_play_clicked(widget)
else:
self.running_dialog(title)
def load_games(self):
try:
with open("games.json", "r", encoding="utf-8") as file:
games_data = json.load(file)
self.games.clear()
for game_data in games_data:
game = Game(
game_data.get("gameid", ""),
game_data.get("title", ""),
game_data.get("path", ""),
game_data.get("prefix", ""),
game_data.get("launch_arguments", ""),
game_data.get("game_arguments", ""),
game_data.get("mangohud", ""),
game_data.get("gamemode", ""),
game_data.get("disable_hidraw", ""),
game_data.get("protonfix", ""),
game_data.get("runner", ""),
game_data.get("addapp_checkbox", ""),
game_data.get("addapp", ""),
game_data.get("addapp_bat", ""),
game_data.get("banner", ""),
game_data.get("lossless_enabled", ""),
game_data.get("lossless_multiplier", ""),
game_data.get("lossless_flow", ""),
game_data.get("lossless_performance", ""),
game_data.get("lossless_hdr", ""),
game_data.get("lossless_present", ""),
game_data.get("playtime", 0),
game_data.get("hidden", False),
game_data.get("prevent_sleep", False),
game_data.get("favorite", False),
game_data.get("startup_wm_class", ""),
)
if not self.show_hidden and game.hidden:
continue
self.games.append(game)
self.games = sorted(self.games, key=lambda x: x.title.lower())
self.filtered_games = self.games[:]
self.flowbox.foreach(Gtk.Widget.destroy)
for game in self.filtered_games:
self.add_item_list(game)
except FileNotFoundError:
pass
except json.JSONDecodeError as e:
print(f"Error reading the JSON file: {e}")
def add_item_list(self, game):
if self.interface_mode == "List":
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
if self.interface_mode == "Blocks":
hbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
hbox.set_size_request(200, -1)
if self.interface_mode == "Banners":
hbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
if game.favorite:
hbox.get_style_context().add_class("hbox-favorite")
else:
hbox.get_style_context().add_class("hbox-normal")
game_icon = get_shortcut_icon_path(game.gameid, icons_dir, faugus_png)
game_label = Gtk.Label.new(game.title)
if self.interface_mode in ("Blocks", "Banners"):
game_label.set_line_wrap(True)
game_label.set_max_width_chars(1)
game_label.set_justify(Gtk.Justification.CENTER)
self.flowbox_child = Gtk.FlowBoxChild()
pixbuf = GdkPixbuf.Pixbuf.new_from_file(game_icon)
if self.interface_mode == "List":
scaled_pixbuf = pixbuf.scale_simple(40, 40, GdkPixbuf.InterpType.BILINEAR)
image = Gtk.Image.new_from_file(game_icon)
image.set_from_pixbuf(scaled_pixbuf)
image.set_margin_start(10)
image.set_margin_end(10)
image.set_margin_top(10)
image.set_margin_bottom(10)
game_label.set_margin_start(10)
game_label.set_margin_end(10)
game_label.set_margin_top(10)
game_label.set_margin_bottom(10)
hbox.pack_start(image, False, False, 0)
hbox.pack_start(game_label, False, False, 0)
self.flowbox_child.set_size_request(300, -1)
self.flowbox.set_homogeneous(True)
self.flowbox_child.set_valign(Gtk.Align.START)
self.flowbox_child.set_halign(Gtk.Align.FILL)
if self.interface_mode == "Blocks":
self.flowbox_child.set_hexpand(True)
self.flowbox_child.set_vexpand(True)
scaled_pixbuf = pixbuf.scale_simple(100, 100, GdkPixbuf.InterpType.BILINEAR)
image = Gtk.Image.new_from_file(game_icon)
image.set_from_pixbuf(scaled_pixbuf)
image.set_margin_top(10)
game_label.set_margin_top(10)
game_label.set_margin_start(10)
game_label.set_margin_end(10)
game_label.set_margin_bottom(10)
hbox.pack_start(image, False, False, 0)
hbox.pack_start(game_label, True, False, 0)
self.flowbox_child.set_valign(Gtk.Align.FILL)
self.flowbox_child.set_halign(Gtk.Align.FILL)
if self.interface_mode == "Banners":
self.flowbox_child.set_hexpand(True)
self.flowbox_child.set_vexpand(True)
image2 = Gtk.Image()
game_label.set_size_request(-1, 50)
game_label.set_margin_start(10)
game_label.set_margin_end(10)
self.flowbox_child.set_margin_start(10)
self.flowbox_child.set_margin_end(10)
self.flowbox_child.set_margin_top(10)
self.flowbox_child.set_margin_bottom(10)
self.flowbox_child.set_valign(Gtk.Align.FILL)
self.flowbox_child.set_halign(Gtk.Align.FILL)
if game.banner == "" or not os.path.isfile(game.banner):
if self.smaller_banners:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
faugus_banner, 180, 270, False)
else:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
faugus_banner, 230, 345, False)
else:
if self.smaller_banners:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
game.banner, 180, 270, False)
else:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
game.banner, 230, 345, False)
image2.set_from_pixbuf(pixbuf)
hbox.pack_start(image2, False, False, 0)
hbox.pack_start(game_label, True, False, 0)
if not self.show_labels:
game_label.set_no_show_all(True)
self.flowbox_child.add(hbox)
self.flowbox_child.game = game
self.flowbox.add(self.flowbox_child)
def on_search_changed(self, entry):
search_text = entry.get_text().lower()
first_visible = None
for child in self.flowbox.get_children():
game = child.game
is_match = search_text in game.title.lower()
child.set_visible(is_match)
if is_match and first_visible is None:
first_visible = child
if first_visible:
self.flowbox.select_child(first_visible)
self.on_item_selected(self.flowbox, first_visible)
def on_item_selected(self, flowbox, child):
if child is not None:
children = child.get_children()
hbox = children[0]
label_children = hbox.get_children()
game_label = label_children[1]
title = game_label.get_text()
self.menu_item_edit.set_sensitive(True)
self.menu_item_delete.set_sensitive(True)
if IS_FLATPAK:
if title in self.processos:
self.menu_item_play.set_sensitive(False)
self.button_play.set_sensitive(False)
self.button_play.set_image(
Gtk.Image.new_from_icon_name("faugus-stop-symbolic", Gtk.IconSize.BUTTON))
else:
self.menu_item_play.set_sensitive(True)
self.button_play.set_sensitive(True)
self.button_play.set_image(
Gtk.Image.new_from_icon_name("faugus-play-symbolic", Gtk.IconSize.BUTTON))
else:
processos = self.load_processes_from_file()
if title in self.button_locked:
self.menu_item_play.set_sensitive(False)
self.button_play.set_sensitive(False)
self.button_play.set_image(Gtk.Image.new_from_icon_name("faugus-stop-symbolic", Gtk.IconSize.BUTTON))
elif title in processos:
self.menu_item_play.set_sensitive(True)
self.button_play.set_sensitive(True)
self.button_play.set_image(Gtk.Image.new_from_icon_name("faugus-stop-symbolic", Gtk.IconSize.BUTTON))
else:
self.menu_item_play.set_sensitive(True)
self.button_play.set_sensitive(True)
self.button_play.set_image(
Gtk.Image.new_from_icon_name("faugus-play-symbolic", Gtk.IconSize.BUTTON))
else:
self.menu_item_edit.set_sensitive(False)
self.menu_item_delete.set_sensitive(False)
self.menu_item_play.set_sensitive(False)
self.button_play.set_sensitive(False)
def on_button_settings_clicked(self, widget):
# Handle add button click event
settings_dialog = Settings(self)
settings_dialog.connect("response", self.on_settings_dialog_response, settings_dialog)
settings_dialog.show()
def on_settings_dialog_response(self, dialog, response_id, settings_dialog):
if faugus_backup:
subprocess.Popen([sys.executable, __file__])
self.destroy()
self.load_config()
self.manage_autostart_file(self.start_boot)
settings_dialog.destroy()
return
# Handle dialog response
if response_id == Gtk.ResponseType.OK:
default_prefix = settings_dialog.entry_default_prefix.get_text()
validation_result = self.validate_settings_fields(settings_dialog, default_prefix)
if not validation_result:
return
if not settings_dialog.logging_warning:
if settings_dialog.checkbox_enable_logging.get_active():
self.show_warning_dialog(self, _("Proton may generate huge log files."), _("Enable logging only when debugging a problem."))
settings_dialog.logging_warning = True
settings_dialog.update_config_file()
self.manage_autostart_file(settings_dialog.checkbox_start_boot.get_active())
if settings_dialog.checkbox_system_tray.get_active():
self.system_tray = True
else:
self.system_tray = False
GLib.timeout_add(1000, self.load_tray_icon)
if validation_result:
combobox_language = settings_dialog.combobox_language.get_active_text()
if self.interface_mode != settings_dialog.combobox_interface.get_active_text():
subprocess.Popen([sys.executable, __file__])
self.destroy()
if self.show_labels != settings_dialog.checkbox_show_labels.get_active():
subprocess.Popen([sys.executable, __file__])
self.destroy()
if self.smaller_banners != settings_dialog.checkbox_smaller_banners.get_active():
subprocess.Popen([sys.executable, __file__])
self.destroy()
if self.language != settings_dialog.lang_codes.get(combobox_language, "en_US"):
subprocess.Popen([sys.executable, __file__])
self.destroy()
settings_dialog.update_envar_file()
if self.show_hidden != settings_dialog.checkbox_show_hidden.get_active():
self.load_config()
self.update_list()
self.button_play.set_sensitive(False)
self.load_config()
settings_dialog.destroy()
else:
settings_dialog.destroy()
def validate_settings_fields(self, settings_dialog, default_prefix):
settings_dialog.entry_default_prefix.get_style_context().remove_class("entry")
if settings_dialog.combobox_interface.get_active_text() == "Banners":
if not default_prefix:
if not default_prefix:
settings_dialog.entry_default_prefix.get_style_context().add_class("entry")
return False
return True
elif not default_prefix:
settings_dialog.entry_default_prefix.get_style_context().add_class("entry")
return False
else:
return True
def manage_autostart_file(self, checkbox_start_boot):
# Define the path for the autostart file
autostart_path = os.path.expanduser('~/.config/autostart/faugus-launcher.desktop')
autostart_dir = os.path.dirname(autostart_path)
# Ensure the autostart directory exists
if not os.path.exists(autostart_dir):
os.makedirs(autostart_dir)
if checkbox_start_boot:
# Create the autostart file if it does not exist
if not os.path.exists(autostart_path):
with open(autostart_path, "w") as f:
if IS_FLATPAK:
f.write(
"[Desktop Entry]\n"
"Categories=Utility;\n"
"Exec=flatpak run io.github.Faugus.faugus-launcher --hide\n"
"Icon=io.github.Faugus.faugus-launcher\n"
"MimeType=application/x-ms-dos-executable;application/x-msi;application/x-ms-shortcut;application/x-bat;text/x-ms-regedit\n"
"Name=Faugus Launcher\n"
"Type=Application\n"
)
else:
f.write(
"[Desktop Entry]\n"
"Categories=Utility;\n"
"Exec=faugus-launcher --hide\n"
"Icon=faugus-launcher\n"
"MimeType=application/x-ms-dos-executable;application/x-msi;application/x-ms-shortcut;application/x-bat;text/x-ms-regedit\n"
"Name=Faugus Launcher\n"
"Type=Application\n"
)
else:
# Delete the autostart file if it exists
if os.path.exists(autostart_path):
os.remove(autostart_path)
def on_button_play_clicked(self, widget):
selected_children = self.flowbox.get_selected_children()
if not selected_children:
return
selected_child = selected_children[0]
game = selected_child.game
title = game.title
game_directory = os.path.dirname(game.path)
cwd = game_directory if game_directory and os.path.isdir(game_directory) else None
if game.runner == "Steam":
subprocess.Popen([sys.executable, faugus_run, "--game", game.gameid],cwd=cwd)
return
processos = self.load_processes_from_file()
self.button_locked[title] = True
if title in processos:
data = processos[title]
pid_main = data.get("faugus-run")
if pid_main:
try:
os.kill(pid_main, signal.SIGUSR1)
except ProcessLookupError:
pass
return
if game:
# Save the game title to the latest_games.txt file
self.update_latest_games_file(title)
if self.close_on_launch:
if IS_FLATPAK:
subprocess.Popen([sys.executable, faugus_run, "--game", game.gameid], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, cwd=cwd)
sys.exit()
else:
self.processo = subprocess.Popen([sys.executable, faugus_run, "--game", game.gameid], cwd=cwd)
self.menu_item_play.set_sensitive(False)
self.button_play.set_sensitive(False)
self.button_play.set_image(Gtk.Image.new_from_icon_name("faugus-stop-symbolic", Gtk.IconSize.BUTTON))
def check_pid_timeout():
if self.find_pid(game):
sys.exit()
return True
GLib.timeout_add(1000, check_pid_timeout)
else:
self.processo = subprocess.Popen([sys.executable, faugus_run, "--game", game.gameid], cwd=cwd)
self.menu_item_play.set_sensitive(False)
self.button_play.set_sensitive(False)
self.button_play.set_image(Gtk.Image.new_from_icon_name("faugus-stop-symbolic", Gtk.IconSize.BUTTON))
GLib.child_watch_add(self.processo.pid, self.on_process_exit)
if IS_FLATPAK:
self.processos[title] = self.processo
else:
def check_pid_periodically():
if self.find_pid(game):
return False
return True
GLib.timeout_add(1000, check_pid_periodically)
def on_process_exit(self, pid, status):
if self.processo and self.processo.pid == pid:
self.processo = None
self.menu_item_play.set_sensitive(True)
self.button_play.set_sensitive(True)
self.button_play.set_image(Gtk.Image.new_from_icon_name("faugus-play-symbolic", Gtk.IconSize.BUTTON))
def find_pid(self, game):
try:
parent = psutil.Process(self.processo.pid)
except psutil.NoSuchProcess:
return False
self.save_process_to_file(
game.title,
main_pid=self.processo.pid
)
self.menu_item_play.set_sensitive(True)
self.button_play.set_sensitive(True)
self.button_play.set_image(Gtk.Image.new_from_icon_name("faugus-stop-symbolic", Gtk.IconSize.BUTTON))
if game.title in self.button_locked:
del self.button_locked[game.title]
return True
def save_process_to_file(self, title, main_pid):
os.makedirs(os.path.dirname(running_games), exist_ok=True)
try:
with open(running_games, "r") as f:
processos = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
processos = {}
processos[title] = {
"faugus-run": main_pid
}
with open(running_games, "w") as f:
json.dump(processos, f, indent=2)
def update_latest_games_file(self, title):
# Read the existing games from the file, if it exists
try:
with open(latest_games, 'r') as f:
games = f.read().splitlines()
except FileNotFoundError:
games = []
# Remove the game if it already exists in the list and add it to the top
if title in games:
games.remove(title)
games.insert(0, title)
# Keep only the 5 most recent games
games = games[:5]
# Write the updated list back to the file
with open(latest_games, 'w') as f:
f.write('\n'.join(games))
self.load_tray_icon()
def on_button_kill_clicked(self, widget):
# Handle kill button click event
subprocess.run(r"""
for pid in $(ls -l /proc/*/exe 2>/dev/null | grep -E 'wine(64)?-preloader|wineserver|winedevice.exe' | awk -F'/' '{print $3}'); do
kill -9 "$pid"
done
""", shell=True)
self.game_running = False
self.button_locked.clear()
def on_button_add_clicked(self, widget):
file_path = ""
# Handle add button click event
add_game_dialog = AddGame(self, self.game_running, file_path, self.interface_mode)
add_game_dialog.connect("response", self.on_dialog_response, add_game_dialog)
add_game_dialog.show()
def on_button_edit_clicked(self, widget):
file_path = ""
selected_children = self.flowbox.get_selected_children()
selected_child = selected_children[0]
game = selected_child.game
title = game.title
if game:
processos = self.load_processes_from_file()
if game.title in processos:
self.game_running = True
else:
self.game_running = False
edit_game_dialog = AddGame(self, self.game_running, file_path, self.interface_mode)
edit_game_dialog.connect("response", self.on_edit_dialog_response, edit_game_dialog, game)
model_steam_title = edit_game_dialog.combobox_steam_title.get_model()
for i, row in enumerate(model_steam_title):
if row[0] == title:
edit_game_dialog.combobox_steam_title.set_active(i)
break
model_runner = edit_game_dialog.combobox_runner.get_model()
index_runner = 0
game_runner = game.runner
game_runner = convert_runner(game.runner)
if game_runner == "Linux-Native":
edit_game_dialog.combobox_launcher.set_active(1)
if game_runner == "Steam":
edit_game_dialog.combobox_launcher.set_active(2)
for i, row in enumerate(model_runner):
if row[0] == game_runner:
index_runner = i
break
if not game_runner:
index_runner = 1
edit_game_dialog.combobox_runner.set_active(index_runner)
edit_game_dialog.entry_title.set_text(game.title)
edit_game_dialog.entry_path.set_text(game.path)
edit_game_dialog.entry_prefix.set_text(game.prefix)
edit_game_dialog.entry_launch_arguments.set_text(game.launch_arguments)
edit_game_dialog.entry_game_arguments.set_text(game.game_arguments)
edit_game_dialog.set_title(_("Edit %s") % game.title)
edit_game_dialog.entry_protonfix.set_text(game.protonfix)
edit_game_dialog.entry_addapp.set_text(game.addapp)
edit_game_dialog.grid_launcher.set_visible(False)
edit_game_dialog.lossless_enabled = game.lossless_enabled
edit_game_dialog.lossless_multiplier = game.lossless_multiplier
edit_game_dialog.lossless_flow = game.lossless_flow
edit_game_dialog.lossless_performance = game.lossless_performance
edit_game_dialog.lossless_hdr = game.lossless_hdr
edit_game_dialog.lossless_present = game.lossless_present
if not os.path.isfile(game.banner):
game.banner = faugus_banner
shutil.copyfile(game.banner, edit_game_dialog.banner_path_temp)
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(game.banner, 260, 390, True)
edit_game_dialog.image_banner.set_from_pixbuf(pixbuf)
edit_game_dialog.image_banner2.set_from_pixbuf(pixbuf)
mangohud_enabled = os.path.exists(mangohud_dir)
if mangohud_enabled:
if game.mangohud == True:
edit_game_dialog.checkbox_mangohud.set_active(True)
else:
edit_game_dialog.checkbox_mangohud.set_active(False)
gamemode_enabled = os.path.exists(gamemoderun) or os.path.exists("/usr/games/gamemoderun")
if gamemode_enabled:
if game.gamemode == True:
edit_game_dialog.checkbox_gamemode.set_active(True)
else:
edit_game_dialog.checkbox_gamemode.set_active(False)
if game.disable_hidraw == True:
edit_game_dialog.checkbox_disable_hidraw.set_active(True)
else:
edit_game_dialog.checkbox_disable_hidraw.set_active(False)
if game.prevent_sleep == True:
edit_game_dialog.checkbox_prevent_sleep.set_active(True)
else:
edit_game_dialog.checkbox_prevent_sleep.set_active(False)
if game.addapp_checkbox == "addapp_enabled":
edit_game_dialog.checkbox_addapp.set_active(True)
else:
edit_game_dialog.checkbox_addapp.set_active(False)
self.updated_steam_id = detect_steam_id()
if self.updated_steam_id is not None:
if self.check_steam_shortcut(title):
edit_game_dialog.checkbox_shortcut_steam.set_active(True)
else:
edit_game_dialog.checkbox_shortcut_steam.set_active(False)
else:
edit_game_dialog.checkbox_shortcut_steam.set_active(False)
edit_game_dialog.checkbox_shortcut_steam.set_sensitive(False)
edit_game_dialog.checkbox_shortcut_steam.set_tooltip_text(
_("Add or remove a shortcut from Steam. Steam needs to be restarted. NO STEAM USERS FOUND."))
edit_game_dialog.check_existing_shortcut()
image = self.set_image_shortcut_icon(game.title, edit_game_dialog.icons_path, edit_game_dialog.icon_temp)
edit_game_dialog.button_shortcut_icon.set_image(image)
edit_game_dialog.entry_title.set_sensitive(False)
edit_game_dialog.combobox_steam_title.set_sensitive(False)
if self.game_running:
edit_game_dialog.button_winecfg.set_sensitive(False)
edit_game_dialog.button_winecfg.set_tooltip_text(_("%s is running. Please close it first.") % game.title)
edit_game_dialog.button_winetricks.set_sensitive(False)
edit_game_dialog.button_winetricks.set_tooltip_text(_("%s is running. Please close it first.") % game.title)
edit_game_dialog.button_run.set_sensitive(False)
edit_game_dialog.button_run.set_tooltip_text(_("%s is running. Please close it first.") % game.title)
edit_game_dialog.show()
def check_steam_shortcut(self, title):
if os.path.exists(steam_shortcuts_path):
try:
with open(steam_shortcuts_path, 'rb') as f:
shortcuts = vdf.binary_load(f)
for game in shortcuts["shortcuts"].values():
if isinstance(game, dict) and "AppName" in game and game["AppName"] == title:
return True
return False
except SyntaxError:
return False
return False
def set_image_shortcut_icon(self, title, icons_path, icon_temp):
title_formatted = format_title(title)
icon_path = get_shortcut_icon_path(title_formatted, icons_path, faugus_png)
if icon_path != faugus_png:
shutil.copyfile(icon_path, icon_temp)
else:
icon_temp = faugus_png
pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_temp)
scaled_pixbuf = pixbuf.scale_simple(50, 50, GdkPixbuf.InterpType.BILINEAR)
image = Gtk.Image.new_from_file(icon_temp)
image.set_from_pixbuf(scaled_pixbuf)
return image
def on_button_delete_clicked(self, widget):
self.reload_playtimes()
selected_children = self.flowbox.get_selected_children()
selected_child = selected_children[0]
game = selected_child.game
title = game.title
if game:
# Display confirmation dialog
confirmation_dialog = ConfirmationDialog(self, title, game.prefix, game.runner)
response = confirmation_dialog.run()
if response == Gtk.ResponseType.YES:
processos = self.load_processes_from_file()
if title in processos:
data = processos[title]
pid = data.get("main")
if pid:
parent = psutil.Process(pid)
children = parent.children(recursive=True)
for child in children:
child.terminate()
parent.terminate()
# Remove game and associated files if required
if confirmation_dialog.get_remove_prefix_state():
prefix_path = os.path.expanduser(game.prefix)
try:
shutil.rmtree(prefix_path)
except PermissionError as e:
try:
os.system(f'chmod -R u+rwX "{prefix_path}"')
shutil.rmtree(prefix_path)
except Exception as e2:
self.show_warning_dialog(
self,
_("Failed to remove prefix."),
str(e2)
)
except FileNotFoundError:
pass
# Remove the shortcut
self.remove_shortcut(game, "both")
self.remove_steam_shortcut(title)
self.remove_banner_icon(game)
self._deleted_gameid = game.gameid
self.save_games()
self.update_list()
# Remove the game from the latest-games file if it exists
self.remove_game_from_latest_games(title)
self.button_play.set_sensitive(False)
self.select_first_child()
confirmation_dialog.destroy()
def reload_playtimes(self):
import json
try:
with open("games.json", "r", encoding="utf-8") as f:
games_data = json.load(f)
except FileNotFoundError:
return
playtime_map = {g["gameid"]: g.get("playtime", 0) for g in games_data}
for game in self.games:
if game.gameid in playtime_map:
game.playtime = playtime_map[game.gameid]
def remove_steam_shortcut(self, title):
if os.path.exists(steam_shortcuts_path):
try:
with open(steam_shortcuts_path, 'rb') as f:
shortcuts = vdf.binary_load(f)
to_remove = [app_id for app_id, game in shortcuts["shortcuts"].items() if
isinstance(game, dict) and "AppName" in game and game["AppName"] == title]
for app_id in to_remove:
del shortcuts["shortcuts"][app_id]
with open(steam_shortcuts_path, 'wb') as f:
vdf.binary_dump(shortcuts, f)
except SyntaxError:
pass
def remove_game_from_latest_games(self, title):
try:
# Read the current list of recent games
with open(latest_games, 'r') as f:
recent_games = f.read().splitlines()
# Remove the game title if it exists in the list
if title in recent_games:
recent_games.remove(title)
# Write the updated list back, maintaining max 5 entries
with open(latest_games, 'w') as f:
f.write("\n".join(recent_games[:5]))
self.load_tray_icon()
except FileNotFoundError:
pass # Ignore if the file doesn't exist yet
def show_warning_dialog(self, parent, text1, text2):
dialog = Gtk.Dialog(title="Faugus Launcher", transient_for=parent, modal=True)
dialog.set_resizable(False)
dialog.set_icon_from_file(faugus_png)
subprocess.Popen(["canberra-gtk-play", "-f", faugus_notification])
label1 = Gtk.Label()
label1.set_label(text1)
label1.set_halign(Gtk.Align.CENTER)
label2 = Gtk.Label()
label2.set_label(text2)
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.response(Gtk.ResponseType.YES))
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(label1, True, True, 0)
if text2:
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()
def on_dialog_response(self, dialog, response_id, add_game_dialog):
# Handle dialog response
if response_id == Gtk.ResponseType.OK:
if not add_game_dialog.validate_fields(entry="path+prefix"):
# If fields are not validated, return and keep the dialog open
return True
# Proceed with adding the game
# Get game information from dialog fields
prefix = os.path.normpath(add_game_dialog.entry_prefix.get_text())
if add_game_dialog.combobox_launcher.get_active() == 0 or add_game_dialog.combobox_launcher.get_active() == 1 or add_game_dialog.combobox_launcher.get_active() == 2:
title = add_game_dialog.entry_title.get_text()
else:
title = add_game_dialog.combobox_launcher.get_active_text()
if os.path.exists(games_json):
with open(games_json, encoding="utf-8") as f:
games = json.load(f)
if any(game.get("title") == title for game in games):
self.show_warning_dialog(
add_game_dialog,
_("%s already exists.") % title,
""
)
return True
path = add_game_dialog.entry_path.get_text()
launch_arguments = add_game_dialog.entry_launch_arguments.get_text()
game_arguments = add_game_dialog.entry_game_arguments.get_text()
protonfix = add_game_dialog.entry_protonfix.get_text()
runner = add_game_dialog.combobox_runner.get_active_text()
addapp = add_game_dialog.entry_addapp.get_text()
lossless_enabled = add_game_dialog.lossless_enabled
lossless_multiplier = add_game_dialog.lossless_multiplier
lossless_flow = add_game_dialog.lossless_flow
lossless_performance = add_game_dialog.lossless_performance
lossless_hdr = add_game_dialog.lossless_hdr
lossless_present = add_game_dialog.lossless_present
playtime = 0
hidden = False
favorite = False
startup_wm_class = ""
if add_game_dialog.combobox_launcher.get_active() == 3:
path = f"{prefix}/drive_c/Program Files (x86)/Battle.net/Battle.net.exe"
startup_wm_class = "battle.net.exe"
if add_game_dialog.combobox_launcher.get_active() == 4:
path = f"{prefix}/drive_c/Program Files/Electronic Arts/EA Desktop/EA Desktop/EALauncher.exe"
if add_game_dialog.combobox_launcher.get_active() == 5:
path = f"{prefix}/drive_c/Program Files/Epic Games/Launcher/Portal/Binaries/Win64/EpicGamesLauncher.exe"
if add_game_dialog.combobox_launcher.get_active() == 6:
path = f"{prefix}/drive_c/Program Files (x86)/Ubisoft/Ubisoft Game Launcher/UbisoftConnect.exe"
if add_game_dialog.combobox_launcher.get_active() == 7:
path = f"{prefix}/drive_c/Program Files/Rockstar Games/Launcher/Launcher.exe"
title_formatted = format_title(title)
addapp_bat = f"{os.path.dirname(path)}/faugus-{title_formatted}.bat"
if self.interface_mode == "Banners":
banner = os.path.join(banners_dir, f"{title_formatted}.png")
temp_banner_path = add_game_dialog.banner_path_temp
try:
# Use `magick` to resize the image
command_magick = shutil.which("magick") or shutil.which("convert")
subprocess.run([command_magick, temp_banner_path, "-resize", "230x345!", banner], check=True)
except subprocess.CalledProcessError as e:
print(f"Error resizing banner: {e}")
else:
banner = ""
runner = convert_runner(runner)
if add_game_dialog.combobox_launcher.get_active() == 1:
runner = "Linux-Native"
if add_game_dialog.combobox_launcher.get_active() == 2:
runner = "Steam"
# Determine mangohud and gamemode status
mangohud = True if add_game_dialog.checkbox_mangohud.get_active() else ""
gamemode = True if add_game_dialog.checkbox_gamemode.get_active() else ""
disable_hidraw = True if add_game_dialog.checkbox_disable_hidraw.get_active() else ""
addapp_checkbox = "addapp_enabled" if add_game_dialog.checkbox_addapp.get_active() else ""
prevent_sleep = True if add_game_dialog.checkbox_prevent_sleep.get_active() else ""
# Create Game object and update UI
game = Game(
title_formatted,
title,
path,
prefix,
launch_arguments,
game_arguments,
mangohud,
gamemode,
disable_hidraw,
protonfix,
runner,
addapp_checkbox,
addapp,
addapp_bat,
banner,
lossless_enabled,
lossless_multiplier,
lossless_flow,
lossless_performance,
lossless_hdr,
lossless_present,
playtime,
hidden,
prevent_sleep,
favorite,
startup_wm_class,
)
# Determine the state of the shortcut checkbox
desktop_shortcut_state = add_game_dialog.checkbox_shortcut_desktop.get_active()
appmenu_shortcut_state = add_game_dialog.checkbox_shortcut_appmenu.get_active()
steam_shortcut_state = add_game_dialog.checkbox_shortcut_steam.get_active()
icon_temp = os.path.expanduser(add_game_dialog.icon_temp)
icon_final = f'{add_game_dialog.icons_path}/{title_formatted}.png'
def check_internet_connection():
try:
socket.gethostbyname("github.com")
return True
except socket.gaierror:
return False
if add_game_dialog.combobox_launcher.get_active() != 0 and add_game_dialog.combobox_launcher.get_active() != 1:
if not check_internet_connection():
self.show_warning_dialog(add_game_dialog, _("No internet connection."), "")
return True
else:
if add_game_dialog.combobox_launcher.get_active() == 3:
add_game_dialog.destroy()
self.launcher_screen(title, "3", title_formatted, runner, prefix, umu_run, game, desktop_shortcut_state, appmenu_shortcut_state, steam_shortcut_state, icon_temp, icon_final)
if add_game_dialog.combobox_launcher.get_active() == 4:
add_game_dialog.destroy()
self.launcher_screen(title, "4", title_formatted, runner, prefix, umu_run, game, desktop_shortcut_state, appmenu_shortcut_state, steam_shortcut_state, icon_temp, icon_final)
if add_game_dialog.combobox_launcher.get_active() == 5:
add_game_dialog.destroy()
self.launcher_screen(title, "5", title_formatted, runner, prefix, umu_run, game, desktop_shortcut_state, appmenu_shortcut_state, steam_shortcut_state, icon_temp, icon_final)
if add_game_dialog.combobox_launcher.get_active() == 6:
add_game_dialog.destroy()
self.launcher_screen(title, "6", title_formatted, runner, prefix, umu_run, game, desktop_shortcut_state, appmenu_shortcut_state, steam_shortcut_state, icon_temp, icon_final)
if add_game_dialog.combobox_launcher.get_active() == 7:
add_game_dialog.destroy()
self.launcher_screen(title, "7", title_formatted, runner, prefix, umu_run, game, desktop_shortcut_state, appmenu_shortcut_state, steam_shortcut_state, icon_temp, icon_final)
game_info = {
"gameid": title_formatted,
"title": title,
"path": path,
"prefix": prefix,
"launch_arguments": launch_arguments,
"game_arguments": game_arguments,
"mangohud": mangohud,
"gamemode": gamemode,
"disable_hidraw": disable_hidraw,
"protonfix": protonfix,
"runner": runner,
"addapp_checkbox": addapp_checkbox,
"addapp": addapp,
"addapp_bat": addapp_bat,
"banner": banner,
"lossless_enabled": lossless_enabled,
"lossless_multiplier": lossless_multiplier,
"lossless_flow": lossless_flow,
"lossless_performance": lossless_performance,
"lossless_hdr": lossless_hdr,
"lossless_present": lossless_present,
"playtime": playtime,
"hidden": hidden,
"prevent_sleep": prevent_sleep,
"favorite": favorite,
"startup_wm_class": startup_wm_class,
}
games = []
if os.path.exists("games.json"):
try:
with open("games.json", "r", encoding="utf-8") as file:
games = json.load(file)
except json.JSONDecodeError as e:
print(f"Error reading the JSON file: {e}")
games.append(game_info)
self.backup_games()
with open("games.json", "w", encoding="utf-8") as file:
json.dump(games, file, ensure_ascii=False, indent=4)
self.games.append(game)
if add_game_dialog.combobox_launcher.get_active() == 0 or add_game_dialog.combobox_launcher.get_active() == 1 or add_game_dialog.combobox_launcher.get_active() == 2:
# Call add_remove_shortcut method
self.add_shortcut(game, desktop_shortcut_state, "desktop", icon_temp, icon_final)
self.add_shortcut(game, appmenu_shortcut_state, "appmenu", icon_temp, icon_final)
self.add_steam_shortcut(game, steam_shortcut_state, icon_temp, icon_final)
if addapp_checkbox == "addapp_enabled":
with open(addapp_bat, "w") as bat_file:
bat_file.write(f'start "" "z:{addapp}"\n')
if game_arguments:
bat_file.write(f'start "" "z:{path}" {game_arguments}\n')
else:
bat_file.write(f'start "" "z:{path}"\n')
self.add_item_list(game)
self.update_list()
# Select the added game
self.select_game_by_title(title)
else:
if os.path.isfile(add_game_dialog.icon_temp):
os.remove(add_game_dialog.icon_temp)
if os.path.isdir(add_game_dialog.icon_directory):
shutil.rmtree(add_game_dialog.icon_directory)
add_game_dialog.destroy()
if os.path.isfile(add_game_dialog.banner_path_temp):
os.remove(add_game_dialog.banner_path_temp)
# Ensure the dialog is destroyed when canceled
add_game_dialog.destroy()
def launcher_screen(self, title, launcher, title_formatted, runner, prefix, umu_run, game, desktop_shortcut_state, appmenu_shortcut_state, steam_shortcut_state, icon_temp, icon_final):
self.box_launcher = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.box_launcher.set_hexpand(True)
self.box_launcher.set_vexpand(True)
self.bar_download = Gtk.ProgressBar()
self.bar_download.set_margin_start(20)
self.bar_download.set_margin_end(20)
self.bar_download.set_margin_bottom(40)
grid_launcher = Gtk.Grid()
grid_launcher.set_halign(Gtk.Align.CENTER)
grid_launcher.set_valign(Gtk.Align.CENTER)
grid_labels = Gtk.Grid()
grid_labels.set_size_request(-1, 128)
self.box_launcher.pack_start(grid_launcher, True, True, 0)
self.label_download = Gtk.Label()
self.label_download.set_margin_start(20)
self.label_download.set_margin_end(20)
self.label_download.set_margin_bottom(20)
self.label_download.set_text(_("Installing %s...") % title)
self.label_download.set_size_request(256, -1)
self.label_download2 = Gtk.Label()
self.label_download2.set_margin_start(20)
self.label_download2.set_margin_end(20)
self.label_download2.set_margin_bottom(20)
self.label_download2.set_text("")
self.label_download2.set_visible(False)
self.label_download2.set_size_request(256, -1)
self.button_finish_install = Gtk.Button(label=_("Finish installation"))
self.button_finish_install.connect("clicked", self.on_button_finish_install_clicked)
self.button_finish_install.set_size_request(150, -1)
self.button_finish_install.set_halign(Gtk.Align.CENTER)
if launcher == "3":
image_path = battle_icon
self.label_download.set_text(_("Downloading Battle.net..."))
self.download_launcher("battle", title, title_formatted, runner, prefix, umu_run, game, desktop_shortcut_state, appmenu_shortcut_state, steam_shortcut_state, icon_temp, icon_final)
elif launcher == "4":
image_path = ea_icon
self.label_download.set_text(_("Downloading EA App..."))
self.download_launcher("ea", title, title_formatted, runner, prefix, umu_run, game, desktop_shortcut_state, appmenu_shortcut_state, steam_shortcut_state, icon_temp, icon_final)
elif launcher == "5":
image_path = epic_icon
self.label_download.set_text(_("Downloading Epic Games..."))
self.download_launcher("epic", title, title_formatted, runner, prefix, umu_run, game, desktop_shortcut_state, appmenu_shortcut_state, steam_shortcut_state, icon_temp, icon_final)
elif launcher == "6":
image_path = ubisoft_icon
self.label_download.set_text(_("Downloading Ubisoft Connect..."))
self.download_launcher("ubisoft", title, title_formatted, runner, prefix, umu_run, game, desktop_shortcut_state, appmenu_shortcut_state, steam_shortcut_state, icon_temp, icon_final)
elif launcher == "7":
image_path = rockstar_icon
self.label_download.set_text(_("Downloading Rockstar Launcher..."))
self.download_launcher("rockstar", title, title_formatted, runner, prefix, umu_run, game, desktop_shortcut_state, appmenu_shortcut_state, steam_shortcut_state, icon_temp, icon_final)
else:
image_path = faugus_png
pixbuf = GdkPixbuf.Pixbuf.new_from_file(image_path)
pixbuf = pixbuf.scale_simple(128, 128, 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_launcher.attach(image, 0, 0, 1, 1)
grid_launcher.attach(grid_labels, 0, 1, 1, 1)
grid_labels.attach(self.label_download, 0, 0, 1, 1)
grid_labels.attach(self.bar_download, 0, 1, 1, 1)
grid_labels.attach(self.label_download2, 0, 2, 1, 1)
#grid_labels.attach(self.button_finish_install, 0, 3, 1, 1)
self.box_main.add(self.box_launcher)
self.box_main.remove(self.box_top)
self.box_main.remove(self.box_bottom)
self.box_main.show_all()
#self.button_finish_install.set_visible(False)
def on_button_finish_install_clicked(self):
self.on_button_kill_clicked(widget)
def monitor_process(self, processo, game, desktop_shortcut_state, appmenu_shortcut_state, steam_shortcut_state, icon_temp, icon_final, title):
retcode = processo.poll()
if retcode is not None:
if os.path.exists(faugus_temp):
shutil.rmtree(faugus_temp)
self.box_main.pack_start(self.box_top, True, True, 0)
self.box_main.pack_end(self.box_bottom, False, True, 0)
self.box_main.remove(self.box_launcher)
self.box_launcher.destroy()
self.box_main.show_all()
if os.path.exists(game.path):
print(f"{title} installed.")
self.add_shortcut(game, desktop_shortcut_state, "desktop", icon_temp, icon_final)
self.add_shortcut(game, appmenu_shortcut_state, "appmenu", icon_temp, icon_final)
self.add_steam_shortcut(game, steam_shortcut_state, icon_temp, icon_final)
self.add_item_list(game)
self.update_list()
self.select_game_by_title(title)
else:
if os.path.exists(game.prefix):
shutil.rmtree(game.prefix)
self.remove_shortcut(game, "both")
self.remove_steam_shortcut(title)
self.remove_banner_icon(game)
self.games.remove(game)
self.save_games()
self.update_list()
self.remove_game_from_latest_games(title)
self.show_warning_dialog(self, _("%s was not installed!") % title, "")
if self.interface_mode != "List":
if self.fullscreen_activated:
self.fullscreen_activated = True
self.grid_corner.set_visible(True)
self.grid_left.set_margin_start(70)
else:
self.fullscreen_activated = False
self.grid_corner.set_visible(False)
self.grid_left.set_margin_start(0)
return False
return True
def download_launcher(self, launcher, title, title_formatted, runner, prefix, umu_run, game, desktop_shortcut_state, appmenu_shortcut_state, steam_shortcut_state, icon_temp, icon_final):
urls = {"ea": "https://origin-a.akamaihd.net/EA-Desktop-Client-Download/installer-releases/EAappInstaller.exe",
"epic": "https://github.com/Faugus/components/releases/download/v1.0.0/epic.msi",
"battle": "https://downloader.battle.net/download/getInstaller?os=win&installer=Battle.net-Setup.exe",
"ubisoft": "https://static3.cdn.ubi.com/orbit/launcher_installer/UbisoftConnectInstaller.exe",
"rockstar": "https://gamedownloads.rockstargames.com/public/installer/Rockstar-Games-Launcher.exe"}
file_name = {"ea": "EAappInstaller.exe", "epic": "EpicGamesLauncherInstaller.msi",
"battle": "Battle.net-Setup.exe", "ubisoft": "UbisoftConnectInstaller.exe", "rockstar": "Rockstar-Games-Launcher.exe"}
if launcher not in urls:
return None
os.makedirs(faugus_temp, exist_ok=True)
file_path = os.path.join(faugus_temp, file_name[launcher])
def report_progress(block_num, block_size, total_size):
if total_size > 0:
downloaded = block_num * block_size
percent = min(downloaded / total_size, 1.0)
GLib.idle_add(self.bar_download.set_fraction, percent)
GLib.idle_add(self.bar_download.set_text, f"{int(percent * 100)}%")
def start_download():
try:
urllib.request.urlretrieve(urls[launcher], file_path, reporthook=report_progress)
GLib.idle_add(self.bar_download.set_fraction, 1.0)
GLib.idle_add(self.bar_download.set_text, _("Download complete"))
GLib.idle_add(on_download_complete)
except Exception as e:
GLib.idle_add(self.show_warning_dialog, self, _("Error during download: %s") % e, "")
def on_download_complete():
self.label_download.set_text(_("Installing %s...") % title)
if launcher == "battle":
self.label_download2.set_text(_("Please close the login window and wait..."))
command = f"PROTON_ENABLE_WAYLAND=0 WINE_SIMULATE_WRITECOPY=1 WINEPREFIX='{prefix}' GAMEID={title_formatted} {umu_run} '{file_path}' --installpath='C:\\Program Files (x86)\\Battle.net' --lang=enUS"
elif launcher == "ea":
self.label_download2.set_text(_("Please close the login window and wait..."))
command = f"PROTON_ENABLE_WAYLAND=0 WINEPREFIX='{prefix}' GAMEID={title_formatted} {umu_run} '{file_path}' /S"
elif launcher == "epic":
self.label_download2.set_text("")
command = f"WINEPREFIX='{prefix}' GAMEID={title_formatted} {umu_run} msiexec /i '{file_path}' /passive"
elif launcher == "ubisoft":
self.label_download2.set_text("")
command = f"PROTON_ENABLE_WAYLAND=0 WINEPREFIX='{prefix}' GAMEID={title_formatted} {umu_run} '{file_path}' /S"
elif launcher == "rockstar":
self.label_download.set_text(_("Please don't change the installation path."))
self.label_download2.set_text(_("Please close the login window and wait..."))
command = f"PROTON_ENABLE_WAYLAND=0 WINEPREFIX='{prefix}' GAMEID={title_formatted} {umu_run} '{file_path}'"
if runner:
if runner == "Proton-CachyOS":
command = f"PROTONPATH='{proton_cachyos}' {command}"
else:
command = f"PROTONPATH='{runner}' {command}"
self.bar_download.set_visible(False)
self.label_download2.set_visible(True)
processo = subprocess.Popen([sys.executable, faugus_run, command])
GLib.timeout_add(100, self.monitor_process, processo, game, desktop_shortcut_state, appmenu_shortcut_state, steam_shortcut_state, icon_temp, icon_final, title)
threading.Thread(target=start_download).start()
return file_path
def select_game_by_title(self, title):
# Selects an item from the FlowBox based on the title
for child in self.flowbox.get_children():
hbox = child.get_children()[0] # The first item is the hbox containing the label
game_label = hbox.get_children()[1] # The second item is the title label
if game_label.get_text() == title:
# Selects the child in the FlowBox
self.flowbox.select_child(child)
break
# Calls the item selection method to ensure the buttons are updated
self.on_item_selected(self.flowbox, child)
def on_edit_dialog_response(self, dialog, response_id, edit_game_dialog, game):
# Handle edit dialog response
if response_id == Gtk.ResponseType.OK:
if not edit_game_dialog.validate_fields(entry="path+prefix"):
# If fields are not validated, return and keep the dialog open
return True
# Update game object with new information
game.title = edit_game_dialog.entry_title.get_text()
game.path = edit_game_dialog.entry_path.get_text()
game.prefix = os.path.normpath(edit_game_dialog.entry_prefix.get_text())
game.launch_arguments = edit_game_dialog.entry_launch_arguments.get_text()
game.game_arguments = edit_game_dialog.entry_game_arguments.get_text()
game.mangohud = edit_game_dialog.checkbox_mangohud.get_active()
game.gamemode = edit_game_dialog.checkbox_gamemode.get_active()
game.disable_hidraw = edit_game_dialog.checkbox_disable_hidraw.get_active()
game.protonfix = edit_game_dialog.entry_protonfix.get_text()
game.runner = edit_game_dialog.combobox_runner.get_active_text()
game.addapp_checkbox = edit_game_dialog.checkbox_addapp.get_active()
game.addapp = edit_game_dialog.entry_addapp.get_text()
game.lossless_enabled = edit_game_dialog.lossless_enabled
game.lossless_multiplier = edit_game_dialog.lossless_multiplier
game.lossless_flow = edit_game_dialog.lossless_flow
game.lossless_performance = edit_game_dialog.lossless_performance
game.lossless_hdr = edit_game_dialog.lossless_hdr
game.lossless_present = edit_game_dialog.lossless_present
game.prevent_sleep = edit_game_dialog.checkbox_prevent_sleep.get_active()
title_formatted = format_title(game.title)
game.gameid = title_formatted
game.addapp_bat = f"{os.path.dirname(game.path)}/faugus-{title_formatted}.bat"
if self.interface_mode == "Banners":
banner = os.path.join(banners_dir, f"{title_formatted}.png")
temp_banner_path = edit_game_dialog.banner_path_temp
try:
# Use `magick` to resize the image
command_magick = shutil.which("magick") or shutil.which("convert")
subprocess.run([command_magick, temp_banner_path, "-resize", "230x345!", banner], check=True)
game.banner = banner
except subprocess.CalledProcessError as e:
print(f"Error resizing banner: {e}")
game.runner = convert_runner(game.runner)
if edit_game_dialog.combobox_launcher.get_active() == 1:
game.runner = "Linux-Native"
if edit_game_dialog.combobox_launcher.get_active() == 2:
game.runner = "Steam"
if edit_game_dialog.combobox_launcher.get_active() == 3:
game.startup_wm_class = "battle.net.exe"
elif getattr(game, "startup_wm_class", "") == "battle.net.exe":
game.startup_wm_class = ""
icon_temp = os.path.expanduser(edit_game_dialog.icon_temp)
icon_final = f'{edit_game_dialog.icons_path}/{title_formatted}.png'
# Determine the state of the shortcut checkbox
desktop_shortcut_state = edit_game_dialog.checkbox_shortcut_desktop.get_active()
appmenu_shortcut_state = edit_game_dialog.checkbox_shortcut_appmenu.get_active()
steam_shortcut_state = edit_game_dialog.checkbox_shortcut_steam.get_active()
# Call add_remove_shortcut method
self.add_shortcut(game, desktop_shortcut_state, "desktop", icon_temp, icon_final)
self.add_shortcut(game, appmenu_shortcut_state, "appmenu", icon_temp, icon_final)
self.add_steam_shortcut(game, steam_shortcut_state, icon_temp, icon_final)
if game.addapp_checkbox == True:
with open(game.addapp_bat, "w") as bat_file:
bat_file.write(f'start "" "z:{game.addapp}"\n')
if game.game_arguments:
bat_file.write(f'start "" "z:{game.path}" {game.game_arguments}\n')
else:
bat_file.write(f'start "" "z:{game.path}"\n')
# Save changes and update UI
self.save_games()
self.update_list()
# Select the game that was edited
self.select_game_by_title(game.title)
else:
if os.path.isfile(edit_game_dialog.icon_temp):
os.remove(edit_game_dialog.icon_temp)
if os.path.isdir(edit_game_dialog.icon_directory):
shutil.rmtree(edit_game_dialog.icon_directory)
os.remove(edit_game_dialog.banner_path_temp)
edit_game_dialog.destroy()
def add_shortcut(self, game, shortcut_state, shortcut, icon_temp, icon_final):
applications_shortcut_path = f"{app_dir}/{game.gameid}.desktop"
desktop_shortcut_path = f"{desktop_dir}/{game.gameid}.desktop"
# Check if the shortcut checkbox is checked
if shortcut == "desktop" and not shortcut_state:
# Remove existing shortcut if it exists
self.remove_shortcut(game, shortcut)
if os.path.isfile(os.path.expanduser(icon_temp)):
os.rename(os.path.expanduser(icon_temp), icon_final)
return
if shortcut == "appmenu" and not shortcut_state:
# Remove existing shortcut if it exists
self.remove_shortcut(game, shortcut)
if os.path.isfile(os.path.expanduser(icon_temp)):
os.rename(os.path.expanduser(icon_temp), icon_final)
return
if os.path.isfile(os.path.expanduser(icon_temp)):
os.rename(os.path.expanduser(icon_temp), icon_final)
new_icon_path = get_shortcut_icon_path(game.gameid, icons_dir, faugus_png)
# Get the directory containing the executable
game_directory = os.path.dirname(game.path)
# Create a .desktop file
if IS_FLATPAK:
desktop_file_content = build_desktop_entry(
game.title,
f"flatpak run --command={faugus_run} io.github.Faugus.faugus-launcher --game {game.gameid}",
new_icon_path,
game_directory,
getattr(game, "startup_wm_class", ""),
)
else:
desktop_file_content = build_desktop_entry(
game.title,
f"{faugus_run} --game {game.gameid}",
new_icon_path,
game_directory,
getattr(game, "startup_wm_class", ""),
)
# Check if the destination directory exists and create if it doesn't
if not os.path.exists(app_dir):
os.makedirs(app_dir)
if not os.path.exists(desktop_dir):
os.makedirs(desktop_dir)
if shortcut == "appmenu":
with open(applications_shortcut_path, 'w') as appmenu_file:
appmenu_file.write(desktop_file_content)
os.chmod(applications_shortcut_path, 0o755)
if shortcut == "desktop":
with open(desktop_shortcut_path, 'w') as desktop_file:
desktop_file.write(desktop_file_content)
os.chmod(desktop_shortcut_path, 0o755)
def add_steam_shortcut(self, game, steam_shortcut_state, icon_temp, icon_final):
def add_game_to_steam(title, game_directory, icon):
# Load existing shortcuts
shortcuts = load_shortcuts(title)
# Check if the game already exists
existing_app_id = None
for app_id, game_info in shortcuts["shortcuts"].items():
if isinstance(game_info, dict) and "AppName" in game_info and game_info["AppName"] == title:
existing_app_id = app_id
break
if existing_app_id:
# Update only the necessary fields without replacing the entire entry
game_info = shortcuts["shortcuts"][existing_app_id]
if IS_FLATPAK:
if IS_STEAM_FLATPAK:
game_info["Exe"] = f'"flatpak-spawn"'
game_info["LaunchOptions"] = f'--host flatpak run --command=/app/bin/faugus-run io.github.Faugus.faugus-launcher --game {game.gameid}'
else:
game_info["Exe"] = f'"flatpak"'
game_info["LaunchOptions"] = f'run --command=/app/bin/faugus-run io.github.Faugus.faugus-launcher --game {game.gameid}'
else:
game_info["Exe"] = f'"{faugus_run}"'
game_info["LaunchOptions"] = f'--game {game.gameid}'
game_info["StartDir"] = game_directory
game_info["icon"] = icon
else:
# Generate a new ID for the game
new_app_id = max([int(k) for k in shortcuts["shortcuts"].keys() if k.isdigit()] or [0]) + 1
# Add the new game
if IS_FLATPAK:
if IS_STEAM_FLATPAK:
shortcuts["shortcuts"][str(new_app_id)] = {
"appid": new_app_id,
"AppName": title,
"Exe": f'"flatpak-spawn"',
"StartDir": game_directory,
"icon": icon,
"ShortcutPath": "",
"LaunchOptions": f'--host flatpak run --command=/app/bin/faugus-run io.github.Faugus.faugus-launcher --game {game.gameid}',
"IsHidden": 0,
"AllowDesktopConfig": 1,
"AllowOverlay": 1,
"OpenVR": 0,
"Devkit": 0,
"DevkitGameID": "",
"LastPlayTime": 0,
"FlatpakAppID": "",
}
else:
shortcuts["shortcuts"][str(new_app_id)] = {
"appid": new_app_id,
"AppName": title,
"Exe": f'"flatpak"',
"StartDir": game_directory,
"icon": icon,
"ShortcutPath": "",
"LaunchOptions": f'run --command=/app/bin/faugus-run io.github.Faugus.faugus-launcher --game {game.gameid}',
"IsHidden": 0,
"AllowDesktopConfig": 1,
"AllowOverlay": 1,
"OpenVR": 0,
"Devkit": 0,
"DevkitGameID": "",
"LastPlayTime": 0,
"FlatpakAppID": "",
}
else:
shortcuts["shortcuts"][str(new_app_id)] = {
"appid": new_app_id,
"AppName": title,
"Exe": f'"{faugus_run}"',
"StartDir": game_directory,
"icon": icon,
"ShortcutPath": "",
"LaunchOptions": f'--game {game.gameid}',
"IsHidden": 0,
"AllowDesktopConfig": 1,
"AllowOverlay": 1,
"OpenVR": 0,
"Devkit": 0,
"DevkitGameID": "",
"LastPlayTime": 0,
"FlatpakAppID": "",
}
# Save shortcuts back to the file
save_shortcuts(shortcuts)
def remove_shortcuts(shortcuts, title):
# Find and remove existing shortcuts with the same title
if os.path.exists(steam_shortcuts_path):
to_remove = [app_id for app_id, game in shortcuts["shortcuts"].items() if
isinstance(game, dict) and "AppName" in game and game["AppName"] == title]
for app_id in to_remove:
del shortcuts["shortcuts"][app_id]
save_shortcuts(shortcuts)
def load_shortcuts(title):
# Check if the file exists
if os.path.exists(steam_shortcuts_path):
try:
# Attempt to load existing shortcuts
with open(steam_shortcuts_path, 'rb') as f:
return vdf.binary_load(f)
except SyntaxError:
# If the file is corrupted, create a new one
return {"shortcuts": {}}
else:
# If the file does not exist, create a new one
return {"shortcuts": {}}
def save_shortcuts(shortcuts):
if not os.path.exists(steam_shortcuts_path):
open(steam_shortcuts_path, 'wb').close()
with open(steam_shortcuts_path, 'wb') as f:
vdf.binary_dump(shortcuts, f)
# Check if the shortcut checkbox is checked
if not steam_shortcut_state:
# Remove existing shortcut if it exists
shortcuts = load_shortcuts(game.title)
remove_shortcuts(shortcuts, game.title)
if os.path.isfile(os.path.expanduser(icon_temp)):
os.rename(os.path.expanduser(icon_temp), icon_final)
return
if os.path.isfile(os.path.expanduser(icon_temp)):
os.rename(os.path.expanduser(icon_temp), icon_final)
new_icon_path = get_shortcut_icon_path(game.gameid, icons_dir, faugus_png)
# Get the directory containing the executable
game_directory = os.path.dirname(game.path)
add_game_to_steam(game.title, game_directory, new_icon_path)
def update_preview(self, dialog):
if file_path := dialog.get_preview_filename():
try:
# Create an image widget for the thumbnail
pixbuf = GdkPixbuf.Pixbuf.new_from_file(file_path)
# Resize the thumbnail if it's too large, maintaining the aspect ratio
max_width = 400
max_height = 400
width = pixbuf.get_width()
height = pixbuf.get_height()
if width > max_width or height > max_height:
# Calculate the new width and height while maintaining the aspect ratio
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 remove_banner_icon(self, game):
banner_file_path = f"{banners_dir}/{game.gameid}.png"
icon_file_path_png = f"{icons_dir}/{game.gameid}.png"
icon_file_path_ico = f"{icons_dir}/{game.gameid}.ico"
if os.path.exists(banner_file_path):
os.remove(banner_file_path)
if os.path.exists(icon_file_path_png):
os.remove(icon_file_path_png)
if os.path.exists(icon_file_path_ico):
os.remove(icon_file_path_ico)
def remove_shortcut(self, game, shortcut):
applications_shortcut_path = f"{app_dir}/{game.gameid}.desktop"
desktop_shortcut_path = f"{desktop_dir}/{game.gameid}.desktop"
if os.path.exists(game.addapp_bat):
os.remove(game.addapp_bat)
if shortcut == "appmenu":
if os.path.exists(applications_shortcut_path):
os.remove(applications_shortcut_path)
if shortcut == "desktop":
if os.path.exists(desktop_shortcut_path):
os.remove(desktop_shortcut_path)
if shortcut == "both":
if os.path.exists(applications_shortcut_path):
os.remove(applications_shortcut_path)
if os.path.exists(desktop_shortcut_path):
os.remove(desktop_shortcut_path)
def update_list(self):
for child in self.flowbox.get_children():
self.flowbox.remove(child)
self.games.clear()
self.load_games()
self.entry_search.set_text("")
self.show_all()
if self.interface_mode != "List":
if self.fullscreen_activated:
self.fullscreen_activated = True
self.grid_corner.set_visible(True)
self.grid_left.set_margin_start(70)
else:
self.fullscreen_activated = False
self.grid_corner.set_visible(False)
self.grid_left.set_margin_start(0)
def save_games(self):
try:
with open("games.json", "r", encoding="utf-8") as file:
all_games_data = json.load(file)
except (FileNotFoundError, json.JSONDecodeError):
all_games_data = []
visible_games_map = {game.gameid: game for game in self.games}
deleted_id = getattr(self, "_deleted_gameid", None)
new_games_data = []
for game_data in all_games_data:
gameid = game_data.get("gameid")
hidden = game_data.get("hidden", False)
if deleted_id and gameid == deleted_id:
continue
if not hidden and gameid not in visible_games_map:
continue
if gameid in visible_games_map:
game = visible_games_map.pop(gameid)
game_data = {
"gameid": game.gameid,
"title": game.title,
"path": game.path,
"prefix": game.prefix,
"launch_arguments": game.launch_arguments,
"game_arguments": game.game_arguments,
"mangohud": True if game.mangohud else "",
"gamemode": True if game.gamemode else "",
"disable_hidraw": True if game.disable_hidraw else "",
"protonfix": game.protonfix,
"runner": game.runner,
"addapp_checkbox": "addapp_enabled" if game.addapp_checkbox else "",
"addapp": game.addapp,
"addapp_bat": game.addapp_bat,
"banner": game.banner,
"lossless_enabled": game.lossless_enabled,
"lossless_multiplier": game.lossless_multiplier,
"lossless_flow": game.lossless_flow,
"lossless_performance": game.lossless_performance,
"lossless_hdr": game.lossless_hdr,
"lossless_present": game.lossless_present,
"playtime": game.playtime,
"hidden": hidden,
"prevent_sleep": game.prevent_sleep,
"favorite": game.favorite,
"startup_wm_class": game.startup_wm_class,
}
new_games_data.append(game_data)
if hasattr(self, "_deleted_gameid"):
del self._deleted_gameid
self.backup_games()
with open("games.json", "w", encoding="utf-8") as file:
json.dump(new_games_data, file, ensure_ascii=False, indent=4)
def backup_games(self):
if os.path.isfile(games_json):
os.makedirs(backup_dir, exist_ok=True)
now = GLib.DateTime.new_now_local()
timestamp = now.format("%Y-%m-%d_%H-%M-%S")
backup_file = os.path.join(
backup_dir,
f"games-data-{timestamp}.json"
)
shutil.copy2(games_json, backup_file)
backups = sorted(
f for f in os.listdir(backup_dir)
if f.startswith("games-data-") and f.endswith(".json")
)
while len(backups) > 20:
oldest = backups.pop(0)
os.remove(os.path.join(backup_dir, oldest))
class Settings(Gtk.Dialog):
def __init__(self, parent):
# Initialize the Settings dialog
super().__init__(title=_("Settings"), transient_for=parent, modal=True)
self.set_resizable(False)
self.set_icon_from_file(faugus_png)
self.parent = parent
self.logging_warning = False
css_provider = Gtk.CssProvider()
css = """
.entry {
border-color: Red;
}
.paypal {
color: white;
background: #001C64;
}
.kofi {
color: white;
background: #1AC0FF;
}
"""
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.LANG_NAMES = {
"af": "Afrikaans",
"am": "Amharic",
"ar": "Arabic",
"az": "Azerbaijani",
"be": "Belarusian",
"bg": "Bulgarian",
"bn": "Bengali",
"bs": "Bosnian",
"ca": "Catalan",
"cs": "Czech",
"cy": "Welsh",
"da": "Danish",
"de": "German",
"el": "Greek",
"en_US": "English",
"eo": "Esperanto",
"es": "Spanish",
"et": "Estonian",
"eu": "Basque",
"fa": "Persian",
"fi": "Finnish",
"fil": "Filipino",
"fr": "French",
"ga": "Irish",
"gl": "Galician",
"gu": "Gujarati",
"he": "Hebrew",
"hi": "Hindi",
"hr": "Croatian",
"ht": "Haitian Creole",
"hu": "Hungarian",
"hy": "Armenian",
"id": "Indonesian",
"is": "Icelandic",
"it": "Italian",
"ja": "Japanese",
"jv": "Javanese",
"ka": "Georgian",
"kk": "Kazakh",
"km": "Khmer",
"kn": "Kannada",
"ko": "Korean",
"ku": "Kurdish (Kurmanji)",
"ky": "Kyrgyz",
"lo": "Lao",
"lt": "Lithuanian",
"lv": "Latvian",
"mg": "Malagasy",
"mi": "Maori",
"mk": "Macedonian",
"ml": "Malayalam",
"mn": "Mongolian",
"mr": "Marathi",
"ms": "Malay",
"mt": "Maltese",
"my": "Burmese",
"nb": "Norwegian (Bokmål)",
"ne": "Nepali",
"nl": "Dutch",
"nn": "Norwegian (Nynorsk)",
"pa": "Punjabi",
"pl": "Polish",
"ps": "Pashto",
"pt": "Portuguese (Portugal)",
"pt_BR": "Portuguese (Brazil)",
"ro": "Romanian",
"ru": "Russian",
"sd": "Sindhi",
"si": "Sinhala",
"sk": "Slovak",
"sl": "Slovenian",
"so": "Somali",
"sq": "Albanian",
"sr": "Serbian",
"sv": "Swedish",
"sw": "Swahili",
"ta": "Tamil",
"te": "Telugu",
"tg": "Tajik",
"th": "Thai",
"tk": "Turkmen",
"tl": "Tagalog",
"tr": "Turkish",
"tt": "Tatar",
"ug": "Uyghur",
"uk": "Ukrainian",
"ur": "Urdu",
"uz": "Uzbek",
"vi": "Vietnamese",
"xh": "Xhosa",
"yi": "Yiddish",
"zh_CN": "Chinese (Simplified)",
"zh_TW": "Chinese (Traditional)",
"zu": "Zulu",
}
self.lang_codes = {}
# Widgets for Interface mode
self.label_language = Gtk.Label(label=_("Language"))
self.label_language.set_halign(Gtk.Align.START)
self.combobox_language = Gtk.ComboBoxText()
# Widgets for Interface mode
self.label_interface = Gtk.Label(label=_("Interface Mode"))
self.label_interface.set_halign(Gtk.Align.START)
self.combobox_interface = Gtk.ComboBoxText()
self.combobox_interface.connect("changed", self.on_combobox_interface_changed)
self.combobox_interface.append_text("List")
self.combobox_interface.append_text("Blocks")
self.combobox_interface.append_text("Banners")
# Create checkbox for 'Start maximized' option
self.checkbox_start_maximized = Gtk.CheckButton(label=_("Start maximized"))
self.checkbox_start_maximized.set_active(False)
self.checkbox_start_maximized.connect("toggled", self.on_checkbox_toggled, "maximized")
# Create checkbox for 'Start fullscreen' option
self.checkbox_start_fullscreen = Gtk.CheckButton(label=_("Start in fullscreen"))
self.checkbox_start_fullscreen.set_active(False)
self.checkbox_start_fullscreen.connect("toggled", self.on_checkbox_toggled, "fullscreen")
self.checkbox_start_fullscreen.set_tooltip_text(_("Alt+Enter toggles fullscreen"))
self.checkbox_show_labels = Gtk.CheckButton(label=_("Show labels"))
self.checkbox_show_labels.set_active(False)
self.checkbox_smaller_banners = Gtk.CheckButton(label=_("Smaller banners"))
self.checkbox_smaller_banners.set_active(False)
# Widgets for prefix
self.label_default_prefix = Gtk.Label(label=_("Default Prefixes Location"))
self.label_default_prefix.set_halign(Gtk.Align.START)
self.entry_default_prefix = Gtk.Entry()
self.entry_default_prefix.set_tooltip_text(_("/path/to/the/prefix"))
self.entry_default_prefix.set_has_tooltip(True)
self.entry_default_prefix.connect("query-tooltip", self.on_entry_query_tooltip)
self.entry_default_prefix.connect("changed", self.on_entry_changed, self.entry_default_prefix)
self.button_search_prefix = Gtk.Button()
self.button_search_prefix.set_image(Gtk.Image.new_from_icon_name("system-search-symbolic", Gtk.IconSize.BUTTON))
self.button_search_prefix.connect("clicked", self.on_button_search_prefix_clicked)
self.button_search_prefix.set_size_request(50, -1)
self.label_lossless = Gtk.Label(label=_("Lossless Scaling Location"))
self.label_lossless.set_halign(Gtk.Align.START)
self.entry_lossless = Gtk.Entry()
self.entry_lossless.set_tooltip_text(_("/path/to/Lossless.dll"))
self.entry_lossless.set_has_tooltip(True)
self.entry_lossless.connect("query-tooltip", self.on_entry_query_tooltip)
self.button_search_lossless = Gtk.Button()
self.button_search_lossless.set_image(Gtk.Image.new_from_icon_name("system-search-symbolic", Gtk.IconSize.BUTTON))
self.button_search_lossless.connect("clicked", self.on_button_search_lossless_clicked)
self.button_search_lossless.set_size_request(50, -1)
self.label_default_prefix_tools = Gtk.Label(label=_("Default Prefix Tools"))
self.label_default_prefix_tools.set_halign(Gtk.Align.START)
self.label_default_prefix_tools.set_margin_start(10)
self.label_default_prefix_tools.set_margin_end(10)
self.label_default_prefix_tools.set_margin_top(10)
# Widgets for runner
self.label_runner = Gtk.Label(label=_("Default Proton"))
self.label_runner.set_halign(Gtk.Align.START)
self.combobox_runner = Gtk.ComboBoxText()
self.button_proton_manager = Gtk.Button(label=_("Proton Manager"))
self.button_proton_manager.connect("clicked", self.on_button_proton_manager_clicked)
self.label_miscellaneous = Gtk.Label(label=_("Miscellaneous"))
self.label_miscellaneous.set_halign(Gtk.Align.START)
self.label_miscellaneous.set_margin_start(10)
self.label_miscellaneous.set_margin_end(10)
#self.label_miscellaneous.set_margin_top(10)
# Create checkbox for 'Use discrete GPU' option
self.checkbox_discrete_gpu = Gtk.CheckButton(label=_("Use discrete GPU"))
self.checkbox_discrete_gpu.set_active(False)
# Create checkbox for 'Close after launch' option
self.checkbox_close_after_launch = Gtk.CheckButton(label=_("Close when running a game/app"))
self.checkbox_close_after_launch.set_active(False)
# Create checkbox for 'System tray' option
self.checkbox_system_tray = Gtk.CheckButton(label=_("System tray icon"))
self.checkbox_system_tray.set_active(False)
self.checkbox_system_tray.connect("toggled", self.on_checkbox_system_tray_toggled)
# Create checkbox for 'Start on boot' option
self.checkbox_start_boot = Gtk.CheckButton(label=_("Start on boot"))
self.checkbox_start_boot.set_active(False)
self.checkbox_start_boot.set_sensitive(False)
self.checkbox_mono_icon = Gtk.CheckButton(label=_("Monochrome icon"))
self.checkbox_mono_icon.set_active(False)
self.checkbox_mono_icon.set_sensitive(False)
# Create checkbox for 'Splash screen' option
self.checkbox_splash_disable = Gtk.CheckButton(label=_("Disable splash window"))
self.checkbox_splash_disable.set_active(False)
# Create checkbox for 'Enable logging' option
self.checkbox_enable_logging = Gtk.CheckButton(label=_("Enable logging"))
self.checkbox_enable_logging.set_active(False)
self.checkbox_show_hidden = Gtk.CheckButton(label=_("Show hidden games"))
self.checkbox_show_hidden.set_active(False)
self.checkbox_show_hidden.set_tooltip_text(_("Press Ctrl+H to show/hide games."))
self.checkbox_wayland_driver = Gtk.CheckButton(label=_("Use Wayland driver (experimental)"))
self.checkbox_wayland_driver.set_active(False)
self.checkbox_wayland_driver.set_tooltip_text(_("Only works with GE-Proton10 or Proton-EM-10."))
self.checkbox_wayland_driver.connect("toggled", self.on_checkbox_wayland_driver_toggled)
self.checkbox_enable_hdr = Gtk.CheckButton(label=_("Enable HDR (experimental)"))
self.checkbox_enable_hdr.set_active(False)
self.checkbox_enable_hdr.set_sensitive(False)
self.checkbox_enable_hdr.set_tooltip_text(_("Only works with GE-Proton10 or Proton-EM-10."))
self.checkbox_enable_wow64 = Gtk.CheckButton(label=_("Enable WOW64 (experimental)"))
self.checkbox_enable_wow64.set_active(False)
self.checkbox_enable_wow64.set_tooltip_text(_("Only works with GE-Proton10-9 or superior and Proton-EM-10-24 or superior."))
# Button Winetricks
self.button_winetricks_default = Gtk.Button(label="Winetricks")
self.button_winetricks_default.connect("clicked", self.on_button_winetricks_default_clicked)
self.button_winetricks_default.set_size_request(120, -1)
# Button Winecfg
self.button_winecfg_default = Gtk.Button(label="Winecfg")
self.button_winecfg_default.connect("clicked", self.on_button_winecfg_default_clicked)
self.button_winecfg_default.set_size_request(120, -1)
# Button for Run
self.button_run_default = Gtk.Button(label=_("Run"))
self.button_run_default.set_size_request(120, -1)
self.button_run_default.connect("clicked", self.on_button_run_default_clicked)
self.button_run_default.set_tooltip_text(_("Run a file inside the prefix"))
# Checkboxes for optional features
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."))
self.checkbox_prevent_sleep = Gtk.CheckButton(label=_("Prevent Sleep"))
self.label_support = Gtk.Label(label=_("Support the Project"))
self.label_support.set_halign(Gtk.Align.START)
self.label_support.set_margin_start(10)
self.label_support.set_margin_end(10)
self.label_support.set_margin_top(10)
button_kofi = Gtk.Button(label="Ko-fi")
button_kofi.connect("clicked", self.on_button_kofi_clicked)
button_kofi.get_style_context().add_class("kofi")
button_paypal = Gtk.Button(label="PayPal")
button_paypal.connect("clicked", self.on_button_paypal_clicked)
button_paypal.get_style_context().add_class("paypal")
# Button Cancel
self.button_cancel = Gtk.Button(label=_("Cancel"))
self.button_cancel.connect("clicked", lambda widget: self.response(Gtk.ResponseType.CANCEL))
self.button_cancel.set_size_request(150, -1)
# Button Ok
self.button_ok = Gtk.Button(label=_("Ok"))
self.button_ok.connect("clicked", lambda widget: self.response(Gtk.ResponseType.OK))
self.button_ok.set_size_request(150, -1)
self.label_settings = Gtk.Label(label=_("Backup/Restore Settings"))
self.label_settings.set_halign(Gtk.Align.START)
self.label_settings.set_margin_start(10)
self.label_settings.set_margin_end(10)
self.label_settings.set_margin_top(10)
# Button Backup
button_backup = Gtk.Button(label=_("Backup"))
button_backup.connect("clicked", self.on_button_backup_clicked)
# Button Restore
button_restore = Gtk.Button(label=_("Restore"))
button_restore.connect("clicked", self.on_button_restore_clicked)
self.button_clearlogs = Gtk.Button()
self.update_button_label()
self.button_clearlogs.connect("clicked", self.on_clear_logs_clicked)
self.label_envar = Gtk.Label(label=_("Global Environment Variables"))
self.label_envar.set_halign(Gtk.Align.START)
self.liststore = Gtk.ListStore(str)
self.liststore.append([""])
treeview = Gtk.TreeView(model=self.liststore)
treeview.set_has_tooltip(True)
treeview.connect("query-tooltip", self.on_query_tooltip)
treeview.connect("key-press-event", self.on_envar_key_press)
renderer = Gtk.CellRendererText()
renderer.set_property("editable", True)
renderer.set_property("ellipsize", 3)
renderer.connect("edited", self.on_cell_edited, 0)
column = Gtk.TreeViewColumn("", renderer, text=0)
treeview.set_headers_visible(False)
treeview.append_column(column)
scrolled_window = Gtk.ScrolledWindow()
scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
scrolled_window.set_min_content_height(130)
scrolled_window.add(treeview)
self.box = self.get_content_area()
self.box.set_margin_start(0)
self.box.set_margin_end(0)
self.box.set_margin_top(0)
self.box.set_margin_bottom(0)
self.box.set_halign(Gtk.Align.CENTER)
self.box.set_valign(Gtk.Align.CENTER)
self.box.set_vexpand(True)
self.box.set_hexpand(True)
url = f"https://github.com/Faugus/faugus-launcher/releases"
label_version = Gtk.Label()
label_version.set_markup(f'<span underline="none"><a href="{url}"> {VERSION} </a></span>')
label_version.set_use_markup(True)
frame = Gtk.Frame()
frame.set_label_widget(label_version)
frame.set_label_align(0.99, 0.5)
frame.set_margin_start(10)
frame.set_margin_end(10)
frame.set_margin_bottom(10)
box_main = Gtk.Grid()
box_main.set_column_homogeneous(True)
box_main.set_column_spacing(10)
box_left = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box_mid = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box_right = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box_buttons = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
grid_language = Gtk.Grid()
grid_language.set_row_spacing(10)
grid_language.set_column_spacing(10)
grid_language.set_margin_start(10)
grid_language.set_margin_end(10)
grid_language.set_margin_top(10)
grid_language.set_margin_bottom(10)
grid_prefix = Gtk.Grid()
grid_prefix.set_row_spacing(10)
grid_prefix.set_column_spacing(10)
grid_prefix.set_margin_start(10)
grid_prefix.set_margin_end(10)
#grid_prefix.set_margin_top(10)
grid_prefix.set_margin_bottom(10)
grid_runner = Gtk.Grid()
grid_runner.set_row_spacing(10)
grid_runner.set_column_spacing(10)
grid_runner.set_margin_start(10)
grid_runner.set_margin_end(10)
grid_runner.set_margin_top(10)
grid_runner.set_margin_bottom(10)
grid_lossless = Gtk.Grid()
grid_lossless.set_row_spacing(10)
grid_lossless.set_column_spacing(10)
grid_lossless.set_margin_start(10)
grid_lossless.set_margin_end(10)
grid_lossless.set_margin_top(10)
grid_lossless.set_margin_bottom(10)
grid_tools = Gtk.Grid()
grid_tools.set_row_spacing(10)
grid_tools.set_column_spacing(10)
grid_tools.set_margin_start(10)
grid_tools.set_margin_end(10)
grid_tools.set_margin_top(10)
grid_tools.set_margin_bottom(10)
grid_logs = Gtk.Grid()
grid_logs.set_row_spacing(10)
grid_logs.set_column_spacing(10)
grid_logs.set_margin_start(10)
grid_logs.set_margin_end(10)
grid_logs.set_margin_top(10)
grid_logs.set_margin_bottom(10)
grid_miscellaneous = Gtk.Grid()
grid_miscellaneous.set_row_spacing(10)
grid_miscellaneous.set_column_spacing(10)
grid_miscellaneous.set_margin_start(10)
grid_miscellaneous.set_margin_end(10)
grid_miscellaneous.set_margin_top(10)
grid_miscellaneous.set_margin_bottom(10)
grid_envar = Gtk.Grid()
grid_envar.set_row_spacing(10)
grid_envar.set_column_spacing(10)
grid_envar.set_margin_start(10)
grid_envar.set_margin_end(10)
#grid_envar.set_margin_top(10)
grid_envar.set_margin_bottom(10)
grid_interface_mode = Gtk.Grid()
grid_interface_mode.set_row_spacing(10)
grid_interface_mode.set_column_spacing(10)
grid_interface_mode.set_margin_start(10)
grid_interface_mode.set_margin_end(10)
grid_interface_mode.set_margin_top(10)
grid_interface_mode.set_margin_bottom(10)
grid_support = Gtk.Grid()
grid_support.set_column_homogeneous(True)
grid_support.set_row_spacing(10)
grid_support.set_column_spacing(10)
grid_support.set_margin_start(10)
grid_support.set_margin_end(10)
grid_support.set_margin_top(10)
grid_support.set_margin_bottom(10)
grid_backup = Gtk.Grid()
grid_backup.set_column_homogeneous(True)
grid_backup.set_row_spacing(10)
grid_backup.set_column_spacing(10)
grid_backup.set_margin_start(10)
grid_backup.set_margin_end(10)
grid_backup.set_margin_top(10)
grid_backup.set_margin_bottom(10)
self.grid_big_interface = Gtk.Grid()
self.grid_big_interface.set_row_spacing(10)
self.grid_big_interface.set_column_spacing(10)
self.grid_big_interface.set_margin_start(10)
self.grid_big_interface.set_margin_end(10)
self.grid_big_interface.set_margin_bottom(10)
grid_language.attach(self.label_language, 0, 0, 1, 1)
grid_language.attach(self.combobox_language, 0, 1, 1, 1)
self.combobox_language.set_hexpand(True)
grid_prefix.attach(self.label_default_prefix, 0, 0, 1, 1)
grid_prefix.attach(self.entry_default_prefix, 0, 1, 3, 1)
self.entry_default_prefix.set_hexpand(True)
grid_prefix.attach(self.button_search_prefix, 3, 1, 1, 1)
grid_runner.attach(self.label_runner, 0, 6, 1, 1)
grid_runner.attach(self.combobox_runner, 0, 7, 1, 1)
grid_runner.attach(self.button_proton_manager, 0, 8, 1, 1)
grid_lossless.attach(self.label_lossless, 0, 0, 1, 1)
grid_lossless.attach(self.entry_lossless, 0, 1, 3, 1)
self.entry_default_prefix.set_hexpand(True)
grid_lossless.attach(self.button_search_lossless, 3, 1, 1, 1)
self.combobox_runner.set_hexpand(True)
self.button_proton_manager.set_hexpand(True)
self.entry_lossless.set_hexpand(True)
box_buttons.pack_start(self.button_winetricks_default, True, True, 0)
box_buttons.pack_start(self.button_winecfg_default, True, True, 0)
box_buttons.pack_start(self.button_run_default, True, True, 0)
grid_tools.attach(self.checkbox_mangohud, 0, 0, 1, 1)
self.checkbox_mangohud.set_hexpand(True)
grid_tools.attach(self.checkbox_gamemode, 0, 1, 1, 1)
grid_tools.attach(self.checkbox_prevent_sleep, 0, 2, 1, 1)
grid_tools.attach(self.checkbox_disable_hidraw, 0, 3, 1, 1)
grid_tools.attach(box_buttons, 2, 0, 1, 4)
grid_logs.attach(self.checkbox_enable_logging, 0, 0, 1, 1)
grid_logs.attach(self.button_clearlogs, 0, 1, 1, 1)
self.button_clearlogs.set_hexpand(True)
grid_miscellaneous.attach(self.checkbox_discrete_gpu, 0, 2, 1, 1)
grid_miscellaneous.attach(self.checkbox_splash_disable, 0, 3, 1, 1)
grid_miscellaneous.attach(self.checkbox_system_tray, 0, 4, 1, 1)
grid_miscellaneous.attach(self.checkbox_start_boot, 0, 5, 1, 1)
grid_miscellaneous.attach(self.checkbox_mono_icon, 0, 6, 1, 1)
grid_miscellaneous.attach(self.checkbox_close_after_launch, 0, 7, 1, 1)
grid_miscellaneous.attach(self.checkbox_show_hidden, 0, 8, 1, 1)
grid_miscellaneous.attach(self.checkbox_wayland_driver, 0, 9, 1, 1)
grid_miscellaneous.attach(self.checkbox_enable_hdr, 0, 10, 1, 1)
grid_miscellaneous.attach(self.checkbox_enable_wow64, 0, 11, 1, 1)
grid_interface_mode.attach(self.label_interface, 0, 0, 1, 1)
grid_interface_mode.attach(self.combobox_interface, 0, 1, 1, 1)
self.combobox_interface.set_hexpand(True)
grid_envar.attach(self.label_envar, 0, 0, 1, 1)
grid_envar.attach(scrolled_window, 0, 1, 1, 1)
scrolled_window.set_hexpand(True)
grid_backup.attach(button_backup, 0, 1, 1, 1)
grid_backup.attach(button_restore, 1, 1, 1, 1)
self.grid_big_interface.attach(self.checkbox_start_maximized, 0, 0, 1, 1)
self.grid_big_interface.attach(self.checkbox_start_fullscreen, 0, 1, 1, 1)
self.grid_big_interface.attach(self.checkbox_show_labels, 0, 2, 1, 1)
self.grid_big_interface.attach(self.checkbox_smaller_banners, 0, 3, 1, 1)
grid_support.attach(button_kofi, 0, 1, 1, 1)
grid_support.attach(button_paypal, 1, 1, 1, 1)
box_left.pack_start(grid_prefix, False, False, 0)
box_left.pack_start(grid_runner, False, False, 0)
box_left.pack_start(self.label_default_prefix_tools, False, False, 0)
box_left.pack_start(grid_tools, False, False, 0)
box_left.pack_start(grid_lossless, False, False, 0)
box_left.pack_end(grid_language, False, False, 0)
box_mid.pack_start(self.label_miscellaneous, False, False, 0)
box_mid.pack_start(grid_miscellaneous, False, False, 0)
box_mid.pack_start(grid_logs, False, False, 0)
box_mid.pack_end(grid_support, False, False, 0)
box_mid.pack_end(self.label_support, False, False, 0)
box_right.pack_start(grid_envar, False, False, 0)
box_right.pack_start(grid_interface_mode, False, False, 0)
box_right.pack_start(self.grid_big_interface, False, False, 0)
box_right.pack_end(grid_backup, False, False, 0)
box_right.pack_end(self.label_settings, False, False, 0)
box_main.attach(box_left, 0, 0, 1, 1)
box_main.attach(box_mid, 1, 0, 1, 1)
box_main.attach(box_right, 2, 0, 1, 1)
box_left.set_hexpand(True)
box_mid.set_hexpand(True)
frame.add(box_main)
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)
self.button_cancel.set_hexpand(True)
self.button_ok.set_hexpand(True)
box_bottom.pack_start(self.button_cancel, True, True, 0)
box_bottom.pack_start(self.button_ok, True, True, 0)
self.box.add(frame)
self.box.add(box_bottom)
self.populate_combobox_with_runners()
self.populate_languages()
self.load_config()
self.show_all()
self.on_combobox_interface_changed(self.combobox_interface)
# Check if optional features are available and enable/disable accordingly
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."))
def on_envar_key_press(self, widget, event):
if event.keyval == Gdk.KEY_Delete:
selection = widget.get_selection()
model, treeiter = selection.get_selected()
if not treeiter:
return False
path = model.get_path(treeiter)
index = path.get_indices()[0]
if index == len(model) - 1:
return True
model.remove(treeiter)
return True
return False
def get_dir_size(self, path):
total = 0
for dirpath, _, filenames in os.walk(path):
for f in filenames:
fp = os.path.join(dirpath, f)
if os.path.isfile(fp):
total += os.path.getsize(fp)
for unit in ['B', 'KB', 'MB', 'GB']:
if total < 1024.0:
return f"{total:.1f} {unit}"
total /= 1024.0
return f"{total:.1f} TB"
def update_button_label(self):
if os.path.exists(logs_dir):
size = self.get_dir_size(logs_dir)
self.button_clearlogs.set_label(_("Clear Logs (%s)") % size)
self.button_clearlogs.set_sensitive(True)
else:
self.button_clearlogs.set_label(_("Clear Logs"))
self.button_clearlogs.set_sensitive(False)
def on_clear_logs_clicked(self, button):
if os.path.exists(logs_dir):
shutil.rmtree(logs_dir)
self.update_button_label()
def on_query_tooltip(self, widget, x, y, keyboard_mode, tooltip):
result = widget.get_path_at_pos(x, y)
if result is not None:
path, column, cell_x, cell_y = result
tree_iter = self.liststore.get_iter(path)
value = self.liststore.get_value(tree_iter, 0)
if value.strip():
tooltip.set_text(value)
return True
return False
def on_cell_edited(self, widget, path, text, column_index):
self.liststore[path][column_index] = text
self.adjust_rows()
def adjust_rows(self):
filled_rows = [row[0] for row in self.liststore if row[0].strip() != ""]
self.liststore.clear()
for value in filled_rows:
self.liststore.append([value])
self.liststore.append([""])
def populate_languages(self):
self.combobox_language.remove_all()
self.combobox_language.append_text("English")
if not os.path.isdir(LOCALE_DIR):
return
for lang in sorted(os.listdir(LOCALE_DIR)):
mo_file = os.path.join(LOCALE_DIR, lang, "LC_MESSAGES", "faugus-launcher.mo")
if os.path.isfile(mo_file):
lang_name = self.LANG_NAMES.get(lang, lang)
self.combobox_language.append_text(lang_name)
self.lang_codes[lang_name] = lang
self.combobox_language.set_active(0)
def on_entry_query_tooltip(self, widget, x, y, keyboard_mode, tooltip):
current_text = widget.get_text()
if current_text.strip():
tooltip.set_text(current_text)
else:
tooltip.set_text(widget.get_tooltip_text())
return True
def on_checkbox_toggled(self, checkbox, option):
if checkbox.get_active():
if option == "maximized":
self.checkbox_start_fullscreen.set_active(False)
elif option == "fullscreen":
self.checkbox_start_maximized.set_active(False)
def on_combobox_interface_changed(self, combobox):
active_index = combobox.get_active()
if active_index == 0:
self.grid_big_interface.set_visible(False)
if active_index == 1:
self.grid_big_interface.set_visible(True)
self.checkbox_show_labels.set_visible(False)
self.checkbox_smaller_banners.set_visible(False)
if active_index == 2:
self.grid_big_interface.set_visible(True)
self.checkbox_show_labels.set_visible(True)
self.checkbox_smaller_banners.set_visible(True)
def on_checkbox_system_tray_toggled(self, widget):
if not widget.get_active():
self.checkbox_start_boot.set_active(False)
self.checkbox_start_boot.set_sensitive(False)
self.checkbox_mono_icon.set_active(False)
self.checkbox_mono_icon.set_sensitive(False)
else:
self.checkbox_start_boot.set_sensitive(True)
self.checkbox_mono_icon.set_sensitive(True)
def on_checkbox_wayland_driver_toggled(self, widget):
if not widget.get_active():
self.checkbox_enable_hdr.set_active(False)
self.checkbox_enable_hdr.set_sensitive(False)
else:
self.checkbox_enable_hdr.set_sensitive(True)
def populate_combobox_with_runners(self):
# List of default entries
self.combobox_runner.append_text("GE-Proton Latest (default)")
self.combobox_runner.append_text("UMU-Proton Latest")
self.combobox_runner.append_text("Proton-EM Latest")
if os.path.exists("/usr/share/steam/compatibilitytools.d/proton-cachyos-slr/"):
self.combobox_runner.append_text("Proton-CachyOS")
# Path to the directory containing the folders
if IS_FLATPAK:
runner_path = Path(os.path.expanduser("~/.local/share/Steam/compatibilitytools.d"))
else:
runner_path = f'{share_dir}/Steam/compatibilitytools.d/'
try:
# Check if the directory exists
if os.path.exists(runner_path):
# List to hold version directories
versions = []
# Iterate over the folders in the directory
for entry in os.listdir(runner_path):
entry_path = os.path.join(runner_path, entry)
# Add to list only if it's a directory and not "UMU-Latest"
if (
os.path.isdir(entry_path)
and entry not in ("UMU-Latest", "LegacyRuntime")
and not entry.startswith("Proton-GE Latest")
and not entry.startswith("Proton-EM Latest")
):
versions.append(entry)
# Sort versions in descending order
def version_key(v):
# Remove 'GE-Proton' and split the remaining part into segments of digits and non-digits
v_parts = re.split(r'(\d+)', v.replace('GE-Proton', ''))
# Convert numeric parts to integers for proper sorting
return [int(part) if part.isdigit() else part for part in v_parts]
versions.sort(key=version_key, reverse=True)
# Add sorted versions to ComboBox
for version in versions:
self.combobox_runner.append_text(version)
except Exception as e:
print(f"Error accessing the directory: {e}")
# Set the active item, if desired
self.combobox_runner.set_active(0)
cell_renderer = self.combobox_runner.get_cells()[0]
cell_renderer.set_property("ellipsize", Pango.EllipsizeMode.END)
cell_renderer.set_property("max-width-chars", 20)
def on_entry_changed(self, widget, entry):
if entry.get_text():
entry.get_style_context().remove_class("entry")
def update_config_file(self):
combobox_language = self.combobox_language.get_active_text()
entry_default_prefix = self.entry_default_prefix.get_text()
combobox_default_runner = self.get_default_runner()
entry_lossless = self.entry_lossless.get_text()
checkbox_mangohud = self.checkbox_mangohud.get_active()
checkbox_gamemode = self.checkbox_gamemode.get_active()
checkbox_disable_hidraw = self.checkbox_disable_hidraw.get_active()
checkbox_prevent_sleep = self.checkbox_prevent_sleep.get_active()
checkbox_discrete_gpu_state = self.checkbox_discrete_gpu.get_active()
checkbox_splash_disable = self.checkbox_splash_disable.get_active()
checkbox_system_tray = self.checkbox_system_tray.get_active()
checkbox_start_boot = self.checkbox_start_boot.get_active()
checkbox_mono_icon = self.checkbox_mono_icon.get_active()
checkbox_close_after_launcher = self.checkbox_close_after_launch.get_active()
checkbox_enable_logging = self.checkbox_enable_logging.get_active()
checkbox_show_hidden = self.checkbox_show_hidden.get_active()
checkbox_wayland_driver = self.checkbox_wayland_driver.get_active()
checkbox_enable_hdr = self.checkbox_enable_hdr.get_active()
checkbox_enable_wow64 = self.checkbox_enable_wow64.get_active()
combobox_interface = self.combobox_interface.get_active_text()
checkbox_start_maximized = self.checkbox_start_maximized.get_active()
checkbox_start_fullscreen = self.checkbox_start_fullscreen.get_active()
checkbox_show_labels = self.checkbox_show_labels.get_active()
checkbox_smaller_banners = self.checkbox_smaller_banners.get_active()
language = self.lang_codes.get(combobox_language, "en_US")
logging_warning = self.logging_warning
config = ConfigManager()
config.save_with_values(
checkbox_close_after_launcher,
entry_default_prefix,
checkbox_mangohud,
checkbox_gamemode,
checkbox_disable_hidraw,
checkbox_prevent_sleep,
combobox_default_runner,
entry_lossless,
checkbox_discrete_gpu_state,
checkbox_splash_disable,
checkbox_system_tray,
checkbox_start_boot,
checkbox_mono_icon,
combobox_interface,
checkbox_start_maximized,
checkbox_start_fullscreen,
checkbox_show_labels,
checkbox_smaller_banners,
checkbox_enable_logging,
checkbox_wayland_driver,
checkbox_enable_hdr,
checkbox_enable_wow64,
language,
logging_warning,
checkbox_show_hidden
)
self.set_sensitive(False)
def get_default_runner(self):
default_runner = self.combobox_runner.get_active_text()
default_runner = convert_runner(default_runner)
return default_runner
def update_envar_file(self):
if hasattr(self, "liststore"):
values = [row[0] for row in self.liststore if row[0].strip() != ""]
with open(envar_dir, "w", encoding="utf-8") as f:
for val in values:
f.write(val + "\n")
def on_button_proton_manager_clicked(self, widget):
if self.entry_default_prefix.get_text() == "":
self.entry_default_prefix.get_style_context().add_class("entry")
else:
self.update_envar_file()
self.update_config_file()
proton_manager = faugus_proton_manager
def run_command():
process = subprocess.Popen([sys.executable, proton_manager])
process.wait()
GLib.idle_add(self.set_sensitive, True)
GLib.idle_add(self.parent.set_sensitive, True)
GLib.idle_add(self.blocking_window.destroy)
GLib.idle_add(lambda: self.combobox_runner.remove_all())
GLib.idle_add(self.populate_combobox_with_runners)
GLib.idle_add(lambda: self.load_config())
self.blocking_window = Gtk.Window()
self.blocking_window.set_transient_for(self.parent)
self.blocking_window.set_decorated(False)
self.blocking_window.set_modal(True)
command_thread = threading.Thread(target=run_command)
command_thread.start()
def on_button_winetricks_default_clicked(self, widget):
if self.entry_default_prefix.get_text() == "":
self.entry_default_prefix.get_style_context().add_class("entry")
else:
self.update_envar_file()
self.update_config_file()
self.parent.manage_autostart_file(self.checkbox_start_boot.get_active())
default_runner = self.get_default_runner()
command_parts = []
# Add command parts if they are not empty
command_parts.append(f"GAMEID=winetricks-gui")
command_parts.append(f"STORE=none")
if default_runner:
if default_runner == "Proton-CachyOS":
command_parts.append(f"PROTONPATH='{proton_cachyos}'")
else:
command_parts.append(f"PROTONPATH='{default_runner}'")
# Add the fixed command and remaining arguments
command_parts.append(f"'{umu_run}'")
command_parts.append("''")
# Join all parts into a single command
command = ' '.join(command_parts)
print(command)
# faugus-run path
faugus_run_path = faugus_run
def run_command():
process = subprocess.Popen([sys.executable, faugus_run_path, command, "winetricks"])
process.wait()
GLib.idle_add(self.set_sensitive, True)
GLib.idle_add(self.parent.set_sensitive, True)
GLib.idle_add(self.blocking_window.destroy)
self.blocking_window = Gtk.Window()
self.blocking_window.set_transient_for(self.parent)
self.blocking_window.set_decorated(False)
self.blocking_window.set_modal(True)
command_thread = threading.Thread(target=run_command)
command_thread.start()
def on_button_winecfg_default_clicked(self, widget):
if self.entry_default_prefix.get_text() == "":
self.entry_default_prefix.get_style_context().add_class("entry")
else:
self.update_envar_file()
self.update_config_file()
self.parent.manage_autostart_file(self.checkbox_start_boot.get_active())
default_runner = self.get_default_runner()
command_parts = []
# Add command parts if they are not empty
command_parts.append(f"GAMEID=default")
if default_runner:
if default_runner == "Proton-CachyOS":
command_parts.append(f"PROTONPATH='{proton_cachyos}'")
else:
command_parts.append(f"PROTONPATH='{default_runner}'")
# Add the fixed command and remaining arguments
command_parts.append(f"'{umu_run}'")
command_parts.append("'winecfg'")
# Join all parts into a single command
command = ' '.join(command_parts)
print(command)
# faugus-run path
faugus_run_path = faugus_run
def run_command():
process = subprocess.Popen([sys.executable, faugus_run_path, command])
process.wait()
GLib.idle_add(self.set_sensitive, True)
GLib.idle_add(self.parent.set_sensitive, True)
GLib.idle_add(self.blocking_window.destroy)
self.blocking_window = Gtk.Window()
self.blocking_window.set_transient_for(self.parent)
self.blocking_window.set_decorated(False)
self.blocking_window.set_modal(True)
command_thread = threading.Thread(target=run_command)
command_thread.start()
def on_button_run_default_clicked(self, widget):
if self.entry_default_prefix.get_text() == "":
self.entry_default_prefix.get_style_context().add_class("entry")
return
self.update_envar_file()
self.update_config_file()
self.parent.manage_autostart_file(self.checkbox_start_boot.get_active())
default_runner = self.get_default_runner()
filechooser = Gtk.FileChooserNative(
title=_("Select a file to run inside the prefix"),
transient_for=self,
action=Gtk.FileChooserAction.OPEN,
accept_label=_("Open"),
cancel_label=_("Cancel"),
)
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:
command_parts = []
file_run = filechooser.get_filename()
escaped_file_run = file_run.replace("'", "'\\''")
if not escaped_file_run.endswith(".reg"):
if escaped_file_run:
command_parts.append(f"GAMEID=default")
if default_runner:
if default_runner == "Proton-CachyOS":
command_parts.append(f"PROTONPATH='{proton_cachyos}'")
else:
command_parts.append(f"PROTONPATH='{default_runner}'")
command_parts.append(f"'{umu_run}' '{escaped_file_run}'")
else:
if escaped_file_run:
command_parts.append(f"GAMEID=default")
if default_runner:
if default_runner == "Proton-CachyOS":
command_parts.append(f"PROTONPATH='{proton_cachyos}'")
else:
command_parts.append(f"PROTONPATH='{default_runner}'")
command_parts.append(f"'{umu_run}' regedit '{escaped_file_run}'")
command = ' '.join(command_parts)
print(command)
faugus_run_path = faugus_run
def run_command():
process = subprocess.Popen([sys.executable, faugus_run_path, command])
process.wait()
GLib.idle_add(self.set_sensitive, True)
GLib.idle_add(self.parent.set_sensitive, True)
GLib.idle_add(self.blocking_window.destroy)
self.blocking_window = Gtk.Window()
self.blocking_window.set_transient_for(self.parent)
self.blocking_window.set_decorated(False)
self.blocking_window.set_modal(True)
command_thread = threading.Thread(target=run_command)
command_thread.start()
else:
self.set_sensitive(True)
filechooser.destroy()
def on_button_backup_clicked(self, widget):
self.response(Gtk.ResponseType.OK)
self.show_warning_dialog(self, _("Prefixes and runners will not be backed up!"))
items = ["banners", "games-backup", "icons", "config.ini", "envar.txt", "games.json", "latest-games.txt"]
temp_dir = os.path.join(faugus_launcher_dir, "temp-backup")
os.makedirs(temp_dir, exist_ok=True)
for item in items:
src = os.path.join(faugus_launcher_dir, item)
dst = os.path.join(temp_dir, item)
if os.path.isdir(src):
shutil.copytree(src, dst, dirs_exist_ok=True)
elif os.path.isfile(src):
os.makedirs(os.path.dirname(dst), exist_ok=True)
shutil.copy2(src, dst)
marker_path = os.path.join(temp_dir, ".faugus_marker")
with open(marker_path, "w") as f:
f.write("faugus-launcher-backup")
current_date = os.popen("date +%Y-%m-%d").read().strip()
zip_filename = f"faugus-launcher-{current_date}"
zip_path = os.path.join(faugus_launcher_dir, zip_filename)
shutil.make_archive(zip_path, "zip", temp_dir)
shutil.rmtree(temp_dir)
filechooser = Gtk.FileChooserNative(
title=_("Save the backup file as..."),
transient_for=self,
action=Gtk.FileChooserAction.SAVE,
accept_label=_("Save"),
cancel_label=_("Cancel"),
)
filechooser.set_current_folder(os.path.expanduser("~"))
filechooser.set_current_name(f"{zip_filename}.zip")
response = filechooser.run()
if response == Gtk.ResponseType.ACCEPT:
dest = filechooser.get_filename()
if not dest.endswith(".zip"):
dest += ".zip"
try:
if os.path.exists(dest):
os.remove(dest)
shutil.copy2(zip_path + ".zip", dest)
print(f"Backup saved at: {dest}")
except Exception as e:
print(f"Error saving backup: {e}")
filechooser.destroy()
os.remove(zip_path + ".zip")
def on_button_restore_clicked(self, widget):
filechooser = Gtk.FileChooserNative(
title=_("Select a backup file to restore"),
transient_for=self,
action=Gtk.FileChooserAction.OPEN,
accept_label=_("Open"),
cancel_label=_("Cancel"),
)
zip_filter = Gtk.FileFilter()
zip_filter.set_name(_("ZIP files"))
zip_filter.add_pattern("*.zip")
filechooser.add_filter(zip_filter)
filechooser.set_filter(zip_filter)
response = filechooser.run()
if response == Gtk.ResponseType.ACCEPT:
zip_file = filechooser.get_filename()
if not os.path.isfile(zip_file):
filechooser.destroy()
self.show_warning_dialog(self, _("This is not a valid Faugus Launcher backup file."))
return
temp_dir = os.path.join(faugus_launcher_dir, "temp-restore")
shutil.unpack_archive(zip_file, temp_dir, "zip")
marker_path = os.path.join(temp_dir, ".faugus_marker")
if not os.path.exists(marker_path):
shutil.rmtree(temp_dir)
filechooser.destroy()
self.show_warning_dialog(self, _("This is not a valid Faugus Launcher backup file."))
return
if self.show_warning_dialog2(self, _("Are you sure you want to overwrite the settings?")):
for item in os.listdir(temp_dir):
if item == ".faugus_marker":
continue
src = os.path.join(temp_dir, item)
dst = os.path.join(faugus_launcher_dir, item)
if os.path.isdir(dst):
shutil.rmtree(dst)
elif os.path.isfile(dst):
os.remove(dst)
if os.path.isdir(src):
shutil.copytree(src, dst)
elif os.path.isfile(src):
shutil.copy2(src, dst)
shutil.rmtree(temp_dir)
global faugus_backup
faugus_backup = True
self.response(Gtk.ResponseType.OK)
filechooser.destroy()
def show_warning_dialog(self, parent, title):
dialog = Gtk.Dialog(title="Faugus Launcher", transient_for=parent, modal=True)
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(title)
label.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.destroy())
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()
def show_warning_dialog2(self, parent, title):
dialog = Gtk.Dialog(title="Faugus Launcher", transient_for=parent, modal=True)
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(title)
label.set_halign(Gtk.Align.CENTER)
button_no = Gtk.Button(label=_("No"))
button_no.set_size_request(150, -1)
button_no.connect("clicked", lambda x: dialog.response(Gtk.ResponseType.CANCEL))
button_yes = Gtk.Button(label=_("Yes"))
button_yes.set_size_request(150, -1)
button_yes.connect("clicked", lambda x: 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_no, 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()
response = dialog.run()
dialog.destroy()
return response == Gtk.ResponseType.OK
def on_button_kofi_clicked(self, widget):
webbrowser.open("https://ko-fi.com/K3K210EMDU")
def on_button_paypal_clicked(self, widget):
webbrowser.open("https://www.paypal.com/donate/?business=57PP9DVD3VWAN&no_recurring=0&currency_code=USD")
def on_button_search_prefix_clicked(self, widget):
filechooser = Gtk.FileChooserNative(
title=_("Select a prefix location"),
transient_for=self,
action=Gtk.FileChooserAction.SELECT_FOLDER,
accept_label=_("Open"),
cancel_label=_("Cancel"),
)
if os.path.isdir(self.default_prefix):
filechooser.set_current_folder(self.default_prefix)
else:
filechooser.set_current_folder(os.path.expanduser("~"))
response = filechooser.run()
if response == Gtk.ResponseType.ACCEPT:
folder = filechooser.get_filename()
if folder:
self.entry_default_prefix.set_text(folder)
filechooser.destroy()
def on_button_search_lossless_clicked(self, widget):
filechooser = Gtk.FileChooserNative(
title=_("Select the Lossless.dll file"),
transient_for=self,
action=Gtk.FileChooserAction.OPEN,
accept_label=_("Open"),
cancel_label=_("Cancel"),
)
filter_dll = Gtk.FileFilter()
filter_dll.set_name("Lossless.dll")
filter_dll.add_pattern("Lossless.dll")
filechooser.add_filter(filter_dll)
filechooser.set_filter(filter_dll)
response = filechooser.run()
if response == Gtk.ResponseType.ACCEPT:
selected_file = filechooser.get_filename()
if selected_file and os.path.basename(selected_file) == "Lossless.dll":
self.entry_lossless.set_text(selected_file)
filechooser.destroy()
def load_config(self):
cfg = ConfigManager()
close_on_launch = cfg.config.get('close-onlaunch', 'False') == 'True'
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'
prevent_sleep = cfg.config.get('prevent-sleep', 'False') == 'True'
self.default_runner = cfg.config.get('default-runner', '').strip('"')
lossless_location = cfg.config.get('lossless-location', '').strip('"')
discrete_gpu = cfg.config.get('discrete-gpu', 'False') == 'True'
splash_disable = cfg.config.get('splash-disable', 'False') == 'True'
system_tray = cfg.config.get('system-tray', 'False') == 'True'
self.start_boot = cfg.config.get('start-boot', 'False') == 'True'
self.mono_icon = cfg.config.get('mono-icon', 'False') == 'True'
start_maximized = cfg.config.get('start-maximized', 'False') == 'True'
self.interface_mode = cfg.config.get('interface-mode', '').strip('"')
start_fullscreen = cfg.config.get('start-fullscreen', 'False') == 'True'
show_labels = cfg.config.get('show-labels', 'False') == 'True'
smaller_banners = cfg.config.get('smaller-banners', 'False') == 'True'
enable_logging = cfg.config.get('enable-logging', 'False') == 'True'
show_hidden = cfg.config.get('show-hidden', 'False') == 'True'
wayland_driver = cfg.config.get('wayland-driver', 'False') == 'True'
enable_hdr = cfg.config.get('enable-hdr', 'False') == 'True'
enable_wow64 = cfg.config.get('enable-wow64', 'False') == 'True'
self.language = cfg.config.get('language', '')
self.logging_warning = cfg.config.get('logging-warning', 'False') == 'True'
self.checkbox_close_after_launch.set_active(close_on_launch)
self.entry_default_prefix.set_text(self.default_prefix)
self.checkbox_mangohud.set_active(mangohud)
self.checkbox_gamemode.set_active(gamemode)
self.checkbox_disable_hidraw.set_active(disable_hidraw)
self.checkbox_prevent_sleep.set_active(prevent_sleep)
lossless_dll_path = find_lossless_dll()
if not lossless_location:
if lossless_dll_path:
self.entry_lossless.set_text(str(lossless_dll_path))
else:
self.entry_lossless.set_text(lossless_location)
self.default_runner = convert_runner(self.default_runner)
model_runner = self.combobox_runner.get_model()
index_runner = 0
for i, row in enumerate(model_runner):
if row[0] == self.default_runner:
index_runner = i
break
self.combobox_runner.set_active(index_runner)
self.checkbox_discrete_gpu.set_active(discrete_gpu)
self.checkbox_splash_disable.set_active(splash_disable)
self.checkbox_system_tray.set_active(system_tray)
self.checkbox_start_boot.set_active(self.start_boot)
self.checkbox_mono_icon.set_active(self.mono_icon)
self.checkbox_start_maximized.set_active(start_maximized)
self.checkbox_start_fullscreen.set_active(start_fullscreen)
self.checkbox_show_labels.set_active(show_labels)
self.checkbox_smaller_banners.set_active(smaller_banners)
self.checkbox_enable_logging.set_active(enable_logging)
self.checkbox_show_hidden.set_active(show_hidden)
self.checkbox_wayland_driver.set_active(wayland_driver)
self.checkbox_enable_hdr.set_active(enable_hdr)
self.checkbox_enable_wow64.set_active(enable_wow64)
model_interface = self.combobox_interface.get_model()
index_interface = 0
for i, row in enumerate(model_interface):
if row[0] == self.interface_mode:
index_interface = i
break
self.combobox_interface.set_active(index_interface)
model_language = self.combobox_language.get_model()
index_language = 0
if self.language == "":
self.combobox_language.set_active(index_language)
else:
for i, row in enumerate(model_language):
lang_name = row[0]
lang_code = self.lang_codes.get(lang_name, "")
if lang_code == self.language:
index_language = i
break
self.combobox_language.set_active(index_language)
self.load_liststore_from_file(envar_dir)
def load_liststore_from_file(self, filename=envar_dir):
self.liststore.clear()
try:
with open(filename, "r", encoding="utf-8") as f:
lines = [line.strip() for line in f.readlines() if line.strip()]
except FileNotFoundError:
lines = []
for line in lines:
self.liststore.append([line])
self.liststore.append([""])
class Game:
def __init__(
self,
gameid,
title,
path,
prefix,
launch_arguments,
game_arguments,
mangohud,
gamemode,
disable_hidraw,
protonfix,
runner,
addapp_checkbox,
addapp,
addapp_bat,
banner,
lossless_enabled,
lossless_multiplier,
lossless_flow,
lossless_performance,
lossless_hdr,
lossless_present,
playtime,
hidden,
prevent_sleep,
favorite,
startup_wm_class="",
):
self.gameid = gameid
self.title = title
self.path = path
self.launch_arguments = launch_arguments
self.game_arguments = game_arguments
self.mangohud = mangohud
self.gamemode = gamemode
self.prefix = prefix
self.disable_hidraw = disable_hidraw
self.protonfix = protonfix
self.runner = runner
self.addapp_checkbox = addapp_checkbox
self.addapp = addapp
self.addapp_bat = addapp_bat
self.banner = banner
self.lossless_enabled = lossless_enabled
self.lossless_multiplier = lossless_multiplier
self.lossless_flow = lossless_flow
self.lossless_performance = lossless_performance
self.lossless_hdr = lossless_hdr
self.lossless_present = lossless_present
self.playtime = playtime
self.hidden = hidden
self.prevent_sleep = prevent_sleep
self.favorite = favorite
self.startup_wm_class = startup_wm_class
class DuplicateDialog(Gtk.Dialog):
def __init__(self, parent, title):
super().__init__(title=_("Duplicate %s") % title, transient_for=parent, modal=True)
self.set_resizable(False)
self.set_icon_from_file(faugus_png)
label_title = Gtk.Label(label=_("Title"))
label_title.set_halign(Gtk.Align.START)
self.entry_title = Gtk.Entry()
self.entry_title.set_tooltip_text(_("Game Title"))
self.entry_title.connect("changed", _validate_text)
button_cancel = Gtk.Button(label=_("Cancel"))
button_cancel.connect("clicked", lambda widget: self.response(Gtk.ResponseType.CANCEL))
button_cancel.set_size_request(150, -1)
button_ok = Gtk.Button(label=_("Ok"))
button_ok.connect("clicked", lambda widget: self.response(Gtk.ResponseType.OK))
button_ok.set_size_request(150, -1)
content_area = self.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(10)
box_top.set_margin_end(10)
box_top.set_margin_top(10)
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_title, True, True, 0)
box_top.pack_start(self.entry_title, True, True, 0)
box_bottom.pack_start(button_cancel, True, True, 0)
box_bottom.pack_start(button_ok, True, True, 0)
content_area.add(box_top)
content_area.add(box_bottom)
self.show_all()
def show_warning_dialog(self, parent, title):
dialog = Gtk.Dialog(title="Faugus Launcher", transient_for=parent, modal=True)
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(title)
label.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.destroy())
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()
class ConfirmationDialog(Gtk.Dialog):
def __init__(self, parent, title, prefix, runner):
super().__init__(title=_("Delete %s") % title, transient_for=parent, modal=True)
self.set_resizable(False)
self.set_icon_from_file(faugus_png)
subprocess.Popen(["canberra-gtk-play", "-f", faugus_notification])
label = Gtk.Label()
label.set_label(_("Are you sure you want to delete %s?") % title)
label.set_halign(Gtk.Align.CENTER)
button_no = Gtk.Button(label=_("No"))
button_no.set_size_request(150, -1)
button_no.connect("clicked", lambda x: self.response(Gtk.ResponseType.NO))
button_yes = Gtk.Button(label=_("Yes"))
button_yes.set_size_request(150, -1)
button_yes.connect("clicked", lambda x: self.response(Gtk.ResponseType.YES))
self.checkbox = Gtk.CheckButton(label=_("Also remove the prefix"))
self.checkbox.set_halign(Gtk.Align.CENTER)
content_area = self.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=20)
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)
if os.path.basename(prefix) != "default" and runner != "Linux-Native" and runner != "Steam":
box_top.pack_start(self.checkbox, True, True, 0)
box_bottom.pack_start(button_no, True, True, 0)
box_bottom.pack_start(button_yes, True, True, 0)
content_area.add(box_top)
content_area.add(box_bottom)
self.show_all()
def get_remove_prefix_state(self):
# Get the state of the checkbox
return self.checkbox.get_active()
class AddGame(Gtk.Dialog):
def __init__(self, parent, game_running2, file_path, interface_mode):
# Initialize the AddGame dialog
super().__init__(title=_("New Game/App"), parent=parent)
self.set_resizable(False)
self.set_modal(True)
self.parent_window = parent
self.set_icon_from_file(faugus_png)
self.interface_mode = interface_mode
self.updated_steam_id = None
self.lossless_enabled = False
self.lossless_multiplier = 1
self.lossless_flow = 100
self.lossless_performance = False
self.lossless_hdr = False
self.lossless_present = False
if not os.path.exists(banners_dir):
os.makedirs(banners_dir)
self.banner_path_temp = os.path.join(banners_dir, "banner_temp.png")
shutil.copyfile(faugus_banner, self.banner_path_temp)
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.png'
self.box = self.get_content_area()
self.box.set_margin_start(0)
self.box.set_margin_end(0)
self.box.set_margin_top(0)
self.box.set_margin_bottom(0)
self.content_area = self.get_content_area()
self.content_area.set_border_width(0)
self.content_area.set_halign(Gtk.Align.CENTER)
self.content_area.set_valign(Gtk.Align.CENTER)
self.content_area.set_vexpand(True)
self.content_area.set_hexpand(True)
box_buttons = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
self.grid_page1 = Gtk.Grid()
self.grid_page1.set_column_homogeneous(True)
self.grid_page1.set_column_spacing(10)
self.grid_page2 = Gtk.Grid()
self.grid_page2.set_column_homogeneous(True)
self.grid_page2.set_column_spacing(10)
self.grid_launcher = Gtk.Grid()
self.grid_launcher.set_row_spacing(10)
self.grid_launcher.set_column_spacing(10)
self.grid_launcher.set_margin_start(10)
self.grid_launcher.set_margin_end(10)
self.grid_launcher.set_margin_top(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_steam_title = Gtk.Grid()
self.grid_steam_title.set_row_spacing(10)
self.grid_steam_title.set_column_spacing(10)
self.grid_steam_title.set_margin_start(10)
self.grid_steam_title.set_margin_end(10)
self.grid_steam_title.set_margin_top(10)
self.grid_path = Gtk.Grid()
self.grid_path.set_row_spacing(10)
self.grid_path.set_column_spacing(10)
self.grid_path.set_margin_start(10)
self.grid_path.set_margin_end(10)
self.grid_path.set_margin_top(10)
self.grid_prefix = Gtk.Grid()
self.grid_prefix.set_row_spacing(10)
self.grid_prefix.set_column_spacing(10)
self.grid_prefix.set_margin_start(10)
self.grid_prefix.set_margin_end(10)
self.grid_prefix.set_margin_top(10)
self.grid_runner = Gtk.Grid()
self.grid_runner.set_row_spacing(10)
self.grid_runner.set_column_spacing(10)
self.grid_runner.set_margin_start(10)
self.grid_runner.set_margin_end(10)
self.grid_runner.set_margin_top(10)
self.grid_shortcut = Gtk.Grid(orientation=Gtk.Orientation.VERTICAL)
self.grid_shortcut.set_row_spacing(10)
self.grid_shortcut.set_column_spacing(10)
self.grid_shortcut.set_margin_start(10)
self.grid_shortcut.set_margin_end(10)
self.grid_shortcut.set_margin_top(10)
self.grid_shortcut.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_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_tools = Gtk.Grid()
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)
css_provider = Gtk.CssProvider()
css = """
.entry {
border-color: Red;
}
.combobox {
border: 1px solid 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.combobox_launcher = Gtk.ComboBoxText()
self.label_steam_title = Gtk.Label(label=_("Title"))
self.label_steam_title.set_halign(Gtk.Align.START)
self.combobox_steam_title = Gtk.ComboBoxText()
FILTER_KEYWORDS = [
"Proton",
"Steam Linux Runtime",
"Steamworks Common Redistributables",
]
for appid, name in read_installed_games():
lname = name.lower()
if any(keyword.lower() in lname for keyword in FILTER_KEYWORDS):
continue
self.combobox_steam_title.append(appid, name)
# Widgets for title
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.connect("changed", _validate_text)
if interface_mode == "Banners":
self.entry_title.connect("focus-out-event", self.on_entry_focus_out)
self.entry_title.set_tooltip_text(_("Game Title"))
self.entry_title.set_has_tooltip(True)
self.entry_title.connect("query-tooltip", self.on_entry_query_tooltip)
# Widgets for path
self.label_path = Gtk.Label(label=_("Path"))
self.label_path.set_halign(Gtk.Align.START)
self.entry_path = Gtk.Entry()
self.entry_path.connect("changed", self.on_entry_changed, self.entry_path)
if file_path:
self.entry_path.set_text(file_path)
self.entry_path.set_tooltip_text(_("/path/to/the/exe"))
self.entry_path.set_has_tooltip(True)
self.entry_path.connect("query-tooltip", self.on_entry_query_tooltip)
self.button_search = Gtk.Button()
self.button_search.set_image(Gtk.Image.new_from_icon_name("system-search-symbolic", Gtk.IconSize.BUTTON))
self.button_search.connect("clicked", self.on_button_search_clicked)
self.button_search.set_size_request(50, -1)
# Widgets for prefix
self.label_prefix = Gtk.Label(label=_("Prefix"))
self.label_prefix.set_halign(Gtk.Align.START)
self.entry_prefix = Gtk.Entry()
self.entry_prefix.connect("changed", self.on_entry_changed, self.entry_prefix)
self.entry_prefix.set_tooltip_text(_("/path/to/the/prefix"))
self.entry_prefix.set_has_tooltip(True)
self.entry_prefix.connect("query-tooltip", self.on_entry_query_tooltip)
self.button_search_prefix = Gtk.Button()
self.button_search_prefix.set_image(Gtk.Image.new_from_icon_name("system-search-symbolic", Gtk.IconSize.BUTTON))
self.button_search_prefix.connect("clicked", self.on_button_search_prefix_clicked)
self.button_search_prefix.set_size_request(50, -1)
# Widgets for runner
self.label_runner = Gtk.Label(label=_("Proton"))
self.label_runner.set_halign(Gtk.Align.START)
self.combobox_runner = Gtk.ComboBoxText()
# Widgets for protonfix
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.entry_protonfix.set_has_tooltip(True)
self.entry_protonfix.connect("query-tooltip", self.on_entry_query_tooltip)
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)
# Widgets for launch arguments
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.entry_launch_arguments.set_has_tooltip(True)
self.entry_launch_arguments.connect("query-tooltip", self.on_entry_query_tooltip)
# Widgets for game arguments
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.entry_game_arguments.set_has_tooltip(True)
self.entry_game_arguments.connect("query-tooltip", self.on_entry_query_tooltip)
# Widgets for lossless scaling
self.button_lossless = Gtk.Button(label=_("Lossless Scaling Frame Generation"))
self.button_lossless.connect("clicked", self.on_button_lossless_clicked)
# Widgets for extra executable
self.checkbox_addapp = Gtk.CheckButton(label=_("Additional Application"))
self.checkbox_addapp.set_tooltip_text(
_("Additional application to run with the game, like Cheat Engine, Trainers, Mods..."))
self.checkbox_addapp.connect("toggled", self.on_checkbox_addapp_toggled)
self.entry_addapp = Gtk.Entry()
self.entry_addapp.set_tooltip_text(_("/path/to/the/app"))
self.entry_addapp.set_has_tooltip(True)
self.entry_addapp.connect("query-tooltip", self.on_entry_query_tooltip)
self.entry_addapp.set_sensitive(False)
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_search_addapp.set_sensitive(False)
# Checkboxes for optional features
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."))
self.checkbox_prevent_sleep = Gtk.CheckButton(label=_("Prevent Sleep"))
# Button for Winecfg
self.button_winecfg = Gtk.Button(label="Winecfg")
self.button_winecfg.set_size_request(120, -1)
self.button_winecfg.connect("clicked", self.on_button_winecfg_clicked)
# Button for Winetricks
self.button_winetricks = Gtk.Button(label="Winetricks")
self.button_winetricks.set_size_request(120, -1)
self.button_winetricks.connect("clicked", self.on_button_winetricks_clicked)
# Button for Run
self.button_run = Gtk.Button(label=_("Run"))
self.button_run.set_size_request(120, -1)
self.button_run.connect("clicked", self.on_button_run_clicked)
self.button_run.set_tooltip_text(_("Run a file inside the prefix"))
# Button for creating shortcut
self.label_shortcut = Gtk.Label(label=_("Shortcut"))
self.label_shortcut.set_margin_start(10)
self.label_shortcut.set_margin_end(10)
self.label_shortcut.set_margin_top(10)
self.label_shortcut.set_halign(Gtk.Align.START)
self.checkbox_shortcut_desktop = Gtk.CheckButton(label=_("Desktop"))
self.checkbox_shortcut_desktop.set_tooltip_text(
_("Add or remove a shortcut from the Desktop."))
self.checkbox_shortcut_appmenu = Gtk.CheckButton(label=_("App Menu"))
self.checkbox_shortcut_appmenu.set_tooltip_text(
_("Add or remove a shortcut from the Application Menu."))
self.checkbox_shortcut_steam = Gtk.CheckButton(label=_("Steam"))
self.checkbox_shortcut_steam.set_tooltip_text(
_("Add or remove a shortcut from Steam. Steam needs to be restarted."))
# Button for selection shortcut icon
self.button_shortcut_icon = Gtk.Button()
self.button_shortcut_icon.set_size_request(120, -1)
self.button_shortcut_icon.connect("clicked", self.on_button_shortcut_icon_clicked)
self.button_shortcut_icon.set_tooltip_text(_("Select an icon for the shortcut"))
# Button Cancel
self.button_cancel = Gtk.Button(label=_("Cancel"))
self.button_cancel.connect("clicked", lambda widget: self.response(Gtk.ResponseType.CANCEL))
self.button_cancel.set_size_request(150, -1)
# Button Ok
self.button_ok = Gtk.Button(label=_("Ok"))
self.button_ok.connect("clicked", lambda widget: self.response(Gtk.ResponseType.OK))
self.button_ok.set_size_request(150, -1)
self.load_config()
self.entry_title.connect("changed", self.update_prefix_entry)
self.notebook = Gtk.Notebook()
self.notebook.set_margin_start(10)
self.notebook.set_margin_end(10)
self.notebook.set_margin_top(10)
self.notebook.set_margin_bottom(10)
# notebook.set_show_border(False)
self.box.add(self.notebook)
self.image_banner = Gtk.Image()
self.image_banner.set_margin_top(10)
self.image_banner.set_margin_bottom(10)
self.image_banner.set_margin_start(10)
self.image_banner.set_margin_end(10)
self.image_banner.set_vexpand(True)
self.image_banner.set_valign(Gtk.Align.CENTER)
self.image_banner2 = Gtk.Image()
self.image_banner2.set_margin_top(10)
self.image_banner2.set_margin_bottom(10)
self.image_banner2.set_margin_start(10)
self.image_banner2.set_margin_end(10)
self.image_banner2.set_vexpand(True)
self.image_banner2.set_valign(Gtk.Align.CENTER)
event_box = Gtk.EventBox()
event_box.add(self.image_banner)
event_box.connect("button-press-event", self.on_image_clicked)
event_box2 = Gtk.EventBox()
event_box2.add(self.image_banner2)
event_box2.connect("button-press-event", self.on_image_clicked)
self.menu = Gtk.Menu()
refresh_item = Gtk.MenuItem(label=_("Refresh"))
refresh_item.connect("activate", self.on_refresh)
self.menu.append(refresh_item)
load_item = Gtk.MenuItem(label=_("Load from file"))
load_item.connect("activate", self.on_load_file)
self.menu.append(load_item)
load_url = Gtk.MenuItem(label=_("Load from URL"))
load_url.connect("activate", self.on_load_url)
self.menu.append(load_url)
self.menu.show_all()
page1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.tab_box1 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
tab_label1 = Gtk.Label(label=_("Game/App"))
tab_label1.set_width_chars(8)
tab_label1.set_xalign(0.5)
self.tab_box1.pack_start(tab_label1, True, True, 0)
self.tab_box1.set_hexpand(True)
self.grid_page1.attach(page1, 0, 0, 1, 1)
if interface_mode == "Banners":
self.grid_page1.attach(event_box, 1, 0, 1, 1)
page1.set_hexpand(True)
event_box.set_hexpand(True)
self.notebook.append_page(self.grid_page1, self.tab_box1)
page2 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.tab_box2 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
tab_label2 = Gtk.Label(label=_("Tools"))
tab_label2.set_width_chars(8)
tab_label2.set_xalign(0.5)
self.tab_box2.pack_start(tab_label2, True, True, 0)
self.tab_box2.set_hexpand(True)
self.grid_page2.attach(page2, 0, 0, 1, 1)
if interface_mode == "Banners":
self.grid_page2.attach(event_box2, 1, 0, 1, 1)
page2.set_hexpand(True)
event_box2.set_hexpand(True)
self.notebook.append_page(self.grid_page2, self.tab_box2)
self.grid_launcher.attach(self.combobox_launcher, 1, 0, 1, 1)
self.combobox_launcher.set_hexpand(True)
self.combobox_launcher.set_valign(Gtk.Align.CENTER)
self.grid_steam_title.attach(self.label_steam_title, 0, 0, 4, 1)
self.grid_steam_title.attach(self.combobox_steam_title, 0, 1, 4, 1)
self.combobox_steam_title.set_hexpand(True)
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_path.attach(self.label_path, 0, 0, 1, 1)
self.grid_path.attach(self.entry_path, 0, 1, 3, 1)
self.entry_path.set_hexpand(True)
self.grid_path.attach(self.button_search, 3, 1, 1, 1)
self.grid_prefix.attach(self.label_prefix, 0, 0, 1, 1)
self.grid_prefix.attach(self.entry_prefix, 0, 1, 3, 1)
self.entry_prefix.set_hexpand(True)
self.grid_prefix.attach(self.button_search_prefix, 3, 1, 1, 1)
self.grid_runner.attach(self.label_runner, 0, 0, 1, 1)
self.grid_runner.attach(self.combobox_runner, 0, 1, 1, 1)
self.combobox_runner.set_hexpand(True)
self.label_shortcut.set_hexpand(True)
self.grid_shortcut.add(self.checkbox_shortcut_desktop)
self.grid_shortcut.add(self.checkbox_shortcut_appmenu)
self.grid_shortcut.add(self.checkbox_shortcut_steam)
self.grid_shortcut_icon.add(self.button_shortcut_icon)
self.grid_shortcut_icon.set_valign(Gtk.Align.CENTER)
self.box_shortcut = Gtk.Box()
self.box_shortcut.pack_start(self.grid_shortcut, False, False, 0)
self.box_shortcut.pack_end(self.grid_shortcut_icon, False, False, 0)
page1.add(self.grid_launcher)
page1.add(self.grid_steam_title)
page1.add(self.grid_title)
page1.add(self.grid_path)
page1.add(self.grid_prefix)
page1.add(self.grid_runner)
page1.add(self.label_shortcut)
page1.add(self.box_shortcut)
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_lossless.attach(self.button_lossless, 0, 0, 1, 1)
self.button_lossless.set_hexpand(True)
self.grid_addapp.attach(self.checkbox_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)
box_buttons.pack_start(self.button_winetricks, True, True, 0)
box_buttons.pack_start(self.button_winecfg, True, True, 0)
box_buttons.pack_start(self.button_run, True, True, 0)
self.grid_tools.attach(self.checkbox_mangohud, 0, 0, 1, 1)
self.checkbox_mangohud.set_hexpand(True)
self.grid_tools.attach(self.checkbox_gamemode, 0, 1, 1, 1)
self.checkbox_gamemode.set_hexpand(True)
self.grid_tools.attach(self.checkbox_prevent_sleep, 0, 2, 1, 1)
self.checkbox_prevent_sleep.set_hexpand(True)
self.grid_tools.attach(self.checkbox_disable_hidraw, 0, 3, 1, 1)
self.checkbox_disable_hidraw.set_hexpand(True)
self.grid_tools.attach(box_buttons, 2, 0, 1, 4)
page2.add(self.grid_protonfix)
page2.add(self.grid_launch_arguments)
page2.add(self.grid_game_arguments)
page2.add(self.grid_addapp)
page2.add(self.grid_lossless)
page2.add(self.grid_tools)
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)
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.box.add(bottom_box)
self.populate_combobox_with_launchers()
self.combobox_launcher.set_active(0)
self.combobox_launcher.connect("changed", self.on_combobox_changed)
self.combobox_steam_title.connect("changed", self.on_combobox_steam_changed)
self.populate_combobox_with_runners()
model = self.combobox_runner.get_model()
index_to_activate = 0
self.default_runner = convert_runner(self.default_runner)
for i, row in enumerate(model):
if row[0] == self.default_runner:
index_to_activate = i
break
self.combobox_runner.set_active(index_to_activate)
self.checkbox_mangohud.set_active(self.default_mangohud)
self.checkbox_gamemode.set_active(self.default_gamemode)
self.checkbox_prevent_sleep.set_active(self.default_prevent_sleep)
self.checkbox_disable_hidraw.set_active(self.default_disable_hidraw)
# Check if optional features are available and enable/disable accordingly
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."))
self.updated_steam_id = detect_steam_id()
if not self.updated_steam_id:
self.checkbox_shortcut_steam.set_sensitive(False)
self.checkbox_shortcut_steam.set_tooltip_text(
_("Add or remove a shortcut from Steam. Steam needs to be restarted. NO STEAM USERS FOUND."))
self.lossless_location = ConfigManager().config.get('lossless-location', '')
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."))
self.button_shortcut_icon.set_image(self.set_image_shortcut_icon())
self.tab_box1.show_all()
self.tab_box2.show_all()
self.show_all()
self.grid_steam_title.set_visible(False)
if interface_mode != "Banners":
self.image_banner.set_visible(False)
self.image_banner2.set_visible(False)
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(self.banner_path_temp, 260, 390, True)
self.image_banner.set_from_pixbuf(pixbuf)
self.image_banner2.set_from_pixbuf(pixbuf)
def on_combobox_steam_changed(self, combobox):
self.combobox_steam_title.get_style_context().remove_class("combobox")
title = self.combobox_steam_title.get_active_text()
steamid = self.combobox_steam_title.get_active_id()
if not title or not steamid:
return
self.entry_title.set_text(title)
self.entry_path.set_text(steamid)
icon_path = get_steam_icon_path(steamid)
if not icon_path:
icon_path = faugus_png
self.on_entry_focus_out(self.entry_title, None)
shutil.copyfile(icon_path, 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)
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
present = val if (val := getattr(self, "lossless_present", False)) != "" else "VSync/FIFO (default)"
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)
label_present = Gtk.Label(label=_("Present Mode (Experimental)"))
label_present.set_halign(Gtk.Align.START)
combobox_present = Gtk.ComboBoxText()
combobox_present.set_tooltip_text(_("Override the present mode."))
options = [
"VSync/FIFO (default)",
"Mailbox",
"Immediate",
]
for opt in options:
combobox_present.append_text(opt)
mapping = {
"fifo": "VSync/FIFO (default)",
"mailbox": "Mailbox",
"immediate": "Immediate",
}
ui_value = mapping.get(present, "VSync/FIFO (default)")
combobox_present.set_active(options.index(ui_value))
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)
label_present.set_sensitive(active)
combobox_present.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)
grid.attach(label_present, 0, 7, 1, 1)
grid.attach(combobox_present, 0, 8, 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()
present = combobox_present.get_active_text()
mapping = {
"VSync/FIFO (default)": "fifo",
"Mailbox": "mailbox",
"Immediate": "immediate",
}
self.lossless_present = mapping.get(present, "fifo")
dialog.destroy()
return response
def check_steam_shortcut(self, title):
if os.path.exists(steam_shortcuts_path):
try:
with open(steam_shortcuts_path, 'rb') as f:
shortcuts = vdf.binary_load(f)
for game in shortcuts["shortcuts"].values():
if isinstance(game, dict) and "AppName" in game and game["AppName"] == title:
return True
return False
except SyntaxError:
return False
return False
def on_entry_query_tooltip(self, widget, x, y, keyboard_mode, tooltip):
current_text = widget.get_text()
if current_text.strip():
tooltip.set_text(current_text)
else:
tooltip.set_text(widget.get_tooltip_text())
return True
def on_image_clicked(self, widget, event):
self.menu.popup_at_pointer(event)
def on_refresh(self, widget):
if self.entry_title.get_text() != "":
self.get_banner()
else:
shutil.copyfile(faugus_banner, self.banner_path_temp)
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(self.banner_path_temp, 260, 390, True)
self.image_banner.set_from_pixbuf(pixbuf)
self.image_banner2.set_from_pixbuf(pixbuf)
def on_load_file(self, widget):
self.set_sensitive(False)
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(
title=_("Select an image for the banner"),
transient_for=self,
action=Gtk.FileChooserAction.OPEN,
accept_label=_("Open"),
cancel_label=_("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("*.jxl")
filter_ico.add_pattern("*.bmp")
filter_ico.add_pattern("*.gif")
filter_ico.add_pattern("*.svg")
filechooser.add_filter(filter_ico)
response = filechooser.run()
if response == Gtk.ResponseType.ACCEPT:
file_path = filechooser.get_filename()
if not file_path or not is_valid_image(file_path):
self.show_invalid_image_dialog()
else:
shutil.copyfile(file_path, self.banner_path_temp)
self.update_image_banner()
filechooser.destroy()
self.set_sensitive(True)
def on_load_url(self, widget):
self.set_sensitive(False)
dialog = Gtk.Dialog(
title=_("Enter the image URL"),
transient_for=self,
modal=True
)
dialog.set_resizable(False)
dialog.set_icon_from_file(faugus_png)
entry = Gtk.Entry()
entry.set_tooltip_text("https://example.com/banner.png")
button_ok = Gtk.Button(label=_("Ok"))
button_ok.set_size_request(120, -1)
button_ok.connect("clicked", lambda x: dialog.response(Gtk.ResponseType.OK))
button_cancel = Gtk.Button(label=_("Cancel"))
button_cancel.set_size_request(120, -1)
button_cancel.connect("clicked", lambda x: dialog.response(Gtk.ResponseType.CANCEL))
box_top = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
box_top.set_margin_start(10)
box_top.set_margin_end(10)
box_top.set_margin_top(10)
box_top.set_margin_bottom(10)
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(entry, False, False, 0)
box_bottom.pack_start(button_cancel, True, True, 0)
box_bottom.pack_start(button_ok, True, True, 0)
dialog.get_content_area().add(box_top)
dialog.get_content_area().add(box_bottom)
dialog.show_all()
while True:
response = dialog.run()
if response == Gtk.ResponseType.CANCEL:
break
if response == Gtk.ResponseType.OK:
url = entry.get_text().strip().replace(" ", "%20")
valid_exts = (".png", ".jpg", ".jpeg", ".bmp", ".gif", ".svg")
if not url.lower().endswith(valid_exts):
self.show_invalid_image_dialog()
dialog.show_all()
continue
try:
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req) as r, open(self.banner_path_temp, "wb") as f:
f.write(r.read())
self.update_image_banner()
break
except Exception:
self.show_invalid_image_dialog()
dialog.show_all()
continue
dialog.destroy()
self.set_sensitive(True)
def show_invalid_image_dialog(self):
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=_("The selected file is not a valid image."))
label.set_halign(Gtk.Align.CENTER)
label2 = Gtk.Label(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()
def get_banner(self):
def fetch_banner():
game_name = self.entry_title.get_text().strip()
if not game_name:
return
api_url = f"https://steamgrid.usebottles.com/api/search/{game_name}"
try:
response = requests.get(api_url)
response.raise_for_status()
image_url = response.text.strip('"')
with open(self.banner_path_temp, "wb") as image_file:
image_file.write(requests.get(image_url).content)
GLib.idle_add(self.update_image_banner)
except requests.RequestException as e:
print(f"Error fetching the banner: {e}")
# Start the thread
threading.Thread(target=fetch_banner, daemon=True).start()
def update_image_banner(self):
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(self.banner_path_temp, 260, 390, True)
self.image_banner.set_from_pixbuf(pixbuf)
self.image_banner2.set_from_pixbuf(pixbuf)
def on_entry_focus_out(self, entry_title, event):
if entry_title.get_text() != "":
self.get_banner()
else:
shutil.copyfile(faugus_banner, self.banner_path_temp)
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(self.banner_path_temp, 260, 390, True)
self.image_banner.set_from_pixbuf(pixbuf)
self.image_banner2.set_from_pixbuf(pixbuf)
def on_checkbox_addapp_toggled(self, checkbox):
is_active = checkbox.get_active()
self.entry_addapp.set_sensitive(is_active)
self.button_search_addapp.set_sensitive(is_active)
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"),
)
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:
selected_file = filechooser.get_filename()
if selected_file:
self.entry_addapp.set_text(selected_file)
filechooser.destroy()
def on_combobox_changed(self, combobox):
active_index = combobox.get_active()
def cleanup_fields():
self.entry_title.set_text("")
self.entry_launch_arguments.set_text("")
self.entry_path.set_text("")
self.entry_prefix.set_text("")
self.checkbox_shortcut_desktop.set_active(False)
self.checkbox_shortcut_appmenu.set_active(False)
self.checkbox_shortcut_steam.set_active(False)
self.entry_protonfix.set_text("")
self.entry_launch_arguments.set_text("")
self.entry_game_arguments.set_text("")
self.checkbox_addapp.set_active(False)
self.entry_addapp.set_text("")
self.checkbox_mangohud.set_active(self.default_mangohud)
self.checkbox_gamemode.set_active(self.default_gamemode)
self.checkbox_disable_hidraw.set_active(self.default_disable_hidraw)
self.checkbox_prevent_sleep.set_active(self.default_prevent_sleep)
self.lossless_enabled = False
self.lossless_multiplier = 1
self.lossless_flow = 100
self.lossless_performance = False
self.lossless_hdr = False
self.lossless_present = "VSync/FIFO"
self.combobox_steam_title.get_style_context().remove_class("combobox")
self.entry_title.get_style_context().remove_class("entry")
self.entry_prefix.get_style_context().remove_class("entry")
self.entry_path.get_style_context().remove_class("entry")
cleanup_fields()
if active_index == 0:
self.grid_title.set_visible(True)
self.grid_steam_title.set_visible(False)
self.grid_path.set_visible(True)
self.grid_runner.set_visible(True)
self.grid_prefix.set_visible(True)
self.button_winetricks.set_visible(True)
self.button_winecfg.set_visible(True)
self.button_run.set_visible(True)
self.grid_protonfix.set_visible(True)
self.grid_addapp.set_visible(True)
self.checkbox_disable_hidraw.set_visible(True)
self.checkbox_prevent_sleep.set_visible(True)
self.button_shortcut_icon.set_image(self.set_image_shortcut_icon())
self.checkbox_shortcut_steam.set_visible(True)
self.grid_page2.set_visible(True)
self.tab_box2.set_visible(True)
self.notebook.set_show_tabs(True)
if active_index == 1:
self.grid_title.set_visible(True)
self.grid_steam_title.set_visible(False)
self.grid_path.set_visible(True)
self.grid_runner.set_visible(False)
self.grid_prefix.set_visible(False)
self.button_winetricks.set_visible(False)
self.button_winecfg.set_visible(False)
self.button_run.set_visible(False)
self.grid_protonfix.set_visible(False)
self.grid_addapp.set_visible(False)
self.checkbox_disable_hidraw.set_visible(False)
self.checkbox_disable_hidraw.set_active(False)
self.checkbox_prevent_sleep.set_visible(True)
self.button_shortcut_icon.set_image(self.set_image_shortcut_icon())
self.checkbox_shortcut_steam.set_visible(True)
self.grid_page2.set_visible(True)
self.tab_box2.set_visible(True)
self.notebook.set_show_tabs(True)
if active_index == 2:
self.grid_title.set_visible(False)
self.grid_steam_title.set_visible(True)
self.grid_path.set_visible(False)
self.grid_runner.set_visible(False)
self.grid_prefix.set_visible(False)
self.button_winetricks.set_visible(False)
self.button_winecfg.set_visible(False)
self.button_run.set_visible(False)
self.grid_protonfix.set_visible(False)
self.grid_addapp.set_visible(False)
self.checkbox_disable_hidraw.set_visible(False)
self.checkbox_disable_hidraw.set_active(False)
self.checkbox_prevent_sleep.set_visible(True)
self.button_shortcut_icon.set_image(self.set_image_shortcut_icon())
self.checkbox_shortcut_steam.set_visible(False)
self.grid_page2.set_visible(False)
self.tab_box2.set_visible(False)
self.notebook.set_show_tabs(False)
elif active_index == 3:
self.grid_title.set_visible(False)
self.grid_steam_title.set_visible(False)
self.grid_path.set_visible(False)
self.grid_runner.set_visible(True)
self.grid_prefix.set_visible(True)
self.button_winetricks.set_visible(True)
self.button_winecfg.set_visible(True)
self.button_run.set_visible(True)
self.grid_protonfix.set_visible(True)
self.grid_addapp.set_visible(False)
self.checkbox_disable_hidraw.set_visible(True)
self.checkbox_prevent_sleep.set_visible(True)
self.entry_launch_arguments.set_text("WINE_SIMULATE_WRITECOPY=1 PROTON_ENABLE_WAYLAND=0")
self.entry_title.set_text(self.combobox_launcher.get_active_text())
self.entry_path.set_text(
f"{self.entry_prefix.get_text()}/drive_c/Program Files (x86)/Battle.net/Battle.net.exe")
shutil.copyfile(battle_icon, 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)
self.checkbox_shortcut_steam.set_visible(True)
self.grid_page2.set_visible(True)
self.tab_box2.set_visible(True)
self.notebook.set_show_tabs(True)
elif active_index == 4:
self.grid_title.set_visible(False)
self.grid_steam_title.set_visible(False)
self.grid_path.set_visible(False)
self.grid_runner.set_visible(True)
self.grid_prefix.set_visible(True)
self.button_winetricks.set_visible(True)
self.button_winecfg.set_visible(True)
self.button_run.set_visible(True)
self.grid_protonfix.set_visible(True)
self.grid_addapp.set_visible(False)
self.checkbox_disable_hidraw.set_visible(True)
self.checkbox_prevent_sleep.set_visible(True)
self.entry_launch_arguments.set_text("PROTON_ENABLE_WAYLAND=0")
self.entry_title.set_text(self.combobox_launcher.get_active_text())
self.entry_path.set_text(
f"{self.entry_prefix.get_text()}/drive_c/Program Files/Electronic Arts/EA Desktop/EA Desktop/EALauncher.exe")
shutil.copyfile(ea_icon, 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)
self.checkbox_shortcut_steam.set_visible(True)
self.grid_page2.set_visible(True)
self.tab_box2.set_visible(True)
self.notebook.set_show_tabs(True)
elif active_index == 5:
self.grid_title.set_visible(False)
self.grid_steam_title.set_visible(False)
self.grid_path.set_visible(False)
self.grid_runner.set_visible(True)
self.grid_prefix.set_visible(True)
self.button_winetricks.set_visible(True)
self.button_winecfg.set_visible(True)
self.button_run.set_visible(True)
self.grid_protonfix.set_visible(True)
self.grid_addapp.set_visible(False)
self.checkbox_disable_hidraw.set_visible(True)
self.checkbox_prevent_sleep.set_visible(True)
self.entry_title.set_text(self.combobox_launcher.get_active_text())
self.entry_path.set_text(
f"{self.entry_prefix.get_text()}/drive_c/Program Files/Epic Games/Launcher/Portal/Binaries/Win64/EpicGamesLauncher.exe")
shutil.copyfile(epic_icon, 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)
self.checkbox_shortcut_steam.set_visible(True)
self.grid_page2.set_visible(True)
self.tab_box2.set_visible(True)
self.notebook.set_show_tabs(True)
elif active_index == 6:
self.grid_title.set_visible(False)
self.grid_steam_title.set_visible(False)
self.grid_path.set_visible(False)
self.grid_runner.set_visible(True)
self.grid_prefix.set_visible(True)
self.button_winetricks.set_visible(True)
self.button_winecfg.set_visible(True)
self.button_run.set_visible(True)
self.grid_protonfix.set_visible(True)
self.grid_addapp.set_visible(False)
self.checkbox_disable_hidraw.set_visible(True)
self.checkbox_prevent_sleep.set_visible(True)
self.entry_launch_arguments.set_text("PROTON_ENABLE_WAYLAND=0")
self.entry_title.set_text(self.combobox_launcher.get_active_text())
self.entry_path.set_text(
f"{self.entry_prefix.get_text()}/drive_c/Program Files (x86)/Ubisoft/Ubisoft Game Launcher/UbisoftConnect.exe")
shutil.copyfile(ubisoft_icon, 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)
self.checkbox_shortcut_steam.set_visible(True)
self.grid_page2.set_visible(True)
self.tab_box2.set_visible(True)
self.notebook.set_show_tabs(True)
elif active_index == 7:
self.grid_title.set_visible(False)
self.grid_steam_title.set_visible(False)
self.grid_path.set_visible(False)
self.grid_runner.set_visible(True)
self.grid_prefix.set_visible(True)
self.button_winetricks.set_visible(True)
self.button_winecfg.set_visible(True)
self.button_run.set_visible(True)
self.grid_protonfix.set_visible(True)
self.grid_addapp.set_visible(False)
self.checkbox_disable_hidraw.set_visible(True)
self.checkbox_prevent_sleep.set_visible(True)
self.entry_launch_arguments.set_text("PROTON_ENABLE_WAYLAND=0")
self.entry_title.set_text(self.combobox_launcher.get_active_text())
self.entry_path.set_text(
f"{self.entry_prefix.get_text()}/drive_c/Program Files/Rockstar Games/Launcher/Launcher.exe")
shutil.copyfile(rockstar_icon, 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)
self.checkbox_shortcut_steam.set_visible(True)
self.grid_page2.set_visible(True)
self.tab_box2.set_visible(True)
self.notebook.set_show_tabs(True)
if self.interface_mode == "Banners":
if self.entry_title.get_text() != "":
self.get_banner()
else:
shutil.copyfile(faugus_banner, self.banner_path_temp)
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(self.banner_path_temp, 260, 390, True)
self.image_banner.set_from_pixbuf(pixbuf)
self.image_banner2.set_from_pixbuf(pixbuf)
def populate_combobox_with_launchers(self):
self.combobox_launcher.append_text(_("Windows Game"))
self.combobox_launcher.append_text(_("Linux Game"))
self.combobox_launcher.append_text(_("Steam Game"))
self.combobox_launcher.append_text("Battle.net")
self.combobox_launcher.append_text("EA App")
self.combobox_launcher.append_text("Epic Games")
self.combobox_launcher.append_text("Ubisoft Connect") # self.combobox_launcher.append_text("HoYoPlay")
self.combobox_launcher.append_text("Rockstar Launcher")
def populate_combobox_with_runners(self):
# List of default entries
self.combobox_runner.append_text("GE-Proton Latest (default)")
self.combobox_runner.append_text("UMU-Proton Latest")
self.combobox_runner.append_text("Proton-EM Latest")
if os.path.exists("/usr/share/steam/compatibilitytools.d/proton-cachyos-slr/"):
self.combobox_runner.append_text("Proton-CachyOS")
# Path to the directory containing the folders
if IS_FLATPAK:
runner_path = Path(os.path.expanduser("~/.local/share/Steam/compatibilitytools.d"))
else:
runner_path = f'{share_dir}/Steam/compatibilitytools.d/'
try:
# Check if the directory exists
if os.path.exists(runner_path):
# List to hold version directories
versions = []
# Iterate over the folders in the directory
for entry in os.listdir(runner_path):
entry_path = os.path.join(runner_path, entry)
# Add to list only if it's a directory and not "UMU-Latest"
if (
os.path.isdir(entry_path)
and entry not in ("UMU-Latest", "LegacyRuntime")
and not entry.startswith("Proton-GE Latest")
and not entry.startswith("Proton-EM Latest")
):
versions.append(entry)
# Sort versions in descending order
def version_key(v):
# Remove 'GE-Proton' and split the remaining part into segments of digits and non-digits
v_parts = re.split(r'(\d+)', v.replace('GE-Proton', ''))
# Convert numeric parts to integers for proper sorting
return [int(part) if part.isdigit() else part for part in v_parts]
versions.sort(key=version_key, reverse=True)
# Add sorted versions to ComboBox
for version in versions:
self.combobox_runner.append_text(version)
except Exception as e:
print(f"Error accessing the directory: {e}")
# Set the active item, if desired
self.combobox_runner.set_active(0)
cell_renderer = self.combobox_runner.get_cells()[0]
cell_renderer.set_property("ellipsize", Pango.EllipsizeMode.END)
cell_renderer.set_property("max-width-chars", 20)
def on_entry_changed(self, widget, entry):
if entry.get_text():
entry.get_style_context().remove_class("entry")
def load_config(self):
cfg = ConfigManager()
self.default_runner = cfg.config.get('default-runner', '')
self.default_prefix = cfg.config.get('default-prefix', '')
self.default_mangohud = cfg.config.get('mangohud') == 'True'
self.default_gamemode = cfg.config.get('gamemode') == 'True'
self.default_disable_hidraw = cfg.config.get('disable-hidraw') == 'True'
self.default_prevent_sleep = cfg.config.get('prevent-sleep') == 'True'
def on_button_run_clicked(self, widget):
self.set_sensitive(False)
validation_result = self.validate_fields(entry="prefix")
if not validation_result:
self.set_sensitive(True)
return
filechooser = Gtk.FileChooserNative(
title=_("Select a file to run inside the prefix"),
transient_for=self,
action=Gtk.FileChooserAction.OPEN,
accept_label=_("Open"),
cancel_label=_("Cancel"),
)
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:
file_run = filechooser.get_filename()
title = self.entry_title.get_text()
prefix = self.entry_prefix.get_text()
title_formatted = format_title(title)
runner = self.combobox_runner.get_active_text()
escaped_file_run = file_run.replace("'", "'\\''")
runner = convert_runner(runner)
command_parts = []
if title_formatted:
command_parts.append(f"LOG_DIR={title_formatted}")
if prefix:
command_parts.append(f"WINEPREFIX='{prefix}'")
if title_formatted:
command_parts.append(f"GAMEID={title_formatted}")
if runner:
if runner == "Proton-CachyOS":
command_parts.append(f"PROTONPATH='{proton_cachyos}'")
else:
command_parts.append(f"PROTONPATH='{runner}'")
if escaped_file_run.endswith(".reg"):
command_parts.append(f"'{umu_run}' regedit '{escaped_file_run}'")
else:
command_parts.append(f"'{umu_run}' '{escaped_file_run}'")
command = ' '.join(command_parts)
print(command)
faugus_run_path = faugus_run
def run_command():
process = subprocess.Popen([sys.executable, faugus_run_path, command])
process.wait()
GLib.idle_add(self.set_sensitive, True)
GLib.idle_add(self.parent_window.set_sensitive, True)
GLib.idle_add(self.blocking_window.destroy)
self.blocking_window = Gtk.Window()
self.blocking_window.set_transient_for(self.parent_window)
self.blocking_window.set_decorated(False)
self.blocking_window.set_modal(True)
command_thread = threading.Thread(target=run_command)
command_thread.start()
else:
self.set_sensitive(True)
filechooser.destroy()
def on_button_search_protonfix_clicked(self, widget):
webbrowser.open("https://umu.openwinecomponents.org/")
def set_image_shortcut_icon(self):
image_path = faugus_png
shutil.copyfile(image_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)
return image
def on_button_shortcut_icon_clicked(self, widget):
self.set_sensitive(False)
validation_result = self.validate_fields(entry="path")
if not validation_result:
self.set_sensitive(True)
return
path = self.entry_path.get_text()
if os.path.isfile(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("*.jxl")
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 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 update_preview(self, dialog):
if file_path := dialog.get_preview_filename():
try:
# Create an image widget for the thumbnail
pixbuf = GdkPixbuf.Pixbuf.new_from_file(file_path)
# Resize the thumbnail if it's too large, maintaining the aspect ratio
max_width = 400
max_height = 400
width = pixbuf.get_width()
height = pixbuf.get_height()
if width > max_width or height > max_height:
# Calculate the new width and height while maintaining the aspect ratio
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 check_existing_shortcut(self):
# Check if the shortcut already exists and mark or unmark the checkbox
title = self.entry_title.get_text().strip()
if not title:
return # If there's no title, there's no shortcut to check
title_formatted = format_title(title)
desktop_file_path = f"{desktop_dir}/{title_formatted}.desktop"
applications_shortcut_path = f"{app_dir}/{title_formatted}.desktop"
# Mark the checkbox if the shortcut exists
self.checkbox_shortcut_desktop.set_active(os.path.exists(desktop_file_path))
self.checkbox_shortcut_appmenu.set_active(os.path.exists(applications_shortcut_path))
def update_prefix_entry(self, entry):
# Update the prefix entry based on the title and self.default_prefix
title_formatted = format_title(entry.get_text())
prefix = os.path.expanduser(self.default_prefix) + "/" + title_formatted
self.entry_prefix.set_text(prefix)
def on_button_winecfg_clicked(self, widget):
self.set_sensitive(False)
# Handle the click event of the Winetricks button
validation_result = self.validate_fields(entry="prefix")
if not validation_result:
self.set_sensitive(True)
return
title = self.entry_title.get_text()
prefix = self.entry_prefix.get_text()
title_formatted = format_title(title)
runner = self.combobox_runner.get_active_text()
runner = convert_runner(runner)
command_parts = []
# Add command parts if they are not empty
if title_formatted:
command_parts.append(f"LOG_DIR='{title_formatted}'")
if prefix:
command_parts.append(f"WINEPREFIX='{prefix}'")
if title_formatted:
command_parts.append(f"GAMEID={title_formatted}")
if runner:
if runner == "Proton-CachyOS":
command_parts.append(f"PROTONPATH='{proton_cachyos}'")
else:
command_parts.append(f"PROTONPATH='{runner}'")
# Add the fixed command and remaining arguments
command_parts.append(f"'{umu_run}'")
command_parts.append("'winecfg'")
# Join all parts into a single command
command = ' '.join(command_parts)
print(command)
# faugus-run path
faugus_run_path = faugus_run
def run_command():
process = subprocess.Popen([sys.executable, faugus_run_path, command])
process.wait()
GLib.idle_add(self.set_sensitive, True)
GLib.idle_add(self.parent_window.set_sensitive, True)
GLib.idle_add(self.blocking_window.destroy)
self.blocking_window = Gtk.Window()
self.blocking_window.set_transient_for(self.parent_window)
self.blocking_window.set_decorated(False)
self.blocking_window.set_modal(True)
command_thread = threading.Thread(target=run_command)
command_thread.start()
def on_button_winetricks_clicked(self, widget):
self.set_sensitive(False)
# Handle the click event of the Winetricks button
validation_result = self.validate_fields(entry="prefix")
if not validation_result:
self.set_sensitive(True)
return
title = self.entry_title.get_text()
prefix = self.entry_prefix.get_text()
title_formatted = format_title(title)
runner = self.combobox_runner.get_active_text()
runner = convert_runner(runner)
command_parts = []
# Add command parts if they are not empty
if title_formatted:
command_parts.append(f"LOG_DIR={title_formatted}")
if prefix:
command_parts.append(f"WINEPREFIX='{prefix}'")
command_parts.append(f"GAMEID=winetricks-gui")
command_parts.append(f"STORE=none")
if runner:
if runner == "Proton-CachyOS":
command_parts.append(f"PROTONPATH='{proton_cachyos}'")
else:
command_parts.append(f"PROTONPATH='{runner}'")
# Add the fixed command and remaining arguments
command_parts.append(f"'{umu_run}'")
command_parts.append("''")
# Join all parts into a single command
command = ' '.join(command_parts)
print(command)
# faugus-run path
faugus_run_path = faugus_run
def run_command():
process = subprocess.Popen([sys.executable, faugus_run_path, command, "winetricks"])
process.wait()
GLib.idle_add(self.set_sensitive, True)
GLib.idle_add(self.parent_window.set_sensitive, True)
GLib.idle_add(self.blocking_window.destroy)
self.blocking_window = Gtk.Window()
self.blocking_window.set_transient_for(self.parent_window)
self.blocking_window.set_decorated(False)
self.blocking_window.set_modal(True)
command_thread = threading.Thread(target=run_command)
command_thread.start()
def on_button_search_clicked(self, widget):
if not self.entry_path.get_text():
initial_folder = os.path.expanduser("~/")
else:
initial_folder = os.path.dirname(self.entry_path.get_text())
filechooser = Gtk.FileChooserNative(
title=_("Select the game's .exe"),
transient_for=self,
action=Gtk.FileChooserAction.OPEN,
accept_label=_("Open"),
cancel_label=_("Cancel"),
)
filechooser.set_current_folder(initial_folder)
if self.combobox_launcher.get_active() != 1:
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:
path = filechooser.get_filename()
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)
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}")
self.entry_path.set_text(path)
if os.path.isdir(self.icon_directory):
shutil.rmtree(self.icon_directory)
filechooser.destroy()
def on_button_search_prefix_clicked(self, widget):
filechooser = Gtk.FileChooserNative(
title=_("Select a prefix location"),
transient_for=self,
action=Gtk.FileChooserAction.SELECT_FOLDER,
accept_label=_("Open"),
cancel_label=_("Cancel"),
)
if not self.entry_prefix.get_text():
filechooser.set_current_folder(os.path.expanduser(self.default_prefix))
else:
filechooser.set_current_folder(self.entry_prefix.get_text())
response = filechooser.run()
if response == Gtk.ResponseType.ACCEPT:
new_prefix = filechooser.get_filename()
self.default_prefix = new_prefix
self.entry_prefix.set_text(self.default_prefix)
filechooser.destroy()
def validate_fields(self, entry):
# Validate the input fields for title, prefix and path
title = self.entry_title.get_text()
prefix = self.entry_prefix.get_text()
path = self.entry_path.get_text()
combobox_steam = self.combobox_steam_title.get_active_text()
self.combobox_steam_title.get_style_context().remove_class("combobox")
self.entry_title.get_style_context().remove_class("entry")
self.entry_prefix.get_style_context().remove_class("entry")
self.entry_path.get_style_context().remove_class("entry")
if self.grid_steam_title.get_visible():
if not combobox_steam:
self.combobox_steam_title.get_style_context().add_class("combobox")
self.notebook.set_current_page(0)
if entry == "prefix":
if not title or not prefix:
if not title:
self.entry_title.get_style_context().add_class("entry")
self.notebook.set_current_page(0)
if not prefix:
self.entry_prefix.get_style_context().add_class("entry")
self.notebook.set_current_page(0)
return False
if entry == "path":
if not title or not path:
if not title:
self.entry_title.get_style_context().add_class("entry")
self.notebook.set_current_page(0)
if not path:
self.entry_path.get_style_context().add_class("entry")
self.notebook.set_current_page(0)
return False
if entry == "path+prefix":
if not title or not path or not prefix:
if not title:
self.entry_title.get_style_context().add_class("entry")
self.notebook.set_current_page(0)
if not path:
self.entry_path.get_style_context().add_class("entry")
self.notebook.set_current_page(0)
if not prefix:
self.entry_prefix.get_style_context().add_class("entry")
self.notebook.set_current_page(0)
return False
return True
def run_file(file_path):
cfg = ConfigManager()
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'
prevent_sleep = cfg.config.get('prevent-sleep', 'False') == 'True'
default_runner = cfg.config.get('default-runner', '').strip('"')
if file_path.endswith(".reg"):
mangohud = False
gamemode = False
disable_hidraw = False
prevent_sleep = False
file_dir = os.path.dirname(os.path.abspath(file_path))
command_parts = []
if disable_hidraw:
command_parts.append("PROTON_DISABLE_HIDRAW=1")
if prevent_sleep:
command_parts.append("PREVENT_SLEEP=1")
command_parts.append(os.path.expanduser(f'WINEPREFIX="{default_prefix}/default"'))
command_parts.append("GAMEID=default")
if default_runner:
if default_runner == "Proton-CachyOS":
command_parts.append(f'PROTONPATH="{proton_cachyos}"')
else:
command_parts.append(f'PROTONPATH="{default_runner}"')
if gamemode:
command_parts.append("gamemoderun")
if mangohud:
command_parts.append("mangohud")
command_parts.append(f'"{umu_run}"')
if file_path.endswith(".reg"):
command_parts.append(f'regedit "{file_path}"')
else:
command_parts.append(f'"{file_path}"')
command = ' '.join(command_parts)
subprocess.run([faugus_run, command], cwd=file_dir)
def update_games_and_config():
if os.path.exists(games_json):
try:
with open(games_json, "r", encoding="utf-8") as f:
games = json.load(f)
except json.JSONDecodeError:
games = []
changed = False
for game in games:
title = game.get("title", "")
if title:
formatted = format_title(title)
if game.get("gameid") != formatted:
game["gameid"] = formatted
changed = True
if game.get("playtime", "") == "":
game["playtime"] = 0
changed = True
runner = game.get("runner")
if runner == "Proton-EM":
game["runner"] = "Proton-EM Latest"
changed = True
elif runner == "GE-Proton":
game["runner"] = "Proton-GE Latest"
changed = True
if title == "Battle.net" and game.get("startup_wm_class", "") != "battle.net.exe":
game["startup_wm_class"] = "battle.net.exe"
changed = True
if changed:
with open(games_json, "w", encoding="utf-8") as f:
json.dump(games, f, indent=4, ensure_ascii=False)
config_path = Path(PathManager.user_config("faugus-launcher/config.ini"))
if not config_path.exists():
return
lines = config_path.read_text(encoding="utf-8").splitlines()
new_lines = []
changed = False
for line in lines:
if line.startswith("default-runner="):
value = line.split("=", 1)[1].strip('"')
if value == "GE-Proton":
line = 'default-runner="Proton-GE Latest"'
changed = True
elif value == "Proton-EM":
line = 'default-runner="Proton-EM Latest"'
changed = True
new_lines.append(line)
if changed:
config_path.write_text("\n".join(new_lines) + "\n", encoding="utf-8")
def faugus_launcher():
os.environ["GTK_USE_PORTAL"] = "1"
apply_dark_theme()
if len(sys.argv) == 1:
app = Main()
app.connect("destroy", app.on_destroy)
Gtk.main()
elif len(sys.argv) == 2:
if sys.argv[1] == "--hide":
app = Main()
app.hide()
app.connect("destroy", app.on_destroy)
Gtk.main()
else:
print("Invalid arguments")
def main():
app = FaugusApp()
app.run(sys.argv)
if __name__ == "__main__":
update_games_and_config()
main()