wizard-central-toolkit 0.2.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.
Files changed (77) hide show
  1. wiz_central_toolkit/__init__.py +2 -0
  2. wiz_central_toolkit/bannerwiz/__main__.py +6 -0
  3. wiz_central_toolkit/bannerwiz/cli.py +79 -0
  4. wiz_central_toolkit/bannerwiz/lib/__init__.py +1 -0
  5. wiz_central_toolkit/bannerwiz/lib/banner_wizard.py +999 -0
  6. wiz_central_toolkit/bannerwiz/lib/banner_wrapper.py +199 -0
  7. wiz_central_toolkit/bannerwiz/lib/cli.py +79 -0
  8. wiz_central_toolkit/codeguardwiz/__init__.py +1 -0
  9. wiz_central_toolkit/codeguardwiz/__main__.py +19 -0
  10. wiz_central_toolkit/codeguardwiz/cli.py +971 -0
  11. wiz_central_toolkit/codeguardwiz/codeguard_wizard.py +269 -0
  12. wiz_central_toolkit/codeguardwiz/rust_engine.py +348 -0
  13. wiz_central_toolkit/codeguardwiz/static_engine.py +307 -0
  14. wiz_central_toolkit/common/__init__.py +1 -0
  15. wiz_central_toolkit/debugwiz/__init__.py +1 -0
  16. wiz_central_toolkit/debugwiz/__main__.py +360 -0
  17. wiz_central_toolkit/debugwiz/debug_wizard.py +364 -0
  18. wiz_central_toolkit/diffwiz/__init__.py +0 -0
  19. wiz_central_toolkit/diffwiz/__main__.py +21 -0
  20. wiz_central_toolkit/diffwiz/cli.py +134 -0
  21. wiz_central_toolkit/diffwiz/lib/__init__.py +1 -0
  22. wiz_central_toolkit/diffwiz/lib/cli.py +144 -0
  23. wiz_central_toolkit/diffwiz/lib/diff_wizard.py +89 -0
  24. wiz_central_toolkit/godfatherwiz/__init__.py +0 -0
  25. wiz_central_toolkit/godfatherwiz/__main__.py +18 -0
  26. wiz_central_toolkit/godfatherwiz/ai.py +136 -0
  27. wiz_central_toolkit/godfatherwiz/lib/__init__.py +1 -0
  28. wiz_central_toolkit/godfatherwiz/lib/cli.py +925 -0
  29. wiz_central_toolkit/godfatherwiz/lib/godfather_wizard.py +1552 -0
  30. wiz_central_toolkit/jsonwiz/__init__.py +0 -0
  31. wiz_central_toolkit/jsonwiz/__main__.py +21 -0
  32. wiz_central_toolkit/jsonwiz/cli.py +4 -0
  33. wiz_central_toolkit/jsonwiz/lib/__init__.py +1 -0
  34. wiz_central_toolkit/jsonwiz/lib/cli.py +138 -0
  35. wiz_central_toolkit/jsonwiz/lib/json_wizard.py +133 -0
  36. wiz_central_toolkit/pythonwiz/__init__.py +0 -0
  37. wiz_central_toolkit/pythonwiz/__main__.py +21 -0
  38. wiz_central_toolkit/pythonwiz/lib/__init__.py +1 -0
  39. wiz_central_toolkit/pythonwiz/lib/cli.py +247 -0
  40. wiz_central_toolkit/pythonwiz/lib/python_wizard.py +353 -0
  41. wiz_central_toolkit/readwiz/__init__.py +5 -0
  42. wiz_central_toolkit/readwiz/__main__.py +15 -0
  43. wiz_central_toolkit/readwiz/cli.py +46 -0
  44. wiz_central_toolkit/readwiz/lib/__init__.py +1 -0
  45. wiz_central_toolkit/readwiz/lib/cli.py +47 -0
  46. wiz_central_toolkit/readwiz/lib/read_wizard.py +121 -0
  47. wiz_central_toolkit/rustwiz/__init__.py +5 -0
  48. wiz_central_toolkit/rustwiz/__main__.py +7 -0
  49. wiz_central_toolkit/rustwiz/cli.py +4 -0
  50. wiz_central_toolkit/rustwiz/lib/__init__.py +1 -0
  51. wiz_central_toolkit/rustwiz/lib/cli.py +53 -0
  52. wiz_central_toolkit/rustwiz/lib/rust_wizard.py +128 -0
  53. wiz_central_toolkit/tmuxwiz/__init__.py +0 -0
  54. wiz_central_toolkit/tmuxwiz/__main__.py +15 -0
  55. wiz_central_toolkit/tmuxwiz/cli.py +7 -0
  56. wiz_central_toolkit/tmuxwiz/lib/__init__.py +1 -0
  57. wiz_central_toolkit/tmuxwiz/lib/cli.py +10 -0
  58. wiz_central_toolkit/tmuxwiz/lib/tmux_wizard.py +229 -0
  59. wiz_central_toolkit/todowiz/__init__.py +0 -0
  60. wiz_central_toolkit/todowiz/__main__.py +22 -0
  61. wiz_central_toolkit/todowiz/cli.py +4 -0
  62. wiz_central_toolkit/todowiz/lib/__init__.py +1 -0
  63. wiz_central_toolkit/todowiz/lib/cli.py +82 -0
  64. wiz_central_toolkit/todowiz/lib/todo_wizard.py +134 -0
  65. wiz_central_toolkit/tools_registry.py +62 -0
  66. wiz_central_toolkit/wiz/__init__.py +0 -0
  67. wiz_central_toolkit/wiz/__main__.py +141 -0
  68. wiz_central_toolkit/writewiz/__init__.py +1 -0
  69. wiz_central_toolkit/writewiz/__main__.py +9 -0
  70. wiz_central_toolkit/writewiz/cli.py +264 -0
  71. wiz_central_toolkit/writewiz/write_wizard.py +384 -0
  72. wizard_central_toolkit-0.2.0.dist-info/METADATA +203 -0
  73. wizard_central_toolkit-0.2.0.dist-info/RECORD +77 -0
  74. wizard_central_toolkit-0.2.0.dist-info/WHEEL +5 -0
  75. wizard_central_toolkit-0.2.0.dist-info/entry_points.txt +50 -0
  76. wizard_central_toolkit-0.2.0.dist-info/licenses/LICENSE +201 -0
  77. wizard_central_toolkit-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,999 @@
1
+ from rich.console import Console
2
+ from rich.panel import Panel
3
+ from rich.table import Table
4
+ from rich import box
5
+ from rich.text import Text
6
+ from rich.align import Align
7
+ from pyfiglet import Figlet
8
+ import shutil
9
+ import os
10
+ import sys
11
+ import datetime
12
+
13
+ def log_error(message: str) -> None:
14
+ """Append errors to debug.log with timestamp."""
15
+ timestamp = datetime.datetime.now().isoformat()
16
+ with open("debug.log", "a") as f:
17
+ f.write(f"[{timestamp}] {message}\n")
18
+ console = Console()
19
+
20
+
21
+ # ═══════════════════════════════════════════════════════════════════════
22
+ # SIZE MANAGER
23
+ # ═══════════════════════════════════════════════════════════════════════
24
+
25
+ class SizeManager:
26
+ """
27
+ Manages banner dimensions, terminal-fit logic, and scaling.
28
+ """
29
+
30
+ def __init__(self, max_width: int | None = None, min_width: int = 40):
31
+ self._terminal_width = shutil.get_terminal_size((80, 24)).columns
32
+ self.max_width = max_width or self._terminal_width
33
+ self.min_width = min_width
34
+
35
+ @property
36
+ def terminal_width(self) -> int:
37
+ return shutil.get_terminal_size((80, 24)).columns
38
+
39
+ @property
40
+ def usable_width(self) -> int:
41
+ return max(self.min_width, self.terminal_width - 6)
42
+
43
+ def refresh(self) -> None:
44
+ self._terminal_width = self.terminal_width
45
+ self.max_width = min(self.max_width, self.terminal_width)
46
+
47
+ def measure_banner(self, ascii_art: str) -> int:
48
+ if not ascii_art:
49
+ return 0
50
+ return max(len(line) for line in ascii_art.splitlines())
51
+
52
+ def fits(self, ascii_art: str, margin: int = 0) -> bool:
53
+ return self.measure_banner(ascii_art) <= (self.usable_width - margin)
54
+
55
+ def auto_fit_font(self, text: str, preferred_font: str = "standard",
56
+ fallback_order: list[str] | None = None) -> str:
57
+ if fallback_order is None:
58
+ fallback_order = [
59
+ preferred_font,
60
+ "big", "standard", "slant", "small",
61
+ "banner", "smkeyboard", "digital", "mini",
62
+ ]
63
+ seen = set()
64
+ ordered = []
65
+ for f in fallback_order:
66
+ if f not in seen:
67
+ ordered.append(f)
68
+ seen.add(f)
69
+ available = ASCIIBannerEngine.FONTS
70
+ for font_key in ordered:
71
+ if font_key not in available:
72
+ continue
73
+ fig = Figlet(font=available[font_key])
74
+ art = fig.renderText(text)
75
+ if self.fits(art):
76
+ return font_key
77
+ return ordered[-1] if ordered else "standard"
78
+
79
+ def truncate_to_width(self, ascii_art: str, width: int | None = None) -> str:
80
+ target = width or self.usable_width
81
+ lines = ascii_art.splitlines()
82
+ result = []
83
+ for line in lines:
84
+ if len(line) > target:
85
+ result.append(line[:target - 1] + "\u2025")
86
+ else:
87
+ result.append(line)
88
+ return "\n".join(result)
89
+
90
+ def pad_banner(self, ascii_art: str, align: str = "left",
91
+ h_pad: int = 2, v_pad: int = 0) -> str:
92
+ lines = ascii_art.splitlines()
93
+ target_width = self.measure_banner(ascii_art) + (h_pad * 2)
94
+ padded = []
95
+ for _ in range(v_pad):
96
+ padded.append("")
97
+ for line in lines:
98
+ stripped = line.rstrip()
99
+ ln = len(stripped)
100
+ if align == "center":
101
+ left = (target_width - ln) // 2
102
+ right = target_width - ln - left
103
+ padded.append(" " * left + stripped + " " * right)
104
+ elif align == "right":
105
+ padded.append(" " * (target_width - ln) + stripped)
106
+ else:
107
+ padded.append(stripped + " " * (target_width - ln))
108
+ for _ in range(v_pad):
109
+ padded.append("")
110
+ return "\n".join(padded)
111
+
112
+ def report(self) -> str:
113
+ return (
114
+ f"Terminal: {self.terminal_width} cols | "
115
+ f"Usable: {self.usable_width} cols | "
116
+ f"Min: {self.min_width} | Max: {self.max_width}"
117
+ )
118
+
119
+
120
+ # ═══════════════════════════════════════════════════════════════════════
121
+ # STYLE MANAGER
122
+ # ═══════════════════════════════════════════════════════════════════════
123
+
124
+ class StyleManager:
125
+ """
126
+ Predefined style presets combining color, border style, alignment,
127
+ and box characters.
128
+ """
129
+
130
+ PRESETS = {
131
+ # ── Classic ──
132
+ "default": {"color": "white", "border": box.ROUNDED, "align": "left"},
133
+ "minimal": {"color": "dim", "border": box.MINIMAL, "align": "left"},
134
+ "clean": {"color": "cyan", "border": box.SQUARE, "align": "left"},
135
+ "boxed": {"color": "yellow", "border": box.DOUBLE, "align": "left"},
136
+ "heavy": {"color": "red", "border": box.HEAVY, "align": "left"},
137
+
138
+ # ── Neon / Cyber ──
139
+ "neon-blue": {"color": "bright_blue", "border": box.DOUBLE_EDGE, "align": "center"},
140
+ "neon-green": {"color": "bright_green", "border": box.DOUBLE_EDGE, "align": "center"},
141
+ "neon-pink": {"color": "bright_magenta", "border": box.DOUBLE_EDGE, "align": "center"},
142
+ "cyber": {"color": "blue", "border": box.ASCII_DOUBLE_HEAD, "align": "center"},
143
+ "matrix": {"color": "green", "border": box.MINIMAL_DOUBLE_HEAD, "align": "left"},
144
+
145
+ # ── Elegant ──
146
+ "elegant": {"color": "magenta", "border": box.ROUNDED, "align": "center"},
147
+ "royal": {"color": "blue", "border": box.DOUBLE, "align": "center"},
148
+ "shadow": {"color": "bright_black", "border": box.SIMPLE_HEAD, "align": "left"},
149
+
150
+ # ── Playful ──
151
+ "retro": {"color": "bright_yellow", "border": box.ASCII, "align": "center"},
152
+ "arcade": {"color": "bright_cyan", "border": box.DOUBLE, "align": "center"},
153
+ "comic": {"color": "bright_red", "border": box.ROUNDED, "align": "center"},
154
+
155
+ # ── Bare / No frame ──
156
+ "bare": {"color": "white", "border": None, "align": "left"},
157
+ "bare-center": {"color": "cyan", "border": None, "align": "center"},
158
+ }
159
+
160
+ @classmethod
161
+ def get(cls, name: str) -> dict:
162
+ return cls.PRESETS.get(name, cls.PRESETS["default"])
163
+
164
+ @classmethod
165
+ def list_presets(cls) -> list[str]:
166
+ return sorted(cls.PRESETS.keys())
167
+
168
+ @classmethod
169
+ def list_presets_table(cls) -> None:
170
+ tbl = Table(
171
+ title="[bold magenta]Style Presets[/bold magenta]",
172
+ show_header=True,
173
+ header_style="bold cyan",
174
+ box=box.DOUBLE,
175
+ )
176
+ tbl.add_column("ID", justify="right", style="dim", width=3)
177
+ tbl.add_column("Preset Name", style="bold yellow")
178
+ tbl.add_column("Color", justify="center")
179
+ tbl.add_column("Border", justify="center")
180
+ tbl.add_column("Align", justify="center")
181
+
182
+ border_names = {
183
+ id(box.ROUNDED): "Rounded",
184
+ id(box.DOUBLE): "Double",
185
+ id(box.SQUARE): "Square",
186
+ id(box.HEAVY): "Heavy",
187
+ id(box.MINIMAL): "Minimal",
188
+ id(box.MINIMAL_DOUBLE_HEAD): "Min-Double",
189
+ id(box.DOUBLE_EDGE): "Dbl-Edge",
190
+ id(box.ASCII): "ASCII",
191
+ id(box.ASCII_DOUBLE_HEAD): "ASCII-Dbl",
192
+ id(box.SIMPLE_HEAD): "Simple-Hd",
193
+ }
194
+
195
+ for i, name in enumerate(sorted(cls.PRESETS.keys())):
196
+ s = cls.PRESETS[name]
197
+ border_label = "None" if s["border"] is None else \
198
+ border_names.get(id(s["border"]), "?")
199
+ tbl.add_row(
200
+ str(i), name,
201
+ f"[{s['color']}]{s['color']}[/{s['color']}]",
202
+ border_label, s["align"],
203
+ )
204
+ console.print(tbl)
205
+
206
+
207
+ # ═══════════════════════════════════════════════════════════════════════
208
+ # PROFILE MANAGER
209
+ # ═══════════════════════════════════════════════════════════════════════
210
+
211
+ class ProfileManager:
212
+ """
213
+ Loads and manages named banner profiles from a YAML config file.
214
+ Falls back gracefully if PyYAML is not installed.
215
+ """
216
+
217
+ DEFAULT_CONFIG_PATH = "config.yaml"
218
+
219
+ def __init__(self, config_path: str | None = None):
220
+ self.config_path = config_path or self.DEFAULT_CONFIG_PATH
221
+ self.profiles: dict = {}
222
+ self._loaded = False
223
+ self.yaml_available = True
224
+ self._load()
225
+
226
+ def _load(self) -> None:
227
+ """Attempt to load profiles from YAML.
228
+
229
+ PyYAML is optional: without it, profiles silently stay empty.
230
+ The one place that says so out loud is the --profiles mode —
231
+ everywhere else the fallback is quiet by design.
232
+ """
233
+ try:
234
+ import yaml
235
+ except ImportError:
236
+ self.yaml_available = False
237
+ return
238
+
239
+ try:
240
+ with open(self.config_path, "r") as f:
241
+ data = yaml.safe_load(f)
242
+ except FileNotFoundError:
243
+ return # No config file — that's OK
244
+ except Exception as e:
245
+ console.print(f"[red]✗ Error loading {self.config_path}: {e}[/red]")
246
+ return
247
+
248
+ if data and "profiles" in data:
249
+ self.profiles = data["profiles"]
250
+ self._loaded = True
251
+
252
+ @property
253
+ def is_loaded(self) -> bool:
254
+ return self._loaded
255
+
256
+ def get(self, name: str) -> dict | None:
257
+ """Return a profile by name, or None if not found."""
258
+ profile = self.profiles.get(name)
259
+ if profile is None:
260
+ console.print(f"[red]✗ Profile '{name}' not found in "
261
+ f"{self.config_path}[/red]")
262
+ self.list_profiles()
263
+ return None
264
+ return profile
265
+
266
+ def list_profiles(self) -> None:
267
+ """Display all available profiles in a table."""
268
+ if not self.profiles:
269
+ console.print(f"[yellow]No profiles loaded. "
270
+ f"Create {self.config_path} or run "
271
+ f"'profiles --create' to generate a template.[/yellow]")
272
+ return
273
+
274
+ tbl = Table(
275
+ title=f"[bold magenta]Banner Profiles ({self.config_path})[/bold magenta]",
276
+ show_header=True,
277
+ header_style="bold cyan",
278
+ box=box.DOUBLE,
279
+ )
280
+ tbl.add_column("ID", justify="right", style="dim", width=3)
281
+ tbl.add_column("Profile Name", style="bold yellow")
282
+ tbl.add_column("Font", justify="center")
283
+ tbl.add_column("Style", justify="center")
284
+ tbl.add_column("Align", justify="center")
285
+ tbl.add_column("Auto-Fit", justify="center")
286
+
287
+ for i, name in enumerate(sorted(self.profiles.keys())):
288
+ p = self.profiles[name]
289
+ auto_fit = "✓" if p.get("auto_fit", True) else "✗"
290
+ tbl.add_row(
291
+ str(i),
292
+ name,
293
+ p.get("font", "standard"),
294
+ p.get("style", "default"),
295
+ p.get("align", "left"),
296
+ auto_fit,
297
+ )
298
+
299
+ console.print(tbl)
300
+
301
+ def create_template(self, path: str | None = None) -> None:
302
+ """Generate a default config.yaml template."""
303
+ target = path or self.DEFAULT_CONFIG_PATH
304
+
305
+ template = """# ASCII Banner Engine — Profile Configuration
306
+ # Define named profiles for quick banner rendering.
307
+ # Usage: python3 banner_engine.py --profile hero "WELCOME"
308
+
309
+ profiles:
310
+ hero:
311
+ font: big
312
+ style: neon-green
313
+ align: center
314
+ auto_fit: true
315
+
316
+ subtitle:
317
+ font: small
318
+ style: clean
319
+ align: center
320
+ auto_fit: false
321
+
322
+ error:
323
+ font: standard
324
+ style: heavy
325
+ align: left
326
+ auto_fit: true
327
+
328
+ success:
329
+ font: slant
330
+ style: neon-green
331
+ align: center
332
+ auto_fit: true
333
+
334
+ warning:
335
+ font: speed
336
+ style: comic
337
+ align: center
338
+ auto_fit: true
339
+
340
+ matrix:
341
+ font: binary
342
+ style: matrix
343
+ align: left
344
+ auto_fit: true
345
+
346
+ gothic:
347
+ font: gothic
348
+ style: royal
349
+ align: center
350
+ auto_fit: true
351
+
352
+ debug:
353
+ font: mini
354
+ style: minimal
355
+ align: left
356
+ auto_fit: false
357
+ """
358
+
359
+ with open(target, "w") as f:
360
+ f.write(template)
361
+
362
+ console.print(f"[green]✓ Template created: {target}[/green]")
363
+ console.print("[dim]Edit it to customize your profiles.[/dim]")
364
+
365
+
366
+ # ═══════════════════════════════════════════════════════════════════════
367
+ # ASCII BANNER ENGINE
368
+ # ═══════════════════════════════════════════════════════════════════════
369
+
370
+ class ASCIIBannerEngine:
371
+ """Multi-style ASCII banner generator using pyfiglet"""
372
+
373
+ # Curated aliases — readable names for cryptic FIGlet font names
374
+ _ALIASES = {
375
+ "3d": "3-d",
376
+ "isomeric": "isometric1",
377
+ "technology": "4x4_offr",
378
+ "fireball": "fire_font-s",
379
+ "pump": "puffy",
380
+ }
381
+
382
+ # Auto-load all available FIGlet fonts, then layer aliases on top
383
+ FONTS = {f: f for f in Figlet().getFonts()}
384
+ FONTS.update(_ALIASES)
385
+
386
+ def __init__(self, size_manager: SizeManager | None = None,
387
+ profile_manager: ProfileManager | None = None):
388
+ self.sizer = size_manager or SizeManager()
389
+ self.profiler = profile_manager or ProfileManager()
390
+
391
+ # ── Core generation ──────────────────────────────────────────────
392
+
393
+ def generate(self, text: str, font: str = "standard",
394
+ auto_fit: bool = False) -> str | None:
395
+ if font not in self.FONTS:
396
+ console.print(f"[red]Font '{font}' not found. "
397
+ f"Use 'fonts' command to list available.[/red]")
398
+ return None
399
+ if auto_fit:
400
+ font = self.sizer.auto_fit_font(text, preferred_font=font)
401
+ fig = Figlet(font=self.FONTS[font])
402
+ return fig.renderText(text)
403
+
404
+ # ── Display ──────────────────────────────────────────────────────
405
+
406
+ def display_banner(self, text: str, font: str = "standard",
407
+ style: str = "default",
408
+ auto_fit: bool = True,
409
+ align: str | None = None) -> None:
410
+ # Resolve style — .copy() prevents mutating the class-level PRESETS
411
+ if style in StyleManager.PRESETS:
412
+ preset = StyleManager.get(style).copy()
413
+ else:
414
+ preset = {"color": style, "border": box.ROUNDED, "align": "left"}
415
+
416
+ if align:
417
+ preset["align"] = align
418
+
419
+ ascii_art = self.generate(text, font, auto_fit=auto_fit)
420
+ if ascii_art is None:
421
+ return
422
+
423
+ if not self.sizer.fits(ascii_art, margin=6):
424
+ ascii_art = self.sizer.truncate_to_width(
425
+ ascii_art, self.sizer.usable_width - 6)
426
+
427
+ rich_text = Text(ascii_art, style=preset["color"])
428
+ aligned = Align(rich_text, align=preset["align"])
429
+
430
+ panel_title = (f"[bold {preset['color']}]\n"
431
+ f"{font.upper()} . {style.upper()}\n"
432
+ f"[/bold {preset['color']}]")
433
+
434
+ if preset["border"] is None:
435
+ console.print(aligned)
436
+ else:
437
+ panel = Panel(
438
+ aligned,
439
+ title=panel_title,
440
+ border_style=preset["color"],
441
+ box=preset["border"],
442
+ padding=(1, 2),
443
+ )
444
+ console.print(panel)
445
+ def render_profile(self, text: str, profile_name: str) -> None:
446
+ """Render banner using a named profile from config.yaml."""
447
+ profile = self.profiler.get(profile_name)
448
+ if profile is None:
449
+ return
450
+
451
+ font = profile.get("font", "standard")
452
+ style = profile.get("style", "default")
453
+ align = profile.get("align")
454
+ auto_fit = profile.get("auto_fit", True)
455
+
456
+ self.display_banner(text, font=font, style=style,
457
+ auto_fit=auto_fit, align=align)
458
+
459
+ # ── Listing ──────────────────────────────────────────────────────
460
+
461
+ def list_fonts(self) -> None:
462
+ ref_table = Table(
463
+ title=f"[bold magenta]Available ASCII Fonts ({len(self.FONTS)})[/bold magenta]",
464
+ show_header=True,
465
+ header_style="bold cyan",
466
+ box=box.DOUBLE,
467
+ )
468
+ ref_table.add_column("ID", justify="right", style="dim", width=5)
469
+ ref_table.add_column("Key", style="bold yellow")
470
+ ref_table.add_column("FIGlet Name", style="dim")
471
+
472
+ for i, (key, figlet_name) in enumerate(sorted(self.FONTS.items())):
473
+ alias_tag = " [cyan](alias)[/cyan]" if key != figlet_name else ""
474
+ ref_table.add_row(str(i), key, figlet_name + alias_tag)
475
+
476
+ console.print(ref_table)
477
+
478
+ def list_styles(self) -> None:
479
+ StyleManager.list_presets_table()
480
+
481
+ # ── Comparison ───────────────────────────────────────────────────
482
+ def compare_fonts(self, text: str, fonts: list[str] | None = None) -> None:
483
+ if fonts is None:
484
+ fonts = [
485
+ "standard", "slant", "big", "ghost",
486
+ "digital", "small", "gothic", "bubble",
487
+ ]
488
+
489
+ preview_grid = Table(
490
+ title="[bold green]Font Comparison Preview[/bold green]",
491
+ box=box.MINIMAL_DOUBLE_HEAD,
492
+ show_lines=True,
493
+ )
494
+ preview_grid.add_column("Font -> Preview", style="white")
495
+
496
+ colors = ["bright_green", "bright_cyan", "bright_yellow",
497
+ "bright_magenta", "bright_blue", "bright_red",
498
+ "bright_white", "green"]
499
+
500
+ for i, font_name in enumerate(fonts):
501
+ if font_name not in self.FONTS:
502
+ preview_grid.add_row(f"[red]x {font_name} (not found)[/red]")
503
+ continue
504
+ try:
505
+ fig = Figlet(font=self.FONTS[font_name])
506
+ preview = fig.renderText(text)
507
+ preview_lines = preview.splitlines()
508
+ if len(preview_lines) > 6:
509
+ preview = "\n".join(preview_lines[:6]) + "\n..."
510
+ color = colors[i % len(colors)]
511
+ cell = (f"[bold {color}]{font_name}[/bold {color}]\n"
512
+ f"[{color}]{preview}[/{color}]")
513
+ preview_grid.add_row(cell)
514
+ except Exception as e:
515
+ preview_grid.add_row(f"[red]x {font_name} (error: {e})[/red]")
516
+
517
+ console.print(preview_grid)
518
+
519
+ def compare_styles(self, text: str = "COMPARE", styles: list[str] | None = None) -> None:
520
+ """Compare multiple style presets side-by-side in a table."""
521
+ if styles is None:
522
+ styles = ["default", "clean", "neon-green", "heavy", "matrix", "royal", "bare"]
523
+
524
+ border_names = {
525
+ id(box.ROUNDED): "Rounded",
526
+ id(box.DOUBLE): "Double",
527
+ id(box.SQUARE): "Square",
528
+ id(box.HEAVY): "Heavy",
529
+ id(box.MINIMAL): "Minimal",
530
+ id(box.MINIMAL_DOUBLE_HEAD): "Min-Double",
531
+ id(box.DOUBLE_EDGE): "Dbl-Edge",
532
+ id(box.ASCII): "ASCII",
533
+ id(box.ASCII_DOUBLE_HEAD): "ASCII-Dbl",
534
+ id(box.SIMPLE_HEAD): "Simple-Hd",
535
+ }
536
+
537
+ grid = Table(
538
+ title=f"[bold magenta]Style Comparison — '{text}'[/bold magenta]",
539
+ box=box.MINIMAL_DOUBLE_HEAD,
540
+ show_lines=True,
541
+ )
542
+ grid.add_column("Style → Preview", style="white")
543
+
544
+ for style_name in styles:
545
+ preset = StyleManager.get(style_name)
546
+
547
+ border_label = "None" if preset["border"] is None else \
548
+ border_names.get(id(preset["border"]), "Unknown")
549
+
550
+ cell = f"[bold {preset['color']}]{style_name}[/bold {preset['color']}]\n"
551
+ cell += f"[dim]Color: {preset['color']} | Border: {border_label} | Align: {preset['align']}[/dim]\n"
552
+
553
+ art = self.generate(text.upper(), "small", auto_fit=True)
554
+ if art:
555
+ lines = art.splitlines()
556
+ preview = "\n".join(lines)
557
+ cell += f"[{preset['color']}]{preview}[/{preset['color']}]"
558
+
559
+ grid.add_row(cell)
560
+
561
+ console.print(grid)
562
+
563
+ # ── Batch / Export ───────────────────────────────────────────────
564
+
565
+ def render_all_fonts(self, text: str, output_dir: str = "./banner_output") -> None:
566
+ os.makedirs(output_dir, exist_ok=True)
567
+ count = 0
568
+ failed = 0
569
+ for key, figlet_name in sorted(self.FONTS.items()):
570
+ try:
571
+ fig = Figlet(font=figlet_name)
572
+ art = fig.renderText(text)
573
+ safe_name = key.replace("/", "_").replace("\\", "_")
574
+ path = os.path.join(output_dir, f"{safe_name}.txt")
575
+ with open(path, "w", encoding="utf-8") as f:
576
+ f.write(f"# Font: {key} -> {figlet_name}\n")
577
+ f.write(f"# Size: {self.sizer.measure_banner(art)} cols\n")
578
+ f.write(art)
579
+ count += 1
580
+ except Exception:
581
+ console.print(f"[red]x Failed: {key} ({figlet_name})[/red]")
582
+ failed += 1
583
+ console.print(f"[green]Rendered {count} fonts to {output_dir}/[/green]")
584
+ if failed:
585
+ console.print(f"[red]{failed} fonts failed.[/red]")
586
+
587
+ def run_diagnostics(self) -> None:
588
+ """Run full engine diagnostics."""
589
+ console.print("\n[bold reverse] ENGINE DIAGNOSTICS [/bold reverse]\n")
590
+
591
+ # Terminal info
592
+ console.print("[bold cyan]Terminal:[/bold cyan]")
593
+ console.print(f" {self.sizer.report()}")
594
+
595
+ # Font stats
596
+ console.print("\n[bold cyan]Fonts:[/bold cyan]")
597
+ console.print(f" Total loaded: {len(self.FONTS)}")
598
+ aliases = {k: v for k, v in self.FONTS.items() if k != v}
599
+ console.print(f" Aliases: {len(aliases)}")
600
+ for k, v in aliases.items():
601
+ console.print(f" {k} -> {v}")
602
+
603
+ # Style stats
604
+ console.print("\n[bold cyan]Styles:[/bold cyan]")
605
+ console.print(f" Presets: {len(StyleManager.PRESETS)}")
606
+ for name in sorted(StyleManager.PRESETS.keys()):
607
+ s = StyleManager.PRESETS[name]
608
+ border = "None" if s["border"] is None else "Set"
609
+ console.print(f" {name}: color={s['color']}, border={border}, align={s['align']}")
610
+
611
+ # Profile stats
612
+ console.print("\n[bold cyan]Profiles:[/bold cyan]")
613
+ console.print(f" Config file: {self.profiler.config_path}")
614
+ console.print(f" Loaded: {self.profiler.is_loaded}")
615
+ console.print(f" Count: {len(self.profiler.profiles)}")
616
+ if self.profiler.profiles:
617
+ for name in sorted(self.profiler.profiles.keys()):
618
+ p = self.profiler.profiles[name]
619
+ console.print(f" {name}: font={p.get('font','?')}, "
620
+ f"style={p.get('style','?')}, "
621
+ f"align={p.get('align','?')}")
622
+
623
+ # Test render
624
+ console.print("\n[bold cyan]Test Render:[/bold cyan]")
625
+ test_text = "DIAG"
626
+ test_font = "standard"
627
+ try:
628
+ art = self.generate(test_text, test_font, auto_fit=False)
629
+ if art:
630
+ width = self.sizer.measure_banner(art)
631
+ fits = self.sizer.fits(art, margin=6)
632
+ console.print(f" Text: '{test_text}'")
633
+ console.print(f" Font: {test_font}")
634
+ console.print(f" Width: {width} cols")
635
+ console.print(f" Fits terminal: {'Yes' if fits else 'No'}")
636
+ console.print(f" Lines: {len(art.splitlines())}")
637
+ else:
638
+ console.print(" [red]Failed to generate test banner[/red]")
639
+ except Exception as e:
640
+ console.print(f" [red]Error: {e}[/red]")
641
+
642
+ console.print("\n[green]Diagnostics complete.[/green]\n")
643
+
644
+ # ═══════════════════════════════════════════════════════════════════════
645
+ # DEMO FUNCTIONS
646
+ # ═══════════════════════════════════════════════════════════════════════
647
+
648
+ def demo_basic_banners():
649
+ engine = ASCIIBannerEngine()
650
+ console.print("\n[bold blue]=== BASIC BANNERS (auto-fit ON) ===[/bold blue]\n")
651
+ engine.display_banner("HELLO", "standard", "clean")
652
+ engine.display_banner("WELCOME", "slant", "neon-green")
653
+ engine.display_banner("AUTHOR", "big", "elegant")
654
+ engine.display_banner("SYSTEM", "ghost", "neon-blue")
655
+
656
+
657
+ def demo_themed_banners():
658
+ engine = ASCIIBannerEngine()
659
+ themes = [
660
+ ("CYBERPUNK", "speed", "cyber"),
661
+ ("GHOST", "ghost", "bare-center"),
662
+ ("MATRIX", "binary", "matrix"),
663
+ ("SPACE", "starwars", "neon-pink"),
664
+ ("URBAN", "graffiti", "retro"),
665
+ ("GOTHIC", "gothic", "royal"),
666
+ ]
667
+ console.print("\n[bold]=== THEMED COLLECTIONS ===[/bold]\n")
668
+ for text, font, style in themes:
669
+ engine.display_banner(text, font, style)
670
+ console.print()
671
+
672
+
673
+ def demo_styles():
674
+ engine = ASCIIBannerEngine()
675
+ console.print("\n[bold magenta]=== STYLE PRESETS ===[/bold magenta]\n")
676
+ StyleManager.list_presets_table()
677
+ console.print()
678
+ for style in ["minimal", "neon-green", "matrix", "royal", "arcade", "bare"]:
679
+ engine.display_banner("STYLES", "slant", style)
680
+
681
+
682
+ def demo_size_manager():
683
+ engine = ASCIIBannerEngine()
684
+ sm = engine.sizer
685
+ console.print(f"\n[bold cyan]=== SIZE MANAGER REPORT ===[/bold cyan]")
686
+ console.print(f" {sm.report()}\n")
687
+ test_text = "AUTOFIT_TESTING_WIDE_BANNERS"
688
+ console.print(f"[yellow]Testing text: '{test_text}'[/yellow]")
689
+ best_font = sm.auto_fit_font(test_text, preferred_font="big")
690
+ console.print(f"[green]Auto-fit selected: '{best_font}'[/green]\n")
691
+ engine.display_banner(test_text, best_font, "clean", auto_fit=False)
692
+
693
+
694
+ # ═══════════════════════════════════════════════════════════════════════
695
+ # INTERACTIVE MENU
696
+ # ═══════════════════════════════════════════════════════════════════════
697
+
698
+ def interactive_menu():
699
+ engine = ASCIIBannerEngine()
700
+ console.print("\n[bold reverse] ASCII BANNER ENGINE v1.0 [/bold reverse]")
701
+ console.print(f"[dim] {engine.sizer.report()}[/dim]")
702
+ console.print(f"[dim] {len(engine.FONTS)} fonts loaded[/dim]\n")
703
+
704
+ while True:
705
+ console.print("\n[bold cyan]Commands:[/bold cyan]")
706
+ console.print(" [yellow]<text>[/yellow] - generate banner")
707
+ console.print(" [yellow]hero: WELCOME[/yellow] - use 'hero' profile")
708
+ console.print(" [yellow]6: WELCOME[/yellow] - use profile ID #6")
709
+ console.print(" [yellow]hero[/yellow] - select profile, then type text")
710
+ console.print(" [yellow]fonts[/yellow] - list all fonts")
711
+ console.print(" [yellow]styles[/yellow] - list style presets")
712
+ console.print(" [yellow]sizes[/yellow] - show size info")
713
+ console.print(" [yellow]compare <text>[/yellow] - compare sample fonts")
714
+ console.print(" [yellow]compare-styles <text>[/yellow] - compare style presets")
715
+ console.print(" [yellow]export <text>[/yellow] - render all fonts to files")
716
+ console.print(" [yellow]profiles[/yellow] - list saved profiles")
717
+ console.print(" [yellow]create-config[/yellow] - generate config.yaml template")
718
+ console.print(" [yellow]debug[/yellow] - run engine diagnostics")
719
+ console.print(" [yellow]debug: <text>[/yellow] - render with debug info")
720
+ console.print(" [yellow]quit[/yellow]")
721
+
722
+ raw = console.input("\n[bold yellow]> [/bold yellow]").strip()
723
+ if not raw:
724
+ continue
725
+
726
+ parts = raw.split(maxsplit=1)
727
+ cmd = parts[0].lower()
728
+ arg = parts[1] if len(parts) > 1 else ""
729
+
730
+ if cmd == "quit":
731
+ console.print("[green]Goodbye.[/green]")
732
+ break
733
+ elif cmd == "fonts":
734
+ engine.list_fonts()
735
+ elif cmd == "styles":
736
+ engine.list_styles()
737
+ elif cmd == "sizes":
738
+ console.print(f"[cyan]{engine.sizer.report()}[/cyan]")
739
+ best = engine.sizer.auto_fit_font("SAMPLE TEXT", "big")
740
+ console.print(f"[green]Best fit for 'SAMPLE TEXT': {best}[/green]")
741
+ elif cmd == "compare":
742
+ engine.compare_fonts(arg or "Hello")
743
+ elif cmd == "compare-styles":
744
+ engine.compare_styles(arg or "TEST")
745
+ elif cmd == "export":
746
+ text = arg or "EXPORT_DEMO"
747
+ engine.render_all_fonts(text)
748
+ elif cmd == "profiles":
749
+ engine.profiler.list_profiles()
750
+ elif cmd == "create-config":
751
+ engine.profiler.create_template()
752
+ else:
753
+ text = raw
754
+
755
+ # ── Profile selection by name or ID with colon ──
756
+ # Examples: "hero: WELCOME" or "6: WELCOME"
757
+ if ":" in text:
758
+ parts = text.split(":", 1)
759
+ selector = parts[0].strip()
760
+ actual_text = parts[1].strip()
761
+
762
+ # Try ID number first
763
+ if selector.isdigit():
764
+ idx = int(selector)
765
+ sorted_names = sorted(engine.profiler.profiles.keys())
766
+ if 0 <= idx < len(sorted_names):
767
+ profile_name = sorted_names[idx]
768
+ engine.render_profile(actual_text, profile_name)
769
+ continue
770
+ else:
771
+ console.print(f"[red]Invalid ID. Use 0-{len(sorted_names)-1}[/red]")
772
+ continue
773
+
774
+ # Try profile name
775
+ profile = engine.profiler.get(selector)
776
+ if profile is not None:
777
+ engine.render_profile(actual_text, selector)
778
+ continue
779
+
780
+ # Not a profile — treat as banner text with colon
781
+ console.print(f"[red]Profile '{selector}' not found.[/red]")
782
+ continue
783
+
784
+ # ── Profile selection by name alone (no colon) ──
785
+ # Example: "hero" → prompts for text
786
+ if text in engine.profiler.profiles:
787
+ console.print(f"[dim]Using profile '{text}'. Enter text to render:[/dim]")
788
+ profile_text = console.input("[bold]> [/bold]").strip()
789
+ if profile_text:
790
+ engine.render_profile(profile_text, text)
791
+ continue
792
+
793
+ # ── Profile selection by ID number alone (no colon) ──
794
+ # Example: "6" → prompts for text
795
+ if text.isdigit():
796
+ idx = int(text)
797
+ sorted_names = sorted(engine.profiler.profiles.keys())
798
+ if 0 <= idx < len(sorted_names):
799
+ profile_name = sorted_names[idx]
800
+ console.print(f"[dim]Using profile '{profile_name}'. Enter text to render:[/dim]")
801
+ profile_text = console.input("[bold]> [/bold]").strip()
802
+ if profile_text:
803
+ engine.render_profile(profile_text, profile_name)
804
+ continue
805
+ else:
806
+ console.print(f"[red]Invalid ID. Use 0-{len(sorted_names)-1}[/red]")
807
+ continue
808
+ # ── Debug mode: render + show diagnostic info ──
809
+ if text.lower().startswith("debug:"):
810
+ debug_text = text.split(":", 1)[1].strip()
811
+ if not debug_text:
812
+ debug_text = "DEBUG"
813
+
814
+ console.print("\n[bold yellow]--- DEBUG INFO ---[/bold yellow]")
815
+ console.print(f" Input text: '{debug_text}'")
816
+ console.print(f" Terminal: {engine.sizer.terminal_width} cols")
817
+ console.print(f" Usable: {engine.sizer.usable_width} cols")
818
+
819
+ art = engine.generate(debug_text, "standard", auto_fit=True)
820
+ if art:
821
+ width = engine.sizer.measure_banner(art)
822
+ fits = engine.sizer.fits(art, margin=6)
823
+ truncated = not fits
824
+
825
+ console.print(f" Font used: standard (auto-fit)")
826
+ console.print(f" Banner width: {width} cols")
827
+ console.print(f" Fits: {'Yes' if fits else 'No (truncated)'}")
828
+ console.print(f" Lines: {len(art.splitlines())}")
829
+ console.print(f" Profile loaded: {engine.profiler.is_loaded}")
830
+ console.print(f" Fonts available: {len(engine.FONTS)}")
831
+ console.print(f" Styles available: {len(StyleManager.PRESETS)}")
832
+ console.print()
833
+
834
+ engine.display_banner(debug_text, "standard", "clean", auto_fit=True)
835
+ continue
836
+
837
+ # ── Standalone 'debug' command runs full diagnostics ──
838
+ if text.lower() == "debug":
839
+ engine.run_diagnostics()
840
+ continue
841
+
842
+ # ── Normal flow: prompt for font + style ──
843
+ console.print("\n[dim]Font (name or number 1-8):[/dim]")
844
+ console.print(" 1-standard 2-slant 3-big 4-ghost "
845
+ "5-banner 6-gothic 7-digital 8-small")
846
+ font_choice = console.input("[bold]Font: [/bold]").strip().lower()
847
+ font_map = {
848
+ "1": "standard", "2": "slant", "3": "big",
849
+ "4": "ghost", "5": "banner", "6": "gothic",
850
+ "7": "digital", "8": "small",
851
+ }
852
+ font = font_map.get(font_choice, font_choice or "standard")
853
+
854
+ console.print("[dim]Style (preset name or color):[/dim]")
855
+ console.print(f" {' | '.join(StyleManager.list_presets()[:8])}")
856
+ style_choice = console.input("[bold]Style: [/bold]").strip().lower()
857
+ style = style_choice or "default"
858
+
859
+ engine.display_banner(text, font, style)
860
+
861
+
862
+ # ═══════════════════════════════════════════════════════════════════════
863
+ # MAIN ENTRY POINT
864
+ # ═══════════════════════════════════════════════════════════════════════
865
+
866
+ if __name__ == "__main__":
867
+ try:
868
+ from pyfiglet import Figlet
869
+ except ImportError:
870
+ console.print("[red]Install required packages:[/red]")
871
+ console.print(" pip install pyfiglet rich")
872
+ sys.exit(1)
873
+
874
+ font_count = len(ASCIIBannerEngine.FONTS)
875
+
876
+ print(f"""
877
+ +-----------------------------------------------------------+
878
+ | |
879
+ | ASCII BANNER ENGINE v1.0 |
880
+ | |
881
+ | {font_count} fonts auto-loaded | 18 style presets | SizeManager |
882
+ | |
883
+ +-----------------------------------------------------------+
884
+ """)
885
+
886
+ try:
887
+ if len(sys.argv) > 1:
888
+ cmd = sys.argv[1].lower()
889
+
890
+ if cmd == "--demo":
891
+ demo_basic_banners()
892
+ elif cmd == "--themes":
893
+ demo_themed_banners()
894
+ elif cmd == "--styles":
895
+ demo_styles()
896
+ elif cmd == "--sizes":
897
+ demo_size_manager()
898
+ elif cmd == "--compare":
899
+ ASCIIBannerEngine().compare_fonts(
900
+ sys.argv[2] if len(sys.argv) > 2 else "Hello")
901
+ elif cmd == "--compare-styles":
902
+ ASCIIBannerEngine().compare_styles(
903
+ sys.argv[2] if len(sys.argv) > 2 else "TEST")
904
+ elif cmd == "--interactive":
905
+ interactive_menu()
906
+ elif cmd == "--fonts":
907
+ ASCIIBannerEngine().list_fonts()
908
+ elif cmd == "--export":
909
+ ASCIIBannerEngine().render_all_fonts(
910
+ sys.argv[2] if len(sys.argv) > 2 else "DEMO")
911
+ elif cmd == "--profiles":
912
+ pm = ProfileManager()
913
+ if not pm.yaml_available:
914
+ console.print("[dim]PyYAML not installed — banner profiles "
915
+ "are unavailable (pip install pyyaml to enable)[/dim]")
916
+ elif not pm.is_loaded:
917
+ console.print("[yellow]No config.yaml found.[/yellow]")
918
+ console.print("[dim]Create one with: "
919
+ "python3 banner_engine.py --create-config[/dim]")
920
+ else:
921
+ pm.list_profiles()
922
+ elif cmd == "--create-config":
923
+ pm = ProfileManager()
924
+ pm.create_template()
925
+ elif cmd == "--profile":
926
+ if len(sys.argv) < 3:
927
+ console.print("[red]Usage: --profile <name> <text>[/red]")
928
+ console.print("[dim]Available profiles: python3 "
929
+ "banner_engine.py --profiles[/dim]")
930
+ else:
931
+ profile_name = sys.argv[2]
932
+ text = sys.argv[3] if len(sys.argv) > 3 else "PROFILE TEST"
933
+ engine = ASCIIBannerEngine()
934
+ engine.render_profile(text, profile_name)
935
+ elif cmd == "--debug":
936
+ engine = ASCIIBannerEngine()
937
+ engine.run_diagnostics()
938
+ elif cmd == "--verbose":
939
+ if len(sys.argv) < 3:
940
+ console.print("[red]Usage: --verbose <text>[/red]")
941
+ else:
942
+ text = sys.argv[2]
943
+ engine = ASCIIBannerEngine()
944
+ console.print("\n[bold yellow]--- VERBOSE RENDER ---[/bold yellow]")
945
+ console.print(f" Input: '{text}'")
946
+ console.print(f" Terminal: {engine.sizer.terminal_width} cols")
947
+
948
+ art = engine.generate(text, "standard", auto_fit=True)
949
+ if art:
950
+ width = engine.sizer.measure_banner(art)
951
+ fits = engine.sizer.fits(art, margin=6)
952
+ console.print(f" Auto-fit: ON")
953
+ console.print(f" Width: {width} cols")
954
+ console.print(f" Fits: {'Yes' if fits else 'No'}")
955
+ console.print(f" Lines: {len(art.splitlines())}")
956
+ console.print()
957
+
958
+ engine.display_banner(text, "standard", "clean", auto_fit=True)
959
+ elif cmd == "--version":
960
+ _v = "unknown"
961
+ try:
962
+ from importlib import metadata as _md
963
+ for _n in ("wizard-central-toolkit", "wiz-central-toolkit"):
964
+ try:
965
+ _v = _md.version(_n)
966
+ break
967
+ except _md.PackageNotFoundError:
968
+ continue
969
+ except Exception:
970
+ pass
971
+ console.print(f"bannerwiz {_v}")
972
+ elif cmd == "--help":
973
+ console.print("""
974
+ [bold]ASCII Banner Engine v1.0 - Commands:[/bold]
975
+
976
+ [cyan]--demo[/cyan] Basic banner examples
977
+ [cyan]--themes[/cyan] Themed banner collection
978
+ [cyan]--styles[/cyan] Style preset showcase
979
+ [cyan]--sizes[/cyan] SizeManager demo
980
+ [cyan]--compare <text>[/cyan] Compare fonts side-by-side
981
+ [cyan]--compare-styles <text>[/cyan] Compare style presets
982
+ [cyan]--fonts[/cyan] List all available fonts
983
+ [cyan]--export <text>[/cyan] Render all fonts to ./banner_output/
984
+ [cyan]--interactive[/cyan] Full interactive menu
985
+ [cyan]--debug[/cyan] Run full engine diagnostics
986
+ [cyan]--verbose <text>[/cyan] Render with detailed output
987
+ [cyan]<text>[/cyan] Quick render with default styling
988
+ """)
989
+ else:
990
+ ASCIIBannerEngine().display_banner(sys.argv[1], "banner", "clean")
991
+ else:
992
+ interactive_menu()
993
+
994
+ except KeyboardInterrupt:
995
+ console.print("\n[yellow]Interrupted. Goodbye.[/yellow]")
996
+ sys.exit(0)
997
+ except EOFError:
998
+ console.print("\n[yellow]End of input. Goodbye.[/yellow]")
999
+ sys.exit(0)