honcho/honcho-cli/src/honcho_cli/output.py

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]")