Files
DroidDeck/tools/linuxfs/desktop/droiddeck-desktop-gpu
T
Kurt Himebauch fe71084f23 feat: replace the LXQt desktop with SteamOS-style KDE Plasma (#473)
* feat: replace the LXQt desktop with SteamOS-style KDE Plasma

* feat: leaving the KDE desktop logs out through Plasma and returns to gaming mode

* feat: DroidDeck logo wallpaper as the KDE desktop default

* feat: build the KDE wallpaper from the DroidDeck mark artwork

* ci: keep the built desktop package as a short-lived artifact for device testing

* fix: the desktop loading screen no longer promises Steam on the desktop

* docs: the output resize comments name the desktop's KWin

* fix: the desktop package verifies every package's Arch Linux ARM signature and never replaces a published tag

* fix: programs sent to the GPU from the desktop drop its software rendering overrides

* fix: an interrupted desktop install no longer counts as installed

* feat: pin the desktop package to its DroidDeck-Components release instead of the catalog
2026-10-09 10:39:50 -04:00

132 lines
5.3 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=on (default: the desktop composites in software) | 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 (droiddeck-steam-launch).
SKIP = re.compile(r"^(steam|droiddeck-game-|droiddeck-steam|steamdeck-)", re.I)
def enabled():
return os.environ.get("BL_DESKTOP_GPU", "on").strip().lower() not in ("off", "0", "no", "false")
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/droiddeck-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()