Last active
July 3, 2026 01:09
-
-
Save mcgrew/d74a299acca800f375c8cc4f2fe012be to your computer and use it in GitHub Desktop.
Python script to launch steam on a remote machine for remote play. Linux only. Probably doesn't work for snap/flatpak installs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python | |
| from argparse import ArgumentParser | |
| from glob import glob | |
| from pathlib import Path | |
| from shutil import which | |
| from subprocess import Popen, DEVNULL, check_output, run, CalledProcessError | |
| from time import sleep | |
| import os | |
| import re | |
| def parse_args(): | |
| parser = ArgumentParser(add_help=False) | |
| parser.add_argument('-w', '--width', type=int, default=1280, | |
| help='The width of the window in pixels') | |
| parser.add_argument('-h', '--height', type=int, default=800, | |
| help='The heigh of the window in pixels') | |
| parser.add_argument('-g', '--game-id', type=int, default=0, | |
| help='The id of the game to run') | |
| parser.add_argument('-n', '--no-display', action='store_true', | |
| help='Launch with gamescope in headless mode.' | |
| 'This only works if steam is not running') | |
| parser.add_argument('-s', '--shutdown', action='store_true', | |
| help='Shut down steam if it is running.') | |
| parser.add_argument('game', nargs='*', | |
| help='The game to search for') | |
| parser.add_argument('-?', '--help', action='help', | |
| help='Show this help message and exit') | |
| return parser.parse_args() | |
| def main(args): | |
| env = os.environ.copy() | |
| env["DISPLAY"] = ":0" | |
| env["WAYLAND_DISPLAY"] = "wayland-0" | |
| nohup = which('nohup') | |
| gamescope = which('gamescope') | |
| steam = which('steam') | |
| steampid = pidof('steam') | |
| if args.shutdown: | |
| return shutdown() | |
| if steampid and not pidof('gamescope'): | |
| shutdown() | |
| steampid = [] | |
| manifest = find_game(args.game_id, args.game) | |
| if steampid and not manifest: | |
| print("Steam is already running") | |
| return # nothing to do | |
| command = [nohup, gamescope, '-b', '-e', | |
| '-W', str(args.width), '-H', str(args.height), | |
| '-w', str(args.width), '-h', str(args.height)] | |
| if args.no_display: | |
| command.extend(['--backend', 'headless']) | |
| command.extend(['--', steam, '-gamepadui', '-nopcshutdown', | |
| '-pipewire-dmabuf']) | |
| if manifest: | |
| gameid = manifest['appid'] | |
| print(f'Launching {manifest["name"]}...') | |
| if steampid: | |
| command = [steam, '-pipewire-dmabuf'] | |
| command.extend(['-applaunch', str(gameid)]) | |
| print(f'Running {" ".join(command)}...') | |
| Popen(command, stdout=DEVNULL, stderr=DEVNULL, env=env) | |
| while not (p := pidof('steam')): | |
| sleep(1) | |
| print(f'Steam pid: {p[0]}') | |
| def find_game(gameid, name): | |
| manifests = read_manifests() | |
| if gameid: | |
| for m in manifests: | |
| if int(m['appid']) == gameid: | |
| return m | |
| if name: | |
| for m in manifests: | |
| if all([word.lower() in m['name'].lower() for word in name]): | |
| return m | |
| def pidof(app): | |
| try: | |
| return [p.decode('utf-8') for p in check_output(['pidof', app]).split() | |
| if p] | |
| except CalledProcessError: | |
| return [] | |
| def shutdown(): | |
| if pidof('steam'): | |
| print('Shutting down steam...') | |
| run(['steam', '-shutdown']) | |
| while pidof('steam'): | |
| sleep(1) | |
| print('Done.') | |
| def read_manifests(): | |
| home = os.environ.get('HOME') | |
| games = [] | |
| for f in glob(f'{home}/.steam/steam/steamapps/*.acf'): | |
| games.append(parse_steam_manifest(f)) | |
| return games | |
| # Vibe coded function because I was tired. Whatever, it works. | |
| def parse_steam_manifest(file_path: str) -> dict: | |
| """Parses a Steam appmanifest_*.acf file into a Python dictionary""" | |
| path = Path(file_path) | |
| # Regex to cleanly extract strings inside double quotes | |
| kv_regex = re.compile(r'"([^"]*)"') | |
| result = {} | |
| stack = [result] # Stack tracking nested dictionaries | |
| with open(path, "r", encoding="utf-8") as f: | |
| for line in f: | |
| line = line.strip() | |
| # Skip empty lines and comment lines | |
| if not line or line.startswith("//"): | |
| continue | |
| # Found a nested block start: add subdir placeholder | |
| if line == "{": | |
| continue | |
| # Found a nested block end: pop up one level in our tracking stack | |
| if line == "}": | |
| if len(stack) > 1: | |
| stack.pop() | |
| continue | |
| # Extract all quoted tokens in the current line | |
| tokens = kv_regex.findall(line) | |
| if len(tokens) == 2: | |
| # Key-Value pair: Assign value to the active dictionary | |
| key, value = tokens[0], tokens[1] | |
| stack[-1][key] = value | |
| elif len(tokens) == 1: | |
| # Key descriptor for an oncoming sub-dictionary block | |
| key = tokens[0] | |
| new_dict = {} | |
| stack[-1][key] = new_dict | |
| stack.append(new_dict) # Push to stack | |
| return result.get('AppState', {}) | |
| if __name__ == '__main__': | |
| args = parse_args() | |
| main(args) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment