main
py 196 lines 7.22 KB
Raw
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)