105 lines
2.9 KiB
Python
105 lines
2.9 KiB
Python
"""Output formatting: JSON, tables, and structured errors.
|
|
|
|
Detects TTY to auto-switch between human-readable and machine-parseable output.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from typing import Any
|
|
|
|
from rich.console import Console
|
|
from rich.table import Table
|
|
|
|
console = Console(stderr=True)
|
|
stdout_console = Console()
|
|
|
|
|
|
def is_tty() -> bool:
|
|
"""Check if stdout is a TTY."""
|
|
return sys.stdout.isatty()
|
|
|
|
|
|
# Global state for --json flag
|
|
_force_json = False
|
|
|
|
|
|
def set_json_mode(enabled: bool) -> None:
|
|
global _force_json
|
|
_force_json = enabled
|
|
|
|
|
|
|
|
def use_json() -> bool:
|
|
"""Should we output JSON?"""
|
|
return _force_json or os.environ.get("HONCHO_JSON", "").lower() in ("1", "true") or not is_tty()
|
|
|
|
|
|
def print_json(data: Any) -> None:
|
|
"""Print a single JSON value to stdout."""
|
|
print(json.dumps(data, indent=2, default=str))
|
|
|
|
|
|
def print_table(columns: list[str], rows: list[list[str]], title: str | None = None) -> None:
|
|
"""Print a rich table to stdout."""
|
|
table = Table(title=title, show_header=True, header_style="bold")
|
|
for col in columns:
|
|
table.add_column(col)
|
|
for row in rows:
|
|
table.add_row(*row)
|
|
stdout_console.print(table)
|
|
|
|
|
|
def print_result(data: Any, columns: list[str] | None = None, title: str | None = None) -> None:
|
|
"""Print data as JSON or table depending on mode.
|
|
|
|
For lists, uses JSON arrays in JSON mode or tables in TTY mode.
|
|
For dicts, uses JSON or key-value display.
|
|
"""
|
|
if use_json():
|
|
print_json(data)
|
|
else:
|
|
if isinstance(data, list) and columns:
|
|
rows = []
|
|
for item in data:
|
|
row = [str(item.get(col, "")) if isinstance(item, dict) else str(item) for col in columns]
|
|
rows.append(row)
|
|
print_table(columns, rows, title=title)
|
|
elif isinstance(data, dict):
|
|
table = Table(show_header=False)
|
|
table.add_column("Field", style="bold")
|
|
table.add_column("Value")
|
|
for k, v in data.items():
|
|
val = json.dumps(v, default=str) if isinstance(v, (dict, list)) else str(v)
|
|
table.add_row(k, val)
|
|
stdout_console.print(table)
|
|
else:
|
|
stdout_console.print(data)
|
|
|
|
|
|
def print_error(code: str, message: str, details: dict | None = None) -> None:
|
|
"""Print structured error."""
|
|
err = {
|
|
"error": {
|
|
"code": code,
|
|
"message": message,
|
|
}
|
|
}
|
|
if details:
|
|
err["error"]["details"] = details
|
|
|
|
if use_json():
|
|
print(json.dumps(err, default=str), file=sys.stderr)
|
|
else:
|
|
console.print(f"[red]Error[/red] ({code}): {message}")
|
|
if details:
|
|
for k, v in details.items():
|
|
console.print(f" {k}: {v}")
|
|
|
|
|
|
def status(msg: str) -> None:
|
|
"""Print a status message to stderr."""
|
|
console.print(f"[dim]{msg}[/dim]")
|