Coverage for fmt.py: 81%

80 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-08 15:04 -0800

1"""Terminal formatting helpers for Vibelexity output.""" 

2 

3from __future__ import annotations 

4 

5import os 

6import re 

7import sys 

8 

9# ── Color support ───────────────────────────────────────────────────── 

10# Auto-detect: color only when stdout is an interactive terminal. 

11# Respect the NO_COLOR convention (https://no-color.org/). 

12 

13_USE_COLOR: bool = sys.stdout.isatty() and not bool(os.environ.get("NO_COLOR")) 

14 

15_RESET = "\033[0m" 

16_BOLD = "\033[1m" 

17_DIM = "\033[2m" 

18 

19_CYAN = "\033[36m" 

20_GREEN = "\033[32m" 

21_YELLOW = "\033[33m" 

22_RED = "\033[31m" 

23_MAGENTA = "\033[35m" 

24 

25 

26# ── Color wrappers ──────────────────────────────────────────────────── 

27 

28 

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 

34 

35 

36def header(text: str) -> str: 

37 return _colorize(text, _BOLD, _CYAN) 

38 

39 

40def label(text: str) -> str: 

41 return _colorize(text, _YELLOW) 

42 

43 

44def num(value: str) -> str: 

45 return _colorize(value, _BOLD, _GREEN) 

46 

47 

48def path(text: str) -> str: 

49 return _colorize(text, _DIM, _MAGENTA) 

50 

51 

52def func_name(text: str) -> str: 

53 return _colorize(text, _BOLD) 

54 

55 

56def warn(text: str) -> str: 

57 return _colorize(text, _BOLD, _RED) 

58 

59 

60def dim(text: str) -> str: 

61 """Format dimmed/muted text.""" 

62 return _colorize(text, _DIM) 

63 

64 

65# ── Number formatting ───────────────────────────────────────────────── 

66 

67 

68def fmt_int(value: int | float) -> str: 

69 """Format an integer with thousand separators. 

70 

71 Accepts float for convenience (truncates to int). 

72 """ 

73 return f"{int(value):,}" 

74 

75 

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}" 

79 

80 

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) 

88 

89 

90# ── Composite helpers ───────────────────────────────────────────────── 

91 

92 

93def colored_num(value: int | float) -> str: 

94 """Format and colorize a number.""" 

95 return num(fmt_number(value)) 

96 

97 

98def colored_float(value: float, decimals: int = 2) -> str: 

99 """Format and colorize a float.""" 

100 return num(fmt_float(value, decimals)) 

101 

102 

103def colored_int(value: int | float) -> str: 

104 """Format and colorize an integer.""" 

105 return num(fmt_int(value)) 

106 

107 

108def rpad(value: int | float, width: int, num_formatter=None) -> str: 

109 """Right-align a formatted number within *width* visible characters, then colorize. 

110 

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) 

118 

119 # Check if the formatter returns a string with ANSI escape codes 

120 has_ansi = bool(re.search(r"\x1b\[", formatted)) 

121 

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 

135 

136 # Formatter returns plain text (no color yet) 

137 if not _USE_COLOR: 

138 return formatted.rjust(width) 

139 

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) 

145 

146 

147def fmt_compact(value: int | float) -> str: 

148 """Format large numbers (5+ digits) with superscript notation. 

149 

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) 

161 

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³" 

169 

170 

171def colored_compact(value: int | float) -> str: 

172 """Format a number with compact notation and color.""" 

173 return num(fmt_compact(value))