twitch-byte-bot / cli /formatters.py
JuanCS-Dev's picture
Deploying CLI completion and HF Token auth support
d5dc912 verified
Raw History Blame Contribute Delete
7.31 kB
# SPDX-License-Identifier: MIT
"""Output formatters for the Byte Agent CLI.
Provides human-readable (tables, key-value, colored) and machine-readable (JSON)
output modes. All output goes through these formatters so the --json flag works
consistently across every command.
"""
from __future__ import annotations
import json
import sys
from datetime import UTC, datetime, timezone
from typing import Any, Final, TextIO
# ---------------------------------------------------------------------------
# ANSI Color Constants
# ---------------------------------------------------------------------------
_SUPPORTS_COLOR: Final[bool] = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
_RESET: Final[str] = "\033[0m" if _SUPPORTS_COLOR else ""
_BOLD: Final[str] = "\033[1m" if _SUPPORTS_COLOR else ""
_DIM: Final[str] = "\033[2m" if _SUPPORTS_COLOR else ""
_GREEN: Final[str] = "\033[32m" if _SUPPORTS_COLOR else ""
_RED: Final[str] = "\033[31m" if _SUPPORTS_COLOR else ""
_YELLOW: Final[str] = "\033[33m" if _SUPPORTS_COLOR else ""
_CYAN: Final[str] = "\033[36m" if _SUPPORTS_COLOR else ""
_MAGENTA: Final[str] = "\033[35m" if _SUPPORTS_COLOR else ""
# ---------------------------------------------------------------------------
# Primitive Formatters
# ---------------------------------------------------------------------------
def print_success(message: str, *, file: TextIO = sys.stdout) -> None:
"""Print a success message in green."""
file.write(f"{_GREEN}βœ“{_RESET} {message}\n")
file.flush()
def print_error(message: str, *, file: TextIO = sys.stderr) -> None:
"""Print an error message in red to stderr."""
file.write(f"{_RED}βœ—{_RESET} {message}\n")
file.flush()
def print_warning(message: str, *, file: TextIO = sys.stderr) -> None:
"""Print a warning message in yellow."""
file.write(f"{_YELLOW}⚠{_RESET} {message}\n")
file.flush()
def print_info(message: str, *, file: TextIO = sys.stdout) -> None:
"""Print an informational message in cyan."""
file.write(f"{_CYAN}β„Ή{_RESET} {message}\n")
file.flush()
def print_header(title: str, *, file: TextIO = sys.stdout) -> None:
"""Print a bold section header."""
file.write(f"\n{_BOLD}{title}{_RESET}\n")
file.write(f"{_DIM}{'─' * min(len(title) + 4, 60)}{_RESET}\n")
file.flush()
# ---------------------------------------------------------------------------
# JSON Output
# ---------------------------------------------------------------------------
def print_json(data: Any, *, file: TextIO = sys.stdout) -> None:
"""Pretty-print data as indented JSON."""
output = json.dumps(data, indent=2, ensure_ascii=False, default=str)
file.write(output)
file.write("\n")
file.flush()
# ---------------------------------------------------------------------------
# Table Output
# ---------------------------------------------------------------------------
def print_table(
headers: list[str],
rows: list[list[str]],
*,
file: TextIO = sys.stdout,
max_col_width: int = 50,
) -> None:
"""Print an ASCII table with auto-sized columns.
Columns are sized to fit the widest value (capped at max_col_width).
"""
if not headers:
return
# Compute column widths
col_count = len(headers)
widths = [len(h) for h in headers]
for row in rows:
for i, cell in enumerate(row[:col_count]):
widths[i] = max(widths[i], len(str(cell)))
widths = [min(w, max_col_width) for w in widths]
def _truncate(text: str, width: int) -> str:
text = str(text)
if len(text) <= width:
return text.ljust(width)
return text[: width - 1] + "…"
# Header row
header_line = " β”‚ ".join(_truncate(h, widths[i]) for i, h in enumerate(headers))
separator = "─┼─".join("─" * w for w in widths)
file.write(f" {_BOLD}{header_line}{_RESET}\n")
file.write(f" {_DIM}{separator}{_RESET}\n")
# Data rows
if not rows:
file.write(f" {_DIM}(empty){_RESET}\n")
else:
for row in rows:
padded = [
_truncate(row[i] if i < len(row) else "", widths[i]) for i in range(col_count)
]
file.write(f" {' β”‚ '.join(padded)}\n")
file.flush()
# ---------------------------------------------------------------------------
# Key-Value Output
# ---------------------------------------------------------------------------
def print_kv(
data: dict[str, Any],
*,
file: TextIO = sys.stdout,
label_width: int = 0,
) -> None:
"""Print key-value pairs aligned vertically.
If label_width is 0, it auto-sizes to the longest key.
"""
if not data:
return
width = label_width or max(len(str(k)) for k in data)
for key, value in data.items():
label = f"{_CYAN}{str(key).ljust(width)}{_RESET}"
formatted_value = _format_value(value)
file.write(f" {label} {formatted_value}\n")
file.flush()
def _format_value(value: Any) -> str:
"""Format a single value for display with semantic coloring."""
if value is None:
return f"{_DIM}β€”{_RESET}"
if isinstance(value, bool):
return f"{_GREEN}yes{_RESET}" if value else f"{_RED}no{_RESET}"
if isinstance(value, list | tuple):
if not value:
return f"{_DIM}(none){_RESET}"
return ", ".join(str(v) for v in value)
if isinstance(value, dict):
if not value:
return f"{_DIM}{{}}{_RESET}"
return json.dumps(value, ensure_ascii=False, default=str)
return str(value)
# ---------------------------------------------------------------------------
# Timestamp Formatter
# ---------------------------------------------------------------------------
def format_timestamp(iso_or_float: str | float | None) -> str:
"""Convert an ISO timestamp or Unix float into a human-readable string.
Returns "β€”" for None/empty values.
"""
if iso_or_float is None:
return "β€”"
if isinstance(iso_or_float, int | float):
if iso_or_float <= 0:
return "β€”"
try:
dt = datetime.fromtimestamp(iso_or_float, tz=UTC)
return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
except (OSError, ValueError, OverflowError):
return str(iso_or_float)
text = str(iso_or_float).strip()
if not text:
return "β€”"
# Try to parse ISO format
for fmt in ("%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%S+00:00", "%Y-%m-%dT%H:%M:%S"):
try:
dt = datetime.strptime(text, fmt).replace(tzinfo=UTC)
return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
except ValueError:
continue
return text
# ---------------------------------------------------------------------------
# Dispatcher Helper
# ---------------------------------------------------------------------------
def output(
data: Any,
*,
json_mode: bool = False,
human_fn: Any = None,
) -> None:
"""Universal output dispatcher.
In JSON mode: prints raw JSON.
In human mode: calls the provided human_fn callback, or falls back to print_json.
"""
if json_mode:
print_json(data)
elif callable(human_fn):
human_fn(data)
else:
print_json(data)