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.
- wiz_central_toolkit/__init__.py +2 -0
- wiz_central_toolkit/bannerwiz/__main__.py +6 -0
- wiz_central_toolkit/bannerwiz/cli.py +79 -0
- wiz_central_toolkit/bannerwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/bannerwiz/lib/banner_wizard.py +999 -0
- wiz_central_toolkit/bannerwiz/lib/banner_wrapper.py +199 -0
- wiz_central_toolkit/bannerwiz/lib/cli.py +79 -0
- wiz_central_toolkit/codeguardwiz/__init__.py +1 -0
- wiz_central_toolkit/codeguardwiz/__main__.py +19 -0
- wiz_central_toolkit/codeguardwiz/cli.py +971 -0
- wiz_central_toolkit/codeguardwiz/codeguard_wizard.py +269 -0
- wiz_central_toolkit/codeguardwiz/rust_engine.py +348 -0
- wiz_central_toolkit/codeguardwiz/static_engine.py +307 -0
- wiz_central_toolkit/common/__init__.py +1 -0
- wiz_central_toolkit/debugwiz/__init__.py +1 -0
- wiz_central_toolkit/debugwiz/__main__.py +360 -0
- wiz_central_toolkit/debugwiz/debug_wizard.py +364 -0
- wiz_central_toolkit/diffwiz/__init__.py +0 -0
- wiz_central_toolkit/diffwiz/__main__.py +21 -0
- wiz_central_toolkit/diffwiz/cli.py +134 -0
- wiz_central_toolkit/diffwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/diffwiz/lib/cli.py +144 -0
- wiz_central_toolkit/diffwiz/lib/diff_wizard.py +89 -0
- wiz_central_toolkit/godfatherwiz/__init__.py +0 -0
- wiz_central_toolkit/godfatherwiz/__main__.py +18 -0
- wiz_central_toolkit/godfatherwiz/ai.py +136 -0
- wiz_central_toolkit/godfatherwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/godfatherwiz/lib/cli.py +925 -0
- wiz_central_toolkit/godfatherwiz/lib/godfather_wizard.py +1552 -0
- wiz_central_toolkit/jsonwiz/__init__.py +0 -0
- wiz_central_toolkit/jsonwiz/__main__.py +21 -0
- wiz_central_toolkit/jsonwiz/cli.py +4 -0
- wiz_central_toolkit/jsonwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/jsonwiz/lib/cli.py +138 -0
- wiz_central_toolkit/jsonwiz/lib/json_wizard.py +133 -0
- wiz_central_toolkit/pythonwiz/__init__.py +0 -0
- wiz_central_toolkit/pythonwiz/__main__.py +21 -0
- wiz_central_toolkit/pythonwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/pythonwiz/lib/cli.py +247 -0
- wiz_central_toolkit/pythonwiz/lib/python_wizard.py +353 -0
- wiz_central_toolkit/readwiz/__init__.py +5 -0
- wiz_central_toolkit/readwiz/__main__.py +15 -0
- wiz_central_toolkit/readwiz/cli.py +46 -0
- wiz_central_toolkit/readwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/readwiz/lib/cli.py +47 -0
- wiz_central_toolkit/readwiz/lib/read_wizard.py +121 -0
- wiz_central_toolkit/rustwiz/__init__.py +5 -0
- wiz_central_toolkit/rustwiz/__main__.py +7 -0
- wiz_central_toolkit/rustwiz/cli.py +4 -0
- wiz_central_toolkit/rustwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/rustwiz/lib/cli.py +53 -0
- wiz_central_toolkit/rustwiz/lib/rust_wizard.py +128 -0
- wiz_central_toolkit/tmuxwiz/__init__.py +0 -0
- wiz_central_toolkit/tmuxwiz/__main__.py +15 -0
- wiz_central_toolkit/tmuxwiz/cli.py +7 -0
- wiz_central_toolkit/tmuxwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/tmuxwiz/lib/cli.py +10 -0
- wiz_central_toolkit/tmuxwiz/lib/tmux_wizard.py +229 -0
- wiz_central_toolkit/todowiz/__init__.py +0 -0
- wiz_central_toolkit/todowiz/__main__.py +22 -0
- wiz_central_toolkit/todowiz/cli.py +4 -0
- wiz_central_toolkit/todowiz/lib/__init__.py +1 -0
- wiz_central_toolkit/todowiz/lib/cli.py +82 -0
- wiz_central_toolkit/todowiz/lib/todo_wizard.py +134 -0
- wiz_central_toolkit/tools_registry.py +62 -0
- wiz_central_toolkit/wiz/__init__.py +0 -0
- wiz_central_toolkit/wiz/__main__.py +141 -0
- wiz_central_toolkit/writewiz/__init__.py +1 -0
- wiz_central_toolkit/writewiz/__main__.py +9 -0
- wiz_central_toolkit/writewiz/cli.py +264 -0
- wiz_central_toolkit/writewiz/write_wizard.py +384 -0
- wizard_central_toolkit-0.2.0.dist-info/METADATA +203 -0
- wizard_central_toolkit-0.2.0.dist-info/RECORD +77 -0
- wizard_central_toolkit-0.2.0.dist-info/WHEEL +5 -0
- wizard_central_toolkit-0.2.0.dist-info/entry_points.txt +50 -0
- wizard_central_toolkit-0.2.0.dist-info/licenses/LICENSE +201 -0
- 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)
|