"""Rich and JSON output helpers for CodexLens CLI.""" from __future__ import annotations import json import sys from dataclasses import asdict, is_dataclass from pathlib import Path from typing import Any, Iterable, Mapping, Sequence from rich.console import Console from rich.table import Table from rich.text import Text from codexlens.entities import SearchResult, Symbol # Force UTF-8 encoding for Windows console to properly display Chinese text # Use force_terminal=True and legacy_windows=False to avoid GBK encoding issues console = Console(force_terminal=True, legacy_windows=False) def _to_jsonable(value: Any) -> Any: if value is None: return None if hasattr(value, "model_dump"): return value.model_dump() if is_dataclass(value): return asdict(value) if isinstance(value, Path): return str(value) if isinstance(value, Mapping): return {k: _to_jsonable(v) for k, v in value.items()} if isinstance(value, (list, tuple, set)): return [_to_jsonable(v) for v in value] return value def print_json(*, success: bool, result: Any = None, error: str | None = None, **kwargs: Any) -> None: """Print JSON output with optional additional fields. Args: success: Whether the operation succeeded result: Result data (used when success=True) error: Error message (used when success=False) **kwargs: Additional fields to include in the payload (e.g., code, details) """ payload: dict[str, Any] = {"success": success} if success: payload["result"] = _to_jsonable(result) else: payload["error"] = error or "Unknown error" # Include additional error details if provided for key, value in kwargs.items(): payload[key] = _to_jsonable(value) console.print_json(json.dumps(payload, ensure_ascii=False)) def render_search_results( results: Sequence[SearchResult], *, title: str = "Search Results", verbose: bool = False ) -> None: """Render search results with optional source tags in verbose mode. Args: results: Search results to display title: Table title verbose: If True, show search source tags ([E], [F], [V]) and fusion scores """ table = Table(title=title, show_lines=False) if verbose: # Verbose mode: show source tags table.add_column("Source", style="dim", width=6, justify="center") table.add_column("Path", style="cyan", no_wrap=True) table.add_column("Score", style="magenta", justify="right") table.add_column("Excerpt", style="white") for res in results: excerpt = res.excerpt or "" score_str = f"{res.score:.3f}" if verbose: # Extract search source tag if available source = getattr(res, "search_source", None) source_tag = "" if source == "exact": source_tag = "[E]" elif source == "fuzzy": source_tag = "[F]" elif source == "vector": source_tag = "[V]" elif source == "fusion": source_tag = "[RRF]" table.add_row(source_tag, res.path, score_str, excerpt) else: table.add_row(res.path, score_str, excerpt) console.print(table) def render_symbols(symbols: Sequence[Symbol], *, title: str = "Symbols") -> None: table = Table(title=title) table.add_column("Name", style="green") table.add_column("Kind", style="yellow") table.add_column("Range", style="white", justify="right") for sym in symbols: start, end = sym.range table.add_row(sym.name, sym.kind, f"{start}-{end}") console.print(table) def render_status(stats: Mapping[str, Any]) -> None: table = Table(title="Index Status") table.add_column("Metric", style="cyan") table.add_column("Value", style="white") for key, value in stats.items(): if isinstance(value, Mapping): value_text = ", ".join(f"{k}:{v}" for k, v in value.items()) elif isinstance(value, (list, tuple)): value_text = ", ".join(str(v) for v in value) else: value_text = str(value) table.add_row(str(key), value_text) console.print(table) def render_file_inspect(path: str, language: str, symbols: Iterable[Symbol]) -> None: header = Text.assemble(("File: ", "bold"), (path, "cyan"), (" Language: ", "bold"), (language, "green")) console.print(header) render_symbols(list(symbols), title="Discovered Symbols")