203 lines
5.6 KiB
Python
203 lines
5.6 KiB
Python
# ui.py
|
||
from contextlib import asynccontextmanager
|
||
from contextlib import contextmanager
|
||
from typing import Iterable, Mapping
|
||
|
||
from rich.live import Live
|
||
from rich.console import Console
|
||
from rich.theme import Theme
|
||
from rich.panel import Panel
|
||
from rich.table import Table
|
||
from rich.text import Text
|
||
from rich.progress import (
|
||
Progress,
|
||
SpinnerColumn,
|
||
BarColumn,
|
||
TextColumn,
|
||
TimeElapsedColumn,
|
||
TimeRemainingColumn,
|
||
)
|
||
from rich.markdown import Markdown
|
||
from rich.traceback import install
|
||
from rich.logging import RichHandler
|
||
import logging
|
||
import json
|
||
|
||
# ─────────────────────────────────────────────────────────────
|
||
# Tema global (cores nomeadas para facilitar padronização)
|
||
# ─────────────────────────────────────────────────────────────
|
||
theme = Theme(
|
||
{
|
||
"accent": "bright_magenta",
|
||
"info": "cyan",
|
||
"success": "green",
|
||
"warning": "yellow",
|
||
"error": "bold red",
|
||
"muted": "grey62",
|
||
}
|
||
)
|
||
|
||
# Console único para todo o sistema
|
||
console = Console(theme=theme)
|
||
install(show_locals=False) # Tracebacks bonitos
|
||
|
||
|
||
# ─────────────────────────
|
||
# BÁSICOS
|
||
# ─────────────────────────
|
||
def banner(titulo: str, subtitulo: str | None = None):
|
||
text = Text(titulo, style="bold accent")
|
||
if subtitulo:
|
||
text.append(f"\n{subtitulo}", style="muted")
|
||
console.print(Panel.fit(text, border_style="accent", padding=(1, 2)))
|
||
|
||
|
||
def rule(texto: str = ""):
|
||
console.rule(Text(texto, style="accent"))
|
||
|
||
|
||
def info(msg: str):
|
||
console.print(f"ℹ️ [info]{msg}[/]")
|
||
|
||
|
||
def ok(msg: str):
|
||
console.print(f"✅ [success]{msg}[/]")
|
||
|
||
|
||
def warn(msg: str):
|
||
console.print(f"⚠️ [warning]{msg}[/]")
|
||
|
||
|
||
def fail(msg: str):
|
||
console.print(f"❌ [error]{msg}[/]")
|
||
|
||
|
||
def md(markdown: str):
|
||
console.print(Markdown(markdown))
|
||
|
||
|
||
def json_pretty(data):
|
||
try:
|
||
console.print_json(data=json.loads(data) if isinstance(data, str) else data)
|
||
except Exception:
|
||
console.print(data)
|
||
|
||
|
||
# ─────────────────────────
|
||
# TABELA RÁPIDA
|
||
# ─────────────────────────
|
||
def table(rows: Iterable[Mapping], title: str | None = None):
|
||
rows = list(rows)
|
||
if not rows:
|
||
warn("Tabela vazia.")
|
||
return
|
||
|
||
t = Table(title=title, title_style="accent")
|
||
|
||
for col in rows[0].keys():
|
||
t.add_column(str(col), style="muted")
|
||
|
||
for r in rows:
|
||
t.add_row(*[str(r[k]) for k in rows[0].keys()])
|
||
|
||
console.print(t)
|
||
|
||
|
||
# ─────────────────────────
|
||
# STATUS / SPINNER
|
||
# ─────────────────────────
|
||
@contextmanager
|
||
def status(msg: str):
|
||
with console.status(f"[info]{msg}[/]"):
|
||
yield
|
||
|
||
|
||
# ─────────────────────────
|
||
# BARRA DE PROGRESSO
|
||
# ─────────────────────────
|
||
@contextmanager
|
||
def progress(task_desc: str, total: int | None = None):
|
||
with Progress(
|
||
SpinnerColumn(),
|
||
TextColumn("[accent]{task.description}"),
|
||
BarColumn(),
|
||
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
||
TimeElapsedColumn(),
|
||
TimeRemainingColumn(),
|
||
transient=True,
|
||
console=console,
|
||
) as p:
|
||
|
||
task_id = p.add_task(task_desc, total=total)
|
||
yield lambda advance=1: p.update(task_id, advance=advance)
|
||
|
||
|
||
# ─────────────────────────
|
||
# LOGGING RICH
|
||
# ─────────────────────────
|
||
def setup_logging(level: int = logging.INFO):
|
||
logging.basicConfig(
|
||
level=level,
|
||
format="%(message)s",
|
||
handlers=[RichHandler(rich_tracebacks=True, console=console)],
|
||
)
|
||
|
||
|
||
# ─────────────────────────
|
||
# FIREBIRD / DB HELPERS
|
||
# ─────────────────────────
|
||
def db_info(msg: str):
|
||
console.print(f"🗄️ [info][DB INFO][/info] {msg}")
|
||
|
||
|
||
def db_ok(msg: str):
|
||
console.print(f"🗄️ [success][DB OK][/success] {msg}")
|
||
|
||
|
||
def db_fail(msg: str):
|
||
console.print(f"🗄️ [error][DB ERROR][/error] {msg}")
|
||
|
||
|
||
def db_warning(msg: str):
|
||
console.print(f"🗄️ [warning][DB WARNING][/warning] {msg}")
|
||
|
||
|
||
def db_dsn(dsn: str):
|
||
# Mascara senha com segurança
|
||
try:
|
||
before_at = dsn.split("@")[0]
|
||
user = before_at.split("//")[1].split(":")[0]
|
||
masked = dsn.replace(user, "******")
|
||
except Exception:
|
||
masked = dsn # fallback
|
||
|
||
console.print(
|
||
Panel(
|
||
Text(f"[accent]DSN de conexão[/accent]\n{masked}", style="muted"),
|
||
border_style="accent",
|
||
)
|
||
)
|
||
|
||
|
||
@asynccontextmanager
|
||
async def progress_manager():
|
||
|
||
# Criar progress com o console compartilhado
|
||
progress = Progress(
|
||
SpinnerColumn(),
|
||
TextColumn("[progress.description]{task.description}"),
|
||
BarColumn(),
|
||
TextColumn("{task.completed}/{task.total}"),
|
||
TimeElapsedColumn(),
|
||
expand=True,
|
||
console=console, # fundamental!
|
||
)
|
||
|
||
# Live controla a tela inteira
|
||
live = Live(progress, refresh_per_second=10, console=console, transient=False)
|
||
live.start()
|
||
|
||
try:
|
||
yield progress
|
||
finally:
|
||
live.stop()
|