Coverage for little_loops / cli / output.py: 52%
29 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-03-18 16:20 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-03-18 16:20 -0500
1"""Shared CLI output utilities: terminal width, text wrapping, and ANSI color."""
3from __future__ import annotations
5import json
6import os
7import shutil
8import sys
9import textwrap
10from typing import TYPE_CHECKING, Any
12if TYPE_CHECKING:
13 from little_loops.config import CliConfig
16def terminal_width(default: int = 80) -> int:
17 """Return the current terminal column width, falling back to *default*."""
18 return shutil.get_terminal_size((default, 24)).columns
21def wrap_text(text: str, indent: str = " ", width: int | None = None) -> str:
22 """Wrap *text* at terminal width with consistent *indent* on every line."""
23 w = width or terminal_width()
24 return textwrap.fill(text, width=w, initial_indent=indent, subsequent_indent=indent)
27# ---------------------------------------------------------------------------
28# ANSI color helpers — suppressed when NO_COLOR=1 or stdout is not a TTY
29# ---------------------------------------------------------------------------
31_USE_COLOR: bool = sys.stdout.isatty() and os.environ.get("NO_COLOR", "") == ""
33PRIORITY_COLOR: dict[str, str] = {
34 "P0": "38;5;208;1",
35 "P1": "38;5;208",
36 "P2": "33",
37 "P3": "0",
38 "P4": "2",
39 "P5": "2",
40}
41TYPE_COLOR: dict[str, str] = {
42 "BUG": "38;5;208",
43 "FEAT": "32",
44 "ENH": "34",
45}
48def configure_output(config: CliConfig | None = None) -> None:
49 """Apply CLI color configuration to module-level color state.
51 Call this once at startup after loading BRConfig. Updates _USE_COLOR,
52 PRIORITY_COLOR, and TYPE_COLOR based on config and NO_COLOR env var.
54 Args:
55 config: CliConfig from BRConfig.cli, or None for defaults.
56 """
57 global _USE_COLOR, PRIORITY_COLOR, TYPE_COLOR
59 # NO_COLOR env var always takes precedence (industry convention)
60 no_color_env = os.environ.get("NO_COLOR", "") != ""
62 if config is None:
63 _USE_COLOR = sys.stdout.isatty() and not no_color_env
64 return
66 _USE_COLOR = config.color and sys.stdout.isatty() and not no_color_env
68 # Merge custom priority colors
69 PRIORITY_COLOR.update(
70 {
71 "P0": config.colors.priority.P0,
72 "P1": config.colors.priority.P1,
73 "P2": config.colors.priority.P2,
74 "P3": config.colors.priority.P3,
75 "P4": config.colors.priority.P4,
76 "P5": config.colors.priority.P5,
77 }
78 )
80 # Merge custom type colors
81 TYPE_COLOR.update(
82 {
83 "BUG": config.colors.type.BUG,
84 "FEAT": config.colors.type.FEAT,
85 "ENH": config.colors.type.ENH,
86 }
87 )
90def colorize(text: str, code: str) -> str:
91 """Wrap *text* in the given ANSI escape *code*, or return it unchanged."""
92 if not _USE_COLOR:
93 return text
94 return f"\033[{code}m{text}\033[0m"
97def print_json(data: Any) -> None:
98 """Print *data* as formatted JSON to stdout."""
99 print(json.dumps(data, indent=2))