Coverage for fmt.py: 81%
80 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-08 15:04 -0800
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-08 15:04 -0800
1"""Terminal formatting helpers for Vibelexity output."""
3from __future__ import annotations
5import os
6import re
7import sys
9# ── Color support ─────────────────────────────────────────────────────
10# Auto-detect: color only when stdout is an interactive terminal.
11# Respect the NO_COLOR convention (https://no-color.org/).
13_USE_COLOR: bool = sys.stdout.isatty() and not bool(os.environ.get("NO_COLOR"))
15_RESET = "\033[0m"
16_BOLD = "\033[1m"
17_DIM = "\033[2m"
19_CYAN = "\033[36m"
20_GREEN = "\033[32m"
21_YELLOW = "\033[33m"
22_RED = "\033[31m"
23_MAGENTA = "\033[35m"
26# ── Color wrappers ────────────────────────────────────────────────────
29def _colorize(text: str, *codes: str) -> str:
30 """Wrap *text* in ANSI escape codes (no-op when color is disabled)."""
31 if not _USE_COLOR:
32 return text
33 return "".join(codes) + text + _RESET
36def header(text: str) -> str:
37 return _colorize(text, _BOLD, _CYAN)
40def label(text: str) -> str:
41 return _colorize(text, _YELLOW)
44def num(value: str) -> str:
45 return _colorize(value, _BOLD, _GREEN)
48def path(text: str) -> str:
49 return _colorize(text, _DIM, _MAGENTA)
52def func_name(text: str) -> str:
53 return _colorize(text, _BOLD)
56def warn(text: str) -> str:
57 return _colorize(text, _BOLD, _RED)
60def dim(text: str) -> str:
61 """Format dimmed/muted text."""
62 return _colorize(text, _DIM)
65# ── Number formatting ─────────────────────────────────────────────────
68def fmt_int(value: int | float) -> str:
69 """Format an integer with thousand separators.
71 Accepts float for convenience (truncates to int).
72 """
73 return f"{int(value):,}"
76def fmt_float(value: float, decimals: int = 2) -> str:
77 """Format a float rounded to *decimals* places, with thousand separators."""
78 return f"{value:,.{decimals}f}"
81def fmt_number(value: int | float) -> str:
82 """Auto-detect int vs float and format accordingly."""
83 if isinstance(value, int):
84 return fmt_int(value)
85 if value == int(value):
86 return fmt_int(value)
87 return fmt_float(value)
90# ── Composite helpers ─────────────────────────────────────────────────
93def colored_num(value: int | float) -> str:
94 """Format and colorize a number."""
95 return num(fmt_number(value))
98def colored_float(value: float, decimals: int = 2) -> str:
99 """Format and colorize a float."""
100 return num(fmt_float(value, decimals))
103def colored_int(value: int | float) -> str:
104 """Format and colorize an integer."""
105 return num(fmt_int(value))
108def rpad(value: int | float, width: int, num_formatter=None) -> str:
109 """Right-align a formatted number within *width* visible characters, then colorize.
111 Args:
112 value: Number to format
113 width: Minimum column width
114 num_formatter: Optional custom formatter (e.g., fmt_compact, colored_compact)
115 """
116 formatter = num_formatter or fmt_number
117 formatted = formatter(value)
119 # Check if the formatter returns a string with ANSI escape codes
120 has_ansi = bool(re.search(r"\x1b\[", formatted))
122 if has_ansi:
123 # Formatter already includes color codes - need to handle padding carefully
124 if not _USE_COLOR:
125 # Color disabled, strip ANSI codes and pad
126 text_only = re.sub(r"\x1b\[[0-9;]*m", "", formatted)
127 visible_len = len(text_only)
128 return text_only.rjust(width)
129 else:
130 # Color enabled, extract text for padding, preserve codes
131 text_only = re.sub(r"\x1b\[[0-9;]*m", "", formatted)
132 visible_len = len(text_only)
133 needed_spaces = max(0, width - visible_len)
134 return " " * needed_spaces + formatted
136 # Formatter returns plain text (no color yet)
137 if not _USE_COLOR:
138 return formatted.rjust(width)
140 # Color enabled, pad then add color
141 plain = formatted
142 visible_len = len(plain)
143 needed_spaces = max(0, width - visible_len)
144 return " " * needed_spaces + num(plain)
147def fmt_compact(value: int | float) -> str:
148 """Format large numbers (5+ digits) with superscript notation.
150 Returns "∞" for values >= 10^6.
151 Uses superscript notation for values >= 100,000 and < 10^6.
152 Examples:
153 - 5,000,000+ → "∞"
154 - 123,456 → "123.5 × 10³"
155 - 99,999 → "99,999" (no compaction)
156 """
157 if value >= 1_000_000:
158 return "∞"
159 if value < 100_000:
160 return fmt_number(value)
162 is_int_input = isinstance(value, int)
163 scaled = value / 1_000
164 if is_int_input:
165 rounded = round(scaled, 1)
166 if abs(rounded - int(rounded)) < 0.01:
167 return f"{int(rounded)} × 10³"
168 return f"{scaled:.1f} × 10³"
171def colored_compact(value: int | float) -> str:
172 """Format a number with compact notation and color."""
173 return num(fmt_compact(value))