Files
DroidDeck/tools/linuxfs/desktop/droiddeck-desktop-gpu
T
Kurt HimebauchandMaxsTechReview 9ed25d8ae5 feat: Flatpak apps and a Flathub store (#77)
* feat: run Flatpak apps through a proot bwrap stand-in and add a Flathub store

* feat: give Flatpak apps the KGSL GPU, the controllers and a CPU LLVM can target

* feat: Flatpak games on the desktop, Store apps on the Desktop page, safer remove

* test: cover the sandbox planner and store helpers; docs: explain Flatpak on DroidDeck

* fix: keep controller focus through Store tabs and app pages; show progress per Flatpak step

* fix: let Chromium, Electron and Firefox Flatpaks start without their own sandboxes

* fix: fetch Flatpak's supporting libraries when the runtime lacks them

* fix: add Flathub without a download, log store commands, keep Electron apps on X11 under gamescope

* feat: import ARM64 AppImages from storage and run them from the Desktop page

* fix: report failed Flatpak installs as failures and deploy runtimes that ship a resolv.conf link

* fix: let Flatpak create its repo where Android denies hard links

* fix: point store commands at the current network's DNS

* fix: keep failed Flatpak extensions non-fatal, give Electron AppImages a bus, X11 and time to relaunch

* docs: AppImage import and what x86 apps would take

* feat: put the Flathub Store and AppImage import behind beta toggles in Setup

---------

Co-authored-by: MaxsTechReview <maxjividen@gmail.com>
2026-09-30 00:35:49 -04:00

137 lines
5.5 KiB
Python

#!/usr/bin/python3
# The desktop's games and emulators, started on the GPU. Every menu entry whose categories say
# Game or Emulator gets a copy of its own in ~/.local/share/applications (which wins over
# /usr/share/applications) whose Exec runs it through droiddeck-gpu: full screen on the app's
# compositor, where Vulkan and OpenGL work. The entry keeps the old way as a second action, "Run in
# a desktop window". Run at every desktop start; copies whose original is gone, or all of them when
# the switch is off, are removed.
#
# BL_DESKTOP_GPU=auto (default: on while the desktop is composited by pixman) | on | off
import os
import re
HOME = os.environ.get("HOME", "/root")
SYSTEM_DIR = "/usr/share/applications"
# The user's own AppImages (the front end's Add AppImage).
LOCAL_DIR = "/usr/local/share/applications"
# Flathub's games, as Flatpak exports them; the copies here win over these as they do over the system's.
FLATPAK_DIR = os.path.join(HOME, ".local/share/flatpak/exports/share/applications")
USER_DIR = os.path.join(HOME, ".local/share/applications")
MARK = "X-DroidDeck-GPU=1"
# The client and its games already hand themselves to the app (bannerlator-steam-launch).
SKIP = re.compile(r"^(steam|droiddeck-game-|droiddeck-steam|steamdeck-)", re.I)
def enabled():
mode = os.environ.get("BL_DESKTOP_GPU", "auto").strip().lower()
if mode in ("on", "1", "yes", "true"):
return True
if mode in ("off", "0", "no", "false"):
return False
return os.environ.get("WLR_RENDERER", "pixman") == "pixman"
def main_group(text):
"""The [Desktop Entry] group's lines, and everything after it."""
lines = text.splitlines()
start = next((i for i, l in enumerate(lines) if l.strip() == "[Desktop Entry]"), None)
if start is None:
return None, None, None
end = next((i for i in range(start + 1, len(lines)) if lines[i].startswith("[")), len(lines))
return lines[:start + 1], lines[start + 1:end], lines[end:]
def gpu_exec(exec_line):
"""The Exec that runs a program on the GPU; a Flatpak app goes through its launcher, which
keeps it on gamescope's X server instead of the desktop's Wayland socket."""
if exec_line.startswith("/usr/bin/flatpak run ") or exec_line.startswith("flatpak run "):
exec_line = "/usr/local/bin/bannerlator-flatpak-run " + exec_line.split(" run ", 1)[1]
return "droiddeck-gpu " + exec_line
def wrap(text):
head, group, rest = main_group(text)
if group is None:
return None
keys = {}
for l in group:
if "=" in l and not l.lstrip().startswith("#"):
k, v = l.split("=", 1)
keys.setdefault(k.strip(), v)
cats = set(filter(None, keys.get("Categories", "").split(";")))
if not cats & {"Game", "Emulator"}:
return None
if keys.get("Type", "Application") != "Application" or keys.get("NoDisplay", "").lower() == "true":
return None
exec_line = keys.get("Exec", "").strip()
if not exec_line or exec_line.startswith("droiddeck-gpu "):
return None
out_group = []
actions = keys.get("Actions", "")
for l in group:
k = l.split("=", 1)[0].strip() if "=" in l else None
if k == "Exec":
out_group.append("Exec=" + gpu_exec(exec_line))
elif k == "Actions":
continue
else:
out_group.append(l)
while out_group and not out_group[-1].strip():
out_group.pop()
out_group.append("Actions=" + ("droiddeck-window;" + actions if actions else "droiddeck-window;"))
out_group.append(MARK)
window = ["", "[Desktop Action droiddeck-window]", "Name=Run in a desktop window", "Exec=" + exec_line]
# The entry's own actions (new window, a different mode...) run on the GPU as well.
wrapped_rest = []
for l in rest:
if l.startswith("Exec=") and not l[5:].startswith("droiddeck-gpu "):
l = "Exec=" + gpu_exec(l[5:])
wrapped_rest.append(l)
if wrapped_rest and wrapped_rest[0].strip():
wrapped_rest.insert(0, "")
return "\n".join(head + out_group + window + wrapped_rest) + "\n"
def ours(path):
try:
with open(path, encoding="utf-8", errors="replace") as f:
return MARK in f.read()
except OSError:
return False
def main():
on = enabled()
os.makedirs(USER_DIR, exist_ok=True)
written = set()
sources = [d for d in (SYSTEM_DIR, LOCAL_DIR, FLATPAK_DIR) if os.path.isdir(d)] if on else []
for source in sources:
for name in sorted(os.listdir(source)):
if not name.endswith(".desktop") or SKIP.match(name):
continue
target = os.path.join(USER_DIR, name)
# A copy the user made or edited themselves is theirs.
if os.path.exists(target) and not ours(target):
continue
try:
with open(os.path.join(source, name), encoding="utf-8", errors="replace") as f:
text = wrap(f.read())
except OSError:
continue
if text is None:
continue
with open(target + ".tmp", "w", encoding="utf-8") as f:
f.write(text)
os.replace(target + ".tmp", target)
written.add(name)
removed = 0
for name in os.listdir(USER_DIR):
path = os.path.join(USER_DIR, name)
if name.endswith(".desktop") and name not in written and ours(path):
os.remove(path)
removed += 1
print(f"droiddeck-desktop-gpu: {'on' if on else 'off'}, {len(written)} entries on the GPU, {removed} removed")
main()