hexlogger 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hexlogger/__init__.py ADDED
@@ -0,0 +1,1069 @@
1
+ """
2
+ ╔═══════════════════════════════════════════╗
3
+ ║ HexLogger v1.0.0 ║
4
+ ║ Premium Console Logging Library ║
5
+ ║ Created by Montage ║
6
+ ╚═══════════════════════════════════════════╝
7
+ """
8
+
9
+ __version__ = "1.0.0"
10
+ __author__ = "Montage"
11
+
12
+ import sys
13
+ import os
14
+ import re
15
+ import time
16
+ import threading
17
+ from datetime import datetime
18
+ from enum import Enum
19
+ from typing import Optional, List, Dict, Any
20
+
21
+ # ══════════════════════════════════════════
22
+ # ANSI Color Codes
23
+ # ══════════════════════════════════════════
24
+
25
+ class Colors:
26
+ RESET = "\033[0m"
27
+ BOLD = "\033[1m"
28
+ DIM = "\033[2m"
29
+ ITALIC = "\033[3m"
30
+ UNDERLINE = "\033[4m"
31
+ BLINK = "\033[5m"
32
+ REVERSE = "\033[7m"
33
+ STRIKE = "\033[9m"
34
+
35
+ BLACK = "\033[30m"
36
+ RED = "\033[31m"
37
+ GREEN = "\033[32m"
38
+ YELLOW = "\033[33m"
39
+ BLUE = "\033[34m"
40
+ MAGENTA = "\033[35m"
41
+ CYAN = "\033[36m"
42
+ WHITE = "\033[37m"
43
+ GRAY = "\033[90m"
44
+
45
+ BRIGHT_RED = "\033[91m"
46
+ BRIGHT_GREEN = "\033[92m"
47
+ BRIGHT_YELLOW = "\033[93m"
48
+ BRIGHT_BLUE = "\033[94m"
49
+ BRIGHT_MAGENTA = "\033[95m"
50
+ BRIGHT_CYAN = "\033[96m"
51
+ BRIGHT_WHITE = "\033[97m"
52
+
53
+ BG_BLACK = "\033[40m"
54
+ BG_RED = "\033[41m"
55
+ BG_GREEN = "\033[42m"
56
+ BG_YELLOW = "\033[43m"
57
+ BG_BLUE = "\033[44m"
58
+ BG_MAGENTA = "\033[45m"
59
+ BG_CYAN = "\033[46m"
60
+ BG_WHITE = "\033[47m"
61
+
62
+
63
+ def rgb(r: int, g: int, b: int) -> str:
64
+ return f"\033[38;2;{r};{g};{b}m"
65
+
66
+ def bg_rgb(r: int, g: int, b: int) -> str:
67
+ return f"\033[48;2;{r};{g};{b}m"
68
+
69
+ def hex_color(hex_code: str) -> str:
70
+ hex_code = hex_code.lstrip("#")
71
+ r, g, b = int(hex_code[0:2], 16), int(hex_code[2:4], 16), int(hex_code[4:6], 16)
72
+ return rgb(r, g, b)
73
+
74
+ def bg_hex(hex_code: str) -> str:
75
+ hex_code = hex_code.lstrip("#")
76
+ r, g, b = int(hex_code[0:2], 16), int(hex_code[2:4], 16), int(hex_code[4:6], 16)
77
+ return bg_rgb(r, g, b)
78
+
79
+
80
+ # ══════════════════════════════════════════
81
+ # Themes
82
+ # ══════════════════════════════════════════
83
+
84
+ class Theme:
85
+ def __init__(self, name: str, colors: Dict[str, str]):
86
+ self.name = name
87
+ self.colors = colors
88
+
89
+ def get(self, key: str) -> str:
90
+ return self.colors.get(key, Colors.RESET)
91
+
92
+
93
+ THEMES = {
94
+ "default": Theme("default", {
95
+ "timestamp": rgb(100, 100, 100),
96
+ "bracket": rgb(70, 70, 70),
97
+ "dot": rgb(255, 255, 255),
98
+ "separator": rgb(100, 100, 100),
99
+ "debug": rgb(138, 180, 248),
100
+ "info": rgb(130, 170, 255),
101
+ "success": rgb(80, 230, 120),
102
+ "warning": rgb(255, 200, 60),
103
+ "error": rgb(255, 85, 85),
104
+ "critical": rgb(255, 50, 50),
105
+ "fatal": rgb(200, 0, 0),
106
+ "trace": rgb(160, 160, 160),
107
+ "divider": rgb(60, 60, 60),
108
+ "label_debug": "DEBUG",
109
+ "label_info": "INFO",
110
+ "label_success": "SUCCESS",
111
+ "label_warning": "WARNING",
112
+ "label_error": "ERROR",
113
+ "label_critical": "CRITICAL",
114
+ "label_fatal": "FATAL",
115
+ "label_trace": "TRACE",
116
+ }),
117
+ "neon": Theme("neon", {
118
+ "timestamp": rgb(0, 255, 200),
119
+ "bracket": rgb(0, 200, 150),
120
+ "dot": rgb(0, 255, 255),
121
+ "separator": rgb(0, 180, 180),
122
+ "debug": rgb(0, 200, 255),
123
+ "info": rgb(0, 255, 200),
124
+ "success": rgb(0, 255, 100),
125
+ "warning": rgb(255, 255, 0),
126
+ "error": rgb(255, 50, 80),
127
+ "critical": rgb(255, 0, 100),
128
+ "fatal": rgb(200, 0, 50),
129
+ "trace": rgb(100, 200, 200),
130
+ "divider": rgb(0, 100, 100),
131
+ "label_debug": "DEBUG",
132
+ "label_info": "INFO",
133
+ "label_success": "SUCCESS",
134
+ "label_warning": "WARNING",
135
+ "label_error": "ERROR",
136
+ "label_critical": "CRITICAL",
137
+ "label_fatal": "FATAL",
138
+ "label_trace": "TRACE",
139
+ }),
140
+ "minimal": Theme("minimal", {
141
+ "timestamp": rgb(120, 120, 120),
142
+ "bracket": rgb(80, 80, 80),
143
+ "dot": rgb(180, 180, 180),
144
+ "separator": rgb(100, 100, 100),
145
+ "debug": rgb(150, 150, 150),
146
+ "info": rgb(200, 200, 200),
147
+ "success": rgb(100, 200, 100),
148
+ "warning": rgb(200, 180, 80),
149
+ "error": rgb(200, 80, 80),
150
+ "critical": rgb(220, 50, 50),
151
+ "fatal": rgb(180, 0, 0),
152
+ "trace": rgb(120, 120, 120),
153
+ "divider": rgb(60, 60, 60),
154
+ "label_debug": "DBG",
155
+ "label_info": "INF",
156
+ "label_success": "OK",
157
+ "label_warning": "WRN",
158
+ "label_error": "ERR",
159
+ "label_critical": "CRT",
160
+ "label_fatal": "FTL",
161
+ "label_trace": "TRC",
162
+ }),
163
+ "hacker": Theme("hacker", {
164
+ "timestamp": rgb(0, 180, 0),
165
+ "bracket": rgb(0, 120, 0),
166
+ "dot": rgb(0, 255, 0),
167
+ "separator": rgb(0, 150, 0),
168
+ "debug": rgb(0, 200, 0),
169
+ "info": rgb(0, 255, 0),
170
+ "success": rgb(50, 255, 50),
171
+ "warning": rgb(200, 255, 0),
172
+ "error": rgb(255, 100, 0),
173
+ "critical": rgb(255, 50, 0),
174
+ "fatal": rgb(255, 0, 0),
175
+ "trace": rgb(0, 150, 0),
176
+ "divider": rgb(0, 80, 0),
177
+ "label_debug": "DEBUG",
178
+ "label_info": "INFO",
179
+ "label_success": "SUCCESS",
180
+ "label_warning": "WARNING",
181
+ "label_error": "ERROR",
182
+ "label_critical": "CRITICAL",
183
+ "label_fatal": "FATAL",
184
+ "label_trace": "TRACE",
185
+ }),
186
+ "sunset": Theme("sunset", {
187
+ "timestamp": rgb(255, 150, 50),
188
+ "bracket": rgb(200, 100, 50),
189
+ "dot": rgb(255, 200, 100),
190
+ "separator": rgb(200, 120, 60),
191
+ "debug": rgb(255, 180, 100),
192
+ "info": rgb(255, 160, 80),
193
+ "success": rgb(255, 220, 50),
194
+ "warning": rgb(255, 120, 30),
195
+ "error": rgb(255, 60, 60),
196
+ "critical": rgb(200, 30, 30),
197
+ "fatal": rgb(150, 0, 0),
198
+ "trace": rgb(200, 150, 100),
199
+ "divider": rgb(150, 80, 40),
200
+ "label_debug": "DEBUG",
201
+ "label_info": "INFO",
202
+ "label_success": "SUCCESS",
203
+ "label_warning": "WARNING",
204
+ "label_error": "ERROR",
205
+ "label_critical": "CRITICAL",
206
+ "label_fatal": "FATAL",
207
+ "label_trace": "TRACE",
208
+ }),
209
+ "ocean": Theme("ocean", {
210
+ "timestamp": rgb(0, 150, 200),
211
+ "bracket": rgb(0, 100, 160),
212
+ "dot": rgb(0, 200, 255),
213
+ "separator": rgb(0, 130, 180),
214
+ "debug": rgb(100, 180, 255),
215
+ "info": rgb(0, 200, 255),
216
+ "success": rgb(0, 255, 200),
217
+ "warning": rgb(255, 220, 80),
218
+ "error": rgb(255, 80, 100),
219
+ "critical": rgb(255, 40, 80),
220
+ "fatal": rgb(200, 0, 50),
221
+ "trace": rgb(80, 150, 200),
222
+ "divider": rgb(0, 80, 120),
223
+ "label_debug": "DEBUG",
224
+ "label_info": "INFO",
225
+ "label_success": "SUCCESS",
226
+ "label_warning": "WARNING",
227
+ "label_error": "ERROR",
228
+ "label_critical": "CRITICAL",
229
+ "label_fatal": "FATAL",
230
+ "label_trace": "TRACE",
231
+ }),
232
+ }
233
+
234
+
235
+ # ══════════════════════════════════════════
236
+ # Log Styles (different display formats)
237
+ # ══════════════════════════════════════════
238
+
239
+ LEVEL_ICONS = {
240
+ "debug": "●",
241
+ "info": "ℹ",
242
+ "success": "✓",
243
+ "warning": "⚠",
244
+ "error": "✗",
245
+ "critical": "☠",
246
+ "fatal": "💀",
247
+ "trace": "→",
248
+ }
249
+
250
+ def _style_default(now, level, level_color, level_label, message, t, show_time):
251
+ R = Colors.RESET
252
+ B = Colors.BOLD
253
+ br = t.get("bracket")
254
+ ts = t.get("timestamp")
255
+ dt = t.get("dot")
256
+ sep = t.get("separator")
257
+ if show_time:
258
+ return (
259
+ f"{br}[-({ts}{now}{R}{br})-]{R} "
260
+ f"{br}[{R} {dt}●{R} {br}]{R} "
261
+ f"{B}{level_color}{level_label}{R} "
262
+ f"{sep}»{R} "
263
+ f"{level_color}{message}{R}"
264
+ )
265
+ return (
266
+ f"{br}[{R} {dt}●{R} {br}]{R} "
267
+ f"{B}{level_color}{level_label}{R} "
268
+ f"{sep}»{R} "
269
+ f"{level_color}{message}{R}"
270
+ )
271
+
272
+ def _style_box(now, level, level_color, level_label, message, t, show_time):
273
+ R = Colors.RESET
274
+ B = Colors.BOLD
275
+ br = t.get("bracket")
276
+ ts = t.get("timestamp")
277
+ icon = LEVEL_ICONS.get(level, "●")
278
+ if show_time:
279
+ return (
280
+ f"{br}┃{R} {ts}{now}{R} "
281
+ f"{br}│{R} {level_color}{icon}{R} "
282
+ f"{B}{level_color}{level_label.ljust(8)}{R} "
283
+ f"{br}│{R} {level_color}{message}{R}"
284
+ )
285
+ return (
286
+ f"{br}┃{R} {level_color}{icon}{R} "
287
+ f"{B}{level_color}{level_label.ljust(8)}{R} "
288
+ f"{br}│{R} {level_color}{message}{R}"
289
+ )
290
+
291
+ def _style_modern(now, level, level_color, level_label, message, t, show_time):
292
+ R = Colors.RESET
293
+ B = Colors.BOLD
294
+ ts = t.get("timestamp")
295
+ icon = LEVEL_ICONS.get(level, "●")
296
+ if show_time:
297
+ return (
298
+ f"{ts}{now}{R} "
299
+ f"{level_color}{icon} {B}{level_label}{R} "
300
+ f"{level_color}{message}{R}"
301
+ )
302
+ return (
303
+ f"{level_color}{icon} {B}{level_label}{R} "
304
+ f"{level_color}{message}{R}"
305
+ )
306
+
307
+ def _style_bracket(now, level, level_color, level_label, message, t, show_time):
308
+ R = Colors.RESET
309
+ B = Colors.BOLD
310
+ br = t.get("bracket")
311
+ ts = t.get("timestamp")
312
+ if show_time:
313
+ return (
314
+ f"{br}[{ts}{now}{R}{br}]{R} "
315
+ f"{br}[{B}{level_color}{level_label}{R}{br}]{R} "
316
+ f"{level_color}{message}{R}"
317
+ )
318
+ return (
319
+ f"{br}[{B}{level_color}{level_label}{R}{br}]{R} "
320
+ f"{level_color}{message}{R}"
321
+ )
322
+
323
+ def _style_arrow(now, level, level_color, level_label, message, t, show_time):
324
+ R = Colors.RESET
325
+ B = Colors.BOLD
326
+ ts = t.get("timestamp")
327
+ sep = t.get("separator")
328
+ if show_time:
329
+ return (
330
+ f"{ts}{now}{R} "
331
+ f"{sep}▸{R} "
332
+ f"{B}{level_color}{level_label}{R} "
333
+ f"{sep}▸{R} "
334
+ f"{level_color}{message}{R}"
335
+ )
336
+ return (
337
+ f"{sep}▸{R} "
338
+ f"{B}{level_color}{level_label}{R} "
339
+ f"{sep}▸{R} "
340
+ f"{level_color}{message}{R}"
341
+ )
342
+
343
+ def _style_pipe(now, level, level_color, level_label, message, t, show_time):
344
+ R = Colors.RESET
345
+ B = Colors.BOLD
346
+ ts = t.get("timestamp")
347
+ br = t.get("bracket")
348
+ if show_time:
349
+ return (
350
+ f"{level_color}▌{R} {ts}{now}{R} "
351
+ f"{br}|{R} {B}{level_color}{level_label.ljust(8)}{R} "
352
+ f"{br}|{R} {level_color}{message}{R}"
353
+ )
354
+ return (
355
+ f"{level_color}▌{R} {B}{level_color}{level_label.ljust(8)}{R} "
356
+ f"{br}|{R} {level_color}{message}{R}"
357
+ )
358
+
359
+ def _style_tag(now, level, level_color, level_label, message, t, show_time):
360
+ R = Colors.RESET
361
+ B = Colors.BOLD
362
+ ts = t.get("timestamp")
363
+ bg = {
364
+ "debug": bg_rgb(50, 80, 120),
365
+ "info": bg_rgb(40, 70, 130),
366
+ "success": bg_rgb(30, 100, 50),
367
+ "warning": bg_rgb(130, 100, 0),
368
+ "error": bg_rgb(130, 30, 30),
369
+ "critical": bg_rgb(150, 20, 20),
370
+ "fatal": bg_rgb(120, 0, 0),
371
+ "trace": bg_rgb(60, 60, 60),
372
+ }.get(level, "")
373
+ if show_time:
374
+ return (
375
+ f"{ts}{now}{R} "
376
+ f"{bg}{B} {level_label} {R} "
377
+ f"{level_color}{message}{R}"
378
+ )
379
+ return (
380
+ f"{bg}{B} {level_label} {R} "
381
+ f"{level_color}{message}{R}"
382
+ )
383
+
384
+ def _style_dots(now, level, level_color, level_label, message, t, show_time):
385
+ R = Colors.RESET
386
+ B = Colors.BOLD
387
+ ts = t.get("timestamp")
388
+ sep = t.get("separator")
389
+ icon = LEVEL_ICONS.get(level, "●")
390
+ if show_time:
391
+ return (
392
+ f"{level_color}{icon}{R} "
393
+ f"{ts}{now}{R} "
394
+ f"{sep}·{R} "
395
+ f"{B}{level_color}{level_label}{R} "
396
+ f"{sep}·{R} "
397
+ f"{level_color}{message}{R}"
398
+ )
399
+ return (
400
+ f"{level_color}{icon}{R} "
401
+ f"{B}{level_color}{level_label}{R} "
402
+ f"{sep}·{R} "
403
+ f"{level_color}{message}{R}"
404
+ )
405
+
406
+ def _style_clean(now, level, level_color, level_label, message, t, show_time):
407
+ R = Colors.RESET
408
+ B = Colors.BOLD
409
+ ts = t.get("timestamp")
410
+ if show_time:
411
+ return (
412
+ f" {ts}{now}{R} "
413
+ f"{B}{level_color}{level_label.ljust(8)}{R} "
414
+ f"{level_color}{message}{R}"
415
+ )
416
+ return (
417
+ f" {B}{level_color}{level_label.ljust(8)}{R} "
418
+ f"{level_color}{message}{R}"
419
+ )
420
+
421
+ STYLES = {
422
+ "default": _style_default,
423
+ "box": _style_box,
424
+ "modern": _style_modern,
425
+ "bracket": _style_bracket,
426
+ "arrow": _style_arrow,
427
+ "pipe": _style_pipe,
428
+ "tag": _style_tag,
429
+ "dots": _style_dots,
430
+ "clean": _style_clean,
431
+ }
432
+
433
+
434
+ # ══════════════════════════════════════════
435
+ # Logger Configuration
436
+ # ══════════════════════════════════════════
437
+
438
+ class LogLevel(Enum):
439
+ TRACE = 0
440
+ DEBUG = 1
441
+ INFO = 2
442
+ SUCCESS = 3
443
+ WARNING = 4
444
+ ERROR = 5
445
+ CRITICAL = 6
446
+ FATAL = 7
447
+
448
+
449
+ class LoggerConfig:
450
+ def __init__(self):
451
+ self.theme: Theme = THEMES["default"]
452
+ self.style: str = "default"
453
+ self.time_format: str = "%H:%M:%S"
454
+ self.show_time: bool = True
455
+ self.log_file: Optional[str] = None
456
+ self.min_level: LogLevel = LogLevel.TRACE
457
+ self._file_handle = None
458
+ self._lock = threading.Lock()
459
+
460
+ def set_theme(self, theme_name: str):
461
+ if theme_name in THEMES:
462
+ self.theme = THEMES[theme_name]
463
+ else:
464
+ raise ValueError(f"Unknown theme: {theme_name}. Available: {list(THEMES.keys())}")
465
+
466
+ def set_style(self, style_name: str):
467
+ if style_name in STYLES:
468
+ self.style = style_name
469
+ else:
470
+ raise ValueError(f"Unknown style: {style_name}. Available: {list(STYLES.keys())}")
471
+
472
+ def add_theme(self, name: str, colors: Dict[str, str]):
473
+ theme = Theme(name, colors)
474
+ THEMES[name] = theme
475
+ return theme
476
+
477
+ def set_log_file(self, path: str):
478
+ self.log_file = path
479
+ if self._file_handle:
480
+ self._file_handle.close()
481
+ self._file_handle = open(path, "a", encoding="utf-8")
482
+
483
+ def close(self):
484
+ if self._file_handle:
485
+ self._file_handle.close()
486
+ self._file_handle = None
487
+
488
+
489
+ # ══════════════════════════════════════════
490
+ # Global Config
491
+ # ══════════════════════════════════════════
492
+
493
+ _config = LoggerConfig()
494
+
495
+ def _strip_ansi(text: str) -> str:
496
+ return re.sub(r'\033\[[0-9;]*m', '', text)
497
+
498
+ def _get_time() -> str:
499
+ return datetime.now().strftime(_config.time_format)
500
+
501
+ def _write_to_file(message: str):
502
+ if _config._file_handle:
503
+ try:
504
+ clean = _strip_ansi(message)
505
+ _config._file_handle.write(clean + "\n")
506
+ _config._file_handle.flush()
507
+ except:
508
+ pass
509
+
510
+
511
+ # ══════════════════════════════════════════
512
+ # Core Log Function
513
+ # ══════════════════════════════════════════
514
+
515
+ def _log(level: str, message: str, level_enum: LogLevel):
516
+ if level_enum.value < _config.min_level.value:
517
+ return
518
+
519
+ t = _config.theme
520
+ now = _get_time()
521
+ level_color = t.get(level)
522
+ level_label = t.get(f"label_{level}")
523
+
524
+ style_fn = STYLES.get(_config.style, _style_default)
525
+ line = style_fn(now, level, level_color, level_label, message, t, _config.show_time)
526
+
527
+ with _config._lock:
528
+ print(line)
529
+ _write_to_file(line)
530
+
531
+
532
+ # ══════════════════════════════════════════
533
+ # Log Functions
534
+ # ══════════════════════════════════════════
535
+
536
+ def debug(message: str):
537
+ _log("debug", message, LogLevel.DEBUG)
538
+
539
+ def info(message: str):
540
+ _log("info", message, LogLevel.INFO)
541
+
542
+ def success(message: str):
543
+ _log("success", message, LogLevel.SUCCESS)
544
+
545
+ def warn(message: str):
546
+ _log("warning", message, LogLevel.WARNING)
547
+
548
+ def warning(message: str):
549
+ _log("warning", message, LogLevel.WARNING)
550
+
551
+ def error(message: str):
552
+ _log("error", message, LogLevel.ERROR)
553
+
554
+ def critical(message: str):
555
+ _log("critical", message, LogLevel.CRITICAL)
556
+
557
+ def fatal(message: str):
558
+ _log("fatal", message, LogLevel.FATAL)
559
+
560
+ def trace(message: str):
561
+ _log("trace", message, LogLevel.TRACE)
562
+
563
+
564
+ # ══════════════════════════════════════════
565
+ # Utility Functions
566
+ # ══════════════════════════════════════════
567
+
568
+ def divider(char: str = "─", length: int = 60, color: Optional[str] = None):
569
+ c = color or _config.theme.get("divider")
570
+ line = f"{c}{char * length}{Colors.RESET}"
571
+ with _config._lock:
572
+ print(line)
573
+ _write_to_file(line)
574
+
575
+ def section(title: str, char: str = "═", length: int = 60):
576
+ t = _config.theme
577
+ title_color = t.get("info")
578
+ div_color = t.get("divider")
579
+ padding = (length - len(title) - 2) // 2
580
+ left = char * max(padding, 1)
581
+ right = char * max(length - len(title) - 2 - padding, 1)
582
+ line = f"{div_color}{left}{Colors.RESET} {Colors.BOLD}{title_color}{title}{Colors.RESET} {div_color}{right}{Colors.RESET}"
583
+ with _config._lock:
584
+ print(line)
585
+ _write_to_file(line)
586
+
587
+ def blank(count: int = 1):
588
+ for _ in range(count):
589
+ print()
590
+
591
+ def rule(text: str = "", char: str = "─", length: int = 60):
592
+ if text:
593
+ section(text, char, length)
594
+ else:
595
+ divider(char, length)
596
+
597
+ def banner(text: str, border_char: str = "═", padding: int = 2):
598
+ lines = text.split("\n")
599
+ max_len = max(len(line) for line in lines)
600
+ width = max_len + (padding * 2) + 2
601
+ t = _config.theme
602
+ bc = t.get("info")
603
+ tc = t.get("success")
604
+ R = Colors.RESET
605
+
606
+ top = f"{bc}╔{border_char * (width - 2)}╗{R}"
607
+ bottom = f"{bc}╚{border_char * (width - 2)}╝{R}"
608
+
609
+ with _config._lock:
610
+ print(top)
611
+ for line in lines:
612
+ padded = line.center(max_len)
613
+ print(f"{bc}║{R}{' ' * padding}{tc}{padded}{R}{' ' * padding}{bc}║{R}")
614
+ print(bottom)
615
+
616
+
617
+ # ══════════════════════════════════════════
618
+ # Gradient Text
619
+ # ══════════════════════════════════════════
620
+
621
+ def _interpolate(c1: tuple, c2: tuple, t: float) -> tuple:
622
+ return (
623
+ int(c1[0] + (c2[0] - c1[0]) * t),
624
+ int(c1[1] + (c2[1] - c1[1]) * t),
625
+ int(c1[2] + (c2[2] - c1[2]) * t),
626
+ )
627
+
628
+ def gradient_text(text: str, start_color: tuple = (255, 0, 100), end_color: tuple = (100, 0, 255)) -> str:
629
+ result = []
630
+ length = max(len(text) - 1, 1)
631
+ for i, char in enumerate(text):
632
+ t = i / length
633
+ r, g, b = _interpolate(start_color, end_color, t)
634
+ result.append(f"{rgb(r, g, b)}{char}")
635
+ result.append(Colors.RESET)
636
+ return "".join(result)
637
+
638
+ def gradient_print(text: str, start_color: tuple = (255, 0, 100), end_color: tuple = (100, 0, 255)):
639
+ print(gradient_text(text, start_color, end_color))
640
+
641
+ def multi_gradient_text(text: str, colors: List[tuple]) -> str:
642
+ if len(colors) < 2:
643
+ return text
644
+ result = []
645
+ segments = len(colors) - 1
646
+ chars_per_seg = max(len(text) / segments, 1)
647
+ for i, char in enumerate(text):
648
+ seg = min(int(i / chars_per_seg), segments - 1)
649
+ t = (i - seg * chars_per_seg) / chars_per_seg
650
+ r, g, b = _interpolate(colors[seg], colors[seg + 1], t)
651
+ result.append(f"{rgb(r, g, b)}{char}")
652
+ result.append(Colors.RESET)
653
+ return "".join(result)
654
+
655
+ def multi_gradient_print(text: str, colors: List[tuple]):
656
+ print(multi_gradient_text(text, colors))
657
+
658
+ def rainbow_text(text: str) -> str:
659
+ colors = [
660
+ (255, 0, 0), (255, 127, 0), (255, 255, 0),
661
+ (0, 255, 0), (0, 127, 255), (75, 0, 130), (148, 0, 211)
662
+ ]
663
+ return multi_gradient_text(text, colors)
664
+
665
+ def rainbow_print(text: str):
666
+ print(rainbow_text(text))
667
+
668
+
669
+ # ══════════════════════════════════════════
670
+ # Panel
671
+ # ══════════════════════════════════════════
672
+
673
+ def panel(content: str, title: str = "", width: int = 50, border_color: Optional[str] = None,
674
+ style: str = "rounded"):
675
+ bc = border_color or _config.theme.get("info")
676
+ R = Colors.RESET
677
+
678
+ borders = {
679
+ "rounded": ("╭", "╮", "╰", "╯", "─", "│"),
680
+ "sharp": ("┌", "┐", "└", "┘", "─", "│"),
681
+ "double": ("╔", "╗", "╚", "╝", "═", "║"),
682
+ "heavy": ("┏", "┓", "┗", "┛", "━", "┃"),
683
+ "ascii": ("+", "+", "+", "+", "-", "|"),
684
+ }
685
+ tl, tr, bl, br_char, h, v = borders.get(style, borders["rounded"])
686
+
687
+ lines = content.split("\n")
688
+ inner_w = width - 4
689
+
690
+ if title:
691
+ title_stripped = _strip_ansi(title)
692
+ top = f"{bc}{tl}{h} {Colors.BOLD}{title}{R}{bc} {h * max(width - len(title_stripped) - 5, 1)}{tr}{R}"
693
+ else:
694
+ top = f"{bc}{tl}{h * (width - 2)}{tr}{R}"
695
+
696
+ bottom = f"{bc}{bl}{h * (width - 2)}{br_char}{R}"
697
+
698
+ with _config._lock:
699
+ print(top)
700
+ for line in lines:
701
+ stripped = _strip_ansi(line)
702
+ pad = inner_w - len(stripped)
703
+ print(f"{bc}{v}{R} {line}{' ' * max(pad, 0)} {bc}{v}{R}")
704
+ print(bottom)
705
+
706
+
707
+ # ══════════════════════════════════════════
708
+ # Table
709
+ # ══════════════════════════════════════════
710
+
711
+ def table(headers: List[str], rows: List[List[str]], border_color: Optional[str] = None,
712
+ style: str = "rounded"):
713
+ bc = border_color or _config.theme.get("info")
714
+ hc = _config.theme.get("success")
715
+ R = Colors.RESET
716
+ B = Colors.BOLD
717
+
718
+ borders = {
719
+ "rounded": ("╭", "╮", "╰", "╯", "─", "│", "┬", "┴", "├", "┤", "┼"),
720
+ "sharp": ("┌", "┐", "└", "┘", "─", "│", "┬", "┴", "├", "┤", "┼"),
721
+ "double": ("╔", "╗", "╚", "╝", "═", "║", "╦", "╩", "╠", "╣", "╬"),
722
+ "heavy": ("┏", "┓", "┗", "┛", "━", "┃", "┳", "┻", "┣", "┫", "╋"),
723
+ "ascii": ("+", "+", "+", "+", "-", "|", "+", "+", "+", "+", "+"),
724
+ }
725
+ tl, tr, bl, br_char, h, v, tm, bm, ml, mr, mm = borders.get(style, borders["rounded"])
726
+
727
+ col_widths = [len(str(hdr)) for hdr in headers]
728
+ for row in rows:
729
+ for i, cell in enumerate(row):
730
+ if i < len(col_widths):
731
+ col_widths[i] = max(col_widths[i], len(str(cell)))
732
+
733
+ def make_sep(left, mid, right, fill):
734
+ parts = [fill * (w + 2) for w in col_widths]
735
+ return f"{bc}{left}{mid.join(parts)}{right}{R}"
736
+
737
+ def make_row(cells, cell_color=""):
738
+ parts = []
739
+ for i, cell in enumerate(cells):
740
+ w = col_widths[i] if i < len(col_widths) else 10
741
+ parts.append(f" {cell_color}{str(cell).ljust(w)}{R} ")
742
+ return f"{bc}{v}{R}{f'{bc}{v}{R}'.join(parts)}{bc}{v}{R}"
743
+
744
+ with _config._lock:
745
+ print(make_sep(tl, tm, tr, h))
746
+ print(make_row(headers, f"{B}{hc}"))
747
+ print(make_sep(ml, mm, mr, h))
748
+ for row in rows:
749
+ print(make_row(row))
750
+ print(make_sep(bl, bm, br_char, h))
751
+
752
+
753
+ # ══════════════════════════════════════════
754
+ # Box (wraps text in a box)
755
+ # ══════════════════════════════════════════
756
+
757
+ def box(text: str, width: Optional[int] = None, style: str = "rounded",
758
+ color: Optional[str] = None, text_color: Optional[str] = None,
759
+ align: str = "left", padding: int = 1):
760
+ bc = color or _config.theme.get("info")
761
+ tc = text_color or Colors.RESET
762
+ R = Colors.RESET
763
+
764
+ borders = {
765
+ "rounded": ("╭", "╮", "╰", "╯", "─", "│"),
766
+ "sharp": ("┌", "┐", "└", "┘", "─", "│"),
767
+ "double": ("╔", "╗", "╚", "╝", "═", "║"),
768
+ "heavy": ("┏", "┓", "┗", "┛", "━", "┃"),
769
+ "ascii": ("+", "+", "+", "+", "-", "|"),
770
+ "stars": ("*", "*", "*", "*", "*", "*"),
771
+ "dashes": ("+", "+", "+", "+", "~", ":"),
772
+ }
773
+ tl, tr, bl, br_char, h, v = borders.get(style, borders["rounded"])
774
+
775
+ lines = text.split("\n")
776
+ max_line = max(len(_strip_ansi(l)) for l in lines)
777
+ inner_w = (width - 4) if width else max_line + (padding * 2)
778
+ total_w = inner_w + 4
779
+
780
+ top = f"{bc}{tl}{h * (total_w - 2)}{tr}{R}"
781
+ bottom = f"{bc}{bl}{h * (total_w - 2)}{br_char}{R}"
782
+
783
+ with _config._lock:
784
+ print(top)
785
+ for _ in range(padding - 1 if padding > 1 else 0):
786
+ print(f"{bc}{v}{R}{' ' * (total_w - 2)}{bc}{v}{R}")
787
+ for line in lines:
788
+ stripped = _strip_ansi(line)
789
+ if align == "center":
790
+ lpad = (inner_w - len(stripped)) // 2
791
+ rpad = inner_w - len(stripped) - lpad
792
+ padded = f"{' ' * lpad}{tc}{line}{R}{' ' * rpad}"
793
+ elif align == "right":
794
+ pad = inner_w - len(stripped)
795
+ padded = f"{' ' * pad}{tc}{line}{R}"
796
+ else:
797
+ pad = inner_w - len(stripped)
798
+ padded = f"{tc}{line}{R}{' ' * pad}"
799
+ print(f"{bc}{v}{R} {padded} {bc}{v}{R}")
800
+ for _ in range(padding - 1 if padding > 1 else 0):
801
+ print(f"{bc}{v}{R}{' ' * (total_w - 2)}{bc}{v}{R}")
802
+ print(bottom)
803
+
804
+
805
+ # ══════════════════════════════════════════
806
+ # Progress Bar
807
+ # ══════════════════════════════════════════
808
+
809
+ class ProgressBar:
810
+ def __init__(self, total: int, width: int = 30, label: str = "Progress",
811
+ fill_char: str = "█", empty_char: str = "░",
812
+ color: Optional[str] = None):
813
+ self.total = total
814
+ self.width = width
815
+ self.label = label
816
+ self.fill_char = fill_char
817
+ self.empty_char = empty_char
818
+ self.color = color or _config.theme.get("success")
819
+ self.current = 0
820
+ self._start_time = time.time()
821
+
822
+ def update(self, amount: int = 1):
823
+ self.current = min(self.current + amount, self.total)
824
+ self._render()
825
+
826
+ def set(self, value: int):
827
+ self.current = min(value, self.total)
828
+ self._render()
829
+
830
+ def _render(self):
831
+ pct = self.current / self.total if self.total > 0 else 0
832
+ filled = int(self.width * pct)
833
+ bar = self.fill_char * filled + self.empty_char * (self.width - filled)
834
+ elapsed = time.time() - self._start_time
835
+ eta = (elapsed / pct - elapsed) if pct > 0 else 0
836
+
837
+ R = Colors.RESET
838
+ line = (
839
+ f"\r{self.color}{self.label}{R} "
840
+ f"│{self.color}{bar}{R}│ "
841
+ f"{Colors.BOLD}{pct * 100:5.1f}%{R} "
842
+ f"({self.current}/{self.total}) "
843
+ f"[{elapsed:.1f}s / ETA {eta:.1f}s]"
844
+ )
845
+ sys.stdout.write(line)
846
+ sys.stdout.flush()
847
+ if self.current >= self.total:
848
+ print()
849
+
850
+ def finish(self):
851
+ self.set(self.total)
852
+
853
+
854
+ # ══════════════════════════════════════════
855
+ # Spinner
856
+ # ══════════════════════════════════════════
857
+
858
+ class Spinner:
859
+ STYLES = {
860
+ "dots": ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
861
+ "line": ["-", "\\", "|", "/"],
862
+ "arrow": ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"],
863
+ "bounce": ["⠁", "⠂", "⠄", "⠂"],
864
+ "box": ["▖", "▘", "▝", "▗"],
865
+ "circle": ["◐", "◓", "◑", "◒"],
866
+ "pulse": ["○", "◎", "●", "◎"],
867
+ "star": ["✶", "✸", "✹", "✺", "✹", "✸"],
868
+ "moon": ["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"],
869
+ "clock": ["🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚", "🕛"],
870
+ }
871
+
872
+ def __init__(self, message: str = "Loading", style: str = "dots", color: Optional[str] = None):
873
+ self.message = message
874
+ self.frames = self.STYLES.get(style, self.STYLES["dots"])
875
+ self.color = color or _config.theme.get("info")
876
+ self._running = False
877
+ self._thread = None
878
+ self._frame_idx = 0
879
+
880
+ def start(self):
881
+ self._running = True
882
+ self._thread = threading.Thread(target=self._spin, daemon=True)
883
+ self._thread.start()
884
+ return self
885
+
886
+ def _spin(self):
887
+ while self._running:
888
+ frame = self.frames[self._frame_idx % len(self.frames)]
889
+ sys.stdout.write(f"\r{self.color}{frame}{Colors.RESET} {self.message}")
890
+ sys.stdout.flush()
891
+ self._frame_idx += 1
892
+ time.sleep(0.08)
893
+
894
+ def stop(self, final_message: str = ""):
895
+ self._running = False
896
+ if self._thread:
897
+ self._thread.join()
898
+ sys.stdout.write("\r" + " " * (len(self.message) + 10) + "\r")
899
+ if final_message:
900
+ success(final_message)
901
+
902
+ def __enter__(self):
903
+ self.start()
904
+ return self
905
+
906
+ def __exit__(self, *args):
907
+ self.stop()
908
+
909
+
910
+ # ══════════════════════════════════════════
911
+ # Timer
912
+ # ══════════════════════════════════════════
913
+
914
+ class Timer:
915
+ def __init__(self, label: str = "Elapsed"):
916
+ self.label = label
917
+ self._start = None
918
+ self._end = None
919
+
920
+ def start(self):
921
+ self._start = time.time()
922
+ return self
923
+
924
+ def stop(self) -> float:
925
+ self._end = time.time()
926
+ elapsed = self._end - self._start
927
+ info(f"{self.label}: {elapsed:.3f}s")
928
+ return elapsed
929
+
930
+ def __enter__(self):
931
+ self.start()
932
+ return self
933
+
934
+ def __exit__(self, *args):
935
+ self.stop()
936
+
937
+
938
+ # ══════════════════════════════════════════
939
+ # Pair Logging (key-value)
940
+ # ══════════════════════════════════════════
941
+
942
+ def pair(key: str, value: str, key_color: Optional[str] = None, value_color: Optional[str] = None):
943
+ kc = key_color or _config.theme.get("info")
944
+ vc = value_color or Colors.RESET
945
+ line = f" {kc}{Colors.BOLD}{key}{Colors.RESET}: {vc}{value}{Colors.RESET}"
946
+ with _config._lock:
947
+ print(line)
948
+ _write_to_file(line)
949
+
950
+
951
+ # ══════════════════════════════════════════
952
+ # Preview: Show all styles
953
+ # ══════════════════════════════════════════
954
+
955
+ def preview_styles():
956
+ current_style = _config.style
957
+ for style_name in STYLES:
958
+ _config.style = style_name
959
+ section(f"Style: {style_name}")
960
+ debug("Debug message")
961
+ info("Info message")
962
+ success("Success message")
963
+ warn("Warning message")
964
+ error("Error message")
965
+ blank()
966
+ _config.style = current_style
967
+
968
+ def preview_themes():
969
+ current_theme_name = _config.theme.name
970
+ for theme_name in THEMES:
971
+ _config.set_theme(theme_name)
972
+ section(f"Theme: {theme_name}")
973
+ debug("Debug message")
974
+ success("Success message")
975
+ error("Error message")
976
+ blank()
977
+ _config.set_theme(current_theme_name)
978
+
979
+
980
+ # ══════════════════════════════════════════
981
+ # Configuration API
982
+ # ══════════════════════════════════════════
983
+
984
+ def set_theme(name: str):
985
+ _config.set_theme(name)
986
+
987
+ def set_style(name: str):
988
+ _config.set_style(name)
989
+
990
+ def add_theme(name: str, colors: Dict[str, str]):
991
+ _config.add_theme(name, colors)
992
+
993
+ def set_time_format(fmt: str):
994
+ _config.time_format = fmt
995
+
996
+ def show_time(show: bool = True):
997
+ _config.show_time = show
998
+
999
+ def set_log_file(path: str):
1000
+ _config.set_log_file(path)
1001
+
1002
+ def set_min_level(level: str):
1003
+ level_map = {
1004
+ "trace": LogLevel.TRACE,
1005
+ "debug": LogLevel.DEBUG,
1006
+ "info": LogLevel.INFO,
1007
+ "success": LogLevel.SUCCESS,
1008
+ "warning": LogLevel.WARNING,
1009
+ "error": LogLevel.ERROR,
1010
+ "critical": LogLevel.CRITICAL,
1011
+ "fatal": LogLevel.FATAL,
1012
+ }
1013
+ if level.lower() in level_map:
1014
+ _config.min_level = level_map[level.lower()]
1015
+
1016
+ def get_config() -> LoggerConfig:
1017
+ return _config
1018
+
1019
+
1020
+ # ══════════════════════════════════════════
1021
+ # Colorize Helpers
1022
+ # ══════════════════════════════════════════
1023
+
1024
+ def colorize(text: str, color: str) -> str:
1025
+ return f"{color}{text}{Colors.RESET}"
1026
+
1027
+ def bold(text: str) -> str:
1028
+ return f"{Colors.BOLD}{text}{Colors.RESET}"
1029
+
1030
+ def italic(text: str) -> str:
1031
+ return f"{Colors.ITALIC}{text}{Colors.RESET}"
1032
+
1033
+ def underline(text: str) -> str:
1034
+ return f"{Colors.UNDERLINE}{text}{Colors.RESET}"
1035
+
1036
+ def dim(text: str) -> str:
1037
+ return f"{Colors.DIM}{text}{Colors.RESET}"
1038
+
1039
+ def strike(text: str) -> str:
1040
+ return f"{Colors.STRIKE}{text}{Colors.RESET}"
1041
+
1042
+
1043
+ # ══════════════════════════════════════════
1044
+ # Module Exports
1045
+ # ══════════════════════════════════════════
1046
+
1047
+ __all__ = [
1048
+ "debug", "info", "success", "warn", "warning", "error",
1049
+ "critical", "fatal", "trace",
1050
+ "divider", "section", "blank", "rule", "banner", "panel", "table", "box", "pair",
1051
+ "gradient_text", "gradient_print", "multi_gradient_text", "multi_gradient_print",
1052
+ "rainbow_text", "rainbow_print",
1053
+ "colorize", "bold", "italic", "underline", "dim", "strike",
1054
+ "rgb", "bg_rgb", "hex_color", "bg_hex",
1055
+ "set_theme", "set_style", "add_theme", "set_time_format", "show_time",
1056
+ "set_log_file", "set_min_level", "get_config",
1057
+ "preview_styles", "preview_themes",
1058
+ "ProgressBar", "Spinner", "Timer",
1059
+ "Colors", "Theme", "LogLevel", "LoggerConfig",
1060
+ "__version__", "__author__",
1061
+ ]
1062
+
1063
+ if os.name == "nt":
1064
+ try:
1065
+ import ctypes
1066
+ kernel32 = ctypes.windll.kernel32
1067
+ kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
1068
+ except:
1069
+ pass
@@ -0,0 +1,266 @@
1
+ Metadata-Version: 2.4
2
+ Name: hexlogger
3
+ Version: 1.0.0
4
+ Summary: Premium console logging library with themes, gradients, panels, tables, progress bars, and spinners.
5
+ Author: Itz Montage
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Montagexd/hexlogger
8
+ Keywords: logging,console,terminal,colors,logger,hex,ansi
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.7
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Classifier: Topic :: System :: Logging
22
+ Classifier: Operating System :: OS Independent
23
+ Requires-Python: >=3.7
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Dynamic: license-file
27
+
28
+ # 🔮 HexLogger
29
+
30
+ **Premium console logging library for Python**
31
+
32
+ A feature-rich, zero-dependency logging library with beautiful colored output, multiple themes, gradient text, panels, tables, progress bars, and spinners.
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install hexlogger
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ```python
43
+ import hexlogger
44
+
45
+ hexlogger.debug("Connecting to server...")
46
+ hexlogger.success("Connected!")
47
+ hexlogger.info("Processing 1000 items")
48
+ hexlogger.warn("Rate limit approaching")
49
+ hexlogger.error("Connection failed")
50
+ hexlogger.critical("System overload!")
51
+ ```
52
+
53
+ **Output:**
54
+ ```
55
+ [-(14:30:00)-] [ ● ] DEBUG » Connecting to server...
56
+ [-(14:30:00)-] [ ● ] SUCCESS » Connected!
57
+ [-(14:30:00)-] [ ● ] INFO » Processing 1000 items
58
+ [-(14:30:00)-] [ ● ] WARNING » Rate limit approaching
59
+ [-(14:30:00)-] [ ● ] ERROR » Connection failed
60
+ [-(14:30:00)-] [ ● ] CRITICAL » System overload!
61
+ ```
62
+
63
+ ## Themes
64
+
65
+ ```python
66
+ import hexlogger
67
+
68
+ # Available themes: default, neon, minimal, hacker
69
+ hexlogger.set_theme("neon")
70
+ hexlogger.success("Neon vibes!")
71
+
72
+ hexlogger.set_theme("hacker")
73
+ hexlogger.success("Matrix mode!")
74
+
75
+ hexlogger.set_theme("minimal")
76
+ hexlogger.success("Clean and simple")
77
+ ```
78
+
79
+ ### Custom Themes
80
+
81
+ ```python
82
+ from hexlogger import add_theme, rgb
83
+
84
+ add_theme("ocean", {
85
+ "timestamp": rgb(0, 100, 200),
86
+ "bracket": rgb(0, 80, 160),
87
+ "dot": rgb(0, 200, 255),
88
+ "separator": rgb(0, 150, 200),
89
+ "debug": rgb(100, 180, 255),
90
+ "info": rgb(0, 200, 255),
91
+ "success": rgb(0, 255, 200),
92
+ "warning": rgb(255, 200, 0),
93
+ "error": rgb(255, 80, 80),
94
+ "critical": rgb(255, 0, 50),
95
+ "fatal": rgb(200, 0, 0),
96
+ "trace": rgb(100, 150, 200),
97
+ "divider": rgb(0, 60, 100),
98
+ "label_debug": "DEBUG",
99
+ "label_info": "INFO",
100
+ "label_success": "SUCCESS",
101
+ "label_warning": "WARNING",
102
+ "label_error": "ERROR",
103
+ "label_critical": "CRITICAL",
104
+ "label_fatal": "FATAL",
105
+ "label_trace": "TRACE",
106
+ })
107
+
108
+ hexlogger.set_theme("ocean")
109
+ ```
110
+
111
+ ## Dividers & Sections
112
+
113
+ ```python
114
+ hexlogger.divider()
115
+ hexlogger.section("RESULTS")
116
+ hexlogger.rule("Settings", char="═")
117
+ hexlogger.blank(2)
118
+ ```
119
+
120
+ ## Banner
121
+
122
+ ```python
123
+ hexlogger.banner("HexLogger\nv1.0.0")
124
+ ```
125
+
126
+ ```
127
+ ╔═══════════════════╗
128
+ ║ HexLogger ║
129
+ ║ v1.0.0 ║
130
+ ╚═══════════════════╝
131
+ ```
132
+
133
+ ## Panel
134
+
135
+ ```python
136
+ hexlogger.panel("Server: online\nPing: 42ms\nUptime: 99.9%", title="Status")
137
+ ```
138
+
139
+ ```
140
+ ╭─ Status ──────────────────────────────╮
141
+ │ Server: online │
142
+ │ Ping: 42ms │
143
+ │ Uptime: 99.9% │
144
+ ╰───────────────────────────────────────╯
145
+ ```
146
+
147
+ ## Table
148
+
149
+ ```python
150
+ hexlogger.table(
151
+ headers=["Name", "Status", "Ping"],
152
+ rows=[
153
+ ["Server 1", "Online", "12ms"],
154
+ ["Server 2", "Offline", "---"],
155
+ ["Server 3", "Online", "45ms"],
156
+ ]
157
+ )
158
+ ```
159
+
160
+ ## Gradient Text
161
+
162
+ ```python
163
+ hexlogger.gradient_print("Hello World!", (255, 0, 100), (100, 0, 255))
164
+ hexlogger.rainbow_print("Rainbow text!")
165
+
166
+ # Get gradient string without printing
167
+ text = hexlogger.gradient_text("Custom gradient", (0, 255, 200), (255, 0, 100))
168
+ ```
169
+
170
+ ## Progress Bar
171
+
172
+ ```python
173
+ import time
174
+
175
+ bar = hexlogger.ProgressBar(total=100, label="Downloading")
176
+ for i in range(100):
177
+ time.sleep(0.05)
178
+ bar.update()
179
+ ```
180
+
181
+ ## Spinner
182
+
183
+ ```python
184
+ import time
185
+
186
+ # As context manager
187
+ with hexlogger.Spinner("Loading data...", style="dots"):
188
+ time.sleep(3)
189
+
190
+ # Manual control
191
+ spinner = hexlogger.Spinner("Processing...", style="circle")
192
+ spinner.start()
193
+ time.sleep(2)
194
+ spinner.stop("Done!")
195
+
196
+ # Available styles: dots, line, arrow, bounce, box, circle
197
+ ```
198
+
199
+ ## Timer
200
+
201
+ ```python
202
+ # As context manager
203
+ with hexlogger.Timer("Database query"):
204
+ time.sleep(1.5)
205
+ # Output: [INFO] Database query: 1.500s
206
+
207
+ # Manual control
208
+ timer = hexlogger.Timer("API call")
209
+ timer.start()
210
+ # ... do work ...
211
+ elapsed = timer.stop()
212
+ ```
213
+
214
+ ## Key-Value Pairs
215
+
216
+ ```python
217
+ hexlogger.pair("Server", "production-01")
218
+ hexlogger.pair("Status", "running")
219
+ hexlogger.pair("Uptime", "99.97%")
220
+ ```
221
+
222
+ ## Text Styling
223
+
224
+ ```python
225
+ from hexlogger import bold, italic, underline, dim, strike, colorize, Colors
226
+
227
+ print(bold("Bold text"))
228
+ print(italic("Italic text"))
229
+ print(underline("Underlined"))
230
+ print(colorize("Custom color", Colors.CYAN))
231
+ ```
232
+
233
+ ## Configuration
234
+
235
+ ```python
236
+ # Set time format
237
+ hexlogger.set_time_format("%Y-%m-%d %H:%M:%S")
238
+
239
+ # Hide timestamps
240
+ hexlogger.show_time(False)
241
+
242
+ # Log to file
243
+ hexlogger.set_log_file("app.log")
244
+
245
+ # Set minimum log level
246
+ hexlogger.set_min_level("warning") # Only WARNING and above
247
+ ```
248
+
249
+ ## Features
250
+
251
+ - ✅ Zero dependencies
252
+ - ✅ Beautiful colored output
253
+ - ✅ 4 built-in themes + custom themes
254
+ - ✅ Gradient & rainbow text
255
+ - ✅ Panels, tables, banners
256
+ - ✅ Progress bars with ETA
257
+ - ✅ Animated spinners (6 styles)
258
+ - ✅ Timer utility
259
+ - ✅ File logging
260
+ - ✅ Thread-safe
261
+ - ✅ Windows compatible
262
+ - ✅ Python 3.7+
263
+
264
+ ## License
265
+
266
+ MIT License - Created by Montage
@@ -0,0 +1,6 @@
1
+ hexlogger/__init__.py,sha256=-4T2w83pDdFjrNICsyAOEt6m4BIXfLNVpffOdY1Sl8c,37324
2
+ hexlogger-1.0.0.dist-info/licenses/LICENSE,sha256=hgTnvuHSQdWzCM80d4P2xDIXBtRzmQkNpKxv0m-Y8qg,1085
3
+ hexlogger-1.0.0.dist-info/METADATA,sha256=o67ioG7FhR_-P9Ko6zpeXQHoP10_GlCBkSSkuv-xzcg,6508
4
+ hexlogger-1.0.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
5
+ hexlogger-1.0.0.dist-info/top_level.txt,sha256=REGVvv7vdZCvSAU2lT7I9_OEGNP7KufdFUYRiflfzow,10
6
+ hexlogger-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Montage
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ hexlogger