255 lines
6.7 KiB
Python
255 lines
6.7 KiB
Python
"""
|
|
©AngelaMos | 2026
|
|
formatter.py
|
|
|
|
Rich terminal output for all CLI commands
|
|
|
|
Handles all display logic for encoded strings, decoded bytes,
|
|
detection tables, peel layer summaries, and chain step results.
|
|
Detects whether stdout is a TTY or a pipe and switches between
|
|
Rich-formatted panels and raw text output. All Rich output goes
|
|
to stderr so piped stdout stays machine-readable.
|
|
|
|
Key exports:
|
|
print_encoded() - Displays an encoded string in a Rich panel or raw to stdout
|
|
print_decoded() - Displays decoded bytes as text or hex fallback
|
|
print_detection() - Renders a confidence table for detect results
|
|
print_peel_result() - Renders each peel layer and a final output panel
|
|
print_chain_result() - Renders each encoding step and the final chain result
|
|
print_score_breakdown() - Renders per-format score table for verbose mode
|
|
is_piped() - Returns True when stdout is not a TTY
|
|
|
|
Connects to:
|
|
constants.py - imports CONFIDENCE_THRESHOLD, PREVIEW_LENGTH, EncodingFormat
|
|
detector.py - imports DetectionResult
|
|
peeler.py - imports PeelResult
|
|
utils.py - imports safe_bytes_preview
|
|
cli.py - imports all print_* functions
|
|
"""
|
|
|
|
import sys
|
|
|
|
from rich.console import Console
|
|
from rich.panel import Panel
|
|
from rich.table import Table
|
|
from rich.text import Text
|
|
|
|
from base64_tool.constants import (
|
|
CONFIDENCE_THRESHOLD,
|
|
EncodingFormat,
|
|
PREVIEW_LENGTH,
|
|
)
|
|
from base64_tool.detector import DetectionResult
|
|
from base64_tool.peeler import PeelResult
|
|
from base64_tool.utils import safe_bytes_preview
|
|
|
|
|
|
console = Console(stderr = True)
|
|
|
|
|
|
def is_piped() -> bool:
|
|
return not sys.stdout.isatty()
|
|
|
|
|
|
def write_raw(text: str) -> None:
|
|
sys.stdout.write(text)
|
|
sys.stdout.flush()
|
|
|
|
|
|
def print_encoded(result: str, fmt: EncodingFormat) -> None:
|
|
if is_piped():
|
|
write_raw(result)
|
|
return
|
|
panel = Panel(
|
|
Text(result,
|
|
style = "green"),
|
|
title = f"[bold cyan]{fmt.value}[/bold cyan] encoded",
|
|
border_style = "cyan",
|
|
)
|
|
console.print(panel)
|
|
|
|
|
|
def print_decoded(result: bytes) -> None:
|
|
preview = safe_bytes_preview(result, length = 4096)
|
|
if is_piped():
|
|
write_raw(preview)
|
|
return
|
|
panel = Panel(
|
|
Text(preview,
|
|
style = "green"),
|
|
title = "[bold cyan]Decoded[/bold cyan]",
|
|
border_style = "cyan",
|
|
)
|
|
console.print(panel)
|
|
|
|
|
|
def print_score_breakdown(
|
|
scores: dict[EncodingFormat,
|
|
float],
|
|
) -> None:
|
|
table = Table(
|
|
title = "Score Breakdown",
|
|
show_header = True,
|
|
header_style = "bold magenta",
|
|
)
|
|
table.add_column("Format", style = "cyan", min_width = 10)
|
|
table.add_column(
|
|
"Score",
|
|
justify = "right",
|
|
min_width = 8,
|
|
)
|
|
table.add_column("Status", min_width = 10)
|
|
|
|
sorted_scores = sorted(
|
|
scores.items(),
|
|
key = lambda x: x[1],
|
|
reverse = True,
|
|
)
|
|
for fmt, score in sorted_scores:
|
|
color = _confidence_color(score)
|
|
if score >= CONFIDENCE_THRESHOLD:
|
|
status = "[green]detected[/green]"
|
|
elif score > 0:
|
|
status = "[yellow]below threshold[/yellow]"
|
|
else:
|
|
status = "[dim]no match[/dim]"
|
|
table.add_row(
|
|
fmt.value,
|
|
f"[{color}]{score:.0%}[/{color}]",
|
|
status,
|
|
)
|
|
|
|
console.print(table)
|
|
|
|
|
|
def print_detection(
|
|
results: list[DetectionResult],
|
|
*,
|
|
verbose_scores: dict[EncodingFormat,
|
|
float] | None = None,
|
|
) -> None:
|
|
if verbose_scores is not None:
|
|
print_score_breakdown(verbose_scores)
|
|
console.print()
|
|
|
|
if not results:
|
|
console.print("[yellow]No encoding format detected.[/yellow]")
|
|
return
|
|
|
|
table = Table(
|
|
title = "Detection Results",
|
|
show_header = True,
|
|
header_style = "bold magenta",
|
|
)
|
|
table.add_column("Format", style = "cyan", min_width = 10)
|
|
table.add_column(
|
|
"Confidence",
|
|
justify = "right",
|
|
style = "green",
|
|
min_width = 12,
|
|
)
|
|
table.add_column("Decoded Preview", style = "dim")
|
|
|
|
for result in results:
|
|
confidence_str = f"{result.confidence:.0%}"
|
|
preview = ""
|
|
if result.decoded is not None:
|
|
preview = safe_bytes_preview(
|
|
result.decoded,
|
|
PREVIEW_LENGTH,
|
|
)
|
|
table.add_row(
|
|
result.format.value,
|
|
confidence_str,
|
|
preview,
|
|
)
|
|
|
|
console.print(table)
|
|
|
|
|
|
def print_peel_result(
|
|
result: PeelResult,
|
|
*,
|
|
verbose: bool = False,
|
|
) -> None:
|
|
if not result.success:
|
|
console.print("[yellow]No encoding layers detected.[/yellow]")
|
|
return
|
|
|
|
layer_count = len(result.layers)
|
|
suffix = "s" if layer_count > 1 else ""
|
|
console.print()
|
|
console.print(
|
|
f"[bold cyan]Peeled {layer_count} encoding "
|
|
f"layer{suffix}[/bold cyan]"
|
|
)
|
|
console.print()
|
|
|
|
for layer in result.layers:
|
|
color = _confidence_color(layer.confidence)
|
|
console.print(
|
|
f" [bold]Layer {layer.depth}[/bold] "
|
|
f"[cyan]{layer.format.value}[/cyan] "
|
|
f"[{color}]{layer.confidence:.0%}[/{color}]"
|
|
)
|
|
console.print(f" [dim]{layer.decoded_preview}[/dim]")
|
|
|
|
if verbose and layer.all_scores:
|
|
console.print()
|
|
print_score_breakdown(dict(layer.all_scores))
|
|
console.print()
|
|
|
|
console.print()
|
|
|
|
preview = safe_bytes_preview(result.final_output, length = 4096)
|
|
panel = Panel(
|
|
Text(preview,
|
|
style = "bold green"),
|
|
title = "[bold]Final Output[/bold]",
|
|
border_style = "green",
|
|
subtitle = (f"[dim]{layer_count} layer{suffix} peeled[/dim]"),
|
|
)
|
|
console.print(panel)
|
|
|
|
|
|
def print_chain_result(
|
|
steps: list[tuple[EncodingFormat,
|
|
str]],
|
|
final: str,
|
|
) -> None:
|
|
if is_piped():
|
|
write_raw(final)
|
|
return
|
|
|
|
console.print()
|
|
console.print("[bold cyan]Encoding Chain[/bold cyan]")
|
|
console.print()
|
|
|
|
for i, (fmt, intermediate) in enumerate(steps):
|
|
marker = "start" if i == 0 else "step"
|
|
arrow = f" [{marker}] " if i == 0 else " -> "
|
|
truncated = intermediate[: PREVIEW_LENGTH]
|
|
ellipsis = "..." if len(intermediate) > PREVIEW_LENGTH else ""
|
|
console.print(
|
|
f"{arrow}[cyan]{fmt.value}[/cyan] "
|
|
f"[dim]{truncated}{ellipsis}[/dim]"
|
|
)
|
|
|
|
console.print()
|
|
panel = Panel(
|
|
Text(final,
|
|
style = "green"),
|
|
title = "[bold]Chain Result[/bold]",
|
|
border_style = "cyan",
|
|
subtitle = f"[dim]{len(steps)} steps[/dim]",
|
|
)
|
|
console.print(panel)
|
|
|
|
|
|
def _confidence_color(confidence: float) -> str:
|
|
if confidence >= 0.9:
|
|
return "green"
|
|
if confidence >= 0.7:
|
|
return "yellow"
|
|
return "red"
|