Skip to content

Instantly share code, notes, and snippets.

@mcgrew
Last active July 3, 2026 01:09
Show Gist options
  • Select an option

  • Save mcgrew/d74a299acca800f375c8cc4f2fe012be to your computer and use it in GitHub Desktop.

Select an option

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.
#!/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