96 lines
2.9 KiB
Python
96 lines
2.9 KiB
Python
|
|
"""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)}
|