mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
136 lines
4.5 KiB
Python
136 lines
4.5 KiB
Python
"""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")
|
|
|