#!/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" 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 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=droiddeck-gpu " + 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=droiddeck-gpu " + 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() if on and os.path.isdir(SYSTEM_DIR): for name in sorted(os.listdir(SYSTEM_DIR)): 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(SYSTEM_DIR, 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()