| 1 | import html |
| 2 | import sys |
| 3 | from collections.abc import Mapping |
| 4 | |
| 5 | import webcolors |
| 6 | |
| 7 | from . import files # Load before strings; helpers.files imports sanitize_string. |
| 8 | from .strings import sanitize_string |
| 9 | |
| 10 | _runtime_module = None |
| 11 | |
| 12 | |
| 13 | def _get_runtime(): |
| 14 | global _runtime_module |
| 15 | if _runtime_module is None: |
| 16 | from . import runtime as runtime_module # Local import to avoid circular dependency |
| 17 | |
| 18 | _runtime_module = runtime_module |
| 19 | return _runtime_module |
| 20 | |
| 21 | class PrintStyle: |
| 22 | last_endline = True |
| 23 | |
| 24 | def __init__(self, bold=False, italic=False, underline=False, font_color="default", background_color="default", padding=False, log_only=False): |
| 25 | self.bold = bold |
| 26 | self.italic = italic |
| 27 | self.underline = underline |
| 28 | self.font_color = font_color |
| 29 | self.background_color = background_color |
| 30 | self.padding = padding |
| 31 | self.padding_added = False # Flag to track if padding was added |
| 32 | self.log_only = log_only |
| 33 | |
| 34 | def _get_rgb_color_code(self, color, is_background=False): |
| 35 | try: |
| 36 | if color.startswith("#") and len(color) == 7: |
| 37 | r = int(color[1:3], 16) |
| 38 | g = int(color[3:5], 16) |
| 39 | b = int(color[5:7], 16) |
| 40 | else: |
| 41 | rgb_color = webcolors.name_to_rgb(color) |
| 42 | r, g, b = rgb_color.red, rgb_color.green, rgb_color.blue |
| 43 | |
| 44 | if is_background: |
| 45 | return f"\033[48;2;{r};{g};{b}m", f"background-color: rgb({r}, {g}, {b});" |
| 46 | else: |
| 47 | return f"\033[38;2;{r};{g};{b}m", f"color: rgb({r}, {g}, {b});" |
| 48 | except ValueError: |
| 49 | return "", "" |
| 50 | |
| 51 | def _get_styled_text(self, text): |
| 52 | start = "" |
| 53 | end = "\033[0m" # Reset ANSI code |
| 54 | if self.bold: |
| 55 | start += "\033[1m" |
| 56 | if self.italic: |
| 57 | start += "\033[3m" |
| 58 | if self.underline: |
| 59 | start += "\033[4m" |
| 60 | font_color_code, _ = self._get_rgb_color_code(self.font_color) |
| 61 | background_color_code, _ = self._get_rgb_color_code(self.background_color, True) |
| 62 | start += font_color_code |
| 63 | start += background_color_code |
| 64 | return start + text + end |
| 65 | |
| 66 | def _get_html_styled_text(self, text): |
| 67 | styles = [] |
| 68 | if self.bold: |
| 69 | styles.append("font-weight: bold;") |
| 70 | if self.italic: |
| 71 | styles.append("font-style: italic;") |
| 72 | if self.underline: |
| 73 | styles.append("text-decoration: underline;") |
| 74 | _, font_color_code = self._get_rgb_color_code(self.font_color) |
| 75 | _, background_color_code = self._get_rgb_color_code(self.background_color, True) |
| 76 | styles.append(font_color_code) |
| 77 | styles.append(background_color_code) |
| 78 | style_attr = " ".join(styles) |
| 79 | escaped_text = html.escape(text).replace("\n", "<br>") # Escape HTML special characters |
| 80 | return f'<span style="{style_attr}">{escaped_text}</span>' |
| 81 | |
| 82 | def _add_padding_if_needed(self): |
| 83 | if self.padding and not self.padding_added: |
| 84 | if not self.log_only: |
| 85 | print() # Print an empty line for padding |
| 86 | self.padding_added = True |
| 87 | |
| 88 | @staticmethod |
| 89 | def _format_args(args, sep): |
| 90 | if not args: |
| 91 | return "" |
| 92 | |
| 93 | head, *tail = args |
| 94 | |
| 95 | if isinstance(head, str) and tail and ("%" in head or "{" in head): |
| 96 | is_mapping = len(tail) == 1 and isinstance(tail[0], Mapping) |
| 97 | try: |
| 98 | return head % (tail[0] if is_mapping else tuple(tail)) |
| 99 | except (TypeError, ValueError, KeyError): |
| 100 | try: |
| 101 | return head.format(**tail[0]) if is_mapping else head.format(*tail) |
| 102 | except (KeyError, IndexError, ValueError): |
| 103 | pass |
| 104 | |
| 105 | return sep.join(str(item) for item in args) |
| 106 | |
| 107 | @staticmethod |
| 108 | def _prefixed_args(prefix: str, args: tuple) -> tuple: |
| 109 | if not args: |
| 110 | return (f"{prefix}:",) |
| 111 | |
| 112 | first, *rest = args |
| 113 | if isinstance(first, str): |
| 114 | return (f"{prefix}: {first}", *rest) |
| 115 | |
| 116 | return (f"{prefix}:", *args) |
| 117 | |
| 118 | def get(self, *args, sep=' ', **kwargs): |
| 119 | text = self._format_args(args, sep) |
| 120 | |
| 121 | # Automatically mask secrets in all print output |
| 122 | try: |
| 123 | if not hasattr(self, "secrets_mgr"): |
| 124 | from helpers.secrets import get_secrets_manager |
| 125 | self.secrets_mgr = get_secrets_manager() |
| 126 | text = self.secrets_mgr.mask_values(text) |
| 127 | except Exception: |
| 128 | # If masking fails, proceed without masking to avoid breaking functionality |
| 129 | pass |
| 130 | |
| 131 | text = sanitize_string(text) |
| 132 | |
| 133 | return text, self._get_styled_text(text), self._get_html_styled_text(text) |
| 134 | |
| 135 | def print(self, *args, sep=' ', end='\n', flush=True): |
| 136 | self._add_padding_if_needed() |
| 137 | if not PrintStyle.last_endline: |
| 138 | if not self.log_only: |
| 139 | print() |
| 140 | _, styled_text, _ = self.get(*args, sep=sep) |
| 141 | if not self.log_only: |
| 142 | print(styled_text, end=end, flush=flush) |
| 143 | PrintStyle.last_endline = end.endswith('\n') |
| 144 | |
| 145 | def stream(self, *args, sep=' ', flush=True): |
| 146 | self._add_padding_if_needed() |
| 147 | _, styled_text, _ = self.get(*args, sep=sep) |
| 148 | if not self.log_only: |
| 149 | print(styled_text, end='', flush=flush) |
| 150 | PrintStyle.last_endline = False |
| 151 | |
| 152 | def is_last_line_empty(self): |
| 153 | lines = sys.stdin.readlines() |
| 154 | return bool(lines) and not lines[-1].strip() |
| 155 | |
| 156 | @staticmethod |
| 157 | def standard(*args, sep=' ', end='\n', flush=True): |
| 158 | PrintStyle().print(*args, sep=sep, end=end, flush=flush) |
| 159 | |
| 160 | @staticmethod |
| 161 | def hint(*args, sep=' ', end='\n', flush=True): |
| 162 | prefixed = PrintStyle._prefixed_args("Hint", args) |
| 163 | PrintStyle(font_color="#6C3483", padding=True).print(*prefixed, sep=sep, end=end, flush=flush) |
| 164 | |
| 165 | @staticmethod |
| 166 | def info(*args, sep=' ', end='\n', flush=True): |
| 167 | prefixed = PrintStyle._prefixed_args("Info", args) |
| 168 | PrintStyle(font_color="#0000FF", padding=True).print(*prefixed, sep=sep, end=end, flush=flush) |
| 169 | |
| 170 | @staticmethod |
| 171 | def success(*args, sep=' ', end='\n', flush=True): |
| 172 | prefixed = PrintStyle._prefixed_args("Success", args) |
| 173 | PrintStyle(font_color="#008000", padding=True).print(*prefixed, sep=sep, end=end, flush=flush) |
| 174 | |
| 175 | @staticmethod |
| 176 | def warning(*args, sep=' ', end='\n', flush=True): |
| 177 | prefixed = PrintStyle._prefixed_args("Warning", args) |
| 178 | PrintStyle(font_color="#FFA500", padding=True).print(*prefixed, sep=sep, end=end, flush=flush) |
| 179 | |
| 180 | @staticmethod |
| 181 | def debug(*args, sep=' ', end='\n', flush=True): |
| 182 | # Only emit debug output when running in development mode |
| 183 | try: |
| 184 | runtime_module = _get_runtime() |
| 185 | if not runtime_module.is_development(): |
| 186 | return |
| 187 | except Exception: |
| 188 | # If runtime detection fails, default to emitting to avoid hiding logs during development setup |
| 189 | pass |
| 190 | prefixed = PrintStyle._prefixed_args("Debug", args) |
| 191 | PrintStyle(font_color="#808080", padding=True).print(*prefixed, sep=sep, end=end, flush=flush) |
| 192 | |
| 193 | @staticmethod |
| 194 | def error(*args, sep=' ', end='\n', flush=True): |
| 195 | prefixed = PrintStyle._prefixed_args("Error", args) |
| 196 | PrintStyle(font_color="red", padding=True).print(*prefixed, sep=sep, end=end, flush=flush) |