diff --git a/src/beaky/cli.py b/src/beaky/cli.py index 5355877..ca1439c 100644 --- a/src/beaky/cli.py +++ b/src/beaky/cli.py @@ -28,17 +28,18 @@ def _verdict_str(verdict: TicketVerdict) -> str: _FC = 14 # field column visual width -_VC = 24 # value column visual width -_BET_W = 1 + (_FC + 2) + 1 + (_VC + 2) + 1 + (_VC + 2) + 1 # one bet table width -_GAP = " " # space between side-by-side tables +_VC = 24 # value column visual width (dual) +_SC = 38 # value column visual width (single classifier) +_BET_W = 1 + (_FC + 2) + 1 + (_VC + 2) + 1 + (_VC + 2) + 1 # dual table width +_BET_WS = 1 + (_FC + 2) + 1 + (_SC + 2) + 1 # single table width +_GAP = " " -_FIELD_LABELS: dict[str, str] = { - "team1Name": "team1", - "team2Name": "team2", -} -_FIELD_ORDER = ["type", "team1Name", "team2Name", "date", "league"] -_SKIP_FIELDS = {"ticketType"} -_BLANK_ROW = f"│{' ' * (_FC + 2)}│{' ' * (_VC + 2)}│{' ' * (_VC + 2)}│" +_FIELD_LABELS: dict[str, str] = {"team1Name": "team1", "team2Name": "team2"} +_FIELD_ORDER = ["type", "team1Name", "team2Name", "date", "league"] +_SKIP_FIELDS = {"ticketType"} + +_BLANK_ROW = f"│{' ' * (_FC + 2)}│{' ' * (_VC + 2)}│{' ' * (_VC + 2)}│" +_BLANK_ROWS = f"│{' ' * (_FC + 2)}│{' ' * (_SC + 2)}│" def _vlen(text: str) -> int: @@ -58,9 +59,10 @@ def _bet_fields(bet: Bet) -> dict[str, str]: return fields +# ── dual-column table (compare) ────────────────────────────────────────────── + def _tbl_row(field: str, lval: str, ival: str) -> str: - label = _FIELD_LABELS.get(field, field) - return f"│ {_vpad(label, _FC)} │ {_vpad(lval, _VC)} │ {_vpad(ival, _VC)} │" + return f"│ {_vpad(_FIELD_LABELS.get(field, field), _FC)} │ {_vpad(lval, _VC)} │ {_vpad(ival, _VC)} │" def _tbl_sep(left: str, mid: str, right: str) -> str: @@ -77,60 +79,85 @@ def _bet_to_lines(idx: int, link_bet: Bet | None, img_bet: Bet | None) -> list[s for key in keys: lval_raw = link_fields.get(key, "") ival_raw = img_fields.get(key, "") - match = lval_raw == ival_raw - both = bool(lval_raw) and bool(ival_raw) - lval_raw = lval_raw[:_VC - 1] + "…" if len(lval_raw) > _VC else lval_raw - ival_raw = ival_raw[:_VC - 1] + "…" if len(ival_raw) > _VC else ival_raw + match = lval_raw == ival_raw + both = bool(lval_raw) and bool(ival_raw) + lval_raw = lval_raw[:_VC - 1] + "…" if len(lval_raw) > _VC else lval_raw + ival_raw = ival_raw[:_VC - 1] + "…" if len(ival_raw) > _VC else ival_raw lval = _ansi.gray("—") if not lval_raw else (lval_raw if (match or not both) else _ansi.yellow(lval_raw)) ival = _ansi.gray("—") if not ival_raw else (ival_raw if (match or not both) else _ansi.yellow(ival_raw)) data_rows.append(_tbl_row(key, lval, ival)) header = _vpad(_ansi.bold(_ansi.cyan(f" Bet {idx} ")), _BET_W) - return [ - header, - _tbl_sep("┌", "┬", "┐"), - _tbl_row("", _ansi.bold("link classifier"), _ansi.bold("image classifier")), - _tbl_sep("├", "┼", "┤"), - *data_rows, - _tbl_sep("└", "┴", "┘"), + return [header, _tbl_sep("┌", "┬", "┐"), _tbl_row("", _ansi.bold("link"), _ansi.bold("image")), + _tbl_sep("├", "┼", "┤"), *data_rows, _tbl_sep("└", "┴", "┘")] + + +# ── single-column table (one classifier) ───────────────────────────────────── + +def _tbl_row_s(field: str, val: str) -> str: + return f"│ {_vpad(_FIELD_LABELS.get(field, field), _FC)} │ {_vpad(val, _SC)} │" + + +def _tbl_sep_s(left: str, mid: str, right: str) -> str: + return f"{left}{'─' * (_FC + 2)}{mid}{'─' * (_SC + 2)}{right}" + + +def _bet_to_lines_single(idx: int, bet: Bet, col_label: str) -> list[str]: + fields = _bet_fields(bet) + keys = [k for k in _FIELD_ORDER if k in fields] + [k for k in fields if k not in _FIELD_ORDER] + data_rows = [ + _tbl_row_s(k, (v[:_SC - 1] + "…" if len(v) > _SC else v)) + for k, v in ((k, fields[k]) for k in keys) ] + header = _vpad(_ansi.bold(_ansi.cyan(f" Bet {idx} ")), _BET_WS) + return [header, _tbl_sep_s("┌", "┬", "┐"), _tbl_row_s("", _ansi.bold(col_label)), + _tbl_sep_s("├", "┼", "┤"), *data_rows, _tbl_sep_s("└", "┴", "┘")] -def _pad_to(lines: list[str], target: int) -> list[str]: +# ── shared grid printer ─────────────────────────────────────────────────────── + +def _pad_to(lines: list[str], target: int, blank: str) -> list[str]: result = list(lines) while len(result) < target: - result.insert(-1, _BLANK_ROW) + result.insert(-1, blank) return result -def _print_compare(link_ticket: Ticket, img_ticket: Ticket) -> None: - n_link = len(link_ticket.bets) - n_img = len(img_ticket.bets) - n_max = max(n_link, n_img) - +def _print_bet_grid(ticket_header: str, all_lines: list[list[str]], blank: str, bet_w: int) -> None: term_w = shutil.get_terminal_size((120, 24)).columns - n_cols = max(1, term_w // (_BET_W + len(_GAP))) - row_w = min(term_w, n_cols * (_BET_W + len(_GAP)) - len(_GAP) + 2) - - header = f" Ticket {link_ticket.id} — link: {n_link} bet{'s' if n_link != 1 else ''} │ img: {n_img} bet{'s' if n_img != 1 else ''}" + n_cols = max(1, term_w // (bet_w + len(_GAP))) + row_w = min(term_w, n_cols * (bet_w + len(_GAP)) - len(_GAP) + 2) print(f"\n{'═' * row_w}") - print(_ansi.bold(header)) + print(_ansi.bold(f" {ticket_header}")) print(f"{'═' * row_w}") - - all_lines = [ - _bet_to_lines(i + 1, link_ticket.bets[i] if i < n_link else None, img_ticket.bets[i] if i < n_img else None) - for i in range(n_max) - ] - - for start in range(0, n_max, n_cols): - chunk = all_lines[start:start + n_cols] - max_h = max(len(b) for b in chunk) - padded = [_pad_to(b, max_h) for b in chunk] + for start in range(0, len(all_lines), n_cols): + chunk = all_lines[start:start + n_cols] + max_h = max(len(b) for b in chunk) + padded = [_pad_to(b, max_h, blank) for b in chunk] print() for row in zip(*padded): print(" " + _GAP.join(row)) +# ── public print functions ──────────────────────────────────────────────────── + +def _print_compare(link_ticket: Ticket, img_ticket: Ticket) -> None: + n_link, n_img = len(link_ticket.bets), len(img_ticket.bets) + header = f"Ticket {link_ticket.id} — link: {n_link} bet{'s' if n_link != 1 else ''} │ img: {n_img} bet{'s' if n_img != 1 else ''}" + all_lines = [ + _bet_to_lines(i + 1, link_ticket.bets[i] if i < n_link else None, img_ticket.bets[i] if i < n_img else None) + for i in range(max(n_link, n_img)) + ] + _print_bet_grid(header, all_lines, _BLANK_ROW, _BET_W) + + +def _print_single(ticket: Ticket, col_label: str) -> None: + n = len(ticket.bets) + header = f"Ticket {ticket.id} — {col_label} │ {n} bet{'s' if n != 1 else ''}" + all_lines = [_bet_to_lines_single(i + 1, ticket.bets[i], col_label) for i in range(n)] + _print_bet_grid(header, all_lines, _BLANK_ROWS, _BET_WS) + + def load_config(path: str) -> Config | None: with open(path) as f: config_dict = yaml.safe_load(f) @@ -145,7 +172,9 @@ def main() -> None: parser = argparse.ArgumentParser(prog="beaky") parser.add_argument("--config", help="Path to config file.", default="config/application.yml") parser.add_argument("--id", type=int, help="Select a single ticket by id.") - parser.add_argument("mode", choices=["screen", "parse", "class", "resolve", "compare"], help="Mode of operation.") + parser.add_argument("mode", choices=["screen", "parse", "compare", "resolve"], help="Mode of operation.") + parser.add_argument("--classifier", choices=["link", "img", "both"], default="both", + help="Which classifier to use in compare mode (default: both).") args = parser.parse_args() config = load_config(args.config) @@ -179,24 +208,19 @@ def main() -> None: for link in selected_links: print(link) - if args.mode == "class": - classifier = LinkClassifier() - results = [] - for link in selected_links: - results.append(classifier.classify(link)) - ticket = results[-1] - print(f"\n=== Link {ticket.id} ({len(ticket.bets)} bets) ===") - for bet in ticket.bets: - print(f" [{type(bet).__name__}]") - for k, v in vars(bet).items(): - print(f" {k}: {v}") - if args.mode == "compare": - linkclassifier = LinkClassifier() + use_link = args.classifier in ("link", "both") + use_img = args.classifier in ("img", "both") + linkclassifier = LinkClassifier() if use_link else None for link in selected_links: - link_ticket = linkclassifier.classify(link) - img_ticket = img_classify([f"./data/screenshots/{link.id}.png"], ticket_id=link.id) - _print_compare(link_ticket, img_ticket) + link_ticket = linkclassifier.classify(link) if use_link else None + img_ticket = img_classify([f"./data/screenshots/{link.id}.png"], ticket_id=link.id) if use_img else None + if args.classifier == "both" and link_ticket and img_ticket: + _print_compare(link_ticket, img_ticket) + elif link_ticket: + _print_single(link_ticket, "link classifier") + elif img_ticket: + _print_single(img_ticket, "image classifier") if args.mode == "resolve": classifier = LinkClassifier()