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.
This commit is contained in:
parent
0a1a19939b
commit
20b2086299
14 changed files with 402 additions and 200 deletions
155
src/nte_history_exporter/console.py
Normal file
155
src/nte_history_exporter/console.py
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
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,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue