No description
Find a file
joe faa587a2d8
Make --merge-dupes rename duplicate modes instead of dropping them
The previous --merge-dupes resolved a mode (option key) defined more than once
by keeping the last value and silently discarding the earlier ones, and it only
acted when there were duplicate section headers — so a mode repeated inside a
single section (e.g. two `wayland =` lines) was flagged by --verify but had no
automated fix, even though the error pointed users at --merge-dupes.

Now the repair is non-destructive and covers both cases: duplicate [section]
blocks are folded into their first occurrence, and a mode defined more than once
(across those blocks or within one section) keeps its first occurrence under the
original name while every later one is renamed to <mode>-DUPE-000N. Renaming
preserves the delimiter, spacing and value verbatim. The report lists each
rename so the duplicates can be reconciled by hand. --verify / parse-error hints
and the game-launcher dialog now mention duplicate modes too.

Also note in the README that, because Steam stores launch options per account,
this centralises them in one per-user config so nothing has to be copied between
accounts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 18:01:12 +01:00
bin Make --merge-dupes rename duplicate modes instead of dropping them 2026-07-30 18:01:12 +01:00
config Rename launch_options to launch-options 2026-07-28 18:36:25 +01:00
docs Add launch-options --add screenshot to README 2026-07-30 16:55:50 +01:00
tests Make --merge-dupes rename duplicate modes instead of dropping them 2026-07-30 18:01:12 +01:00
.gitignore Add AGENTS.md and a pytest test suite 2026-07-28 19:01:42 +01:00
AGENTS.md Add AGENTS.md and a pytest test suite 2026-07-28 19:01:42 +01:00
CHANGELOG.md Make --merge-dupes rename duplicate modes instead of dropping them 2026-07-30 18:01:12 +01:00
Makefile Add AGENTS.md and a pytest test suite 2026-07-28 19:01:42 +01:00
README.md Make --merge-dupes rename duplicate modes instead of dropping them 2026-07-30 18:01:12 +01:00

game-launcher

A small per-game launch-options manager for Steam on Linux.

Steam only lets you set one launch-option string per game, and editing it for tweaks (different resolutions, wrappers like gamemoderun/mangohud/gamescope, Proton env vars, pre/post hooks) is tedious. This project moves all of that into a single config file and an optional picker dialog, so one Steam launch option — game-launcher %command% — drives every game.

Steam's launch options are also stored per account, so if you have more than one account you normally have to re-enter (and keep in sync) the same long, fiddly strings for every game on each one. Here they live in a single per-user config file instead: set game-launcher %command% once per game on each account and all the real options come from ~/.config/launch-options.ini — nothing to copy between accounts.

Components

Component Installed to Role
launch-options /usr/local/bin/launch-options Python CLI. Resolves a game alias or Steam AppID to a canonical key and prints the launch command for a named mode, or lists games/modes.
game-launcher /usr/local/bin/game-launcher Bash wrapper you set as a game's Steam launch option. Reads $SteamAppId, asks launch-options for the game's modes, shows a yad picker when there's more than one, substitutes %command%, and runs it.
launch-options.ini ~/.config/launch-options.ini Your data: alias map plus a section of named modes per game.

Registering a game is built into the CLI: launch-options --add lists your installed Steam games, lets you pick one, asks for a friendly name, and adds the alias (name + AppID) plus a starter section to your config.

The scripts install to a system directory (/usr/local/bin) rather than ~/.local/bin because native Steam doesn't reliably put ~/.local/bin on the PATH it launches games with, so game-launcher %command% wouldn't resolve. /usr/local/bin is on the standard system PATH that Steam inherits.

launch-options and launch-options.ini are usable on their own (e.g. to print a command); game-launcher is the Steam integration layer on top.

Install

sudo make install

sudo is needed because the scripts go to /usr/local/bin (a system directory on Steam's PATH). The config still lands in the invoking user's ~/.config/launch-options.ini — the Makefile detects $SUDO_USER so it never ends up in /root — and is installed only if you don't already have one (your existing config is never clobbered).

Override locations with the standard variables:

sudo make install PREFIX=/usr            # scripts to /usr/bin
sudo make install BINDIR=/opt/bin
make install-config CONFIGDIR=~/.config  # config only, no sudo

Other targets:

  • make install-config — install just the config (only if absent), no sudo.
  • make install-config-force — (re)install the bundled config, overwriting the existing one.
  • sudo make uninstall — remove the scripts (your config is left in place).
  • make help — list targets and show the resolved install paths.

Requirements

  • python3 (standard library only) for launch-options.
  • bash and yad for game-launcher's picker.
  • Whatever each mode invokes — gamemoderun, mangohud, gamescope, etc.
  • Native Steam (the scripts target /usr/local/bin on its PATH). Flatpak Steam runs sandboxed and won't see host /usr/local/bin without extra setup.
  • Optional: fzf for a nicer game picker in launch-options --add (it falls back to a numbered menu without it).

Usage

  1. In Steam, set the game's launch option to:

    game-launcher %command%
    

    (Run game-launcher in a terminal with no SteamAppId set and it prints this line for you.)

  2. Add the game to ~/.config/launch-options.ini. The quickest way is:

    launch-options --add
    

    It lists your installed Steam games, lets you pick one, asks for a friendly name, and writes the alias (name + AppID) plus a starter section for you. Then edit that section to define the game's modes (see below).

    launch-options --add fuzzy-filtering the list of installed Steam games

    To add a game by hand, map its Steam AppID to a game key in [aliases] and give that key a section. The AppID mapping is what game-launcher needs to resolve the game from $SteamAppId; a friendly alias alongside it is optional but usually preferred so you can refer to the game by name. Every lookup goes through [aliases] — a section name is never an implicit alias, so the self-mapping cs2 = cs2 is what makes launch-options cs2 resolve (without it you'd have to use the AppID). For CS2 (AppID 730):

    [aliases]
    ; cs2 = cs2 is the optional friendly alias; 730 = cs2 maps the AppID (required)
    cs2 = cs2
    730 = cs2
    
    [cs2]
    default = %command%
    
  3. Launch from Steam. If the game has more than one mode, a picker appears; otherwise it launches directly. Each run is logged to /tmp/game-launcher.log.

You can also query the config directly:

launch-options cs2              # print the 'default' mode's command for cs2
launch-options cs2 fullscreen   # print the 'fullscreen' mode's command
launch-options --list           # list every game and its modes
launch-options --list cs2       # list modes for one game
launch-options --verify         # check the config for problems
launch-options --merge-dupes    # fold duplicate sections, rename duplicate modes

Checking and repairing the config

configparser rejects a config with a duplicate [section] header or a mode (key) defined more than once. launch-options --verify reports such problems (with line numbers) in plain language instead of a traceback, and also warns about softer issues like an alias that points to a section that doesn't exist:

$ launch-options --verify
launch-options.ini has 1 problem(s) that stop it from loading:
  • line 132: duplicate key 'wayland' in [cs2] (first defined at line 128)

Run `launch-options --merge-dupes` to fold duplicate sections together and rename duplicate modes.

launch-options --merge-dupes repairs both, non-destructively — nothing you wrote is thrown away:

  • Duplicate [section] blocks are folded into their first occurrence.
  • A mode defined more than once (across those blocks, or repeated inside a single section) keeps its first occurrence under the original name; each later one is renamed to <mode>-DUPE-000N. The command output tells you exactly which modes were renamed, e.g. [cs2] wayland → wayland-DUPE-0001, so you can reconcile the duplicates and delete the extras.

It writes a .bak backup before rewriting. When game-launcher hits an unloadable config it runs --verify first and offers this same repair in a yad dialog, rather than launching the game with none of your options applied.

Config format

~/.config/launch-options.ini is an INI file with three kinds of section: [aliases], one [<game-key>] section per game, and an optional [vars].

[aliases] maps any string — a short name or a Steam AppID — to a canonical game key. Mapping the AppID is what lets game-launcher resolve a game from $SteamAppId automatically.

[aliases]
; 730 is CS2's Steam AppID
cs2 = cs2
730 = cs2

Comments must be on their own line. configparser here has inline comments disabled, so a ; after a value is kept as literal text (and in a mode command it's a shell command separator) — not a comment.

[<game-key>] sections define named modes. Each value is a command line containing the %command% placeholder, which is replaced with the actual game invocation Steam would have run. Env vars, wrappers, pre/post hooks (separated by ;), and trailing game args all go here.

[cs2]
default    = SDL_VIDEO_DRIVER=wayland mangohud %command% -w 1920 -h 1440
overlay    = SDL_VIDEO_DRIVER=x11 mangohud %command% -w 1920 -h 1440
fullscreen = %command% -w 2560 -h 1440

A game with a single default mode launches without a prompt; multiple modes trigger the picker. Game keys are matched case-insensitively with whitespace collapsed. Override the config path with the LAUNCH_OPTS_INI environment variable.

[vars] (optional) holds reusable fragments, referenced as %name% in any mode and expanded before launch. Handy for something shared across a game's modes:

[vars]
autoexec = +exec autoexec.cfg

[cs2]
default = mangohud %command% %autoexec% -w 1920 -h 1440
plain   = %command% %autoexec% -w 1920 -h 1440

Only names defined in [vars] are expanded — %command% and shell syntax ($VAR, ${VAR}, $(...)) pass through untouched. Vars may reference other vars. (This is a small custom mechanism, not configparser interpolation, which would collide with %command% and shell ${...}.)

Security note

game-launcher evals the selected mode string, so the config file is trusted, executable input. Only put commands in it you'd be willing to run yourself.