File size: 3,310 Bytes
78d2329
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
"""Shared rich console for consistent, professional terminal output.

A single `CONSOLE` instance keeps styling uniform across the codebase. rich
degrades gracefully when stdout is not a tty (e.g. SLURM log files): no
animations, plain box-drawing characters — still fully readable.

Usage:
    from optgs.misc.console import CONSOLE, banner, rule, warn

    banner("optgs", ["host  galvani", "mode  test"])
    rule("Testing scene 3: room_0")
    warn("Skipping batch 7 due to OOM")
"""
from __future__ import annotations

import sys

from rich import box
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.theme import Theme

OPTGS_THEME = Theme(
    {
        "info": "cyan",
        "warning": "yellow",
        "error": "bold red",
        "success": "bold green",
        "metric": "bold magenta",
        "path": "blue underline",
        "muted": "dim",
    }
)

# Off-tty (SLURM logs, pipes) rich defaults to width 80, which wraps config
# rows awkwardly. Pin a wider, fixed width there; let interactive terminals
# auto-size so output adapts to the real window.
_console_kwargs: dict = {"theme": OPTGS_THEME}
if not sys.stdout.isatty():
    _console_kwargs["width"] = 120

CONSOLE = Console(**_console_kwargs)


def banner(title: str, lines: list[str] | None = None, style: str = "info") -> None:
    """Print a titled panel; `lines` form the panel body."""
    body = "\n".join(lines) if lines else ""
    CONSOLE.print(
        Panel(body, title=title, title_align="left", border_style=style, expand=False)
    )


def rule(title: str, style: str = "info") -> None:
    """Print a horizontal section divider with a centered title."""
    CONSOLE.rule(f"[{style}]{title}[/{style}]", style=style)


def warn(msg: str) -> None:
    """Print a styled warning line."""
    CONSOLE.print(f"[warning]⚠ {msg}[/warning]")


def error(msg: str) -> None:
    """Print a styled error line."""
    CONSOLE.print(f"[error]✖ {msg}[/error]")


def success(msg: str) -> None:
    """Print a styled success line."""
    CONSOLE.print(f"[success]✔ {msg}[/success]")


def metrics_table(
    rows: list[tuple], headers: list[str], title: str | None = None
) -> None:
    """Print a metrics table. `rows` is a list of tuples, one per row."""
    table = Table(title=title, header_style="bold", box=box.SIMPLE_HEAD)
    for h in headers:
        table.add_column(str(h))
    for row in rows:
        table.add_row(*[str(c) for c in row])
    CONSOLE.print(table)


def config_table(
    sections: dict[str, list[tuple[str, str]]], title: str = "Config"
) -> None:
    """Print a grouped key/value table.

    `sections` maps a group name to a list of (key, value) pairs. Empty groups
    are skipped; group names appear as styled separator rows.
    """
    table = Table(title=title, box=box.SIMPLE, show_header=False, pad_edge=False)
    table.add_column("key", style="muted", no_wrap=True)
    table.add_column("value")
    first = True
    for group, pairs in sections.items():
        if not pairs:
            continue
        if not first:
            table.add_row("", "")
        first = False
        table.add_row(f"[info]{group}[/info]", "")
        for key, value in pairs:
            table.add_row(f"  {key}", str(value))
    CONSOLE.print(table)