Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Importing Desktop Files (Native Games) #28

Closed
Saroufim opened this issue Mar 25, 2023 · 11 comments
Closed

Support for Importing Desktop Files (Native Games) #28

Saroufim opened this issue Mar 25, 2023 · 11 comments
Assignees
Labels
enhancement New feature or request

Comments

@Saroufim
Copy link

Games like Battle for Wesnoth and Unciv might be installed through flatpak or the package manager without using a game client.
It would be nice if cartridges imported those games automatically. Adding them manually works fine but it is still a chore.

@Saroufim Saroufim added the enhancement New feature or request label Mar 25, 2023
@kra-mo kra-mo self-assigned this Mar 25, 2023
@Arcitec
Copy link
Contributor

Arcitec commented Mar 25, 2023

This is more complex than it might sound.

Scanning

I can think of 2 ways to do this, both have issues though.

Method 1:

  • Scan the common .desktop file locations, just like how a desktop environment would do it. That's quite a lot of locations for places like Flatpak exported desktop files, user's ~/.local/ ones, global ones, global-local /usr/local override ones, etc. Applying proper override rules for each path (ie two desktop files with same name in both a global and a local location, should prefer the local one). The parsing/scanning rules are pretty complex but hopefully documented somewhere, or at least findable in the GNOME source code. If someone can dig that up or find a good Python library which handles it, please do.
  • Next, scan all the "winning" desktop files to look for the "Games" category.
  • Downsides: This will add games that were exported by Steam, Bottles, etc. So you will import duplicate copies of the games via both Steam/Bottles integration AND via their Desktop files. Not good. It also won't be able to handle games that aren't tagged as "Games" in their desktop files, which is probably a lot of them because desktop file authors can be lazy (they often only bother to insert the icon and executable, without any categories).

Workarounds/Ideas:

  1. Implement proper "desktop file" priority system that scans the correct locations with the correct overrides. Hopefully there's a robust Python library which already implements this.
  2. Blacklist all .desktop files that point at Steam, Bottles, etc, things we support natively, to avoid duplicates of things we already imported in a better way.
  3. Only import .desktop files tagged as "Game" category.
  4. When we import a Desktop file, it can have plenty of Exec= commands based on how it's launched. There's a [Desktop Entry] section which has the main double-click action. That's the only section we need to analyze.
  5. Remove the %U stuff and any other dynamic junk from the exec line. That's stuff like the "exec with the user's extra arguments" and is meant for mime-handlers. We don't want those dynamic %something flags when launching it externally.

Method 2:

  • Attempt to hook into either GNOME Software or Flatpak's local database and see if they have some way of querying "all installed games".
  • Upsides: It won't accidentally add games installed via Steam, Bottles, etc, since they won't exist in those databases. It most likely has better "this package is a game" tagging than the ".desktop" files method.
  • Downsides: Won't support games installed in other ways (such as via shell scripts). And still won't import any games that are not tagged as games in the databases. It would also be harder to locate the actual install location/launcher path of the installed package (we'd have to find its desktop files and go from there). And it may not even be possible to access those databases, so this method may not even work!
  • Overall: It has some upsides, but plenty of downsides. So I vote for Option 1, which has the lowest amount of downsides.

Game Image Banners

  • How do we get those?
  • Proposal: Fetch the icon from the .desktop file, and overlay it on a static gradient background to dynamically generate a banner image.
  • The issue with .desktop file icons is how to find the icons. Some desktop files have a Icon=/full/path/to/icon.png path, some have a Icon=identifier. If it's an identifier there is a search-path logic involved to find it from the user's correct Icon Theme folder. Maybe there's already Python libraries to parse this. Hopefully. Edit: @kra-mo discovered that Gio can handle looking up the icons via identifiers and full paths. So that part is solved.
  • Sandboxing: Even though Gio can look up icons by both identifier and full path, we would need Flatpak permissions to actually read the icons outside the sandbox. If we can't read the icon, we'd need to use a fallback "dummy" icon of some kind. But if we just give ourselves :ro (read-only) access to all common icon paths (the two system-wide icon folders, and the 3 or so user-specific icon theme folders), then it should work for 99% of games as long as the games don't use any custom icon paths (like sometimes pointing at icons inside its own installation folder).
  • Later implementation: Fetch banners from steamgriddb, via an API key (this would have to be looked into, to avoid API key limits; Bottles solved it via an API proxy which presumably caches the response URLs for each query to limit API usage). If no banner is available, fall back to the dynamically generated icon technique mentioned above.
  • Further considerations: What to do when the icon on disk changes in an app update or if the user changes their icon theme? If the "use icon" fallback is used, does there need to be a way to re-generate the cover based on the latest on-disk-icon?

Overall, this is not easy at all. If someone wants to help out with the implementation or at least help us do more research about potential solutions, or looking for any robust Python libraries that could help with this, then please share your findings! :)

@Arcitec
Copy link
Contributor

Arcitec commented Mar 27, 2023

We have decided to go forward with Option 1, the .desktop file parsing.

Here's some more research I've been doing:

.desktop file parsing library

.desktop file loading priority

These are the locations we need to RECURSIVELY scan (because desktop files are allowed to be within subfolders).

The later entries in this list will OVERRIDE any conflicting filenames found in any earlier entries in the list. So let's say two locations contain a brave.desktop file. In that case, we ignore every brave.desktop that's from a lower-priority location. I don't know how the recursive desktop file handling should deal with that, though, when "1 location" has two copies of an identically named desktop file, like /foo/brave.desktop and /foo/bar/brave.desktop both being inside /foo. That is why I hope someone takes the time to dig into GNOME Shell's source code to find their scanning logic.

Flatpak folders and priorities are taken from their docs: https://github.com/flatpak/flatpak/wiki/Filesystem

Anyway, here's the list of locations, from lowest override priority (at the top) to highest priority (at the bottom):

/usr/share/applications
/usr/local/share/applications
/var/lib/flatpak/exports/share/application
~/.local/share/flatpak/exports/share/applications
~/.local/share/applications

This means:

  • /usr/share/applications (system packages default desktop files, lowest priority)
  • /usr/local/share/applications (system-wide overrides)
  • /var/lib/flatpak/exports/share/applications (system-wide flatpaks)
  • ~/.local/share/flatpak/exports/share/applications (local per-user flatpaks, very high priority)
  • ~/.local/share/applications (local per-user desktop files and overrides, highest priority)

And before anyone suggests it: We cannot use the XDG environment vars because they don't work inside Flatpaks:

flatpak run --command=bash hu.kramo.Cartridges   
[📦 hu.kramo.Cartridges ~]$ echo $XDG_DATA_DIRS
/app/share:/usr/share:/usr/share/runtime/share:/run/host/user-share:/run/host/share
[📦 hu.kramo.Cartridges ~]$ echo $XDG_DATA_HOME
/home/johnny/.var/app/hu.kramo.Cartridges/data

Anyway... We need to do this:

  • Ensure that we have Flatpak read-only permissions to all of those paths. And ensure that we read the host-paths (not the sandboxed versions of those paths).
  • Loop through all of those locations. Scanning for .desktop files RECURSIVELY in every location. Generate a dictionary{} of ["foo.desktop"] = "/path/to/that/foo.desktop". (Again, as I said, I don't know the correct behavior if a single location has multiple identically named desktop files that clash, let's just consider that "undefined behavior" and ignore it unless someone decides to dig into GNOME Shell's code to find out how they solve that issue.)
  • As we go through every location, we'll gradually be overwriting clashing desktop filenames with the latest "winning" path automatically.
  • In the end we'll have a list of all winning desktop files, and their winning paths.
  • Now just loop through all of them with the desktop file parsing library, and do the work described in the previous message.

@Arcitec
Copy link
Contributor

Arcitec commented Mar 27, 2023

Here's a quick implementation to get us started. I don't have time to finish it, rewriting it to work inside the Flatpak sandbox, adding Flatpak permissions to the manifest, etc. But at least it helps us get started.

from pathlib import Path
from pprint import pprint

scan_desktop_paths = [
    "/usr/share/applications",
    "/usr/local/share/applications",
    "/var/lib/flatpak/exports/share/applications",
    Path.home() / ".local/share/flatpak/exports/share/applications",
    Path.home() / ".local/share/applications",
]

found_desktop_files = {}
for desktop_path in scan_desktop_paths:
    for desktop_file in Path(desktop_path).glob("**/*.desktop"):  # Note: Glob automatically ignores missing `desktop_path` locations.
        if desktop_file.name in found_desktop_files:
            print(
                f"Overriding: {found_desktop_files[desktop_file.name]} -> {desktop_file}"
            )
        found_desktop_files[desktop_file.name] = desktop_file

# Now just process every desktop file via the desktop parsing library: https://pypi.org/project/desktop-parser/
# Read the "Desktop Entry" section from the files (see https://github.com/ArianeTeam/Ariane/blob/main/ariane/main.py#L68).
# Check if they have a game-related category, if not then ignore them.
# Filter out all desktop files that are from a source that's natively supported by Cartridges (ie Steam, Bottles, Heroic)
# All remaining desktop files are now unique, sideloaded games
# Use the Gio library to read their icon.
# Generate a fake banner for the game using their icon overlaid on top of a static gradient background (possibly enlarge + blur the official icon, and overlay a black/gray gradient to darken it, to get a really nice banner image).
# And lastly, take their "Exec=" data and use that as launch argument (but remove `%U` and other dynamic arguments).
pprint(found_desktop_files)

It's basically solved but I don't have time to finish it. I have so much to do...

@Arcitec Arcitec changed the title Support for Importing System Games Support for Importing Desktop Files (Native Games) Apr 2, 2023
@GeoffreyCoulaud
Copy link
Contributor

GeoffreyCoulaud commented Apr 6, 2023

Hi ! Just a bit of knowledge coming from my prior development on Gali.

I agree with the idea of looking for the .desktop files yourself, I would even advise not using an external library for this since the XDG spec is quite clear on the locations considered and their priority.

I don't have time to finish it, rewriting it to work inside the Flatpak sandbox, adding Flatpak permissions to the manifest, etc. But at least it helps us get started.

There, in the current state of things you will hit a roablock for all apps coming from flatpaks, since their desktop entries cannot be exposed to the sandbox, /var is blacklisted in the sandbox permissions. This means that you will not be able to read /var/lib/flatpak/exports/share/applications which contains these desktop entries.

A solution would be a portal to request desktop entries, which I already requested a while back : xdg-desktop-portal issue #809.

@GeoffreyCoulaud
Copy link
Contributor

As discussed with @kra-mo on the side, while /var is marked as blacklisted in the docs, we can expose /var/lib/flatpak/exports specifically and access it in the sandbox. So no worries then !

@ghost
Copy link

ghost commented May 26, 2023

Under Preference -> Import tab, where the app lists all the source locations for games, would it make more sense to allow user to add new paths to this list. It give a bit more flexibility and achieve a similar result to what we are discussing.

@GeoffreyCoulaud
Copy link
Contributor

We can already follow the XDG specification and detect the game .desktop files just like any desktop environment would. So I don't think adding user-set overrides there makes sense

@michaelneverwins
Copy link

michaelneverwins commented Aug 21, 2023

I just recently started using Cartridges. Until now, I hadn't been using any fancy GUI for launching games, and had in fact been relying on Linux Mint's application menu as the one place to access all of my games. Ensuring that all of them were easily accessible in this way meant manually creating some desktop entries in ~/.local/share/applications.

Knowing that all of my games have desktop entries, and now having decided that I wanted to add all of my miscellaneous non-Steam/Itch/Bottles/Flatpak games to Cartridges, I wrote a script (purely for personal use) which partially automates the process of adding games from desktop entries. The script takes any number of desktop entry locations as arguments; for each one, it parses the Name, Terminal, Path, and Exec fields and then uses this information to create a JSON file which is dropped directly into ~/.var/app/hu.kramo.Cartridges/data/cartridges/games/. It doesn't do anything about cover art, and isn't nearly as sophisticated as what's being discussed here (in that it processes only the desktop entries I explicitly pass to it), but it worked well enough to save me some time... well, maybe. I probably spent just as much time going back and fixing a few things that my first iteration of the script was missing, namely the use of the Terminal and Path fields, which is the reason I'm bringing this up. Discussion so far seems to imply that you want to parse the Exec field for the command to run, and I don't see any mention of Terminal and Path, which are easy to forget but are occasionally important.

*.desktop-parsing gotchas

If a desktop entry's Terminal field is set to true then the program needs to be opened in a terminal. When given one of these desktop entries, my script puts gnome-terminal -- before the command given by the Exec field (similar to what is recommended in #153), and it works on my machine but I realize it's not a distro-agnostic solution. How to open a terminal without knowing which terminal programs are available is for you all to figure out, I suppose, if you want to support importing and parsing desktop entries with Terminal=true. Granted, it's not common for games; on my machine, vitetris would be the only example of a game whose desktop entry has Terminal=true by default. (Personally, I cared about this field primarily because I have some custom-made desktop entries that open a Bash script presenting a choice of multiple options, e.g. for Quake and its expansions. Adding Desktop Action sections would accomplish the same thing, I guess, but Cinnamon doesn't seem to do anything with those in desktop entries I've copied to ~/Desktop, and of course it turns out that a desktop entry whose multiple options are wrapped up in a single shell command makes for easier importing into Cartridges anyway.)

Meanwhile, a desktop entry's Path field will indicate the directory from which the program should be executed, so if a desktop entry has Path=/foo, my script puts cd /foo && before the command to run when creating the JSON file. Most games won't care what the current working directory is (and the one case in which the Path field actually matters on my machine is in a custom-made desktop entry for the DOS game Jetpack which has Exec=dosbox JETPACK.EXE and sets Path to the directory where I installed JETPACK.EXE), but just to be safe, any desktop entry parsing done by Cartridges should respect the Path field as well. If a desktop entry had Terminal=true as well as Path=/foo and Exec=bar, then I think cd foo && gnome-terminal -- bar would work.

But all of this advice assumes that the plan is to parse each *.desktop file to extract the raw command that Cartridges needs to run, as my own script did. However, that might not be the best way, unless it's the only sufficiently distro-agnostic way.

Letting the OS do the hard part

I realized only after writing my script and fixing all the bugs that, at least on my machine, I could have just had Cartridges use gtk-launch to run each game without parsing *.desktop files at all — e.g. gtk-launch vitetris to launch vitetris from /usr/share/applications/vitetris.desktop, thus properly launching it in a terminal without explicitly invoking gnome-terminal, or gtk-launch Jetpack to run Jetpack from my ~/.local/share/applications/Jetpack.desktop, thus executing DOSBox in the right directory without explicitly invoking cd. I don't know whether gtk-launch is expected to work on every machine supporting *.desktop files, but if so, I think that's definitely the best way to launch desktop-entry-defined games.

Unwanted *.desktop files

My only other concern is regarding the automatic detection of desktop entries. My own script doesn't even attempt it because of the inherent difficulty in avoiding accidental imports of unwanted files. Some of this has already been discussed, namely the need to avoid importing desktop entries for games that were already added by other import methods. Skipping desktop entries for Steam games would be easy enough, because they're easy to identify: every unmodified desktop entry created by Steam will have Exec=steam steam://rungameid/* (where the app ID * is the only part of the Exec field which differs). I'll be super optimistic and assume that desktop entries created by other already-integrated platforms are similarly identifiable. The more challenging problems, when it comes to the automatic import of desktop entries, are as follows:

  • Games already added manually. I'm sure that I'm not the only one who has already added games to Cartridges (either manually or with the help of a hacked-together Python script) which also have desktop entries. You can disregard my obsessive habit of manually creating a desktop entry for every manually installed game, and instead consider (as @Saroufim mentioned) desktop entries created automatically for native games installed via the package manager. If I've added SuperTuxKart to Cartridges, I wouldn't want it automatically importing /usr/share/applications/supertuxkart.desktop and adding SuperTuxKart again. You could skip desktop entries whose Name field is the same as the name of a game already in Cartridges, but that's not exactly foolproof (as someone may have manually added, say, "Super Tux Kart" while the desktop entry spells it "SuperTuxKart"). You could skip desktop entries whose Exec field is the same as the command executed for a game already in Cartridges, but that might not be foolproof either, as the user might have messed with the command (e.g. changing supertuxkart to, say, gamescope -b -- supertuxkart), but maybe that situation is uncommon enough that comparing Exec fields to commands already known to Cartridges would be good enough.
  • Desktop entries that aren't games. Quite a few desktop entries on my system have "Game" in the Categories field despite not actually being games, such as: Steam, ScummVM, DOSBox, QJoyPad, and a few source ports (e.g. GZDoom which has its own desktop entry in addition to the separate ones for individual games which use it as an engine). You can add the obvious examples to an "ignore" list but you can't predict every game-related-but-not-a-game program which will include "Game" in its categories.

@kra-mo
Copy link
Owner

kra-mo commented Aug 21, 2023

Thanks for the comment. Overall, I agree with what you said.

Letting the OS do the hard part

Pretty sure gtk-launch is part of GTK3 so we shouldn't rely on it. cd-ing into a directory is easy enough and there is a distro-agnostic way to run a command in any terminal (though the standard is fairly new).

Gio.AppInfo.launch does handle all of this, but since we want to run the command on the host, we would need to run a module from the sandbox on the host using this, which is pretty much a hack and probably not doable.

We could also do some automatic detection of terminals' desktop entries and hard code some, (GNOME Console, GNOME Terminal, Konsole, xterm), but that could change in the future and all of this is messy.

I think the best way to handle this would be to not import terminal games initially unless xdg-terminal-exec is present. If it isn't, inform the user in a popup and let them pick which terminal to use from a hard-coded list of presets or optionally, type in a command of their own.

Games already added manually

This hasn't been considered when adding new sources previously either, because I don't think it can work. Any solution to this would be a hack and could lead to unexpected results. Games added manually don't play a role in imports in any way and I don't think they should. Worst case scenario is that the user has to delete/hide some games.

@michaelneverwins
Copy link

michaelneverwins commented Aug 21, 2023

Games already added manually

This hasn't been considered when adding new sources previously either, because I don't think it can work. Any solution to this would be a hack and could lead to unexpected results. Games added manually don't play a role in imports in any way and I don't think they should. Worst case scenario is that the user has to delete/hide some games.

Fair enough. Even in the absence of manually added games, the possibility of duplicates is nothing new. I got one game imported twice because I had installed it from Itch.io and then added it to Bottles after vanilla Wine failed to run it smoothly. Hiding the imported Itch.io copy was easy.

Having added so many games manually, though, I would probably prefer to turn off desktop entry imports on my system. I see that other import sources can already be toggled, so I assume it will be possible for desktop entry imports as well.

As for non-game programs being added because they are categorized as "Game", I should point out that this isn't new either. Cartridges imported the Flatpak version of ScummVM on my system, for example. Some might prefer that behavior, actually. I just hid it after the import.

@GeoffreyCoulaud
Copy link
Contributor

Resolved by #174 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
Status: Done
Development

No branches or pull requests

5 participants