Skip to content

feat: add cli support linux proton issue #128 (draft)#162

Draft
charlesmerritt wants to merge 21 commits intocoder:mainfrom
charlesmerritt:issue-128-cli-support-linux-proton
Draft

feat: add cli support linux proton issue #128 (draft)#162
charlesmerritt wants to merge 21 commits intocoder:mainfrom
charlesmerritt:issue-128-cli-support-linux-proton

Conversation

@charlesmerritt
Copy link
Copy Markdown

Hello, I am working on my first PR here to bring Steam Deck/Proton support to BalatroBot developers.

So far, pretty much everything seems to work, but I still need to fix an error where fixtures are not found which causes all of the Lua tests to fail.

Summary

This PR introduces initial support for running BalatroBot on Steam Deck via Proton. Core functionality appears to work, but Lua tests are currently failing due to missing fixtures. This is a draft PR for early feedback.

Here is what I have added.

  • src/balatrobot/platforms/linux.py: the main compatibility functionality for proton.
  • tests/cli/test_platforms.py: testing for the added functionality
  • Support for Proton over terminal SSH sessions by detecting display paths.
  • scripts/dev.sh: Steam deck has a read-only OS, and so you cannot use make commands or install packages to the host os with pacman, so this shell script is intended to reproduce the behavior of the Makefile.
  • The rest is mostly altering the documentation slightly to correspond with the progress I'm making on bringing support to this platform.

Note on deviations between dev.sh and Makefile:

  • I added a stop command, which has been very helpful to stop and clean up all of the Balatro processes that get started up during testing and various development tasks. I think something like this should be added to the Makefile perchance.
  • The number of workers for the lua tests had to be altered on my Steam Deck to avoid a health check time out and get the tests to run, even with the fast and headless options set.
  • I had to change the fixtures command to avoid calling a command like 'balatrobot --fast --debug' I interpreted this as intended to be balatrobot serve --fast --debug, as the original command would not work on my machine. In dev.sh, spawning balatrobot processes hung up the cli command, and so I opted to remove it completely under the assumption balatrobot was already started from another terminal. A simple error message was added if the command fails, suggesting that users make sure that balatrobot is running and reachable.

This is a draft PR and any advice/review is appreciated. Here is what I know I have left to do for sure.

  • Get uv run scripts/dev.sh test to pass all tests.
  • Consider refactoring the name src/balatrobot/platforms/linux.py to linux_proton.py and class LinuxLauncher to LinuxProtonLauncher for added clarity and to avoid conflict with the native Linux support in src/balatrobot/platforms/native.py
  • Confirm functionality works in the same way on Steam Deck directly without using SSH.
  • Possibly patch certain bugged consumable items like magician throwing errors in their unique card selection screens.
  • Double check that all code and documentation follows the contributing guidelines.
  • Consider adding the development commands from dev.sh as tool aliases in your pyproject.toml later. Which would allow you to run things like uv run dev test instead of pointing to the script path every time.

charlesmerritt and others added 3 commits March 28, 2026 03:28
Implements LinuxLauncher that auto-detects Steam root, Balatro.exe,
Proton installation, version.dll (Lovely), and Wine prefix (compatdata).
Sets WINEDLLOVERRIDES="version=n,b" so Wine loads the native Lovely
injector instead of its built-in version.dll.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When SSH'd into a Steam Deck, DISPLAY and XAUTHORITY are unset even
though gamescope provides an X server. Without these, Proton fails with
"Authorization required" and the game window never renders.

The Linux launcher now auto-detects both from the system:
- DISPLAY from X11 sockets in /tmp/.X11-unix/
- XAUTHORITY from XDG_RUNTIME_DIR/xauth_* or ~/.Xauthority

This eliminates the need to manually export these variables before
running `balatrobot serve` or the test suite.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds experimental Steam Deck / Proton support by introducing a Linux (Steam/Proton) launcher and documenting/dev-tooling updates to help develop and test on SteamOS.

Changes:

  • Implement LinuxLauncher to auto-detect Steam libraries, Balatro.exe, Proton, compatdata, and set required Proton/X11 environment.
  • Extend CLI platform dispatch and add a broad Linux/Proton-focused test suite.
  • Add Steam Deck–friendly dev script, an example bot script, and documentation updates for Proton usage and tooling setup.

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/balatrobot/platforms/linux.py New Proton-based Linux launcher with Steam/Proton/compatdata detection and X11 env auto-detect.
src/balatrobot/platforms/__init__.py Routes platform="linux" to LinuxLauncher.
tests/cli/test_platforms.py Adds Linux launcher + helper parsing/detection tests.
scripts/dev.sh Adds Makefile-like task runner for environments without make (Steam Deck).
examples/bot.py Adds a small JSON-RPC example bot runnable via uv.
docs/cli.md Documents the experimental Linux (Steam/Proton) platform behavior and requirements.
docs/installation.md Updates installation guidance to reference the new Proton-capable linux platform.
docs/example-bot.md Updates example instructions to run the new example script from repo root.
docs/contributing.md Updates contributor guidance for Proton progress + external Lua tooling + Steam Deck workflow.
CLAUDE.md Updates architecture overview to include Linux (Steam/Proton) platform.
.gitignore Ignores .lua-lsp/ helper directory.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +14 to +21
def rpc(method: str, params: dict = {}) -> dict:
"""Send a JSON-RPC 2.0 request to the BalatroBot API."""
response = requests.post(
URL,
json={
"jsonrpc": "2.0",
"method": method,
"params": params,
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rpc() uses a mutable default argument (params={}), which can leak state between calls if the dict is ever mutated. Use params: dict | None = None and create a new dict inside the function when needed.

Suggested change
def rpc(method: str, params: dict = {}) -> dict:
"""Send a JSON-RPC 2.0 request to the BalatroBot API."""
response = requests.post(
URL,
json={
"jsonrpc": "2.0",
"method": method,
"params": params,
def rpc(method: str, params: dict | None = None) -> dict:
"""Send a JSON-RPC 2.0 request to the BalatroBot API."""
response = requests.post(
URL,
json={
"jsonrpc": "2.0",
"method": method,
"params": params or {},

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +24
response = requests.post(
URL,
json={
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": 1,
},
)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example bot’s requests.post(...) call has no timeout. If the local API is unreachable, this can hang indefinitely and make the example look broken. Consider adding a reasonable timeout (and optionally a clearer exception message for connection errors).

Suggested change
response = requests.post(
URL,
json={
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": 1,
},
)
try:
response = requests.post(
URL,
json={
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": 1,
},
timeout=5,
)
except requests.exceptions.RequestException as exc:
raise RuntimeError(f"Failed to connect to BalatroBot API at {URL}") from exc

Copilot uses AI. Check for mistakes.
print_msg "${YELLOW}Available targets:${RESET}"
printf " ${GREEN}%-18s${RESET} %s\n" "help" "Show this help message"
printf " ${GREEN}%-18s${RESET} %s\n" "install" "Install balatrobot and all dependencies (including dev)"
printf " ${GREEN}%-18s${RESET} %s\n" "lint" "Run ruff linter (check only)"
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the help output, the lint target is described as "check only", but cmd_lint runs ruff check --fix ..., which modifies files. Update the help text (or the command) so the behavior matches what’s advertised.

Suggested change
printf " ${GREEN}%-18s${RESET} %s\n" "lint" "Run ruff linter (check only)"
printf " ${GREEN}%-18s${RESET} %s\n" "lint" "Run ruff linter (apply fixes)"

Copilot uses AI. Check for mistakes.
Comment on lines +464 to +471
monkeypatch.setattr(
"balatrobot.platforms.linux._detect_display",
lambda: (
_detect_display.__wrapped__(x11_dir)
if hasattr(_detect_display, "__wrapped__")
else None
),
)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test sets balatrobot.platforms.linux._detect_display twice; the first patch tries to call _detect_display.__wrapped__, but _detect_display isn’t decorated (no __wrapped__), and the patch is overwritten immediately after. This dead/unused monkeypatch adds confusion and doesn’t validate the real detection logic—remove it or refactor _detect_display to accept an injectable socket dir for a deterministic unit test.

Suggested change
monkeypatch.setattr(
"balatrobot.platforms.linux._detect_display",
lambda: (
_detect_display.__wrapped__(x11_dir)
if hasattr(_detect_display, "__wrapped__")
else None
),
)

Copilot uses AI. Check for mistakes.
| Linux (Native) | `~/.config/love/Mods/balatrobot/` |

> Steam/Proton launcher not supported yet. Track progress in [#128](https://github.com/coder/balatrobot/issues/128)
> Linux (Steam/Proton) is an experimental platform via the `linux` platform. See the [CLI Reference](cli.md#linux-steamproton-platform) for details.
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The link target cli.md#linux-steamproton-platform likely doesn’t match the actual heading slug in docs/cli.md (### Linux (Steam/Proton) Platform (Experimental) typically slugs to #linux-steamproton-platform-experimental in MkDocs/Material). This will produce a broken in-page link; adjust the fragment or add an explicit anchor to the CLI heading.

Suggested change
> Linux (Steam/Proton) is an experimental platform via the `linux` platform. See the [CLI Reference](cli.md#linux-steamproton-platform) for details.
> Linux (Steam/Proton) is an experimental platform via the `linux` platform. See the [CLI Reference](cli.md#linux-steamproton-platform-experimental) for details.

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +37
sockets = sorted(Path("/tmp/.X11-unix").iterdir())
if sockets:
# Socket names are like X0, X1 — extract the display number
name = sockets[0].name # e.g. "X0"
return f":{name[1:]}"
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In _detect_display(), the code picks the first entry in /tmp/.X11-unix and blindly slices name[1:]. If the directory contains non-display entries (or display sockets like X10), this can return an invalid DISPLAY. Filter entries by a ^X\d+$ pattern and choose the lowest numeric display (or otherwise deterministically choose the intended one).

Suggested change
sockets = sorted(Path("/tmp/.X11-unix").iterdir())
if sockets:
# Socket names are like X0, X1 — extract the display number
name = sockets[0].name # e.g. "X0"
return f":{name[1:]}"
display_numbers: list[int] = []
for entry in Path("/tmp/.X11-unix").iterdir():
# Socket names are expected to be like "X0", "X1", etc.
m = re.fullmatch(r"X(\d+)", entry.name)
if m:
display_numbers.append(int(m.group(1)))
if display_numbers:
# Choose the lowest numeric display deterministically.
return f":{min(display_numbers)}"

Copilot uses AI. Check for mistakes.
runtime_dir = os.environ.get("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}")
matches = glob.glob(os.path.join(runtime_dir, "xauth_*"))
if matches:
return matches[0]
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_detect_xauthority() returns matches[0] from glob.glob, but glob ordering is filesystem-dependent and there can be multiple xauth_* files; this can select a stale/incorrect Xauthority file and break X11 auth. Sort the matches (e.g., by mtime descending) and pick the most recent, or otherwise make the selection deterministic.

Suggested change
return matches[0]
# Multiple xauth_* files can exist; pick the most recently modified one
latest = max(matches, key=os.path.getmtime)
return latest

Copilot uses AI. Check for mistakes.
if self._steam_root is None:
errors.append(
"Steam installation not found.\n"
" Expected: ~/.local/share/Steam or ~/.steam/steam"
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Steam-not-found message says it "Expected: ~/.local/share/Steam or ~/.steam/steam", but _STEAM_ROOT_CANDIDATES also includes Snap and Flatpak locations. Update the message to list all supported candidate paths (or print the full candidate list) so users aren’t misled when Steam is installed via Snap/Flatpak.

Suggested change
" Expected: ~/.local/share/Steam or ~/.steam/steam"
" Searched locations:\n"
+ "".join(f" - {path}\n" for path in _STEAM_ROOT_CANDIDATES)

Copilot uses AI. Check for mistakes.
@S1M0N38
Copy link
Copy Markdown
Collaborator

S1M0N38 commented Mar 30, 2026

I like the effort you are putting in this PR (e.g. new tests for the platform, dev.sh, bot.py example, ...) but at the end I'd like to only merge the file src/balatrobot/platforms/linux.py. (which should follow the other platform implementations).

I don't know if there is difference of the Linux + Proton (Desktop) and Proton (on steamdeck). So maybe it make most sense to treat SteamDeck as another platform (e.g. scr/balatrobot/platforms/steamdeck.py). If you are able to get this mod working on just steamdeck im happy to merge it.

I'll take a closer look at this PR when you marking as "Ready for Review".

@charlesmerritt charlesmerritt force-pushed the issue-128-cli-support-linux-proton branch from 0024ac5 to ca55228 Compare March 30, 2026 22:04
@charlesmerritt charlesmerritt changed the title Issue 128 cli support linux proton (draft) feat: add cli support linux proton issue #128 (draft) Mar 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants