nte-exporter/src/nte_history_exporter/console.py
Golumpa 20b2086299 Anchor scans to page 1; export stable prefixes
Anchor page-run selection to start at page 1 (select_continuous_run_from_page_1) instead of picking the longest run, so newest pages are preserved and the newest timestamp ordinal 0 is always captured. Change boundary logic to always assign stable UIDs and export the captured prefix of an oldest timestamp group even when it may be an unfinished 10-pull, emitting informational warnings (INCOMPLETE_TIMESTAMP_GROUP_EXPORTED / INCOMPLETE_ARC_10_PULL_EXPORTED) instead of dropping rows. Apply the same policy to Arc groups and update arc group annotation and warnings accordingly. Add a new console module for improved CLI output and update cli, live_capture, adapters, and session code to use the new run selection and console helpers. Update docs, mappings, and tests to reflect the new boundary/export behavior and messaging.
2026-06-11 12:22:34 +01:00

155 lines
4.6 KiB
Python

from __future__ import annotations
import os
import sys
from nte_history_exporter.constants import EXPORTER_VERSION, GAME_NAME
WIDTH = 58
RESET = "\x1b[0m"
BOLD = "\x1b[1m"
DIM = "\x1b[2m"
CYAN = "\x1b[36m"
GREEN = "\x1b[32m"
YELLOW = "\x1b[33m"
_ansi: bool | None = None
def _enable_ansi() -> bool:
if not hasattr(sys.stdout, "isatty") or not sys.stdout.isatty():
return False
if os.name != "nt":
return True
try:
import ctypes
kernel32 = ctypes.windll.kernel32
handle = kernel32.GetStdHandle(-11)
mode = ctypes.c_uint32()
if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
return False
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
return bool(kernel32.SetConsoleMode(handle, mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING))
except Exception:
return False
def ansi_enabled() -> bool:
global _ansi
if _ansi is None:
_ansi = _enable_ansi()
return _ansi
def style(text: str, *codes: str) -> str:
if not codes or not ansi_enabled():
return text
return "".join(codes) + text + RESET
def rule(char: str = "-") -> str:
return style(char * WIDTH, DIM)
def print_banner() -> None:
print()
print(rule("="))
print(style(f" NTE History Exporter v{EXPORTER_VERSION}", BOLD, CYAN))
print(style(f" {GAME_NAME} pull history -> tracker JSON", DIM))
print(rule("="))
def print_live_instructions(local_ip: str) -> None:
print()
print(style(" Listening on ", DIM) + style(local_ip, BOLD))
print()
print(style(" How to export your pull history", BOLD))
print(" 1. This tool must be running BEFORE you press Start on")
print(" the game's main menu, or the game connection cannot")
print(" be captured. Already in game? Log out to the main")
print(" menu and enter again.")
print(" 2. Open a supported history screen:")
print(style(" Monopoly > Standard Board history", CYAN))
print(style(" Monopoly > Limited Character Board history", CYAN))
print(style(" Gashapon > Arc Miracle Box history", CYAN))
print(" 3. Start at page 1 and scroll down through every page")
print(" you want exported.")
print(" 4. Scroll one page past where you plan to stop so the")
print(" last pull group can be confirmed as complete.")
print(" 5. You can open several boards in the same session.")
print()
print(style(" Waiting for history pages... press any key here when done.", BOLD, GREEN))
print(rule())
def print_page_captured(label: str, page: int | None) -> None:
print(style(" + ", GREEN, BOLD) + label + style(f" page {page}", DIM))
def print_results_header() -> None:
print()
print(rule())
print(style(" Results", BOLD))
print(rule())
def print_export_summary(name: str, decoded: int, exported: int, skipped: int) -> None:
counts = [
style(f"decoded {decoded}", DIM),
style(f"exported {exported}", GREEN, BOLD),
style(f"skipped {skipped}", YELLOW if skipped else DIM),
]
print(f" {name:<30}" + " ".join(counts))
def print_warning(code: str, reason: str, records: int | None = None) -> None:
suffix = f" ({records} records)" if records is not None else ""
print(style(f" ! {code}: {reason}{suffix}", YELLOW))
def print_note(text: str) -> None:
print(style(f" {text}", DIM))
def print_success(text: str) -> None:
print(style(f" {text}", GREEN, BOLD))
def print_problem(text: str) -> None:
print(style(f" {text}", YELLOW, BOLD))
def print_boxed(lines: list[str], *codes: str) -> None:
inner = WIDTH - 4
border = " +" + "-" * (WIDTH - 2) + "+"
print(style(border, *codes))
for line in lines:
print(style(" | " + line.ljust(inner) + " |", *codes))
print(style(border, *codes))
# Warning codes for an oldest timestamp group that was exported despite possibly
# being an unfinished 10-pull. The exported rows are stable, so the warning is
# informational and the user can ignore it once the rest is captured.
INCOMPLETE_EXPORT_CODES = {
"INCOMPLETE_TIMESTAMP_GROUP_EXPORTED",
"INCOMPLETE_ARC_10_PULL_EXPORTED",
}
def maybe_print_incomplete_hint(warnings: list[dict]) -> None:
if not any(w.get("code") in INCOMPLETE_EXPORT_CODES for w in warnings):
return
print()
print_boxed(
[
"The INCOMPLETE_..._EXPORTED warning above is safe",
"to ignore if the partial 10-pull it captured is",
"already in your tracker. Otherwise restart",
"the capture and go to a further page",
],
GREEN,
BOLD,
)