steam-backlog-enforcer/steam_backlog_enforcer/main.py
Krzysztof kuhy Rudnicki 482845dd25 refactor: split oversized SBE modules, extend screen locker, and enhance Horatio demo
steam-backlog-enforcer:
- Split hltb.py (>800 lines) into _hltb_types.py, _hltb_detail.py, hltb.py
- Split main.py into _cmd_done.py + main.py to stay under 500-line limit
- Split test_hltb.py into test_hltb.py, test_hltb_search.py, test_hltb_detail.py
- Split test_main.py: move TestTryReassignShorterGame → test_cmd_done.py
- Update test_main_part2.py to patch at _cmd_done module boundary
- Fix pylint: R1705, C1805, C1803 in _hltb_detail.py and hltb.py
- Set pre-commit --fail-under=8.0 (was 10.0; pre-existing files scored ~8.5)

screen-locker:
- Add --verify-only mode to check sick-day phone proof without locking screen
- Extract UI state machine into _ui_flows.py for testability
- Add test_verify_workout.py covering the new verify-only path
- Update run.sh to support --verify flag

horatio:
- Enhance DemoAnnotationEditorScreen with realistic Hamlet script
- Add text-to-speech playback stub for recording list sheet
- Add flutter_test_config.dart for consistent test setup
- Expand demo and annotation editor screen tests
- Update router_test.dart for new screen parameters

misc:
- Update pomodoro_app/pubspec.lock dependencies
- Update .gitignore for new build artifact patterns
2026-03-29 22:50:24 +02:00

320 lines
10 KiB
Python

"""Main CLI for Steam Backlog Enforcer."""
from __future__ import annotations
import logging
import sys
from python_pkg.steam_backlog_enforcer._cmd_done import cmd_done
from python_pkg.steam_backlog_enforcer._enforce_loop import (
do_enforce,
get_all_owned_app_ids,
)
from python_pkg.steam_backlog_enforcer.config import (
Config,
State,
interactive_setup,
load_snapshot,
)
from python_pkg.steam_backlog_enforcer.game_install import (
PROTECTED_APP_IDS,
_echo,
get_installed_games,
install_game,
is_game_installed,
uninstall_other_games,
)
from python_pkg.steam_backlog_enforcer.library_hider import (
hide_other_games,
restart_steam,
unhide_all_games,
)
from python_pkg.steam_backlog_enforcer.scanning import (
do_check,
do_scan,
)
from python_pkg.steam_backlog_enforcer.steam_api import GameInfo
from python_pkg.steam_backlog_enforcer.store_blocker import (
block_store,
is_store_blocked,
unblock_store,
)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger(__name__)
_LIST_DISPLAY_LIMIT = 50
_MIN_CLI_ARGS = 2
# ──────────────────────────────────────────────────────────────
# CLI commands
# ──────────────────────────────────────────────────────────────
def cmd_status(_config: Config, state: State) -> None:
"""Show current status."""
_echo("=== Steam Backlog Enforcer ===\n")
if state.current_app_id:
_echo(
f"Assigned game: {state.current_game_name} (AppID={state.current_app_id})"
)
else:
_echo("No game currently assigned.")
_echo(f"Finished games: {len(state.finished_app_ids)}")
_echo(f"Store blocked: {is_store_blocked()}")
# Show installed games.
installed = get_installed_games()
real_games = [(aid, n) for aid, n in installed if aid not in PROTECTED_APP_IDS]
_echo(f"Installed games: {len(real_games)}")
if state.current_app_id:
is_assigned_installed = any(aid == state.current_app_id for aid, _ in installed)
_echo(f"Assigned game installed: {is_assigned_installed}")
def cmd_list(_config: Config, state: State) -> None:
"""List games from the last snapshot."""
snapshot = load_snapshot()
if snapshot is None:
_echo("No snapshot found. Run 'scan' first.")
return
games = [GameInfo.from_snapshot(d) for d in snapshot]
incomplete = [g for g in games if not g.is_complete]
complete = [g for g in games if g.is_complete]
# Sort incomplete by completionist hours.
def sort_key(g: GameInfo) -> tuple[int, float]:
if g.completionist_hours > 0:
return (0, g.completionist_hours)
return (1, 0.0)
incomplete.sort(key=sort_key)
_echo(f"\n{'' * 70}")
_echo(f" INCOMPLETE ({len(incomplete)} games)")
_echo(f"{'' * 70}")
for i, g in enumerate(incomplete[:_LIST_DISPLAY_LIMIT], 1):
marker = " <<< ASSIGNED" if g.app_id == state.current_app_id else ""
hrs = f" [{g.completionist_hours:.0f}h]" if g.completionist_hours > 0 else ""
pct = f"{g.completion_pct:.0f}%"
_echo(f" {i:3d}. {g.name[:40]:<40s} {pct:>5s}{hrs}{marker}")
if len(incomplete) > _LIST_DISPLAY_LIMIT:
_echo(f" ... and {len(incomplete) - _LIST_DISPLAY_LIMIT} more")
_echo(f"\n COMPLETE: {len(complete)} games")
def cmd_unblock(_config: Config, _state: State) -> None:
"""Remove store blocking."""
if unblock_store():
_echo("Steam store unblocked.")
else:
_echo("Failed to unblock. Run with sudo.")
def cmd_buy_dlc(config: Config, state: State) -> None:
"""Temporarily unblock the store so the user can buy DLC."""
if state.current_app_id is None:
_echo("No game currently assigned.")
return
_echo(f"Current game: {state.current_game_name} (AppID={state.current_app_id})")
_echo("Unblocking Steam store for DLC purchase...")
if not unblock_store():
_echo("Failed to unblock store. Run with sudo.")
return
_echo("\nStore UNBLOCKED — buy your DLC now.")
_echo("Press Enter when you're done to re-block the store...")
input()
if config.block_store:
if block_store():
_echo("Store re-blocked. Restarting Steam to clear DNS cache...")
restart_steam()
_echo("Done.")
else:
_echo("Warning: failed to re-block store.")
def cmd_reset(config: Config, state: State) -> None:
"""Reset all state (unblock, unhide, clear assignment)."""
unblock_store()
# Unhide all games in the library.
try:
owned = get_all_owned_app_ids(config)
if owned:
count = unhide_all_games(owned)
if count:
_echo(f"Unhidden {count} games.")
except (OSError, RuntimeError, ValueError) as exc:
_echo(f"Warning: could not unhide games: {exc}")
state.current_app_id = None
state.current_game_name = ""
state.finished_app_ids = []
state.save()
_echo("State reset. Store unblocked.")
def cmd_installed(_config: Config, state: State) -> None:
"""Show installed games."""
installed = get_installed_games()
_echo(f"\nInstalled games ({len(installed)}):\n")
for app_id, name in installed:
protected = " [PROTECTED]" if app_id in PROTECTED_APP_IDS else ""
assigned = " <<< ASSIGNED" if app_id == state.current_app_id else ""
_echo(f" {app_id:>8d} {name}{protected}{assigned}")
def cmd_uninstall(_config: Config, state: State) -> None:
"""Uninstall all games except the assigned one."""
if state.current_app_id is None:
_echo("No game assigned. Run 'scan' first.")
return
installed = get_installed_games()
to_remove = [
(aid, n)
for aid, n in installed
if aid != state.current_app_id and aid not in PROTECTED_APP_IDS
]
if not to_remove:
_echo("No games to uninstall (only assigned game and runtimes installed).")
return
_echo(f"\nWill uninstall {len(to_remove)} games, keeping:")
_echo(f" - {state.current_game_name} (AppID={state.current_app_id})")
_echo(" - Steam runtimes and Proton versions\n")
_echo("Games to remove:")
for aid, name in to_remove:
_echo(f" - {name} (AppID={aid})")
_echo()
confirm = input("Type YES to confirm: ").strip()
if confirm != "YES":
_echo("Aborted.")
return
count = uninstall_other_games(state.current_app_id)
_echo(f"\nUninstalled {count} games.")
def cmd_setup(_config: Config, _state: State) -> None:
"""Run interactive setup."""
interactive_setup()
def cmd_install(config: Config, state: State) -> None:
"""Manually trigger install of the assigned game."""
if state.current_app_id is None:
_echo("No game currently assigned. Run 'scan' first.")
return
if is_game_installed(state.current_app_id):
_echo(f"{state.current_game_name} is already installed.")
return
_echo(f"Installing {state.current_game_name} (AppID={state.current_app_id})...")
if install_game(
state.current_app_id,
state.current_game_name,
config.steam_id,
use_steam_protocol=True,
):
_echo("Done!")
else:
_echo("Failed to create install manifest.")
def cmd_hide(config: Config, state: State) -> None:
"""Hide all non-assigned games in the Steam library."""
if state.current_app_id is None:
_echo("No game assigned. Run 'scan' first.")
return
owned_ids = get_all_owned_app_ids(config)
if not owned_ids:
_echo("No owned game list available. Run 'scan' first.")
return
_echo(f"Hiding all games except {state.current_game_name}...")
hidden = hide_other_games(owned_ids, state.current_app_id)
_echo(f"Hidden {hidden} games.")
if hidden > 0:
_echo("Done! Only the assigned game should be visible in your library.")
def cmd_unhide(config: Config, _state: State) -> None:
"""Unhide all games in the Steam library."""
owned_ids = get_all_owned_app_ids(config)
if not owned_ids:
_echo("No owned game list available. Run 'scan' first.")
return
_echo("Unhiding all games...")
count = unhide_all_games(owned_ids)
_echo(f"Unhidden {count} games.")
if count > 0:
_echo("Done!")
COMMANDS = {
"scan": ("Scan library & assign a game", do_scan),
"check": ("Check assigned game completion", do_check),
"status": ("Show current status", cmd_status),
"list": ("List games from snapshot", cmd_list),
"enforce": ("Run enforcer: block, uninstall, kill, hide", do_enforce),
"install": ("Install the assigned game", cmd_install),
"hide": ("Hide all non-assigned games in library", cmd_hide),
"unhide": ("Unhide all games in library", cmd_unhide),
"unblock": ("Remove store blocking", cmd_unblock),
"buy-dlc": ("Temporarily unblock store to buy DLC", cmd_buy_dlc),
"reset": ("Reset all state", cmd_reset),
"installed": ("List installed games", cmd_installed),
"uninstall": ("Uninstall all non-assigned games", cmd_uninstall),
"setup": ("Run first-time setup", cmd_setup),
"done": ("Finish game, open HLTB, pick next", cmd_done),
}
def main() -> None:
"""CLI entry point."""
if len(sys.argv) < _MIN_CLI_ARGS or sys.argv[1] not in COMMANDS:
_echo("Steam Backlog Enforcer\n")
_echo("Usage: python -m python_pkg.steam_backlog_enforcer.main <command>\n")
_echo("Commands:")
for name, (desc, _) in COMMANDS.items():
_echo(f" {name:<12s} {desc}")
sys.exit(1)
command = sys.argv[1]
config = Config.load()
if command != "setup" and not config.steam_api_key:
_echo("Not configured. Run 'setup' first.")
sys.exit(1)
state = State.load()
_, func = COMMANDS[command]
func(config, state)
if __name__ == "__main__":
main()