diff --git a/faugus/meson.build b/faugus/meson.build index 41d301d..127ea8e 100644 --- a/faugus/meson.build +++ b/faugus/meson.build @@ -2,6 +2,7 @@ py.install_sources( 'components.py', 'proton_downloader.py', 'shortcut.py', + 'shortcut_utils.py', 'config_manager.py', 'path_manager.py', 'language_config.py', diff --git a/faugus/shortcut_utils.py b/faugus/shortcut_utils.py new file mode 100644 index 0000000..2192375 --- /dev/null +++ b/faugus/shortcut_utils.py @@ -0,0 +1,32 @@ +#!/usr/bin/python3 + +import os + + +def get_shortcut_icon_path(gameid, icons_dir, fallback_icon): + icon_png = os.path.join(icons_dir, f"{gameid}.png") + if os.path.exists(icon_png): + return icon_png + + icon_ico = os.path.join(icons_dir, f"{gameid}.ico") + if os.path.exists(icon_ico): + return icon_ico + + return fallback_icon + + +def build_desktop_entry(name, exec_command, icon_path, game_directory, startup_wm_class=""): + entry = [ + "[Desktop Entry]", + f"Name={name}", + f"Exec={exec_command}", + f"Icon={icon_path}", + "Type=Application", + "Categories=Game;", + f"Path={game_directory}", + ] + + if startup_wm_class: + entry.append(f"StartupWMClass={startup_wm_class}") + + return "\n".join(entry) + "\n" diff --git a/faugus_launcher.py b/faugus_launcher.py index b020b06..be49f43 100644 --- a/faugus_launcher.py +++ b/faugus_launcher.py @@ -25,6 +25,7 @@ from gi.repository import Gtk, Gdk, GdkPixbuf, GLib, AyatanaAppIndicator3, Gio, from PIL import Image from faugus.config_manager import * from faugus.dark_theme import * +from faugus.shortcut_utils import build_desktop_entry, get_shortcut_icon_path from faugus.steam_setup import * VERSION = "1.15.1" @@ -1456,7 +1457,8 @@ class Main(Gtk.ApplicationWindow): game_data.get("playtime", 0), game_data.get("hidden", False), game_data.get("prevent_sleep", False), - game_data.get("favorite", False) + game_data.get("favorite", False), + game_data.get("startup_wm_class", ""), ) if not self.show_hidden and game.hidden: @@ -1491,7 +1493,7 @@ class Main(Gtk.ApplicationWindow): else: hbox.get_style_context().add_class("hbox-normal") - game_icon = f'{icons_dir}/{game.gameid}.ico' + game_icon = get_shortcut_icon_path(game.gameid, icons_dir, faugus_png) game_label = Gtk.Label.new(game.title) if self.interface_mode in ("Blocks", "Banners"): @@ -1499,9 +1501,6 @@ class Main(Gtk.ApplicationWindow): game_label.set_max_width_chars(1) game_label.set_justify(Gtk.Justification.CENTER) - if not os.path.isfile(game_icon): - game_icon = faugus_png - self.flowbox_child = Gtk.FlowBoxChild() pixbuf = GdkPixbuf.Pixbuf.new_from_file(game_icon) @@ -2063,12 +2062,11 @@ class Main(Gtk.ApplicationWindow): def set_image_shortcut_icon(self, title, icons_path, icon_temp): title_formatted = format_title(title) - # Check if the icon file exists - icon_path = os.path.join(icons_path, f"{title_formatted}.ico") + icon_path = get_shortcut_icon_path(title_formatted, icons_path, faugus_png) - if os.path.exists(icon_path): + if icon_path != faugus_png: shutil.copyfile(icon_path, icon_temp) - if not os.path.exists(icon_path): + else: icon_temp = faugus_png pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_temp) @@ -2278,9 +2276,11 @@ class Main(Gtk.ApplicationWindow): playtime = 0 hidden = False favorite = False + startup_wm_class = "" if add_game_dialog.combobox_launcher.get_active() == 3: path = f"{prefix}/drive_c/Program Files (x86)/Battle.net/Battle.net.exe" + startup_wm_class = "battle.net.exe" if add_game_dialog.combobox_launcher.get_active() == 4: path = f"{prefix}/drive_c/Program Files/Electronic Arts/EA Desktop/EA Desktop/EALauncher.exe" if add_game_dialog.combobox_launcher.get_active() == 5: @@ -2346,6 +2346,7 @@ class Main(Gtk.ApplicationWindow): hidden, prevent_sleep, favorite, + startup_wm_class, ) # Determine the state of the shortcut checkbox @@ -2354,7 +2355,7 @@ class Main(Gtk.ApplicationWindow): steam_shortcut_state = add_game_dialog.checkbox_shortcut_steam.get_active() icon_temp = os.path.expanduser(add_game_dialog.icon_temp) - icon_final = f'{add_game_dialog.icons_path}/{title_formatted}.ico' + icon_final = f'{add_game_dialog.icons_path}/{title_formatted}.png' def check_internet_connection(): try: @@ -2414,6 +2415,7 @@ class Main(Gtk.ApplicationWindow): "hidden": hidden, "prevent_sleep": prevent_sleep, "favorite": favorite, + "startup_wm_class": startup_wm_class, } games = [] @@ -2729,9 +2731,13 @@ class Main(Gtk.ApplicationWindow): game.runner = "Linux-Native" if edit_game_dialog.combobox_launcher.get_active() == 2: game.runner = "Steam" + if edit_game_dialog.combobox_launcher.get_active() == 3: + game.startup_wm_class = "battle.net.exe" + elif getattr(game, "startup_wm_class", "") == "battle.net.exe": + game.startup_wm_class = "" icon_temp = os.path.expanduser(edit_game_dialog.icon_temp) - icon_final = f'{edit_game_dialog.icons_path}/{title_formatted}.ico' + icon_final = f'{edit_game_dialog.icons_path}/{title_formatted}.png' # Determine the state of the shortcut checkbox desktop_shortcut_state = edit_game_dialog.checkbox_shortcut_desktop.get_active() @@ -2787,34 +2793,27 @@ class Main(Gtk.ApplicationWindow): if os.path.isfile(os.path.expanduser(icon_temp)): os.rename(os.path.expanduser(icon_temp), icon_final) - # Check if the icon file exists - new_icon_path = f"{icons_dir}/{game.gameid}.ico" - if not os.path.exists(new_icon_path): - new_icon_path = faugus_png + new_icon_path = get_shortcut_icon_path(game.gameid, icons_dir, faugus_png) # Get the directory containing the executable game_directory = os.path.dirname(game.path) # Create a .desktop file if IS_FLATPAK: - desktop_file_content = ( - f'[Desktop Entry]\n' - f'Name={game.title}\n' - f'Exec=flatpak run --command={faugus_run} io.github.Faugus.faugus-launcher --game {game.gameid}\n' - f'Icon={new_icon_path}\n' - f'Type=Application\n' - f'Categories=Game;\n' - f'Path={game_directory}\n' + desktop_file_content = build_desktop_entry( + game.title, + f"flatpak run --command={faugus_run} io.github.Faugus.faugus-launcher --game {game.gameid}", + new_icon_path, + game_directory, + getattr(game, "startup_wm_class", ""), ) else: - desktop_file_content = ( - f'[Desktop Entry]\n' - f'Name={game.title}\n' - f'Exec={faugus_run} --game {game.gameid}\n' - f'Icon={new_icon_path}\n' - f'Type=Application\n' - f'Categories=Game;\n' - f'Path={game_directory}\n' + desktop_file_content = build_desktop_entry( + game.title, + f"{faugus_run} --game {game.gameid}", + new_icon_path, + game_directory, + getattr(game, "startup_wm_class", ""), ) # Check if the destination directory exists and create if it doesn't @@ -2967,10 +2966,7 @@ class Main(Gtk.ApplicationWindow): if os.path.isfile(os.path.expanduser(icon_temp)): os.rename(os.path.expanduser(icon_temp), icon_final) - # Check if the icon file exists - new_icon_path = f"{icons_dir}/{game.gameid}.ico" - if not os.path.exists(new_icon_path): - new_icon_path = faugus_png + new_icon_path = get_shortcut_icon_path(game.gameid, icons_dir, faugus_png) # Get the directory containing the executable game_directory = os.path.dirname(game.path) @@ -3007,11 +3003,14 @@ class Main(Gtk.ApplicationWindow): def remove_banner_icon(self, game): banner_file_path = f"{banners_dir}/{game.gameid}.png" - icon_file_path = f"{icons_dir}/{game.gameid}.ico" + icon_file_path_png = f"{icons_dir}/{game.gameid}.png" + icon_file_path_ico = f"{icons_dir}/{game.gameid}.ico" if os.path.exists(banner_file_path): os.remove(banner_file_path) - if os.path.exists(icon_file_path): - os.remove(icon_file_path) + if os.path.exists(icon_file_path_png): + os.remove(icon_file_path_png) + if os.path.exists(icon_file_path_ico): + os.remove(icon_file_path_ico) def remove_shortcut(self, game, shortcut): applications_shortcut_path = f"{app_dir}/{game.gameid}.desktop" @@ -3097,6 +3096,7 @@ class Main(Gtk.ApplicationWindow): "hidden": hidden, "prevent_sleep": game.prevent_sleep, "favorite": game.favorite, + "startup_wm_class": game.startup_wm_class, } new_games_data.append(game_data) @@ -4596,6 +4596,7 @@ class Game: hidden, prevent_sleep, favorite, + startup_wm_class="", ): self.gameid = gameid self.title = title @@ -4622,6 +4623,7 @@ class Game: self.hidden = hidden self.prevent_sleep = prevent_sleep self.favorite = favorite + self.startup_wm_class = startup_wm_class class DuplicateDialog(Gtk.Dialog): @@ -4804,7 +4806,7 @@ class AddGame(Gtk.Dialog): 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.icon_temp = f'{self.icons_path}/icon_temp.png' self.box = self.get_content_area() self.box.set_margin_start(0) @@ -6767,6 +6769,10 @@ def update_games_and_config(): game["runner"] = "Proton-GE Latest" changed = True + if title == "Battle.net" and game.get("startup_wm_class", "") != "battle.net.exe": + game["startup_wm_class"] = "battle.net.exe" + changed = True + if changed: with open(games_json, "w", encoding="utf-8") as f: json.dump(games, f, indent=4, ensure_ascii=False) diff --git a/tests/test_shortcut_utils.py b/tests/test_shortcut_utils.py new file mode 100644 index 0000000..b560015 --- /dev/null +++ b/tests/test_shortcut_utils.py @@ -0,0 +1,58 @@ +import os +import tempfile +import unittest + +from faugus.shortcut_utils import build_desktop_entry, get_shortcut_icon_path + + +class ShortcutUtilsTest(unittest.TestCase): + def test_get_shortcut_icon_path_prefers_png(self): + with tempfile.TemporaryDirectory() as tmpdir: + png = os.path.join(tmpdir, "battlenet.png") + ico = os.path.join(tmpdir, "battlenet.ico") + with open(png, "wb") as f: + f.write(b"png") + with open(ico, "wb") as f: + f.write(b"ico") + + path = get_shortcut_icon_path("battlenet", tmpdir, "/fallback.png") + self.assertEqual(path, png) + + def test_get_shortcut_icon_path_falls_back_to_ico(self): + with tempfile.TemporaryDirectory() as tmpdir: + ico = os.path.join(tmpdir, "battlenet.ico") + with open(ico, "wb") as f: + f.write(b"ico") + + path = get_shortcut_icon_path("battlenet", tmpdir, "/fallback.png") + self.assertEqual(path, ico) + + def test_get_shortcut_icon_path_uses_fallback(self): + with tempfile.TemporaryDirectory() as tmpdir: + path = get_shortcut_icon_path("battlenet", tmpdir, "/fallback.png") + self.assertEqual(path, "/fallback.png") + + def test_build_desktop_entry_includes_startup_wm_class(self): + entry = build_desktop_entry( + "Battle.net", + "faugus-run --game battlenet", + "/icons/battlenet.png", + "/games", + "battle.net.exe", + ) + self.assertIn("StartupWMClass=battle.net.exe\n", entry) + self.assertIn("Icon=/icons/battlenet.png\n", entry) + + def test_build_desktop_entry_omits_empty_startup_wm_class(self): + entry = build_desktop_entry( + "Game", + "faugus-run --game game", + "/icons/game.png", + "/games", + "", + ) + self.assertNotIn("StartupWMClass=", entry) + + +if __name__ == "__main__": + unittest.main()