steam-backlog-enforcer/steam_backlog_enforcer/main/abandon.py
Krzysztof kuhy Rudnicki 1bdf360193 pick keeps already-picked games; fast install reports progress
pick and pick-manual no longer uninstall games that are already allowed:
the outgoing assignment is kept as a released pick (installed, no lock)
until 'pick --replace', done/check, or abandon-pick. The allowed set is
capped at max_manual_picks + 1; a pick over it is refused before the
candidate list / confirmation, naming the games. status and the web
Picks card list kept games with an Abandon button.

Fast install now tracks the installer log from the spawn offset: web
jobs drive the progress bar (per-depot %, phase) and echo a line every
30 s; daemon-started runs are exposed via ctl status / /api/daemon and
shown on the Dashboard.

Also: web deps bumped for dependency freshness (react-query 5.104.5,
typescript-eslint 8.72.0, vite 8.3.5, node 24.22.1 in CI).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CUaX4zAa6H4sMu3jd6RFdb
2026-10-23 20:14:12 +02:00

133 lines
4.5 KiB
Python

"""``abandon-pick``: back out of a manual pick."""
from __future__ import annotations
import sys
from typing import TYPE_CHECKING, Any
from steam_backlog_enforcer._actions import (
ABANDON_COOLDOWN_DAYS as _ABANDON_COOLDOWN_DAYS,
)
from steam_backlog_enforcer._actions import (
active_manual_picks,
)
from steam_backlog_enforcer._manual_pick_lifecycle import abandon_manual_pick
from steam_backlog_enforcer._prompter import confirm_phrase
from steam_backlog_enforcer.game_install import (
_echo,
is_game_installed,
uninstall_game,
)
if TYPE_CHECKING:
from steam_backlog_enforcer.config import Config, State
_ABANDON_PICK_USAGE = (
"Usage: abandon-pick <app_id>\n"
" app_id : the manually-picked game you want to back out of.\n\n"
"A pick can be abandoned at any time, however long ago it was made."
)
def _abandon_pick_target(state: State, args: list[str]) -> int | None:
"""Validate the abandon-pick argument against the active manual pick.
Requiring the explicit app_id (rather than defaulting to the current pick)
makes an accidental abandon impossible to trigger by muscle memory.
Args:
state: Current enforcer state.
args: Remaining CLI args (first element should be the app_id).
Returns:
The validated app id, or ``None`` if the input was unusable (a message
has already been printed in that case).
"""
if not args:
_echo(_ABANDON_PICK_USAGE)
return None
try:
app_id = int(args[0])
except ValueError:
_echo(f"Error: app_id must be a number, got '{args[0]}'.")
return None
# Every stored pick, not only active ones: a released pick, or a game
# 'pick' kept, is still installed and is what 'pick' points users at.
picks = state.manual_picks
if not picks:
_echo("No manual pick or kept game — nothing to abandon.")
return None
if _stored_pick(state, app_id) is None:
listed = ", ".join(f"{p['game_name']} (AppID={p['app_id']})" for p in picks)
_echo(
f"Error: AppID={app_id} is not one of your manual picks.\nYours: {listed}."
)
return None
return app_id
def _stored_pick(state: State, app_id: int) -> dict[str, Any] | None:
"""Return the stored pick for *app_id*, active, released or kept."""
return next((p for p in state.manual_picks if p.get("app_id") == app_id), None)
def cmd_abandon_pick(_config: Config, state: State, args: list[str]) -> None:
"""Back out of a manual pick, whatever its age.
Args:
_config: Enforcer configuration (unused, kept for dispatch symmetry).
state: Current enforcer state.
args: Remaining CLI args (first element should be the app_id).
"""
app_id = _abandon_pick_target(state, args)
if app_id is None:
sys.exit(1)
pick = _stored_pick(state, app_id)
game_name = str(pick["game_name"]) if pick else ""
others = [p for p in active_manual_picks(state) if p["app_id"] != app_id]
_echo(f"\nAbandoning manual pick: {game_name} (AppID={app_id})")
_echo(
f"\nThis will:"
f"\n - Drop this pick from your allowed games"
f"\n - Uninstall {game_name}"
f"\n - Keep it out of auto-assignment for"
f" {_ABANDON_COOLDOWN_DAYS} days"
)
if others:
kept = ", ".join(f"{p['game_name']} (AppID={p['app_id']})" for p in others)
_echo(f"\n Your other pick(s) stay locked in: {kept}")
elif state.current_app_id not in (None, app_id):
# A kept game is not the assignment, so abandoning it leaves one.
_echo(f"\n - {state.current_game_name} stays assigned")
else:
_echo("\n - Leaves you with no assigned game (run 'scan' to get one)")
_echo()
if not confirm_phrase(
"abandon-pick", f"Abandon manual pick {game_name}?", game_name=game_name
):
_echo("Aborted.")
return
abandon_manual_pick(state, app_id)
_echo(f"\nManual pick abandoned: {game_name}")
if is_game_installed(app_id):
_echo(f" Uninstalling {game_name}...")
if uninstall_game(app_id, game_name):
_echo(" Uninstalled.")
else:
_echo(" Warning: could not uninstall — remove it from Steam manually.")
if state.current_app_id is None:
_echo("\nNo game is assigned now. Run 'scan' to get a new assignment,")
_echo("or 'pick-manual <app_id>' to choose one yourself.\n")
else:
_echo(f"\nStill assigned: {state.current_game_name}\n")