Files
faugus-launcher/faugus-launcher.py
T
2024-12-16 13:38:14 -03:00

5162 lines
218 KiB
Python

#!/usr/bin/python3
import os
import re
import shutil
import signal
import subprocess
import sys
import threading
import webbrowser
import gi
import socket
import urllib.request
import json
import requests
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
gi.require_version('AppIndicator3', '0.1')
from gi.repository import Gtk, Gdk, GdkPixbuf, GLib, AppIndicator3, Gio
from PIL import Image
xdg_data_dirs = os.getenv('XDG_DATA_DIRS', '/usr/local/share:/usr/share')
data_dirs = xdg_data_dirs.split(':')
share_dir_system = data_dirs[-1]
faugus_banner = '/usr/share/faugus-launcher/faugus-banner.png'
config_dir = os.getenv('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
faugus_launcher_dir = f'{config_dir}/faugus-launcher'
prefixes_dir = f'{faugus_launcher_dir}/prefixes'
icons_dir = f'{faugus_launcher_dir}/icons'
banners_dir = f'{faugus_launcher_dir}/banners'
config_file_dir = f'{faugus_launcher_dir}/config.ini'
share_dir = os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share'))
app_dir = f'{share_dir}/applications'
faugus_png = "/usr/share/icons/hicolor/256x256/apps/faugus-launcher.png"
tray_icon = "/usr/share/icons/hicolor/256x256/apps/faugus-launcher.png"
epic_icon = "/usr/share/icons/hicolor/256x256/apps/faugus-epic-games.png"
battle_icon = "/usr/share/icons/hicolor/256x256/apps/faugus-battlenet.png"
ubisoft_icon = "/usr/share/icons/hicolor/256x256/apps/faugus-ubisoft-connect.png"
ea_icon = "/usr/share/icons/hicolor/256x256/apps/faugus-ea.png"
faugus_run = "/usr/bin/faugus-run"
faugus_proton_manager = "/usr/bin/faugus-proton-manager"
umu_run = "/usr/bin/umu-run"
mangohud_dir = "/usr/bin/mangohud"
gamemoderun = "/usr/bin/gamemoderun"
games_txt = f'{faugus_launcher_dir}/games.txt'
games_json = f'{faugus_launcher_dir}/games.json'
latest_games = f'{faugus_launcher_dir}/latest-games.txt'
faugus_launcher_share_dir = f"{share_dir}/faugus-launcher"
faugus_temp = os.path.expanduser('~/faugus_temp')
lock_file_path = f"{faugus_launcher_share_dir}/faugus_launcher.lock"
lock_file = None
if not os.path.exists(faugus_launcher_share_dir):
os.makedirs(faugus_launcher_share_dir)
def is_already_running():
current_pid = str(os.getpid())
if os.path.exists(lock_file_path):
with open(lock_file_path, 'r') as lock_file:
lock_pid = lock_file.read().strip()
try:
os.kill(int(lock_pid), 0)
return True
except OSError:
pass
with open(lock_file_path, 'w') as lock_file:
lock_file.write(current_pid)
return False
def get_desktop_dir():
try:
# Run the command and capture its output
desktop_dir = subprocess.check_output(['xdg-user-dir', 'DESKTOP'], text=True).strip()
return desktop_dir
except FileNotFoundError:
print("xdg-user-dir not found; falling back to ~/Desktop")
# xdg-user-dir is not installed, fallback to ~/Desktop
return os.path.expanduser('~/Desktop')
except subprocess.CalledProcessError:
print("Error running xdg-user-dir; falling back to ~/Desktop")
# xdg-user-dir command failed for some other reason
return os.path.expanduser('~/Desktop')
desktop_dir = get_desktop_dir()
class Main(Gtk.Window):
def __init__(self):
# Initialize the main window with title and default size
Gtk.Window.__init__(self, title="Faugus Launcher")
self.set_icon_from_file(faugus_png)
self.banner_mode = False
self.start_maximized = False
self.start_fullscreen = False
self.fullscreen_activated = False
self.gamepad_navigation = False
self.gamepad_process = False
self.theme = None
self.game_running = None
self.system_tray = False
self.start_boot = False
self.games = []
self.processos = {}
self.last_click_time = 0
self.last_clicked_item = None
self.double_click_time_threshold = 500
self.flowbox_child = None
# Define the configuration path
config_path = faugus_launcher_dir
# Create the configuration directory if it doesn't exist
if not os.path.exists(config_path):
os.makedirs(config_path)
self.working_directory = config_path
os.chdir(self.working_directory)
config_file = config_file_dir
if not os.path.exists(config_file):
self.save_config("False", prefixes_dir, "False", "False", "False", "GE-Proton", "True", "False", "False", "False", "List", "False", "", "False", "False")
self.games = []
self.provider = Gtk.CssProvider()
self.provider.load_from_data(b"""
.hbox-dark-background {
background-color: rgba(25, 25, 25, 0.5);
}
.hbox-light-background {
background-color: rgba(25, 25, 25, 0.1);
}
.hbox-red-background {
background-color: rgba(255, 0, 0, 0.5);
}
""")
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(), self.provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
self.check_theme()
self.load_config()
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
if self.gamepad_navigation:
self.gamepad_process = subprocess.Popen(["faugus-gamepad"])
self.big_interface()
if self.interface_mode == "Banners":
self.banner_mode = True
if self.start_maximized:
self.maximize()
if self.start_fullscreen:
self.fullscreen()
self.fullscreen_activated = True
if self.gamepad_navigation:
self.gamepad_process = subprocess.Popen(["faugus-gamepad"])
self.big_interface()
if not self.interface_mode:
self.interface_mode = "List"
self.small_interface()
# Create the tray indicator
self.indicator = AppIndicator3.Indicator.new(
"Faugus Launcher", # Application name
tray_icon, # Path to the icon
AppIndicator3.IndicatorCategory.APPLICATION_STATUS
)
self.indicator.set_menu(self.create_tray_menu()) # Tray menu
self.indicator.set_title("Faugus Launcher") # Change the tooltip text
if self.system_tray:
self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
self.connect("delete-event", self.on_window_delete_event)
self.context_menu = Gtk.Menu()
menu_item_play = Gtk.MenuItem(label="Play")
menu_item_play.connect("activate", self.on_context_menu_play)
self.context_menu.append(menu_item_play)
menu_item_edit = Gtk.MenuItem(label="Edit")
menu_item_edit.connect("activate", self.on_context_menu_edit)
self.context_menu.append(menu_item_edit)
menu_item_delete = Gtk.MenuItem(label="Delete")
menu_item_delete.connect("activate", self.on_context_menu_delete)
self.context_menu.append(menu_item_delete)
self.context_menu.show_all()
self.flowbox.connect("button-press-event", self.on_item_right_click)
self.game_running2 = False
# Set signal handler for child process termination
signal.signal(signal.SIGCHLD, self.on_child_process_closed)
def check_theme(self):
settings = Gtk.Settings.get_default()
prefer_dark = settings.get_property('gtk-application-prefer-dark-theme')
output = subprocess.check_output(['gsettings', 'get', 'org.gnome.desktop.interface', 'gtk-theme']).decode('utf-8')
theme = output.strip().strip("'")
if prefer_dark or 'dark' in theme:
self.theme = "hbox-dark-background"
else:
self.theme = "hbox-light-background"
def small_interface(self):
self.set_default_size(400, 620)
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.HORIZONTAL)
box_left = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box_right = 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_margin_top(10)
self.button_add.set_margin_start(10)
self.button_add.set_margin_end(10)
label_add = Gtk.Label(label="New")
label_add.set_margin_start(0)
label_add.set_margin_end(0)
label_add.set_margin_top(0)
label_add.set_margin_bottom(0)
self.button_add.add(label_add)
self.button_edit = Gtk.Button()
self.button_edit.connect("clicked", self.on_button_edit_clicked)
self.button_edit.set_can_focus(False)
self.button_edit.set_size_request(50, 50)
self.button_edit.set_margin_top(10)
self.button_edit.set_margin_start(10)
self.button_edit.set_margin_end(10)
label_edit = Gtk.Label(label="Edit")
label_edit.set_margin_start(0)
label_edit.set_margin_end(0)
label_edit.set_margin_top(0)
label_edit.set_margin_bottom(0)
self.button_edit.add(label_edit)
self.button_delete = Gtk.Button()
self.button_delete.connect("clicked", self.on_button_delete_clicked)
self.button_delete.set_can_focus(False)
self.button_delete.set_size_request(50, 50)
self.button_delete.set_margin_top(10)
self.button_delete.set_margin_start(10)
self.button_delete.set_margin_end(10)
label_delete = Gtk.Label(label="Del")
label_delete.set_margin_start(0)
label_delete.set_margin_end(0)
label_delete.set_margin_top(0)
label_delete.set_margin_bottom(0)
self.button_delete.add(label_delete)
# 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_margin_top(10)
button_kill.set_margin_end(10)
button_kill.set_margin_bottom(10)
label_kill = Gtk.Label(label="Kill")
label_kill.set_margin_start(0)
label_kill.set_margin_end(0)
label_kill.set_margin_top(0)
label_kill.set_margin_bottom(0)
button_kill.add(label_kill)
# 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("open-menu-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(150, 50)
self.button_play.set_image(Gtk.Image.new_from_icon_name("media-playback-start-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(-1, 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_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.START)
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)
scroll_box.add(self.flowbox)
self.load_games()
# Pack left and scrolled box into the top box
self.box_top.pack_start(box_left, False, True, 0)
self.box_top.pack_start(box_right, True, True, 0)
# Pack buttons into the left box
box_left.pack_start(self.button_add, False, False, 0)
box_left.pack_start(self.button_edit, False, False, 0)
box_left.pack_start(self.button_delete, False, False, 0)
box_right.pack_start(scroll_box, True, True, 0)
# Pack buttons and other components into the bottom box
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.button_edit.set_sensitive(False)
self.button_delete.set_sensitive(False)
self.button_play.set_sensitive(False)
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])
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_margin_top(10)
self.button_add.set_margin_start(10)
self.button_add.set_margin_bottom(10)
label_add = Gtk.Label(label="New")
label_add.set_margin_start(0)
label_add.set_margin_end(0)
label_add.set_margin_top(0)
label_add.set_margin_bottom(0)
self.button_add.add(label_add)
self.button_edit = Gtk.Button()
self.button_edit.connect("clicked", self.on_button_edit_clicked)
self.button_edit.set_can_focus(False)
self.button_edit.set_size_request(50, 50)
self.button_edit.set_margin_top(10)
self.button_edit.set_margin_start(10)
self.button_edit.set_margin_bottom(10)
label_edit = Gtk.Label(label="Edit")
label_edit.set_margin_start(0)
label_edit.set_margin_end(0)
label_edit.set_margin_top(0)
label_edit.set_margin_bottom(0)
self.button_edit.add(label_edit)
self.button_delete = Gtk.Button()
self.button_delete.connect("clicked", self.on_button_delete_clicked)
self.button_delete.set_can_focus(False)
self.button_delete.set_size_request(50, 50)
self.button_delete.set_margin_top(10)
self.button_delete.set_margin_start(10)
self.button_delete.set_margin_end(10)
self.button_delete.set_margin_bottom(10)
label_delete = Gtk.Label(label="Del")
label_delete.set_margin_start(0)
label_delete.set_margin_end(0)
label_delete.set_margin_top(0)
label_delete.set_margin_bottom(0)
self.button_delete.add(label_delete)
# 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_margin_start(10)
button_kill.set_margin_top(10)
button_kill.set_margin_bottom(10)
label_kill = Gtk.Label(label="Kill")
label_kill.set_margin_start(0)
label_kill.set_margin_end(0)
label_kill.set_margin_top(0)
label_kill.set_margin_bottom(0)
button_kill.add(label_kill)
# 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_margin_start(10)
button_bye.set_margin_top(10)
button_bye.set_margin_bottom(10)
button_bye.set_margin_end(10)
label_bye = Gtk.Label(label="Bye")
label_bye.set_margin_start(0)
label_bye.set_margin_end(0)
label_bye.set_margin_top(0)
label_bye.set_margin_bottom(0)
button_bye.add(label_bye)
# 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("open-menu-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("media-playback-start-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(20)
self.entry_search.set_margin_bottom(10)
self.entry_search.set_margin_end(20)
self.grid_left = Gtk.Grid()
self.grid_left.get_style_context().add_class(self.theme)
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(self.button_edit)
self.grid_left.add(self.button_delete)
grid_middle = Gtk.Grid()
grid_middle.get_style_context().add_class(self.theme)
grid_middle.add(self.entry_search)
grid_right = Gtk.Grid()
grid_right.get_style_context().add_class(self.theme)
grid_right.set_hexpand(True)
grid_right.set_halign(Gtk.Align.START)
grid_right.add(button_settings)
grid_right.add(button_kill)
grid_right.add(self.button_play)
self.grid_corner = Gtk.Grid()
self.grid_corner.get_style_context().add_class(self.theme)
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)
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.button_edit.set_sensitive(False)
self.button_delete.set_sensitive(False)
self.button_play.set_sensitive(False)
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])
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):
if self.gamepad_process:
self.gamepad_process.terminate()
self.gamepad_process.wait()
if os.path.exists(lock_file_path):
os.remove(lock_file_path)
Gtk.main_quit()
def on_button_bye_clicked(self, widget):
menu = Gtk.Menu()
shutdown_item = Gtk.MenuItem(label="Shut down")
reboot_item = Gtk.MenuItem(label="Reboot")
logout_item = Gtk.MenuItem(label="Log out")
close_item = Gtk.MenuItem(label="Close")
shutdown_item.connect("activate", self.on_shutdown)
reboot_item.connect("activate", self.on_reboot)
logout_item.connect("activate", self.on_logout)
close_item.connect("activate", self.on_close)
menu.append(shutdown_item)
menu.append(reboot_item)
menu.append(logout_item)
menu.append(close_item)
menu.show_all()
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_logout(self, widget):
subprocess.run(["loginctl", "terminate-user", os.getlogin()])
def on_close(self, widget):
if os.path.exists(lock_file_path):
os.remove(lock_file_path)
Gtk.main_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.select_child(item)
self.context_menu.popup_at_pointer(event)
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_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):
hbox = item.get_child()
game_label = hbox.get_children()[1]
title = game_label.get_text()
if title not in self.processos:
self.on_button_play_clicked(item)
else:
dialog = Gtk.MessageDialog(title=title, text=f"'{title}' is already running.",
buttons=Gtk.ButtonsType.OK, parent=self)
dialog.set_resizable(False)
dialog.set_modal(True)
dialog.set_halign(Gtk.Align.CENTER)
dialog.set_valign(Gtk.Align.CENTER)
dialog.set_vexpand(True)
dialog.set_hexpand(True)
dialog.set_icon_from_file(faugus_png)
dialog.run()
dialog.destroy()
def on_key_press_event(self, widget, event):
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 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 event.keyval == Gdk.KEY_Return:
if title not in self.processos:
widget = self.button_play
self.on_button_play_clicked(selected_child)
else:
dialog = Gtk.MessageDialog(
title=title,
text=f"'{title}' is already running.",
buttons=Gtk.ButtonsType.OK,
parent=self
)
dialog.set_resizable(False)
dialog.set_modal(True)
dialog.set_halign(Gtk.Align.CENTER)
dialog.set_valign(Gtk.Align.CENTER)
dialog.set_vexpand(True)
dialog.set_hexpand(True)
dialog.fullscreen()
dialog.set_icon_from_file(faugus_png)
dialog.run()
dialog.destroy()
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 load_config(self):
config_file = config_file_dir
if os.path.isfile(config_file):
with open(config_file, 'r') as f:
config_dict = {}
for line in f.read().splitlines():
if '=' in line:
key, value = line.split('=', 1)
key = key.strip()
value = value.strip().strip('"')
config_dict[key] = value
self.system_tray = config_dict.get('system-tray', 'False') == 'True'
self.start_boot = config_dict.get('start-boot', 'False') == 'True'
self.start_maximized = config_dict.get('start-maximized', 'False') == 'True'
self.interface_mode = config_dict.get('interface-mode', '').strip('"')
self.api_key = config_dict.get('api-key', '').strip('"')
self.start_fullscreen = config_dict.get('start-fullscreen', 'False') == 'True'
self.gamepad_navigation = config_dict.get('gamepad-navigation', 'False') == 'True'
else:
self.save_config(False, '', "False", "False", "False", "GE-Proton", "True", "False", "False", "False", "List", "False", "", "False", "False")
def create_tray_menu(self):
# Create the tray menu
menu = Gtk.Menu()
# Add game items from latest-games.txt
games_file_path = latest_games
if os.path.exists(games_file_path):
with open(games_file_path, "r") as games_file:
for line in games_file:
game_name = line.strip()
if game_name:
game_item = Gtk.MenuItem(label=game_name)
game_item.connect("activate", self.on_game_selected, game_name)
menu.append(game_item)
# Add a separator between game items and the other menu items
separator = Gtk.SeparatorMenuItem()
menu.append(separator)
# Item to restore the window
restore_item = Gtk.MenuItem(label="Open Faugus Launcher")
restore_item.connect("activate", self.restore_window)
menu.append(restore_item)
# Item to quit the application
quit_item = Gtk.MenuItem(label="Quit")
quit_item.connect("activate", self.on_quit_activate)
menu.append(quit_item)
menu.show_all()
return menu
def on_game_selected(self, widget, game_name):
# Find the game in the FlowBox by name and select it
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
if game_label.get_text() == game_name:
# Select this item in FlowBox
child.set_state_flags(Gtk.StateFlags.SELECTED, True)
break
# Call the function to run the selected game
self.on_button_play_clicked(widget)
def on_window_delete_event(self, widget, event):
# Only prevent closing when system tray is active
self.load_config()
if self.system_tray:
self.hide() # Minimize the window instead of closing
return True # Stop the event to keep the app running
return False # Allow the window to close
def restore_window(self, widget):
# Restore the window when clicking the tray icon
self.show_all()
self.present()
def on_quit_activate(self, widget):
if os.path.exists(lock_file_path):
os.remove(lock_file_path)
# Quit the application
Gtk.main_quit()
def load_games(self):
# Load games from JSON file
try:
with open("games.json", "r", encoding="utf-8") as file:
games_data = json.load(file)
for game_data in games_data:
title = game_data.get("title", "")
path = game_data.get("path", "")
prefix = game_data.get("prefix", "")
launch_arguments = game_data.get("launch_arguments", "")
game_arguments = game_data.get("game_arguments", "")
mangohud = game_data.get("mangohud", "")
gamemode = game_data.get("gamemode", "")
sc_controller = game_data.get("sc_controller", "")
protonfix = game_data.get("protonfix", "")
runner = game_data.get("runner", "")
addapp_checkbox = game_data.get("addapp_checkbox", "")
addapp = game_data.get("addapp", "")
addapp_bat = game_data.get("addapp_bat", "")
banner = game_data.get("banner", "")
game = Game(title, path, prefix, launch_arguments, game_arguments, mangohud, gamemode,
sc_controller, protonfix, runner, addapp_checkbox, addapp, addapp_bat, banner)
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"Erro ao ler o arquivo JSON: {e}")
def add_item_list(self, game):
# Add a game item to the list
if self.interface_mode == "List":
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
hbox.set_size_request(400, -1)
if self.interface_mode == "Blocks":
hbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
hbox.set_size_request(300, 200)
if self.interface_mode == "Banners":
hbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
#hbox.set_size_request(200, 300)
hbox.get_style_context().add_class(self.theme)
# Handle the click event of the Create Shortcut button
title_formatted = re.sub(r'[^a-zA-Z0-9\s]', '', game.title)
title_formatted = title_formatted.replace(' ', '-')
title_formatted = '-'.join(title_formatted.lower().split())
game_icon = f'{icons_dir}/{title_formatted}.ico'
game_label = Gtk.Label.new(game.title)
if self.interface_mode == "Blocks" or self.interface_mode == "Banners":
game_label.set_line_wrap(True)
game_label.set_max_width_chars(1)
game_label.set_justify(Gtk.Justification.CENTER)
if os.path.isfile(game_icon):
pass
else:
game_icon = faugus_png
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)
if self.interface_mode == "Blocks":
scaled_pixbuf = pixbuf.scale_simple(100, 100, GdkPixbuf.InterpType.BILINEAR)
image = Gtk.Image.new_from_file(game_icon)
image.set_from_pixbuf(scaled_pixbuf)
hbox.pack_start(image, True, True, 0)
hbox.pack_start(game_label, True, True, 0)
game_label.set_margin_end(20)
game_label.set_margin_start(20)
if self.interface_mode == "Banners":
image2 = Gtk.Image()
game_label.set_size_request(-1, 50)
game_label.set_margin_end(10)
game_label.set_margin_start(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)
if game.banner == "" or not os.path.isfile(game.banner):
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(faugus_banner, 230, 345, False)
else:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(game.banner, 230, 345, False)
image2.set_from_pixbuf(pixbuf)
hbox.pack_start(image2, True, True, 0)
hbox.pack_start(game_label, True, True, 0)
self.flowbox_child.add(hbox)
self.flowbox.add(self.flowbox_child)
self.flowbox_child.set_valign(Gtk.Align.START)
self.flowbox_child.set_halign(Gtk.Align.START)
def on_search_changed(self, entry):
search_text = entry.get_text().lower()
self.filtered_games = [game for game in self.games if search_text in game.title.lower()]
for child in self.flowbox.get_children():
self.flowbox.remove(child)
if self.filtered_games:
for game in self.filtered_games:
self.add_item_list(game)
first_child = self.flowbox.get_children()[0]
self.flowbox.select_child(first_child)
self.on_item_selected(self.flowbox, first_child)
else:
pass
self.flowbox.show_all()
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.button_edit.set_sensitive(True)
self.button_delete.set_sensitive(True)
if title in self.processos:
self.button_play.set_sensitive(False)
self.button_play.set_image(
Gtk.Image.new_from_icon_name("media-playback-stop-symbolic", Gtk.IconSize.BUTTON))
else:
self.button_play.set_sensitive(True)
self.button_play.set_image(
Gtk.Image.new_from_icon_name("media-playback-start-symbolic", Gtk.IconSize.BUTTON))
else:
self.button_edit.set_sensitive(False)
self.button_delete.set_sensitive(False)
self.button_play.set_sensitive(False)
def update_button_sensitivity(self, row):
# Enable buttons based on the selected row
if row:
hbox = row.get_child()
game_label = hbox.get_children()[1]
title = game_label.get_text()
self.button_edit.set_sensitive(True)
self.button_delete.set_sensitive(True)
if title in self.processos:
self.button_play.set_sensitive(False)
self.button_play.set_image(
Gtk.Image.new_from_icon_name("media-playback-stop-symbolic", Gtk.IconSize.BUTTON))
else:
self.button_play.set_sensitive(True)
self.button_play.set_image(
Gtk.Image.new_from_icon_name("media-playback-start-symbolic", Gtk.IconSize.BUTTON))
else:
# Disable buttons if no row is selected
self.button_edit.set_sensitive(False)
self.button_delete.set_sensitive(False)
self.button_play.set_sensitive(False)
def load_close_onlaunch(self):
config_file = config_file_dir
close_onlaunch = False
if os.path.exists(config_file):
with open(config_file, 'r') as f:
config_data = f.read().splitlines()
config_dict = dict(line.split('=') for line in config_data)
close_onlaunch_value = config_dict.get('close-onlaunch', '').strip('"')
if close_onlaunch_value.lower() == 'true':
close_onlaunch = True
return close_onlaunch
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):
self.checkbox_discrete_gpu = settings_dialog.checkbox_discrete_gpu
self.checkbox_close_after_launch = settings_dialog.checkbox_close_after_launch
self.checkbox_splash_disable = settings_dialog.checkbox_splash_disable
self.checkbox_system_tray = settings_dialog.checkbox_system_tray
self.checkbox_start_boot = settings_dialog.checkbox_start_boot
self.checkbox_start_maximized = settings_dialog.checkbox_start_maximized
self.entry_default_prefix = settings_dialog.entry_default_prefix
self.combo_box_interface = settings_dialog.combo_box_interface
self.entry_api_key = settings_dialog.entry_api_key
self.checkbox_start_fullscreen = settings_dialog.checkbox_start_fullscreen
self.checkbox_gamepad_navigation = settings_dialog.checkbox_gamepad_navigation
self.checkbox_mangohud = settings_dialog.checkbox_mangohud
self.checkbox_gamemode = settings_dialog.checkbox_gamemode
self.checkbox_sc_controller = settings_dialog.checkbox_sc_controller
self.combo_box_runner = settings_dialog.combo_box_runner
checkbox_state = self.checkbox_close_after_launch.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_start_maximized = self.checkbox_start_maximized.get_active()
default_prefix = self.entry_default_prefix.get_text()
combo_box_interface = self.combo_box_interface.get_active_text()
entry_api_key = self.entry_api_key.get_text()
checkbox_start_fullscreen = self.checkbox_start_fullscreen.get_active()
checkbox_gamepad_navigation = self.checkbox_gamepad_navigation.get_active()
mangohud_state = self.checkbox_mangohud.get_active()
gamemode_state = self.checkbox_gamemode.get_active()
sc_controller_state = self.checkbox_sc_controller.get_active()
default_runner = self.combo_box_runner.get_active_text()
if default_runner == "UMU-Proton Latest":
default_runner = ""
if default_runner == "GE-Proton Latest (default)":
default_runner = "GE-Proton"
# Handle dialog response
if response_id == Gtk.ResponseType.OK:
validation_result = self.validate_settings_fields(settings_dialog, default_prefix, entry_api_key)
if not validation_result:
return
self.save_config(checkbox_state, default_prefix, mangohud_state, gamemode_state, sc_controller_state, default_runner, checkbox_discrete_gpu_state, checkbox_splash_disable, checkbox_system_tray, checkbox_start_boot, combo_box_interface, checkbox_start_maximized, entry_api_key, checkbox_start_fullscreen, checkbox_gamepad_navigation)
self.manage_autostart_file(checkbox_start_boot)
if checkbox_system_tray:
self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
if not hasattr(self, "window_delete_event_connected") or not self.window_delete_event_connected:
self.connect("delete-event", self.on_window_delete_event)
self.window_delete_event_connected = True
self.indicator.set_menu(self.create_tray_menu())
else:
self.indicator.set_status(AppIndicator3.IndicatorStatus.PASSIVE)
if hasattr(self, "window_delete_event_connected") and self.window_delete_event_connected:
self.disconnect_by_func(self.on_window_delete_event)
self.window_delete_event_connected = False
if checkbox_gamepad_navigation:
self.gamepad_process = subprocess.Popen(["faugus-gamepad"])
else:
if self.gamepad_process:
self.gamepad_process.terminate()
self.gamepad_process.wait()
if validation_result:
if self.interface_mode != combo_box_interface:
dialog = Gtk.MessageDialog(title="Faugus Launcher", text="Please restart Faugus Launcher\nto switch the interface mode.",
buttons=Gtk.ButtonsType.OK, parent=self)
dialog.set_resizable(False)
dialog.set_modal(True)
dialog.set_icon_from_file(faugus_png)
dialog.run()
dialog.destroy()
self.load_config()
settings_dialog.destroy()
else:
settings_dialog.destroy()
def validate_settings_fields(self, settings_dialog, default_prefix, entry_api_key):
settings_dialog.entry_default_prefix.get_style_context().remove_class("entry")
settings_dialog.entry_api_key.get_style_context().remove_class("entry")
if settings_dialog.combo_box_interface.get_active_text() == "Banners":
if not default_prefix or not entry_api_key:
if not default_prefix:
settings_dialog.entry_default_prefix.get_style_context().add_class("entry")
if not entry_api_key:
settings_dialog.entry_api_key.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:
f.write("""[Desktop Entry]
Categories=Utility;
Exec=faugus-launcher %f hide
Icon=faugus-launcher
MimeType=application/x-ms-dos-executable;application/x-msi;application/x-ms-shortcut;application/x-bat;text/x-ms-regedit
Name=Faugus Launcher
Type=Application
""")
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):
#if not (listbox_row := self.game_list.get_selected_row()):
# return
selected_children = self.flowbox.get_selected_children()
selected_child = selected_children[0]
hbox = selected_child.get_child()
game_label = hbox.get_children()[1]
title = game_label.get_text()
# Find the selected game object
game = next((j for j in self.games if j.title == title), None)
if game:
# Format the title for command execution
title_formatted = re.sub(r'[^a-zA-Z0-9\s]', '', game.title)
title_formatted = title_formatted.replace(' ', '-')
title_formatted = '-'.join(title_formatted.lower().split())
# Extract game launch information
launch_arguments = game.launch_arguments
path = game.path
prefix = game.prefix
game_arguments = game.game_arguments
mangohud = game.mangohud
sc_controller = game.sc_controller
protonfix = game.protonfix
runner = game.runner
addapp_checkbox = game.addapp_checkbox
addapp = game.addapp
addapp_bat = game.addapp_bat
gamemode_enabled = os.path.exists(gamemoderun) or os.path.exists("/usr/games/gamemoderun")
gamemode = game.gamemode if gamemode_enabled else ""
# Get the directory containing the executable
game_directory = os.path.dirname(path)
command_parts = []
# Add command parts if they are not empty
if mangohud:
command_parts.append(mangohud)
if sc_controller:
command_parts.append(sc_controller)
if prefix:
command_parts.append(f'WINEPREFIX={prefix}')
if protonfix:
command_parts.append(f'GAMEID={protonfix}')
else:
command_parts.append(f'GAMEID={title_formatted}')
if runner:
if runner == "Linux-Native":
command_parts.append('UMU_NO_PROTON=1')
else:
command_parts.append(f'PROTONPATH={runner}')
if gamemode:
command_parts.append(gamemode)
if launch_arguments:
command_parts.append(launch_arguments)
# Add the fixed command and remaining arguments
command_parts.append(f'"{umu_run}"')
if addapp_checkbox == "addapp_enabled":
command_parts.append(f'"{addapp_bat}"')
elif path:
command_parts.append(f'"{path}"')
if game_arguments:
command_parts.append(f'{game_arguments}')
# Join all parts into a single command
command = ' '.join(command_parts)
print(command)
# faugus-run path
faugus_run_path = faugus_run
# Save the game title to the latest_games.txt file
self.update_latest_games_file(title)
if os.path.exists(lock_file_path):
os.remove(lock_file_path)
# Launch the game with subprocess
if self.load_close_onlaunch():
subprocess.Popen([sys.executable, faugus_run_path, command], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, cwd=game_directory)
sys.exit()
else:
processo = subprocess.Popen([sys.executable, faugus_run_path, command], cwd=game_directory)
self.processos[title] = processo
self.button_play.set_sensitive(False)
self.button_play.set_image(
Gtk.Image.new_from_icon_name("media-playback-stop-symbolic", Gtk.IconSize.BUTTON))
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.indicator.set_menu(self.create_tray_menu())
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 = None
self.game_running2 = False
def on_button_add_clicked(self, widget):
file_path=""
# Handle add button click event
add_game_dialog = AddGame(self, self.game_running2, file_path, self.api_key, 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]
hbox = selected_child.get_child()
game_label = hbox.get_children()[1]
title = game_label.get_text()
if game := next((j for j in self.games if j.title == title), None):
if game.title in self.processos:
self.game_running2 = True
else:
self.game_running2 = False
edit_game_dialog = AddGame(self, self.game_running2, file_path, self.api_key, self.interface_mode)
edit_game_dialog.connect("response", self.on_edit_dialog_response, edit_game_dialog, game)
model = edit_game_dialog.combo_box_runner.get_model()
index_to_activate = 0
game_runner = game.runner
if game.runner == "GE-Proton":
game_runner = "GE-Proton Latest (default)"
if game.runner == "":
game_runner = "UMU-Proton Latest"
if game_runner == "Linux-Native":
edit_game_dialog.combo_box_launcher.set_active(1)
for i, row in enumerate(model):
if row[0] == game_runner:
index_to_activate = i
break
if not game_runner:
index_to_activate = 1
edit_game_dialog.combo_box_runner.set_active(index_to_activate)
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(f"Edit {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)
if not os.path.isfile(game.banner):
game.banner = faugus_banner
shutil.copy(game.banner, edit_game_dialog.banner_path_temp)
allocation = edit_game_dialog.entry_title.get_allocation()
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(game.banner, (allocation.width - 10), -1, 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 == "MANGOHUD=1":
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 == "gamemoderun":
edit_game_dialog.checkbox_gamemode.set_active(True)
else:
edit_game_dialog.checkbox_gamemode.set_active(False)
sc_controller_enabled = os.path.exists("/usr/bin/sc-controller") or os.path.exists(
"/usr/local/bin/sc-controller")
if sc_controller_enabled:
if game.sc_controller == "SC_CONTROLLER=1":
edit_game_dialog.checkbox_sc_controller.set_active(True)
else:
edit_game_dialog.checkbox_sc_controller.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)
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)
if self.game_running2:
edit_game_dialog.button_winecfg.set_sensitive(False)
edit_game_dialog.button_winecfg.set_tooltip_text(f'{game.title} is running. Please close it first.')
edit_game_dialog.button_winetricks.set_sensitive(False)
edit_game_dialog.button_winetricks.set_tooltip_text(f'{game.title} is running. Please close it first.')
edit_game_dialog.button_run.set_sensitive(False)
edit_game_dialog.button_run.set_tooltip_text(f'{game.title} is running. Please close it first.')
edit_game_dialog.show()
def set_image_shortcut_icon(self, title, icons_path, icon_temp):
# Handle the click event of the Create Shortcut button
title_formatted = re.sub(r'[^a-zA-Z0-9\s]', '', title)
title_formatted = title_formatted.replace(' ', '-')
title_formatted = '-'.join(title_formatted.lower().split())
# Check if the icon file exists
icon_path = os.path.join(icons_path, f"{title_formatted}.ico")
if os.path.exists(icon_path):
shutil.copy(icon_path, icon_temp)
if not os.path.exists(icon_path):
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):
#if not (listbox_row := self.game_list.get_selected_row()):
# return
selected_children = self.flowbox.get_selected_children()
selected_child = selected_children[0]
hbox = selected_child.get_child()
game_label = hbox.get_children()[1]
title = game_label.get_text()
if game := next((j for j in self.games if j.title == title), None):
# Display confirmation dialog
confirmation_dialog = ConfirmationDialog(self, title, game.prefix)
response = confirmation_dialog.run()
if response == Gtk.ResponseType.YES:
# Remove game and associated files if required
if confirmation_dialog.get_remove_prefix_state():
game_prefix = game.prefix
prefix_path = os.path.expanduser(game_prefix)
try:
shutil.rmtree(prefix_path)
except FileNotFoundError:
pass
# Remove the shortcut
self.remove_shortcut(game)
self.remove_banner(game)
self.games.remove(game)
self.save_games()
self.update_list()
self.button_edit.set_sensitive(False)
self.button_delete.set_sensitive(False)
self.button_play.set_sensitive(False)
# Remove the game from the latest-games file if it exists
self.remove_game_from_latest_games(title)
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])
confirmation_dialog.destroy()
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.indicator.set_menu(self.create_tray_menu())
except FileNotFoundError:
pass # Ignore if the file doesn't exist yet
def show_warning_dialog(self, parent, title):
dialog = Gtk.MessageDialog(
transient_for=parent,
modal=True,
message_type=Gtk.MessageType.WARNING,
buttons=Gtk.ButtonsType.OK,
text=title,
)
dialog.set_icon_from_file(faugus_png)
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 = add_game_dialog.entry_prefix.get_text()
if add_game_dialog.combo_box_launcher.get_active() == 0 or add_game_dialog.combo_box_launcher.get_active() == 1:
title = add_game_dialog.entry_title.get_text()
else:
title = add_game_dialog.combo_box_launcher.get_active_text()
if any(game.title == title for game in self.games):
# Display an error message and prevent the dialog from closing
self.show_warning_dialog(add_game_dialog, f"{title} already exists")
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.combo_box_runner.get_active_text()
addapp = add_game_dialog.entry_addapp.get_text()
title_formatted = re.sub(r'[^a-zA-Z0-9\s]', '', title)
title_formatted = title_formatted.replace(' ', '-')
title_formatted = '-'.join(title_formatted.lower().split())
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 = ""
if runner == "UMU-Proton Latest":
runner = ""
if runner == "GE-Proton Latest (default)":
runner = "GE-Proton"
if add_game_dialog.combo_box_launcher.get_active() == 1:
runner = "Linux-Native"
# Determine mangohud and gamemode status
mangohud = "MANGOHUD=1" if add_game_dialog.checkbox_mangohud.get_active() else ""
gamemode = "gamemoderun" if add_game_dialog.checkbox_gamemode.get_active() else ""
sc_controller = "SC_CONTROLLER=1" if add_game_dialog.checkbox_sc_controller.get_active() else ""
addapp_checkbox = "addapp_enabled" if add_game_dialog.checkbox_addapp.get_active() else ""
game_info = {
"title": title,
"path": path,
"prefix": prefix,
"launch_arguments": launch_arguments,
"game_arguments": game_arguments,
"mangohud": mangohud,
"gamemode": gamemode,
"sc_controller": sc_controller,
"protonfix": protonfix,
"runner": runner,
"addapp_checkbox": addapp_checkbox,
"addapp": addapp,
"addapp_bat": addapp_bat,
"banner": banner,
}
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)
# Create Game object and update UI
game = Game(title, path, prefix, launch_arguments, game_arguments, mangohud, gamemode, sc_controller, protonfix, runner, addapp_checkbox, addapp, addapp_bat, banner)
self.games.append(game)
# Determine the state of the shortcut checkbox
shortcut_state = add_game_dialog.checkbox_shortcut.get_active()
icon_temp = os.path.expanduser(add_game_dialog.icon_temp)
icon_final = f'{add_game_dialog.icons_path}/{title_formatted}.ico'
def check_internet_connection():
try:
socket.create_connection(("8.8.8.8", 53), timeout=5)
return True
except socket.gaierror:
return False
except OSError as e:
if e.errno == 101:
return False
raise
if not check_internet_connection() and add_game_dialog.combo_box_launcher.get_active() != 0:
self.show_warning_dialog(add_game_dialog, "No internet connection")
return True
else:
if add_game_dialog.combo_box_launcher.get_active() == 2:
add_game_dialog.destroy()
self.launcher_screen(title, "2", title_formatted, runner, prefix, umu_run, game, shortcut_state, icon_temp, icon_final)
if add_game_dialog.combo_box_launcher.get_active() == 3:
add_game_dialog.destroy()
self.launcher_screen(title, "3", title_formatted, runner, prefix, umu_run, game, shortcut_state, icon_temp, icon_final)
if add_game_dialog.combo_box_launcher.get_active() == 4:
add_game_dialog.destroy()
self.launcher_screen(title, "4", title_formatted, runner, prefix, umu_run, game, shortcut_state, icon_temp, icon_final)
if add_game_dialog.combo_box_launcher.get_active() == 5:
add_game_dialog.destroy()
self.launcher_screen(title, "5", title_formatted, runner, prefix, umu_run, game, shortcut_state, icon_temp, icon_final)
if add_game_dialog.combo_box_launcher.get_active() == 0 or add_game_dialog.combo_box_launcher.get_active() == 1:
# Call add_remove_shortcut method
self.add_shortcut(game, 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')
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, 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(f"Installing {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 == "2":
image_path = battle_icon
self.label_download.set_text("Downloading Battle.net...")
self.download_launcher("battle", title, title_formatted, runner, prefix, umu_run, game, shortcut_state, icon_temp, icon_final)
elif launcher == "3":
image_path = ea_icon
self.label_download.set_text("Downloading EA App...")
self.download_launcher("ea", title, title_formatted, runner, prefix, umu_run, game, shortcut_state, icon_temp, icon_final)
elif launcher == "4":
image_path = epic_icon
self.label_download.set_text("Downloading Epic Games...")
self.download_launcher("epic", title, title_formatted, runner, prefix, umu_run, game, shortcut_state, icon_temp, icon_final)
elif launcher == "5":
image_path = ubisoft_icon
self.label_download.set_text("Downloading Ubisoft Connect...")
self.download_launcher("ubisoft", title, title_formatted, runner, prefix, umu_run, game, 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, widget):
self.on_button_kill_clicked(widget)
def monitor_process(self, processo, game, shortcut_state, icon_temp, icon_final, title):
retcode = processo.poll()
if retcode is not None:
print(f"{title} installed.")
if os.path.exists(faugus_temp):
shutil.rmtree(faugus_temp)
self.add_shortcut(game, shortcut_state, icon_temp, icon_final)
self.add_item_list(game)
self.update_list()
self.select_game_by_title(title)
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()
return False
return True
def download_launcher(self, launcher, title, title_formatted, runner, prefix, umu_run, game, shortcut_state, icon_temp, icon_final):
urls = {
"ea": "https://origin-a.akamaihd.net/EA-Desktop-Client-Download/installer-releases/EAappInstaller.exe",
"epic": "https://launcher-public-service-prod06.ol.epicgames.com/launcher/api/installer/download/EpicGamesLauncherInstaller.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"
}
file_name = {
"ea": "EAappInstaller.exe",
"epic": "EpicGamesLauncherInstaller.msi",
"battle": "Battle.net-Setup.exe",
"ubisoft": "UbisoftConnectInstaller.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, f"Error during download: {e}")
def on_download_complete():
self.label_download.set_text(f"Installing {title}...")
if launcher == "battle":
self.label_download2.set_text("Please close the login window and press:")
self.button_finish_install.set_visible(True)
command = f"WINE_SIMULATE_WRITECOPY=1 WINEPREFIX='{prefix}' GAMEID={title_formatted} PROTONPATH={runner} {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"WINEPREFIX={prefix} GAMEID={title_formatted} PROTONPATH={runner} {umu_run} '{file_path}' /S"
elif launcher == "epic":
self.label_download2.set_text("")
command = f"WINEPREFIX={prefix} GAMEID={title_formatted} PROTONPATH={runner} {umu_run} msiexec /i '{file_path}' /passive"
elif launcher == "ubisoft":
self.label_download2.set_text("")
command = f"WINEPREFIX={prefix} GAMEID={title_formatted} PROTONPATH={runner} {umu_run} '{file_path}' /S"
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, shortcut_state, icon_temp, icon_final, title)
threading.Thread(target=start_download).start()
return file_path
def select_game_by_title(self, title):
# Seleciona um item do FlowBox com base no título
for child in self.flowbox.get_children():
hbox = child.get_children()[0] # O primeiro item é o hbox que contém o label
game_label = hbox.get_children()[1] # O segundo item é o label do título
if game_label.get_text() == title:
# Seleciona o child no FlowBox
self.flowbox.select_child(child)
break
# Atualiza a interface dos botões
self.button_edit.set_sensitive(True)
self.button_delete.set_sensitive(True)
self.button_play.set_sensitive(True)
self.button_play.set_image(
Gtk.Image.new_from_icon_name("media-playback-start-symbolic", Gtk.IconSize.BUTTON))
# Chama o método de seleção do item para garantir que os botões estejam atualizados
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 = 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.sc_controller = edit_game_dialog.checkbox_sc_controller.get_active()
game.protonfix = edit_game_dialog.entry_protonfix.get_text()
game.runner = edit_game_dialog.combo_box_runner.get_active_text()
game.addapp_checkbox = edit_game_dialog.checkbox_addapp.get_active()
game.addapp = edit_game_dialog.entry_addapp.get_text()
# Handle the click event of the Create Shortcut button
title_formatted = re.sub(r'[^a-zA-Z0-9\s]', '', game.title)
title_formatted = title_formatted.replace(' ', '-')
title_formatted = '-'.join(title_formatted.lower().split())
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}")
else:
game.banner = ""
if game.runner == "UMU-Proton Latest":
game.runner = ""
if game.runner == "GE-Proton Latest (default)":
game.runner = "GE-Proton"
if edit_game_dialog.combo_box_launcher.get_active() == 1:
game.runner = "Linux-Native"
icon_temp = os.path.expanduser(edit_game_dialog.icon_temp)
icon_final = f'{edit_game_dialog.icons_path}/{title_formatted}.ico'
# Determine the state of the shortcut checkbox
shortcut_state = edit_game_dialog.checkbox_shortcut.get_active()
# Call add_remove_shortcut method
self.add_shortcut(game, 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')
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, icon_temp, icon_final):
# Check if the shortcut checkbox is checked
if not shortcut_state:
# Remove existing shortcut if it exists
self.remove_shortcut(game)
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)
# Handle the click event of the Create Shortcut button
title_formatted = re.sub(r'[^a-zA-Z0-9\s]', '', game.title)
title_formatted = title_formatted.replace(' ', '-')
title_formatted = '-'.join(title_formatted.lower().split())
prefix = game.prefix
path = game.path
launch_arguments = game.launch_arguments
game_arguments = game.game_arguments
protonfix = game.protonfix
runner = game.runner
addapp_checkbox = game.addapp_checkbox
addapp_bat = game.addapp_bat
mangohud = "MANGOHUD=1" if game.mangohud else ""
gamemode = "gamemoderun" if game.gamemode else ""
sc_controller = "SC_CONTROLLER=1" if game.sc_controller else ""
addapp = "addapp_enabled" if game.addapp_checkbox else ""
# Check if the icon file exists
icons_path = icons_dir
new_icon_path = f"{icons_dir}/{title_formatted}.ico"
if not os.path.exists(new_icon_path):
new_icon_path = faugus_png
# Get the directory containing the executable
game_directory = os.path.dirname(path)
command_parts = []
# Add command parts if they are not empty
if mangohud:
command_parts.append(mangohud)
if sc_controller:
command_parts.append(sc_controller)
if prefix:
command_parts.append(f'WINEPREFIX={prefix}')
if protonfix:
command_parts.append(f'GAMEID={protonfix}')
else:
command_parts.append(f'GAMEID={title_formatted}')
if runner:
if runner == "Linux-Native":
command_parts.append('UMU_NO_PROTON=1')
else:
command_parts.append(f'PROTONPATH={runner}')
if gamemode:
command_parts.append(gamemode)
if launch_arguments:
command_parts.append(launch_arguments)
# Add the fixed command and remaining arguments
command_parts.append(f"'{umu_run}'")
print(addapp)
if addapp == "addapp_enabled":
command_parts.append(f"'{addapp_bat}'")
elif path:
command_parts.append(f"'{path}'")
if game_arguments:
command_parts.append(f"{game_arguments}")
# Join all parts into a single command
command = ' '.join(command_parts)
# Create a .desktop file
desktop_file_content = f"""[Desktop Entry]
Name={game.title}
Exec={faugus_run} "{command}"
Icon={new_icon_path}
Type=Application
Categories=Game;
Path={game_directory}
"""
# Check if the destination directory exists and create if it doesn't
applications_directory = app_dir
if not os.path.exists(applications_directory):
os.makedirs(applications_directory)
desktop_directory = desktop_dir
if not os.path.exists(desktop_directory):
os.makedirs(desktop_directory)
applications_shortcut_path = f"{app_dir}/{title_formatted}.desktop"
with open(applications_shortcut_path, 'w') as desktop_file:
desktop_file.write(desktop_file_content)
# Make the .desktop file executable
os.chmod(applications_shortcut_path, 0o755)
# Copy the shortcut to Desktop
desktop_shortcut_path = f"{desktop_dir}/{title_formatted}.desktop"
shutil.copy(applications_shortcut_path, desktop_shortcut_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(self, game):
title_formatted = re.sub(r'[^a-zA-Z0-9\s]', '', game.title)
title_formatted = title_formatted.replace(' ', '-')
title_formatted = '-'.join(title_formatted.lower().split())
# Remove banner file
banner_file_path = f"{banners_dir}/{title_formatted}.png"
if os.path.exists(banner_file_path):
os.remove(banner_file_path)
def remove_shortcut(self, game):
# Remove existing shortcut if it exists
title_formatted = re.sub(r'[^a-zA-Z0-9\s]', '', game.title)
title_formatted = title_formatted.replace(' ', '-')
title_formatted = '-'.join(title_formatted.lower().split())
desktop_file_path = f"{app_dir}/{title_formatted}.desktop"
if os.path.exists(desktop_file_path):
os.remove(desktop_file_path)
# Remove shortcut from Desktop if exists
desktop_shortcut_path = f"{desktop_dir}/{title_formatted}.desktop"
if os.path.exists(desktop_shortcut_path):
os.remove(desktop_shortcut_path)
# Remove icon file
icon_file_path = f"{icons_dir}/{title_formatted}.ico"
if os.path.exists(icon_file_path):
os.remove(icon_file_path)
def remove_desktop_entry(self, game):
# Remove the .desktop file from ~/.local/share/applications/
desktop_file_path = f"{app_dir}/{game.title}.desktop"
if os.path.exists(desktop_file_path):
os.remove(desktop_file_path)
def remove_shortcut_from_desktop(self, game):
# Remove the shortcut from the desktop if it exists
desktop_link_path = f"{desktop_dir}/{game.title}.desktop"
if os.path.exists(desktop_link_path):
os.remove(desktop_link_path)
def update_list(self):
# Update the game list
#for row in self.game_list.get_children():
# self.game_list.remove(row)
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.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 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.button_play.set_sensitive(True)
self.button_play.set_image(
Gtk.Image.new_from_icon_name("media-playback-start-symbolic", Gtk.IconSize.BUTTON))
else:
self.button_play.set_sensitive(False)
self.button_play.set_image(
Gtk.Image.new_from_icon_name("media-playback-stop-symbolic", Gtk.IconSize.BUTTON))
def save_games(self):
games_data = []
for game in self.games:
game_info = {
"title": game.title,
"path": game.path,
"prefix": game.prefix,
"launch_arguments": game.launch_arguments,
"game_arguments": game.game_arguments,
"mangohud": "MANGOHUD=1" if game.mangohud else "",
"gamemode": "gamemoderun" if game.gamemode else "",
"sc_controller": "SC_CONTROLLER=1" if game.sc_controller 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,
}
games_data.append(game_info)
with open("games.json", "w", encoding="utf-8") as file:
json.dump(games_data, file, ensure_ascii=False, indent=4)
def show_warning_message(self, message):
# Show a warning message dialog
dialog = Gtk.MessageDialog(transient_for=self, flags=0, message_type=Gtk.MessageType.WARNING,
buttons=Gtk.ButtonsType.OK, text=message, )
dialog.set_icon_from_file(faugus_png)
dialog.run()
dialog.destroy()
def save_config(self, checkbox_state, default_prefix, mangohud_state, gamemode_state, sc_controller_state, default_runner, checkbox_discrete_gpu_state, checkbox_splash_disable, checkbox_system_tray, checkbox_start_boot, combo_box_interface, checkbox_start_maximized, entry_api_key, checkbox_start_fullscreen, checkbox_gamepad_navigation):
# Path to the configuration file
config_file = os.path.join(self.working_directory, 'config.ini')
# Dictionary to store existing configurations
config = {}
# Read the existing configuration file
if os.path.exists(config_file):
with open(config_file, 'r') as f:
for line in f:
key, value = line.strip().split('=', 1)
config[key] = value.strip('"')
default_runner = (f'"{default_runner}"')
# Update configurations with new values
config['close-onlaunch'] = checkbox_state
config['default-prefix'] = default_prefix
config['mangohud'] = mangohud_state
config['gamemode'] = gamemode_state
config['sc-controller'] = sc_controller_state
config['default-runner'] = default_runner
config['discrete-gpu'] = checkbox_discrete_gpu_state
config['splash-disable'] = checkbox_splash_disable
config['system-tray'] = checkbox_system_tray
config['start-boot'] = checkbox_start_boot
config['interface-mode'] = combo_box_interface
config['start-maximized'] = checkbox_start_maximized
config['api-key'] = entry_api_key
config['start-fullscreen'] = checkbox_start_fullscreen
config['gamepad-navigation'] = checkbox_gamepad_navigation
# Write configurations back to the file
with open(config_file, 'w') as f:
for key, value in config.items():
if key == 'default-prefix':
f.write(f'{key}="{value}"\n')
else:
f.write(f'{key}={value}\n')
class Settings(Gtk.Dialog):
def __init__(self, parent):
# Initialize the Settings dialog
super().__init__(title="Settings", parent=parent)
self.set_resizable(False)
self.set_modal(True)
self.parent = parent
self.set_icon_from_file(faugus_png)
css_provider = Gtk.CssProvider()
css = """
.entry {
border-color: Red;
}
.paypal {
color: black;
background: white;
}
.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)
# Widgets for Interface mode
self.label_interface = Gtk.Label(label="Interface Mode")
self.label_interface.set_halign(Gtk.Align.START)
self.combo_box_interface = Gtk.ComboBoxText()
self.combo_box_interface.connect("changed", self.on_combobox_interface_changed)
self.combo_box_interface.append_text("List")
self.combo_box_interface.append_text("Blocks")
self.combo_box_interface.append_text("Banners")
self.label_api_key = Gtk.Label(label="SteamGridDB API Key")
self.label_api_key.set_halign(Gtk.Align.START)
self.label_api_key.set_markup('<a href="https://www.steamgriddb.com/profile/preferences/api">SteamGridDB API Key</a>')
self.label_api_key.connect("activate-link", self.on_link_clicked)
self.entry_api_key = Gtk.Entry()
# 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 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")
# Create checkbox for 'Gamepad navigation' option
self.checkbox_gamepad_navigation = Gtk.CheckButton(label="Gamepad navigation")
self.checkbox_gamepad_navigation.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.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_default_prefix_tools = Gtk.Label(label="Default prefix tools")
self.label_default_prefix_tools.set_halign(Gtk.Align.START)
# Widgets for runner
self.label_runner = Gtk.Label(label="Default Runner")
self.label_runner.set_halign(Gtk.Align.START)
self.combo_box_runner = Gtk.ComboBoxText()
self.button_proton_manager = Gtk.Button(label="GE-Proton Manager")
self.button_proton_manager.connect("clicked", self.on_button_proton_manager_clicked)
# 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")
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)
# Create checkbox for 'Splash screen' option
self.checkbox_splash_disable = Gtk.CheckButton(label="Disable splash window")
self.checkbox_splash_disable.set_active(False)
# 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_sc_controller = Gtk.CheckButton(label="SC Controller")
self.checkbox_sc_controller.set_tooltip_text(
"Emulates a Xbox controller if the game doesn't support yours. Put the profile at ~/.config/faugus-launcher/controller.sccprofile.")
self.label_support = Gtk.Label(label="Support the project")
self.label_support.set_halign(Gtk.Align.START)
button_kofi = Gtk.Button(label="Buy me a Coffee")
button_kofi.set_size_request(150, -1)
button_kofi.connect("clicked", self.on_button_kofi_clicked)
button_kofi.get_style_context().add_class("kofi")
button_kofi.set_halign(Gtk.Align.CENTER)
button_paypal = Gtk.Button(label="PayPal Donation")
button_paypal.set_size_request(150, -1)
button_paypal.connect("clicked", self.on_button_paypal_clicked)
button_paypal.get_style_context().add_class("paypal")
button_paypal.set_halign(Gtk.Align.CENTER)
# 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)
self.button_cancel.set_halign(Gtk.Align.CENTER)
# 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.button_ok.set_halign(Gtk.Align.CENTER)
self.box = self.get_content_area()
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)
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_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_tools_wrap = Gtk.Grid()
grid_tools_wrap.set_row_spacing(10)
grid_tools_wrap.set_column_spacing(10)
grid_tools_wrap.set_margin_start(10)
grid_tools_wrap.set_margin_end(10)
grid_tools_wrap.set_margin_top(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_support = Gtk.Grid()
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_buttons = Gtk.Grid()
grid_buttons.set_row_spacing(10)
grid_buttons.set_column_spacing(10)
grid_buttons.set_margin_start(10)
grid_buttons.set_margin_end(10)
grid_buttons.set_margin_top(10)
grid_buttons.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_checkboxes = Gtk.Grid()
grid_checkboxes.set_row_spacing(10)
grid_checkboxes.set_column_spacing(10)
grid_checkboxes.set_margin_start(10)
grid_checkboxes.set_margin_end(10)
grid_checkboxes.set_margin_top(10)
grid_checkboxes.set_margin_bottom(10)
# Create a frame
frame = Gtk.Frame()
frame.set_label_align(0.5, 0.5)
frame.set_margin_start(10)
frame.set_margin_end(10)
frame.set_margin_top(10)
frame.set_margin_bottom(10)
# Add grid to frame
frame.add(grid_tools)
# Attach widgets to the grid layout
grid_interface_mode.attach(self.label_interface, 0, 0, 1, 1)
grid_interface_mode.attach(self.combo_box_interface, 0, 1, 1, 1)
self.combo_box_interface.set_hexpand(True)
# Attach widgets to the grid layout
self.grid_big_interface.attach(self.label_api_key, 0, 0, 1, 1)
self.grid_big_interface.attach(self.entry_api_key, 3, 0, 1, 1)
self.grid_big_interface.attach(self.checkbox_start_maximized, 0, 1, 1, 1)
self.grid_big_interface.attach(self.checkbox_start_fullscreen, 3, 1, 1, 1)
self.grid_big_interface.attach(self.checkbox_gamepad_navigation, 0, 2, 1, 1)
self.entry_api_key.set_hexpand(True)
# Attach widgets to the grid layout
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.combo_box_runner, 0, 7, 1, 1)
grid_runner.attach(self.button_proton_manager, 0, 8, 1, 1)
self.combo_box_runner.set_hexpand(True)
self.button_proton_manager.set_hexpand(True)
grid_checkboxes.attach(self.checkbox_discrete_gpu, 0, 2, 1, 1)
grid_checkboxes.attach(self.checkbox_splash_disable, 0, 3, 1, 1)
grid_checkboxes.attach(self.checkbox_system_tray, 0, 4, 1, 1)
grid_checkboxes.attach(self.checkbox_start_boot, 2, 4, 1, 1)
grid_checkboxes.attach(self.checkbox_close_after_launch, 0, 6, 1, 1)
grid_tools_wrap.attach(self.label_default_prefix_tools, 0, 0, 1, 1)
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_sc_controller, 0, 2, 1, 1)
grid_tools.attach(self.button_winetricks_default, 1, 0, 1, 1)
grid_tools.attach(self.button_winecfg_default, 1, 1, 1, 1)
grid_tools.attach(self.button_run_default, 1, 2, 1, 1)
grid_support.attach(self.label_support, 0, 0, 1, 1)
grid_support.attach(button_kofi, 0, 1, 1, 1)
grid_support.attach(button_paypal, 1, 1, 1, 1)
grid_support.set_halign(Gtk.Align.CENTER)
grid_buttons.attach(self.button_cancel, 0, 0, 1, 1)
grid_buttons.attach(self.button_ok, 1, 0, 1, 1)
grid_buttons.set_halign(Gtk.Align.CENTER)
self.populate_combobox_with_runners()
self.load_config()
# 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.sc_controller_enabled = os.path.exists("/usr/bin/sc-controller") or os.path.exists(
"/usr/local/bin/sc-controller")
if not self.sc_controller_enabled:
self.checkbox_sc_controller.set_sensitive(False)
self.checkbox_sc_controller.set_active(False)
self.checkbox_sc_controller.set_tooltip_text(
"Emulates a Xbox controller if the game doesn't support yours. Put the profile at ~/.config/faugus-launcher/controller.sccprofile. NOT INSTALLED.")
self.box.add(grid_interface_mode)
self.box.add(self.grid_big_interface)
self.box.add(grid_prefix)
self.box.add(grid_runner)
self.box.add(grid_tools_wrap)
self.box.add(frame)
self.box.add(grid_checkboxes)
self.box.add(grid_support)
self.box.add(grid_buttons)
self.show_all()
self.on_combobox_interface_changed(self.combo_box_interface)
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_link_clicked(self, label, uri):
webbrowser.open(uri)
def on_combobox_interface_changed(self, combo_box):
active_index = combo_box.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.label_api_key.set_visible(False)
self.entry_api_key.set_visible(False)
if active_index == 2:
self.grid_big_interface.set_visible(True)
self.label_api_key.set_visible(True)
self.entry_api_key.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)
else:
self.checkbox_start_boot.set_sensitive(True)
def on_button_proton_manager_clicked(self, widget):
self.set_sensitive(False)
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.combo_box_runner.remove_all())
GLib.idle_add(self.populate_combobox_with_runners)
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 populate_combobox_with_runners(self):
# List of default entries
self.combo_box_runner.append_text("GE-Proton Latest (default)")
self.combo_box_runner.append_text("UMU-Proton Latest")
# Path to the directory containing the folders
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 != "UMU-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.combo_box_runner.append_text(version)
except Exception as e:
print(f"Error accessing the directory: {e}")
# Set the active item, if desired
self.combo_box_runner.set_active(0)
def on_entry_changed(self, widget, entry):
if entry.get_text():
entry.get_style_context().remove_class("entry")
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")
else:
checkbox_state = self.checkbox_close_after_launch.get_active()
default_prefix = self.entry_default_prefix.get_text()
checkbox_discrete_gpu_state = self.checkbox_discrete_gpu.get_active()
checkbox_splash_disable = self.checkbox_splash_disable.get_active()
checkbox_start_boot = self.checkbox_start_boot.get_active()
checkbox_system_tray = self.checkbox_system_tray.get_active()
checkbox_start_maximized = self.checkbox_start_maximized.get_active()
combo_box_interface = self.combo_box_interface.get_active_text()
entry_api_key = self.entry_api_key.get_text()
checkbox_start_fullscreen = self.checkbox_start_fullscreen.get_active()
checkbox_gamepad_navigation = self.checkbox_gamepad_navigation.get_active()
mangohud_state = self.checkbox_mangohud.get_active()
gamemode_state = self.checkbox_gamemode.get_active()
sc_controller_state = self.checkbox_sc_controller.get_active()
default_runner = self.combo_box_runner.get_active_text()
if default_runner == "UMU-Proton Latest":
default_runner = ""
if default_runner == "GE-Proton Latest (default)":
default_runner = "GE-Proton"
self.parent.save_config(checkbox_state, default_prefix, mangohud_state, gamemode_state, sc_controller_state, default_runner, checkbox_discrete_gpu_state, checkbox_splash_disable, checkbox_system_tray, checkbox_start_boot, combo_box_interface, checkbox_start_maximized, entry_api_key, checkbox_start_fullscreen, checkbox_gamepad_navigation)
self.set_sensitive(False)
self.parent.manage_autostart_file(checkbox_start_boot)
if checkbox_system_tray:
self.parent.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
if not hasattr(self, "window_delete_event_connected") or not self.window_delete_event_connected:
self.connect("delete-event", self.parent.on_window_delete_event)
self.parent.window_delete_event_connected = True
self.parent.indicator.set_menu(self.parent.create_tray_menu())
else:
self.parent.indicator.set_status(AppIndicator3.IndicatorStatus.PASSIVE)
if hasattr(self, "window_delete_event_connected") and self.window_delete_event_connected:
self.disconnect_by_func(self.parent.on_window_delete_event)
self.parent.window_delete_event_connected = False
dialog = Gtk.FileChooserDialog(title="Select a file to run inside the prefix",
action=Gtk.FileChooserAction.OPEN)
dialog.set_current_folder(os.path.expanduser("~/"))
dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
# Windows files filter
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")
dialog.add_filter(windows_filter)
# All files filter
all_files_filter = Gtk.FileFilter()
all_files_filter.set_name("All files")
all_files_filter.add_pattern("*")
dialog.add_filter(all_files_filter)
response = dialog.run()
if response == Gtk.ResponseType.OK:
command_parts = []
file_run = dialog.get_filename()
if not file_run.endswith(".reg"):
if file_run:
command_parts.append(f'GAMEID=default')
if default_runner:
command_parts.append(f'PROTONPATH={default_runner}')
command_parts.append(f'"{umu_run}" "{file_run}"')
else:
if file_run:
command_parts.append(f'GAMEID=default')
if default_runner:
command_parts.append(f'PROTONPATH={default_runner}')
command_parts.append(f'"{umu_run}" regedit "{file_run}"')
# 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()
else:
self.set_sensitive(True)
dialog.destroy()
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:
checkbox_state = self.checkbox_close_after_launch.get_active()
default_prefix = self.entry_default_prefix.get_text()
checkbox_discrete_gpu_state = self.checkbox_discrete_gpu.get_active()
checkbox_splash_disable = self.checkbox_splash_disable.get_active()
checkbox_start_boot = self.checkbox_start_boot.get_active()
checkbox_system_tray = self.checkbox_system_tray.get_active()
checkbox_start_maximized = self.checkbox_start_maximized.get_active()
combo_box_interface = self.combo_box_interface.get_active_text()
entry_api_key = self.entry_api_key.get_text()
checkbox_start_fullscreen = self.checkbox_start_fullscreen.get_active()
checkbox_gamepad_navigation = self.checkbox_gamepad_navigation.get_active()
mangohud_state = self.checkbox_mangohud.get_active()
gamemode_state = self.checkbox_gamemode.get_active()
sc_controller_state = self.checkbox_sc_controller.get_active()
default_runner = self.combo_box_runner.get_active_text()
if default_runner == "UMU-Proton Latest":
default_runner = ""
if default_runner == "GE-Proton Latest (default)":
default_runner = "GE-Proton"
self.parent.save_config(checkbox_state, default_prefix, mangohud_state, gamemode_state, sc_controller_state, default_runner, checkbox_discrete_gpu_state, checkbox_splash_disable, checkbox_system_tray, checkbox_start_boot, combo_box_interface, checkbox_start_maximized, entry_api_key, checkbox_start_fullscreen, checkbox_gamepad_navigation)
self.set_sensitive(False)
self.parent.manage_autostart_file(checkbox_start_boot)
if checkbox_system_tray:
self.parent.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
if not hasattr(self, "window_delete_event_connected") or not self.window_delete_event_connected:
self.connect("delete-event", self.parent.on_window_delete_event)
self.parent.window_delete_event_connected = True
self.parent.indicator.set_menu(self.parent.create_tray_menu())
else:
self.parent.indicator.set_status(AppIndicator3.IndicatorStatus.PASSIVE)
if hasattr(self, "window_delete_event_connected") and self.window_delete_event_connected:
self.disconnect_by_func(self.parent.on_window_delete_event)
self.parent.window_delete_event_connected = False
command_parts = []
# Add command parts if they are not empty
command_parts.append(f'GAMEID=default')
if default_runner:
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_winetricks_default_clicked(self, widget):
if self.entry_default_prefix.get_text() == "":
self.entry_default_prefix.get_style_context().add_class("entry")
else:
checkbox_state = self.checkbox_close_after_launch.get_active()
default_prefix = self.entry_default_prefix.get_text()
checkbox_discrete_gpu_state = self.checkbox_discrete_gpu.get_active()
checkbox_splash_disable = self.checkbox_splash_disable.get_active()
checkbox_start_boot = self.checkbox_start_boot.get_active()
checkbox_system_tray = self.checkbox_system_tray.get_active()
checkbox_start_maximized = self.checkbox_start_maximized.get_active()
combo_box_interface = self.combo_box_interface.get_active_text()
entry_api_key = self.entry_api_key.get_text()
checkbox_start_fullscreen = self.checkbox_start_fullscreen.get_active()
checkbox_gamepad_navigation = self.checkbox_gamepad_navigation.get_active()
mangohud_state = self.checkbox_mangohud.get_active()
gamemode_state = self.checkbox_gamemode.get_active()
sc_controller_state = self.checkbox_sc_controller.get_active()
default_runner = self.combo_box_runner.get_active_text()
if default_runner == "UMU-Proton Latest":
default_runner = ""
if default_runner == "GE-Proton Latest (default)":
default_runner = "GE-Proton"
self.parent.save_config(checkbox_state, default_prefix, mangohud_state, gamemode_state, sc_controller_state, default_runner, checkbox_discrete_gpu_state, checkbox_splash_disable, checkbox_system_tray, checkbox_start_boot, combo_box_interface, checkbox_start_maximized, entry_api_key, checkbox_start_fullscreen, checkbox_gamepad_navigation)
self.set_sensitive(False)
self.parent.manage_autostart_file(checkbox_start_boot)
if checkbox_system_tray:
self.parent.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
if not hasattr(self, "window_delete_event_connected") or not self.window_delete_event_connected:
self.connect("delete-event", self.parent.on_window_delete_event)
self.parent.window_delete_event_connected = True
self.parent.indicator.set_menu(self.parent.create_tray_menu())
else:
self.parent.indicator.set_status(AppIndicator3.IndicatorStatus.PASSIVE)
if hasattr(self, "window_delete_event_connected") and self.window_delete_event_connected:
self.disconnect_by_func(self.parent.on_window_delete_event)
self.parent.window_delete_event_connected = False
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:
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_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):
# Handle the click event of the search button to select the game's .exe
dialog = Gtk.FileChooserDialog(title="Select a prefix location", parent=self,
action=Gtk.FileChooserAction.SELECT_FOLDER)
dialog.set_current_folder(os.path.expanduser(self.default_prefix))
dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
response = dialog.run()
if response == Gtk.ResponseType.OK:
self.entry_default_prefix.set_text(dialog.get_filename())
dialog.destroy()
def load_config(self):
# Load configuration from file
config_file = os.path.join(self.parent.working_directory, 'config.ini')
if os.path.exists(config_file):
with open(config_file, 'r') as f:
config_data = f.read().splitlines()
config_dict = dict(line.split('=') for line in config_data)
close_on_launch = config_dict.get('close-onlaunch', 'False') == 'True'
self.default_prefix = config_dict.get('default-prefix', '').strip('"')
mangohud = config_dict.get('mangohud', 'False') == 'True'
gamemode = config_dict.get('gamemode', 'False') == 'True'
sc_controller = config_dict.get('sc-controller', 'False') == 'True'
self.default_runner = config_dict.get('default-runner', '').strip('"')
discrete_gpu = config_dict.get('discrete-gpu', 'False') == 'True'
splash_disable = config_dict.get('splash-disable', 'False') == 'True'
system_tray = config_dict.get('system-tray', 'False') == 'True'
start_boot = config_dict.get('start-boot', 'False') == 'True'
start_maximized = config_dict.get('start-maximized', 'False') == 'True'
self.interface_mode = config_dict.get('interface-mode', '').strip('"')
self.api_key = config_dict.get('api-key', '').strip('"')
start_fullscreen = config_dict.get('start-fullscreen', 'False') == 'True'
gamepad_navigation = config_dict.get('gamepad-navigation', '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_sc_controller.set_active(sc_controller)
if self.default_runner == "":
self.default_runner = "UMU-Proton Latest"
model = self.combo_box_runner.get_model()
index_to_activate = 0
for i, row in enumerate(model):
if row[0] == self.default_runner:
index_to_activate = i
break
self.combo_box_runner.set_active(index_to_activate)
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(start_boot)
self.checkbox_start_maximized.set_active(start_maximized)
self.checkbox_start_fullscreen.set_active(start_fullscreen)
self.checkbox_gamepad_navigation.set_active(gamepad_navigation)
model = self.combo_box_interface.get_model()
index_to_activate2 = 0
for i, row in enumerate(model):
if row[0] == self.interface_mode:
index_to_activate2 = i
break
self.combo_box_interface.set_active(index_to_activate2)
self.entry_api_key.set_text(self.api_key)
else:
# Save default configuration if file does not exist
print("else")
self.parent.save_config(False, '', "False", "False", "False", "GE-Proton", "True", "False", "False", "False", "List", "False", "", "False", "False")
class Game:
def __init__(self, title, path, prefix, launch_arguments, game_arguments, mangohud, gamemode, sc_controller, protonfix, runner, addapp_checkbox, addapp, addapp_bat, banner):
# Initialize a Game object with various attributes
self.title = title # Title of the game
self.path = path # Path to the game executable
self.launch_arguments = launch_arguments # Arguments to launch the game
self.game_arguments = game_arguments # Arguments specific to the game
self.mangohud = mangohud # Boolean indicating whether Mangohud is enabled
self.gamemode = gamemode # Boolean indicating whether Gamemode is enabled
self.prefix = prefix # Prefix for Wine games
self.sc_controller = sc_controller # Boolean indicating whether SC Controller is enabled
self.protonfix = protonfix
self.runner = runner
self.addapp_checkbox = addapp_checkbox
self.addapp = addapp
self.addapp_bat = addapp_bat
self.banner = banner
class ConfirmationDialog(Gtk.Dialog):
def __init__(self, parent, title, prefix):
# Initialize the ConfirmationDialog
Gtk.Dialog.__init__(self, title=f"Delete {title}", parent=parent, modal=True)
self.set_icon_from_file(faugus_png)
# Configure dialog properties
self.set_resizable(False)
# Create a grid layout for the dialog content area
grid = Gtk.Grid()
grid.set_row_spacing(20)
grid.set_column_spacing(10)
grid.set_margin_start(10)
grid.set_margin_end(10)
grid.set_margin_top(10)
grid.set_margin_bottom(10)
# Add grid to dialog's content area
content_area = self.get_content_area()
content_area.set_border_width(0)
content_area.add(grid)
# Create a label
label = Gtk.Label()
label.set_label(f"Are you sure you want to delete {title}?")
label.set_halign(Gtk.Align.CENTER)
grid.attach(label, 0, 0, 2, 1)
# Create "No" button
button_no = Gtk.Button(label="Cancel")
button_no.set_size_request(150, -1)
button_no.connect("clicked", lambda x: self.response(Gtk.ResponseType.NO))
grid.attach(button_no, 0, 2, 1, 1)
# Create "Yes" button
button_yes = Gtk.Button(label="Confirm")
button_yes.set_size_request(150, -1)
button_yes.connect("clicked", lambda x: self.response(Gtk.ResponseType.YES))
grid.attach(button_yes, 1, 2, 1, 1)
# Create a checkbox to optionally remove the prefix
self.checkbox = Gtk.CheckButton(label="Also remove the prefix")
self.checkbox.set_halign(Gtk.Align.CENTER)
if os.path.basename(prefix) != "default":
grid.attach(self.checkbox, 0, 1, 2, 1)
# Display all widgets
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, api_key, 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.api_key = api_key
self.interface_mode = interface_mode
self.icon_directory = f"{icons_dir}/icon_temp/"
if not os.path.exists(banners_dir):
os.makedirs(banners_dir)
self.banner_path_temp = os.path.join(banners_dir, "banner_temp.png")
shutil.copy(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.ico'
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)
grid_page1 = Gtk.Grid()
grid_page2 = Gtk.Grid()
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_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()
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_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_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;
}
"""
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.combo_box_launcher = Gtk.ComboBoxText()
self.combo_box_launcher.connect("changed", self.on_combobox_changed)
# 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)
if interface_mode == "Banners":
self.entry_title.connect("focus-out-event", self.on_entry_focus_out)
self.entry_title.set_tooltip_text("Game Title")
# 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.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.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="Runner")
self.label_runner.set_halign(Gtk.Align.START)
self.combo_box_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.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")
# 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")
# 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_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_sc_controller = Gtk.CheckButton(label="SC Controller")
self.checkbox_sc_controller.set_tooltip_text(
"Emulates a Xbox controller if the game doesn't support yours. Put the profile at ~/.config/faugus-launcher/controller.sccprofile.")
# 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.checkbox_shortcut = Gtk.CheckButton(label="Create Shortcut")
# 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)
# Event handlers
self.default_prefix = self.load_default_prefix()
self.entry_title.connect("changed", self.update_prefix_entry)
self.default_runner = self.load_default_runner()
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_end(10)
self.image_banner.set_margin_top(10)
self.image_banner.set_margin_bottom(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_end(10)
self.image_banner2.set_margin_top(10)
self.image_banner2.set_margin_bottom(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)
self.menu.show_all()
page1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
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)
tab_box1.pack_start(tab_label1, True, True, 0)
tab_box1.set_hexpand(True)
grid_page1.add(page1)
grid_page1.add(event_box)
self.notebook.append_page(grid_page1, tab_box1)
page2 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
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)
tab_box2.pack_start(tab_label2, True, True, 0)
tab_box2.set_hexpand(True)
grid_page2.add(page2)
grid_page2.add(event_box2)
self.notebook.append_page(grid_page2, tab_box2)
self.grid_launcher.attach(self.combo_box_launcher, 0, 0, 4, 1)
self.combo_box_launcher.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.combo_box_runner, 0, 1, 1, 1)
self.combo_box_runner.set_hexpand(True)
self.grid_shortcut.attach(self.button_shortcut_icon, 2, 0, 1, 1)
self.grid_shortcut.attach(self.checkbox_shortcut, 0, 0, 1, 1)
self.checkbox_shortcut.set_hexpand(True)
page1.add(self.grid_launcher)
page1.add(self.grid_title)
page1.add(self.grid_path)
page1.add(self.grid_prefix)
page1.add(self.grid_runner)
page1.add(self.grid_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_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)
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_sc_controller, 0, 2, 1, 1)
self.checkbox_sc_controller.set_hexpand(True)
self.grid_tools.attach(self.button_winetricks, 2, 0, 1, 1)
self.grid_tools.attach(self.button_winecfg, 2, 1, 1, 1)
self.grid_tools.attach(self.button_run, 2, 2, 1, 1)
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_tools)
botton_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
botton_box.set_margin_start(10)
botton_box.set_margin_end(10)
#botton_box.set_margin_top(10)
botton_box.set_margin_bottom(10)
self.button_cancel.set_hexpand(True)
self.button_ok.set_hexpand(True)
botton_box.pack_start(self.button_cancel, True, True, 0)
botton_box.pack_start(self.button_ok, True, True, 0)
self.box.add(botton_box)
self.populate_combobox_with_launchers()
self.combo_box_launcher.set_active(0)
self.populate_combobox_with_runners()
model = self.combo_box_runner.get_model()
index_to_activate = 0
if self.default_runner == "":
self.default_runner = "UMU-Proton Latest"
if self.default_runner == "GE-Proton":
self.default_runner = "GE-Proton Latest (default)"
for i, row in enumerate(model):
if row[0] == self.default_runner:
index_to_activate = i
break
self.combo_box_runner.set_active(index_to_activate)
# 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.sc_controller_enabled = os.path.exists("/usr/bin/sc-controller") or os.path.exists(
"/usr/local/bin/sc-controller")
if not self.sc_controller_enabled:
self.checkbox_sc_controller.set_sensitive(False)
self.checkbox_sc_controller.set_active(False)
self.checkbox_sc_controller.set_tooltip_text(
"Emulates a Xbox controller if the game doesn't support yours. Put the profile at ~/.config/faugus-launcher/controller.sccprofile. NOT INSTALLED.")
# self.create_remove_shortcut(self)
self.button_shortcut_icon.set_image(self.set_image_shortcut_icon())
tab_box1.show_all()
tab_box2.show_all()
self.show_all()
if interface_mode != "Banners":
self.image_banner.set_visible(False)
self.image_banner2.set_visible(False)
allocation = self.entry_title.get_allocation()
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(self.banner_path_temp, (allocation.width - 10), -1, True)
self.image_banner.set_from_pixbuf(pixbuf)
self.image_banner2.set_from_pixbuf(pixbuf)
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.copy(faugus_banner, self.banner_path_temp)
allocation = self.image_banner.get_allocation()
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(self.banner_path_temp, allocation.width, -1, True)
self.image_banner.set_from_pixbuf(pixbuf)
self.image_banner2.set_from_pixbuf(pixbuf)
def on_load_file(self, widget):
dialog = Gtk.FileChooserDialog(title="Select an image for the banner", action=Gtk.FileChooserAction.OPEN)
dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
# Add a filter to limit selection to .ico files
filter_ico = Gtk.FileFilter()
filter_ico.set_name("Image files")
filter_ico.add_mime_type("image/*") # Other image formats
dialog.add_filter(filter_ico)
# Set the initial directory to the icon directory
dialog.set_current_folder(os.path.expanduser("~/"))
# Connect signal to update preview widget when file selection changes
dialog.connect("update-preview", self.update_preview)
response = dialog.run()
if response == Gtk.ResponseType.OK:
file_path = dialog.get_filename()
shutil.copy(file_path, self.banner_path_temp)
self.update_image_banner()
dialog.destroy()
def get_banner(self):
def fetch_banner():
title = self.entry_title.get_text()
try:
# Request SteamGridDB API
headers = {"Authorization": f"Bearer {self.api_key}"}
response = requests.get(f"https://www.steamgriddb.com/api/v2/search/autocomplete/{title}", headers=headers)
response.raise_for_status()
data = response.json()
# Check if any game was found
if not data["data"]:
print(f"No game found with the title: {title}")
return
# Get the ID of the first game found
game_id = data["data"][0]["id"]
# Fetch images for the game with 2:3 filter (Steam Vertical)
params = {"dimensions": "600x900"} # Adjust for the desired format
images_response = requests.get(
f"https://www.steamgriddb.com/api/v2/grids/game/{game_id}",
headers=headers,
params=params
)
images_response.raise_for_status()
images_data = images_response.json()
# Select the first available image
if not images_data["data"]:
print("No image found for this game.")
return
image_url = images_data["data"][0]["url"]
# Download and save the image
image_response = requests.get(image_url)
image_response.raise_for_status()
# Check if the banners directory exists, otherwise create it
os.makedirs(banners_dir, exist_ok=True)
with open(self.banner_path_temp, "wb") as image_file:
image_file.write(image_response.content)
# Display the image in Gtk.Image on the main thread
GLib.idle_add(self.update_image_banner)
except requests.exceptions.RequestException as e:
dialog = Gtk.MessageDialog(title="Faugus Launcher", text="Error downloading the banner from SteamDBGrid.\nCheck the API Key and the internet connection.",
buttons=Gtk.ButtonsType.OK, parent=self)
dialog.set_resizable(False)
dialog.set_modal(True)
dialog.set_icon_from_file(faugus_png)
dialog.run()
dialog.destroy()
# Start the thread
threading.Thread(target=fetch_banner, daemon=True).start()
def update_image_banner(self):
allocation = self.image_banner.get_allocation()
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(self.banner_path_temp, allocation.width, -1, 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.copy(faugus_banner, self.banner_path_temp)
allocation = self.image_banner.get_allocation()
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(self.banner_path_temp, allocation.width, -1, 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):
# Handle the click event of the search button to select the game's .exe
dialog = Gtk.FileChooserDialog(title="Select the additional application", parent=self, action=Gtk.FileChooserAction.OPEN)
if not self.entry_path.get_text():
dialog.set_current_folder(os.path.expanduser("~/"))
else:
dialog.set_current_folder(os.path.dirname(self.entry_path.get_text()))
dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
# Windows files filter
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")
dialog.add_filter(windows_filter)
# All files filter
all_files_filter = Gtk.FileFilter()
all_files_filter.set_name("All files")
all_files_filter.add_pattern("*")
dialog.add_filter(all_files_filter)
response = dialog.run()
if response == Gtk.ResponseType.OK:
self.entry_addapp.set_text(dialog.get_filename())
dialog.destroy()
def on_combobox_changed(self, combo_box):
active_index = combo_box.get_active()
if active_index == 0:
self.grid_title.set_visible(True)
self.grid_path.set_visible(True)
self.grid_runner.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.entry_launch_arguments.set_text("")
self.entry_title.set_text("")
self.entry_path.set_text("")
if active_index == 1:
self.grid_title.set_visible(True)
self.grid_path.set_visible(True)
self.grid_runner.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.entry_launch_arguments.set_text("")
self.entry_title.set_text("")
self.entry_path.set_text("")
self.button_shortcut_icon.set_image(self.set_image_shortcut_icon())
elif active_index == 2:
self.grid_title.set_visible(False)
self.grid_path.set_visible(False)
self.grid_runner.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.entry_launch_arguments.set_text("WINE_SIMULATE_WRITECOPY=1")
self.entry_title.set_text(self.combo_box_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.copy(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)
elif active_index == 3:
self.grid_title.set_visible(False)
self.grid_path.set_visible(False)
self.grid_runner.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.entry_launch_arguments.set_text("")
self.entry_title.set_text(self.combo_box_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.copy(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)
elif active_index == 4:
self.grid_title.set_visible(False)
self.grid_path.set_visible(False)
self.grid_runner.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.entry_launch_arguments.set_text("")
self.entry_title.set_text(self.combo_box_launcher.get_active_text())
self.entry_path.set_text(f"{self.entry_prefix.get_text()}/drive_c/Program Files (x86)/Epic Games/Launcher/Portal/Binaries/Win32/EpicGamesLauncher.exe")
shutil.copy(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)
elif active_index == 5:
self.grid_title.set_visible(False)
self.grid_path.set_visible(False)
self.grid_runner.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.entry_launch_arguments.set_text("")
self.entry_title.set_text(self.combo_box_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.copy(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)
if self.interface_mode == "Banners":
if self.entry_title.get_text() != "":
self.get_banner()
else:
shutil.copy(faugus_banner, self.banner_path_temp)
allocation = self.entry_title.get_allocation()
width = allocation.width - 10
if width > 0:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(self.banner_path_temp, width, -1, True)
self.image_banner.set_from_pixbuf(pixbuf)
self.image_banner2.set_from_pixbuf(pixbuf)
def populate_combobox_with_launchers(self):
self.combo_box_launcher.append_text("Windows game")
self.combo_box_launcher.append_text("Linux game")
self.combo_box_launcher.append_text("Battle.net")
self.combo_box_launcher.append_text("EA App")
self.combo_box_launcher.append_text("Epic Games")
self.combo_box_launcher.append_text("Ubisoft Connect")
#self.combo_box_launcher.append_text("HoYoPlay")
def populate_combobox_with_runners(self):
# List of default entries
self.combo_box_runner.append_text("GE-Proton Latest (default)")
self.combo_box_runner.append_text("UMU-Proton Latest")
# Path to the directory containing the folders
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 != "UMU-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.combo_box_runner.append_text(version)
except Exception as e:
print(f"Error accessing the directory: {e}")
# Set the active item, if desired
self.combo_box_runner.set_active(0)
def on_entry_changed(self, widget, entry):
if entry.get_text():
entry.get_style_context().remove_class("entry")
def load_default_prefix(self):
config_file = config_file_dir
default_prefix = ""
if os.path.exists(config_file):
with open(config_file, 'r') as f:
config_data = f.read().splitlines()
config_dict = dict(line.split('=') for line in config_data)
default_prefix = config_dict.get('default-prefix', '').strip('"')
return default_prefix
def load_default_runner(self):
config_file = config_file_dir
default_runner = ""
if os.path.exists(config_file):
with open(config_file, 'r') as f:
config_data = f.read().splitlines()
config_dict = dict(line.split('=') for line in config_data)
default_runner = config_dict.get('default-runner', '').strip('"')
return default_runner
def on_button_run_clicked(self, widget):
self.set_sensitive(False)
# Handle the click event of the Run button
validation_result = self.validate_fields(entry="prefix")
if not validation_result:
self.set_sensitive(True)
return
dialog = Gtk.FileChooserDialog(title="Select a file to run inside the prefix",
action=Gtk.FileChooserAction.OPEN)
dialog.set_current_folder(os.path.expanduser("~/"))
dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
# Windows files filter
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")
dialog.add_filter(windows_filter)
# All files filter
all_files_filter = Gtk.FileFilter()
all_files_filter.set_name("All files")
all_files_filter.add_pattern("*")
dialog.add_filter(all_files_filter)
response = dialog.run()
if response == Gtk.ResponseType.OK:
title = self.entry_title.get_text()
prefix = self.entry_prefix.get_text()
title_formatted = re.sub(r'[^a-zA-Z0-9\s]', '', title)
title_formatted = title_formatted.replace(' ', '-')
title_formatted = '-'.join(title_formatted.lower().split())
runner = self.combo_box_runner.get_active_text()
if runner == "UMU-Proton Latest":
runner = ""
if runner == "GE-Proton Latest (default)":
runner = "GE-Proton"
command_parts = []
# Add command parts if they are not empty
file_run = dialog.get_filename()
if not file_run.endswith(".reg"):
if prefix:
command_parts.append(f'WINEPREFIX={prefix}')
if title_formatted:
command_parts.append(f'GAMEID={title_formatted}')
if runner:
command_parts.append(f'PROTONPATH={runner}')
command_parts.append(f'"{umu_run}" "{file_run}"')
else:
if prefix:
command_parts.append(f'WINEPREFIX={prefix}')
if title_formatted:
command_parts.append(f'GAMEID={title_formatted}')
if runner:
command_parts.append(f'PROTONPATH={runner}')
command_parts.append(f'"{umu_run}" regedit "{file_run}"')
# 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()
else:
self.set_sensitive(True)
dialog.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.copy(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 not os.path.exists(self.icon_directory):
os.makedirs(self.icon_directory)
try:
# Attempt to extract the icon
command = f'icoextract "{path}" "{self.icon_extracted}"'
result = subprocess.run(command, shell=True, text=True, capture_output=True)
# Check if there was an error in executing the command
if result.returncode != 0:
if "NoIconsAvailableError" in result.stderr or "PEFormatError" in result.stderr:
print("The file does not contain icons.")
self.button_shortcut_icon.set_image(self.set_image_shortcut_icon())
else:
print(f"Error extracting icon: {result.stderr}")
else:
# Convert the extracted icon to PNG
command_magick = shutil.which("magick") or shutil.which("convert")
os.system(f'{command_magick} "{self.icon_extracted}" "{self.icon_converted}"')
if os.path.isfile(self.icon_extracted):
os.remove(self.icon_extracted)
except Exception as e:
print(f"An error occurred: {e}")
# Open file dialog to select .ico file
dialog = Gtk.FileChooserDialog(title="Select an icon for the shortcut", action=Gtk.FileChooserAction.OPEN)
dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
# Add a filter to limit selection to .ico files
filter_ico = Gtk.FileFilter()
filter_ico.set_name("Image files")
filter_ico.add_mime_type("image/*") # Other image formats
dialog.add_filter(filter_ico)
# Set the initial directory to the icon directory
dialog.set_current_folder(self.icon_directory)
# Connect signal to update preview widget when file selection changes
dialog.connect("update-preview", self.update_preview)
response = dialog.run()
if response == Gtk.ResponseType.OK:
file_path = dialog.get_filename()
# Move and rename the icon file
shutil.copy(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)
# Delete the folder after the icon is moved
if os.path.isdir(self.icon_directory):
shutil.rmtree(self.icon_directory)
dialog.destroy()
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 = re.sub(r'[^a-zA-Z0-9\s]', '', title).replace(' ', '-').lower()
desktop_file_path = f"{app_dir}/{title_formatted}.desktop"
# Check if the shortcut file exists
shortcut_exists = os.path.exists(desktop_file_path)
# Mark the checkbox if the shortcut exists
self.checkbox_shortcut.set_active(shortcut_exists)
def update_prefix_entry(self, entry):
# Update the prefix entry based on the title and self.default_prefix
title_formatted = re.sub(r'[^a-zA-Z0-9\s]', '', entry.get_text())
title_formatted = title_formatted.replace(' ', '-')
title_formatted = '-'.join(title_formatted.lower().split())
self.default_prefix = self.load_default_prefix()
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 = re.sub(r'[^a-zA-Z0-9\s]', '', title)
title_formatted = title_formatted.replace(' ', '-')
title_formatted = '-'.join(title_formatted.lower().split())
runner = self.combo_box_runner.get_active_text()
if runner == "UMU-Proton Latest":
runner = ""
if runner == "GE-Proton Latest (default)":
runner = "GE-Proton"
command_parts = []
# Add command parts if they are not empty
if prefix:
command_parts.append(f'WINEPREFIX={prefix}')
if title_formatted:
command_parts.append(f'GAMEID={title_formatted}')
if runner:
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
prefix = self.entry_prefix.get_text()
runner = self.combo_box_runner.get_active_text()
if runner == "UMU-Proton Latest":
runner = ""
if runner == "GE-Proton Latest (default)":
runner = "GE-Proton"
command_parts = []
# Add command parts if they are not empty
if prefix:
command_parts.append(f'WINEPREFIX={prefix}')
command_parts.append(f'GAMEID=winetricks-gui')
command_parts.append(f'STORE=none')
if runner:
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):
# Handle the click event of the search button to select the game's .exe
dialog = Gtk.FileChooserDialog(title="Select the game's .exe", parent=self, action=Gtk.FileChooserAction.OPEN)
if not self.entry_path.get_text():
dialog.set_current_folder(os.path.expanduser("~/"))
else:
dialog.set_current_folder(os.path.dirname(self.entry_path.get_text()))
dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
if self.combo_box_launcher.get_active() != 1:
# Windows files filter
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")
dialog.add_filter(windows_filter)
# All files filter
all_files_filter = Gtk.FileFilter()
all_files_filter.set_name("All files")
all_files_filter.add_pattern("*")
dialog.add_filter(all_files_filter)
response = dialog.run()
if response == Gtk.ResponseType.OK:
path = dialog.get_filename()
if not os.path.exists(self.icon_directory):
os.makedirs(self.icon_directory)
try:
# Attempt to extract the icon
command = f'icoextract "{path}" "{self.icon_extracted}"'
result = subprocess.run(command, shell=True, text=True, capture_output=True)
# Check if there was an error in executing the command
if result.returncode != 0:
if "NoIconsAvailableError" in result.stderr or "PEFormatError" in result.stderr:
print("The file does not contain icons.")
self.button_shortcut_icon.set_image(self.set_image_shortcut_icon())
else:
print(f"Error extracting icon: {result.stderr}")
else:
# Convert the extracted icon to PNG
command_magick = shutil.which("magick") or shutil.which("convert")
os.system(f'{command_magick} "{self.icon_extracted}" "{self.icon_converted}"')
if os.path.isfile(self.icon_extracted):
os.remove(self.icon_extracted)
largest_image = self.find_largest_resolution(self.icon_directory)
shutil.move(largest_image, os.path.expanduser(self.icon_temp))
pixbuf = GdkPixbuf.Pixbuf.new_from_file(self.icon_temp)
scaled_pixbuf = pixbuf.scale_simple(50, 50, GdkPixbuf.InterpType.BILINEAR)
image = Gtk.Image.new_from_file(self.icon_temp)
image.set_from_pixbuf(scaled_pixbuf)
self.button_shortcut_icon.set_image(image)
except Exception as e:
print(f"An error occurred: {e}")
self.entry_path.set_text(dialog.get_filename())
if os.path.isdir(self.icon_directory):
shutil.rmtree(self.icon_directory)
dialog.destroy()
def on_button_search_prefix_clicked(self, widget):
dialog = Gtk.FileChooserDialog(title="Select a prefix location", parent=self,
action=Gtk.FileChooserAction.SELECT_FOLDER)
if not self.entry_prefix.get_text():
dialog.set_current_folder(os.path.expanduser(self.default_prefix))
else:
dialog.set_current_folder(self.entry_prefix.get_text())
dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
response = dialog.run()
if response == Gtk.ResponseType.OK:
new_prefix = dialog.get_filename()
self.default_prefix = new_prefix
#self.entry_title.emit("changed")
self.entry_prefix.set_text(self.default_prefix)
dialog.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()
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 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
class CreateShortcut(Gtk.Window):
def __init__(self, file_path):
super().__init__(title="Faugus Launcher")
self.file_path = file_path
self.set_resizable(False)
self.set_icon_from_file(faugus_png)
game_title = os.path.basename(file_path)
self.set_title(game_title)
print(self.file_path)
self.icon_directory = f"{icons_dir}/icon_temp/"
if not os.path.exists(self.icon_directory):
os.makedirs(self.icon_directory)
self.icons_path = icons_dir
self.icon_extracted = os.path.expanduser(f'{self.icons_path}/icon_temp/icon.ico')
self.icon_converted = os.path.expanduser(f'{self.icons_path}/icon_temp/icon.png')
self.icon_temp = f'{self.icons_path}/icon_temp.ico'
self.default_prefix = ""
self.label_title = Gtk.Label(label="Title")
self.label_title.set_halign(Gtk.Align.START)
self.entry_title = Gtk.Entry()
self.entry_title.connect("changed", self.on_entry_changed, self.entry_title)
self.entry_title.set_tooltip_text("Game Title")
self.label_protonfix = Gtk.Label(label="Protonfix")
self.label_protonfix.set_halign(Gtk.Align.START)
self.entry_protonfix = Gtk.Entry()
self.entry_protonfix.set_tooltip_text("UMU ID")
self.button_search_protonfix = Gtk.Button()
self.button_search_protonfix.set_image(Gtk.Image.new_from_icon_name("system-search-symbolic", Gtk.IconSize.BUTTON))
self.button_search_protonfix.connect("clicked", self.on_button_search_protonfix_clicked)
self.button_search_protonfix.set_size_request(50, -1)
self.label_launch_arguments = Gtk.Label(label="Launch Arguments")
self.label_launch_arguments.set_halign(Gtk.Align.START)
self.entry_launch_arguments = Gtk.Entry()
self.entry_launch_arguments.set_tooltip_text("e.g.: PROTON_USE_WINED3D=1 gamescope -W 2560 -H 1440")
self.label_game_arguments = Gtk.Label(label="Game Arguments")
self.label_game_arguments.set_halign(Gtk.Align.START)
self.entry_game_arguments = Gtk.Entry()
self.entry_game_arguments.set_tooltip_text("e.g.: -d3d11 -fullscreen")
self.label_addapp = Gtk.Label(label="Additional Application")
self.entry_addapp = Gtk.Entry()
self.entry_addapp.set_tooltip_text("/path/to/the/app")
self.button_search_addapp = Gtk.Button()
self.button_search_addapp.set_image(Gtk.Image.new_from_icon_name("system-search-symbolic", Gtk.IconSize.BUTTON))
self.button_search_addapp.connect("clicked", self.on_button_search_addapp_clicked)
self.button_search_addapp.set_size_request(50, -1)
self.button_shortcut_icon = Gtk.Button()
self.button_shortcut_icon.set_tooltip_text("Select an icon for the shortcut")
self.button_shortcut_icon.connect("clicked", self.on_button_shortcut_icon_clicked)
self.checkbox_mangohud = Gtk.CheckButton(label="MangoHud")
self.checkbox_mangohud.set_tooltip_text(
"Shows an overlay for monitoring FPS, temperatures, CPU/GPU load and more.")
self.checkbox_gamemode = Gtk.CheckButton(label="GameMode")
self.checkbox_gamemode.set_tooltip_text("Tweaks your system to improve performance.")
self.checkbox_sc_controller = Gtk.CheckButton(label="SC Controller")
self.checkbox_sc_controller.set_tooltip_text(
"Emulates a Xbox controller if the game doesn't support yours. Put the profile at ~/.config/faugus-launcher/controller.sccprofile.")
# Button Cancel
self.button_cancel = Gtk.Button(label="Cancel")
self.button_cancel.connect("clicked", self.on_cancel_clicked)
self.button_cancel.set_size_request(120, -1)
self.button_cancel.set_halign(Gtk.Align.CENTER)
# Button Ok
self.button_ok = Gtk.Button(label="Ok")
self.button_ok.connect("clicked", self.on_ok_clicked)
self.button_ok.set_size_request(120, -1)
self.button_ok.set_halign(Gtk.Align.CENTER)
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)
# Create the Grids
self.grid1 = Gtk.Grid()
self.grid1.set_row_spacing(10)
self.grid1.set_column_spacing(10)
self.grid1.set_margin_start(10)
self.grid1.set_margin_end(10)
self.grid1.set_margin_top(10)
self.grid1.set_margin_bottom(10)
self.grid1.set_valign(Gtk.Align.CENTER)
self.grid2 = Gtk.Grid()
self.grid2.set_row_spacing(10)
self.grid2.set_column_spacing(10)
self.grid2.set_margin_start(10)
self.grid2.set_margin_end(10)
self.grid2.set_margin_top(10)
self.grid2.set_margin_bottom(10)
self.grid3 = Gtk.Grid()
self.grid3.set_row_spacing(10)
self.grid3.set_column_spacing(10)
self.grid3.set_margin_start(10)
self.grid3.set_margin_end(10)
self.grid3.set_margin_top(10)
self.grid3.set_margin_bottom(10)
self.grid3.set_valign(Gtk.Align.CENTER)
self.grid3.set_halign(Gtk.Align.END)
self.grid4 = Gtk.Grid()
self.grid4.set_row_spacing(10)
self.grid4.set_column_spacing(10)
self.grid4.set_margin_start(10)
self.grid4.set_margin_end(10)
self.grid4.set_margin_top(10)
self.grid4.set_margin_bottom(10)
self.entry_title.set_hexpand(True)
self.entry_title.set_valign(Gtk.Align.CENTER)
self.grid1.attach(self.label_title, 0, 0, 1, 1)
self.grid1.attach(self.entry_title, 0, 1, 4, 1)
self.grid1.attach(self.label_protonfix, 0, 2, 1, 1)
self.grid1.attach(self.entry_protonfix, 0, 3, 3, 1)
self.entry_protonfix.set_hexpand(True)
self.grid1.attach(self.button_search_protonfix, 3, 3, 1, 1)
self.grid1.attach(self.label_launch_arguments, 0, 4, 1, 1)
self.grid1.attach(self.entry_launch_arguments, 0, 5, 4, 1)
self.entry_launch_arguments.set_hexpand(True)
self.grid1.attach(self.label_game_arguments, 0, 6, 1, 1)
self.grid1.attach(self.entry_game_arguments, 0, 7, 4, 1)
self.entry_game_arguments.set_hexpand(True)
self.grid1.attach(self.label_addapp, 0, 8, 1, 1)
self.grid1.attach(self.entry_addapp, 0, 9, 3, 1)
self.entry_addapp.set_hexpand(True)
self.grid1.attach(self.button_search_addapp, 3, 9, 1, 1)
self.grid2.attach(self.checkbox_mangohud, 0, 0, 1, 1)
self.grid2.attach(self.checkbox_gamemode, 0, 1, 1, 1)
self.grid2.attach(self.checkbox_sc_controller, 0, 2, 1, 1)
self.grid3.attach(self.button_shortcut_icon, 0, 0, 1, 1)
self.grid4.attach(self.button_cancel, 0, 0, 1, 1)
self.grid4.attach(self.button_ok, 1, 0, 1, 1)
# Create a main grid to hold the grids
self.main_grid = Gtk.Grid()
# Attach grid1 and grid2 to the main grid in the same row
self.main_grid.attach(self.grid1, 0, 0, 2, 1)
self.main_grid.attach(self.grid2, 0, 1, 2, 1)
self.main_grid.attach(self.grid3, 1, 1, 1, 1)
# Attach grid3 to the main grid in the next row
self.main_grid.attach(self.grid4, 0, 2, 2, 1)
self.load_config()
# 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.sc_controller_enabled = os.path.exists("/usr/bin/sc-controller") or os.path.exists(
"/usr/local/bin/sc-controller")
if not self.sc_controller_enabled:
self.checkbox_sc_controller.set_sensitive(False)
self.checkbox_sc_controller.set_active(False)
self.checkbox_sc_controller.set_tooltip_text(
"Emulates a Xbox controller if the game doesn't support yours. Put the profile at ~/.config/faugus-launcher/controller.sccprofile. NOT INSTALLED.")
# Add the main grid to the window
self.add(self.main_grid)
# Handle the click event of the Create Shortcut button
title_formatted = re.sub(r'[^a-zA-Z0-9\s]', '', game_title)
title_formatted = title_formatted.replace(' ', '-')
title_formatted = '-'.join(title_formatted.lower().split())
if not os.path.exists(self.icon_directory):
os.makedirs(self.icon_directory)
try:
# Attempt to extract the icon
command = f'icoextract "{file_path}" "{self.icon_extracted}"'
result = subprocess.run(command, shell=True, text=True, capture_output=True)
# Check if there was an error in executing the command
if result.returncode != 0:
if "NoIconsAvailableError" in result.stderr or "PEFormatError" in result.stderr:
print("The file does not contain icons.")
self.button_shortcut_icon.set_image(self.set_image_shortcut_icon())
else:
print(f"Error extracting icon: {result.stderr}")
else:
# Convert the extracted icon to PNG
command_magick = shutil.which("magick") or shutil.which("convert")
os.system(f'{command_magick} "{self.icon_extracted}" "{self.icon_converted}"')
if os.path.isfile(self.icon_extracted):
os.remove(self.icon_extracted)
largest_image = self.find_largest_resolution(self.icon_directory)
shutil.move(largest_image, os.path.expanduser(self.icon_temp))
pixbuf = GdkPixbuf.Pixbuf.new_from_file(self.icon_temp)
scaled_pixbuf = pixbuf.scale_simple(50, 50, GdkPixbuf.InterpType.BILINEAR)
image = Gtk.Image.new_from_file(self.icon_temp)
image.set_from_pixbuf(scaled_pixbuf)
self.button_shortcut_icon.set_image(image)
except Exception as e:
print(f"An error occurred: {e}")
shutil.rmtree(self.icon_directory)
# Connect the destroy signal to Gtk.main_quit
self.connect("destroy", Gtk.main_quit)
def on_button_search_addapp_clicked(self, widget):
# Handle the click event of the search button to select the game's .exe
dialog = Gtk.FileChooserDialog(title="Select the additional application", parent=self, action=Gtk.FileChooserAction.OPEN)
dialog.set_current_folder(os.path.dirname(self.file_path))
dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
# Windows files filter
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")
dialog.add_filter(windows_filter)
# All files filter
all_files_filter = Gtk.FileFilter()
all_files_filter.set_name("All files")
all_files_filter.add_pattern("*")
dialog.add_filter(all_files_filter)
response = dialog.run()
if response == Gtk.ResponseType.OK:
self.entry_addapp.set_text(dialog.get_filename())
dialog.destroy()
def find_largest_resolution(self, directory):
largest_image = None
largest_resolution = (0, 0) # (width, height)
# Define a set of valid image extensions
valid_image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff'}
for file_name in os.listdir(directory):
file_path = os.path.join(directory, file_name)
if os.path.isfile(file_path):
# Check if the file has a valid image extension
if os.path.splitext(file_name)[1].lower() in valid_image_extensions:
try:
with Image.open(file_path) as img:
width, height = img.size
if width * height > largest_resolution[0] * largest_resolution[1]:
largest_resolution = (width, height)
largest_image = file_path
except IOError:
print(f'Unable to open {file_path}')
return largest_image
def on_button_search_protonfix_clicked(self, widget):
webbrowser.open("https://umu.openwinecomponents.org/")
def load_config(self):
# Load configuration from file
config_file = config_file_dir
if os.path.isfile(config_file):
with open(config_file, 'r') as f:
config_data = f.read().splitlines()
config_dict = dict(line.split('=') for line in config_data)
self.default_prefix = config_dict.get('default-prefix', '').strip('"')
mangohud = config_dict.get('mangohud', 'False') == 'True'
gamemode = config_dict.get('gamemode', 'False') == 'True'
sc_controller = config_dict.get('sc-controller', 'False') == 'True'
self.default_runner = config_dict.get('default-runner', '').strip('"')
self.checkbox_mangohud.set_active(mangohud)
self.checkbox_gamemode.set_active(gamemode)
self.checkbox_sc_controller.set_active(sc_controller)
else:
# Save default configuration if file does not exist
self.save_config(False, '', "False", "False", "False", "GE-Proton", "True", "False", "False", "False", "List", "False", "", "False", "False")
def save_config(self, checkbox_state, default_prefix, mangohud_state, gamemode_state, sc_controller_state, default_runner, checkbox_discrete_gpu_state, checkbox_splash_disable, checkbox_system_tray, checkbox_start_boot, combo_box_interface, checkbox_start_maximized, entry_api_key, checkbox_start_fullscreen, checkbox_gamepad_navigation):
# Path to the configuration file
config_file = config_file_dir
config_path = faugus_launcher_dir
# Create the configuration directory if it doesn't exist
if not os.path.exists(config_path):
os.makedirs(config_path)
default_prefix = prefixes_dir
self.default_prefix = prefixes_dir
default_runner = (f'"{default_runner}"')
# Dictionary to store existing configurations
config = {}
# Read the existing configuration file
if os.path.exists(config_file):
with open(config_file, 'r') as f:
for line in f:
key, value = line.strip().split('=', 1)
config[key] = value.strip('"')
# Update configurations with new values
config['close-onlaunch'] = checkbox_state
config['default-prefix'] = default_prefix
config['mangohud'] = mangohud_state
config['gamemode'] = gamemode_state
config['sc-controller'] = sc_controller_state
config['default-runner'] = default_runner
config['discrete-gpu'] = checkbox_discrete_gpu_state
config['splash-disable'] = checkbox_splash_disable
config['system-tray'] = checkbox_system_tray
config['start-boot'] = checkbox_start_boot
config['interface-mode'] = combo_box_interface
config['start-maximized'] = checkbox_start_maximized
config['api-key'] = entry_api_key
config['start-fullscreen'] = checkbox_start_fullscreen
config['gamepad-navigation'] = checkbox_gamepad_navigation
# Write configurations back to the file
with open(config_file, 'w') as f:
for key, value in config.items():
if key == 'default-prefix':
f.write(f'{key}="{value}"\n')
else:
f.write(f'{key}={value}\n')
def on_cancel_clicked(self, widget):
if os.path.isfile(self.icon_temp):
os.remove(self.icon_temp)
if os.path.isdir(self.icon_directory):
shutil.rmtree(self.icon_directory)
self.destroy()
def on_ok_clicked(self, widget):
validation_result = self.validate_fields()
if not validation_result:
self.set_sensitive(True)
return
title = self.entry_title.get_text()
# Handle the click event of the Create Shortcut button
title_formatted = re.sub(r'[^a-zA-Z0-9\s]', '', title)
title_formatted = title_formatted.replace(' ', '-')
title_formatted = '-'.join(title_formatted.lower().split())
addapp = self.entry_addapp.get_text()
addapp_bat = f"{os.path.dirname(self.file_path)}/faugus-{title_formatted}.bat"
if self.entry_addapp.get_text():
with open(addapp_bat, "w") as bat_file:
bat_file.write(f'start "" "z:{addapp}"\n')
bat_file.write(f'start "" "z:{self.file_path}"\n')
if os.path.isfile(os.path.expanduser(self.icon_temp)):
os.rename(os.path.expanduser(self.icon_temp),f'{self.icons_path}/{title_formatted}.ico')
# Check if the icon file exists
icons_path = icons_dir
new_icon_path = f"{icons_dir}/{title_formatted}.ico"
if not os.path.exists(new_icon_path):
new_icon_path = faugus_png
protonfix = self.entry_protonfix.get_text()
launch_arguments = self.entry_launch_arguments.get_text()
game_arguments = self.entry_game_arguments.get_text()
mangohud = "MANGOHUD=1" if self.checkbox_mangohud.get_active() else ""
gamemode = "gamemoderun" if self.checkbox_gamemode.get_active() else ""
sc_controller = "SC_CONTROLLER=1" if self.checkbox_sc_controller.get_active() else ""
# Get the directory containing the executable
game_directory = os.path.dirname(self.file_path)
command_parts = []
# Add command parts if they are not empty
if mangohud:
command_parts.append(mangohud)
if sc_controller:
command_parts.append(sc_controller)
#command_parts.append(f'WINEPREFIX={self.default_prefix}/default')
if protonfix:
command_parts.append(f'GAMEID={protonfix}')
else:
command_parts.append(f'GAMEID={title_formatted}')
if gamemode:
command_parts.append(gamemode)
if launch_arguments:
command_parts.append(launch_arguments)
# Add the fixed command and remaining arguments
command_parts.append(f"'{umu_run}'")
if self.entry_addapp.get_text():
command_parts.append(f"'{addapp_bat}'")
elif self.file_path:
command_parts.append(f"'{self.file_path}'")
if game_arguments:
command_parts.append(f"{game_arguments}")
# Join all parts into a single command
command = ' '.join(command_parts)
# Create a .desktop file
desktop_file_content = f"""[Desktop Entry]
Name={title}
Exec={faugus_run} "{command}"
Icon={new_icon_path}
Type=Application
Categories=Game;
Path={game_directory}
"""
# Check if the destination directory exists and create if it doesn't
applications_directory = app_dir
if not os.path.exists(applications_directory):
os.makedirs(applications_directory)
desktop_directory = desktop_dir
if not os.path.exists(desktop_directory):
os.makedirs(desktop_directory)
applications_shortcut_path = f"{app_dir}/{title_formatted}.desktop"
with open(applications_shortcut_path, 'w') as desktop_file:
desktop_file.write(desktop_file_content)
# Make the .desktop file executable
os.chmod(applications_shortcut_path, 0o755)
# Copy the shortcut to Desktop
desktop_shortcut_path = f"{desktop_dir}/{title_formatted}.desktop"
shutil.copy(applications_shortcut_path, desktop_shortcut_path)
if os.path.isfile(self.icon_temp):
os.remove(self.icon_temp)
if os.path.isdir(self.icon_directory):
shutil.rmtree(self.icon_directory)
self.destroy()
def on_entry_changed(self, widget, entry):
if entry.get_text():
entry.get_style_context().remove_class("entry")
def set_image_shortcut_icon(self):
image_path = faugus_png
pixbuf = GdkPixbuf.Pixbuf.new_from_file(image_path)
scaled_pixbuf = pixbuf.scale_simple(50, 50, GdkPixbuf.InterpType.BILINEAR)
image = Gtk.Image.new_from_pixbuf(scaled_pixbuf)
return image
def on_button_shortcut_icon_clicked(self, widget):
path = self.file_path
if not os.path.exists(self.icon_directory):
os.makedirs(self.icon_directory)
try:
# Attempt to extract the icon
command = f'icoextract "{path}" "{self.icon_extracted}"'
result = subprocess.run(command, shell=True, text=True, capture_output=True)
# Check if there was an error in executing the command
if result.returncode != 0:
if "NoIconsAvailableError" in result.stderr or "PEFormatError" in result.stderr:
print("The file does not contain icons.")
self.button_shortcut_icon.set_image(self.set_image_shortcut_icon())
else:
print(f"Error extracting icon: {result.stderr}")
else:
# Convert the extracted icon to PNG
command_magick = shutil.which("magick") or shutil.which("convert")
os.system(f'{command_magick} "{self.icon_extracted}" "{self.icon_converted}"')
if os.path.isfile(self.icon_extracted):
os.remove(self.icon_extracted)
except Exception as e:
print(f"An error occurred: {e}")
# Open file dialog to select .ico file
dialog = Gtk.FileChooserDialog(title="Select an icon for the shortcut", action=Gtk.FileChooserAction.OPEN)
dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
# Add a filter to limit selection to .ico files
filter_ico = Gtk.FileFilter()
filter_ico.set_name("Image files")
filter_ico.add_mime_type("image/*") # Other image formats
dialog.add_filter(filter_ico)
# Set the initial directory to the icon directory
dialog.set_current_folder(self.icon_directory)
# Connect signal to update preview widget when file selection changes
dialog.connect("update-preview", self.update_preview)
response = dialog.run()
if response == Gtk.ResponseType.OK:
file_path = dialog.get_filename()
# Move and rename the icon file
shutil.copy(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)
# Delete the folder after the icon is moved
if os.path.isdir(self.icon_directory):
shutil.rmtree(self.icon_directory)
dialog.destroy()
self.set_sensitive(True)
def update_preview(self, dialog):
if file_path := dialog.get_preview_filename():
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file(file_path)
max_width = 400
max_height = 400
width = pixbuf.get_width()
height = pixbuf.get_height()
if width > max_width or height > max_height:
ratio = min(max_width / width, max_height / height)
new_width = int(width * ratio)
new_height = int(height * ratio)
pixbuf = pixbuf.scale_simple(new_width, new_height, GdkPixbuf.InterpType.BILINEAR)
image = Gtk.Image.new_from_pixbuf(pixbuf)
dialog.set_preview_widget(image)
dialog.set_preview_widget_active(True)
dialog.get_preview_widget().set_size_request(max_width, max_height)
except GLib.Error:
dialog.set_preview_widget_active(False)
else:
dialog.set_preview_widget_active(False)
def validate_fields(self):
title = self.entry_title.get_text()
self.entry_title.get_style_context().remove_class("entry")
if not title:
self.entry_title.get_style_context().add_class("entry")
return False
return True
def run_file(file_path):
config_file = config_file_dir
if os.path.exists(config_file):
with open(config_file, 'r') as f:
config_data = f.read().splitlines()
config_dict = dict(line.split('=') for line in config_data)
default_prefix = config_dict.get('default-prefix', '').strip('"')
mangohud = config_dict.get('mangohud', 'False') == 'True'
gamemode = config_dict.get('gamemode', 'False') == 'True'
sc_controller = config_dict.get('sc-controller', 'False') == 'True'
default_runner = config_dict.get('default-runner', '').strip('"')
else:
# Define the configuration path
config_path = faugus_launcher_dir
# Create the configuration directory if it doesn't exist
if not os.path.exists(config_path):
os.makedirs(config_path)
default_prefix = prefixes_dir
mangohud = 'False'
gamemode = 'False'
sc_controller = 'False'
default_runner = 'GE-Proton'
with open(config_file, 'w') as f:
f.write(f'close-onlaunch=False\n')
f.write(f'default-prefix="{default_prefix}"\n')
f.write(f'mangohud=False\n')
f.write(f'gamemode=False\n')
f.write(f'sc-controller=False\n')
f.write(f'default-runner="GE-Proton"\n')
f.write(f'discrete-gpu=True\n')
f.write(f'splash-disable=False\n')
f.write(f'system-tray=False\n')
f.write(f'start-boot=False\n')
f.write(f'interface-mode=List\n')
f.write(f'start-maximized=False\n')
f.write(f'api-key=\n')
f.write(f'gamepad-navigation=False\n')
if not file_path.endswith(".reg"):
mangohud = "MANGOHUD=1" if mangohud else ""
gamemode = "gamemoderun" if gamemode else ""
sc_controller = "SC_CONTROLLER=1" if sc_controller else ""
# Get the directory of the file
file_dir = os.path.dirname(os.path.abspath(file_path))
# Define paths
prefix_path = os.path.expanduser(f"{default_prefix}/default")
faugus_run_path = faugus_run
if not file_path.endswith(".reg"):
mangohud_enabled = os.path.exists(mangohud_dir)
gamemode_enabled = os.path.exists(gamemoderun) or os.path.exists("/usr/games/gamemoderun")
sc_controller_enabled = os.path.exists("/usr/bin/sc-controller") or os.path.exists("/usr/local/bin/sc-controller")
if default_runner == "UMU-Proton Latest":
default_runner = ""
if default_runner == "GE-Proton Latest (default)":
default_runner = "GE-Proton"
command_parts = []
if not file_path.endswith(".reg"):
# Add command parts if they are not empty
if mangohud_enabled and mangohud:
command_parts.append(mangohud)
if sc_controller_enabled and sc_controller:
command_parts.append(sc_controller)
command_parts.append(os.path.expanduser(f"WINEPREFIX={default_prefix}/default"))
command_parts.append('GAMEID=default')
if default_runner:
command_parts.append(f'PROTONPATH={default_runner}')
if not file_path.endswith(".reg"):
if gamemode_enabled and gamemode:
command_parts.append(gamemode)
# Add the fixed command and remaining arguments
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}"')
# Join all parts into a single command
command = ' '.join(command_parts)
# Run the command in the directory of the file
subprocess.run([faugus_run_path, command], cwd=file_dir)
def convert_games_txt_to_json(txt_file_path, json_file_path):
if not os.path.exists(txt_file_path):
return
games = []
with open(txt_file_path, 'r', encoding='utf-8') as txt_file:
for line in txt_file:
fields = line.strip().split(';')
while len(fields) < 13:
fields.append("")
game = {
"title": fields[0],
"path": fields[1],
"prefix": fields[2],
"launch_arguments": fields[3],
"game_arguments": fields[4],
"mangohud": fields[5],
"gamemode": fields[6],
"sc_controller": fields[7],
"protonfix": fields[8],
"runner": fields[9],
"addapp_checkbox": fields[10],
"addapp": fields[11],
"addapp_bat": fields[12],
}
games.append(game)
with open(json_file_path, 'w', encoding='utf-8') as json_file:
json.dump(games, json_file, ensure_ascii=False, indent=4)
old_file_path = txt_file_path.replace(".txt", "-old.txt")
os.rename(txt_file_path, old_file_path)
def apply_dark_theme():
desktop_env = Gio.Settings.new("org.gnome.desktop.interface")
try:
is_dark_theme = desktop_env.get_string("color-scheme") == "prefer-dark"
except Exception:
is_dark_theme = "-dark" in desktop_env.get_string("gtk-theme")
if is_dark_theme:
Gtk.Settings.get_default().set_property("gtk-application-prefer-dark-theme", True)
def main():
convert_games_txt_to_json(games_txt, games_json)
apply_dark_theme()
if len(sys.argv) == 1:
app = Main()
if is_already_running():
print("Faugus Launcher is already running.")
sys.exit(0)
app.connect("destroy", app.on_destroy)
Gtk.main()
elif len(sys.argv) == 2 and sys.argv[1] == "hide":
app = Main()
if is_already_running():
print("Faugus Launcher is already running.")
sys.exit(0)
app.hide()
app.connect("destroy", app.on_destroy)
Gtk.main()
elif len(sys.argv) == 2:
run_file(sys.argv[1])
elif len(sys.argv) == 3 and sys.argv[2] == "shortcut":
app = CreateShortcut(sys.argv[1])
app.show_all()
Gtk.main()
else:
print("Invalid arguments")
if __name__ == "__main__":
main()