"""General tab: usage balances, theme picker, redeem entrypoint. Usage parsing moved here from invite.py (single copy). All flows are ws-level: sessions and error shaping live in toggles.py. """ import re from approvals import cdp_evaluate from hatch_menu import controls, dialog TAB = "General" THEME_VALUES = ("match", "default", "blue", "purple", "pink", "orange", "green", "beige", "monochrome") _FREE_RE = re.compile(r"\bfree plan\b", re.IGNORECASE) _PCT_RE = re.compile(r"(\d+)%\s*used") _RESET_RE = re.compile(r"resets?\s+on\s+([A-Z][a-z]+\s+\d{1,2})", re.IGNORECASE) _TOK_RE = re.compile(r"\(([0-9.,]+\s*[BMK]?)\s*tokens?\s+left\)", re.IGNORECASE) def parse_usage_text(text): """Parse a General-tab usage block into a balance dict. Returns None for empty/unreadable text. Weekly fields stay None when the block only carries additional-tokens rows. """ if not text or not text.strip(): return None out = {"plan": None, "weekly": {"pct_used": None, "resets_on": None}, "additional": {"pct_used": None, "tokens_left": None, "never_expires": False}} if _FREE_RE.search(text): out["plan"] = "free" pcts = _PCT_RE.findall(text) if pcts: out["weekly"]["pct_used"] = int(pcts[0]) if len(pcts) > 1: out["additional"]["pct_used"] = int(pcts[1]) m = _RESET_RE.search(text) if m: out["weekly"]["resets_on"] = m.group(1) m = _TOK_RE.search(text) if m: out["additional"]["tokens_left"] = m.group(1).strip() if "never expires" in text.lower(): out["additional"]["never_expires"] = True return out def _eval(ws, js, timeout=8.0): try: return cdp_evaluate(ws, js, timeout=timeout) except Exception: return None def usage(ws): """Usage balances from General tab. Dict, or None when unreadable.""" if not dialog.goto_tab(ws, TAB): return None return parse_usage_text(dialog.dialog_text(ws)) def theme(ws): """Current theme radio value, or None when unreadable.""" if not dialog.goto_tab(ws, TAB): return None for r in controls.list_radios(ws): if r.get("checked"): return r.get("value") return None def set_theme(ws, value): """Set theme by aria-label; verify-then-fallback. Bool.""" if not dialog.goto_tab(ws, TAB): return False return controls.set_radio_by_aria(ws, value) def redeem_row(ws): """True when the 'Redeem invite code' row is present (eligible).""" if not dialog.goto_tab(ws, TAB): return False text = (dialog.dialog_text(ws) or "").lower() return "redeem" in text and "invite code" in text def describe(ws): """Full General inventory: usage, theme, redeem entrypoint.""" return {"usage": usage(ws), "theme": theme(ws), "redeem_row": redeem_row(ws)}