1313
1414from create_python_app_core .paths import default_cache_dir , resolve_source
1515from rich .console import Console
16- from rich .table import Table
1716
1817from create_awesome_python_app import __version__
1918from create_awesome_python_app .prompt_style import (
@@ -159,6 +158,17 @@ def validate_extension_compatibility(
159158 raise IncompatibleExtensionsError (pairs )
160159
161160
161+ @dataclass (frozen = True )
162+ class CategoryInfo :
163+ """Catalog category with optional rich metadata (CNA parity)."""
164+
165+ slug : str
166+ name : str
167+ description : str = ""
168+ details : str = ""
169+ labels : tuple [str , ...] = ()
170+
171+
162172def short_category_label (category_name : str ) -> str :
163173 """Derive a compact badge label from a catalog category name."""
164174 stop_words = {"Applications" , "Application" , "Boilerplate" }
@@ -168,11 +178,48 @@ def short_category_label(category_name: str) -> str:
168178 return " " .join (words [:2 ]) or category_name
169179
170180
181+ def category_index (data : dict [str , Any ]) -> dict [str , CategoryInfo ]:
182+ """Index categories by slug, including description/details/labels."""
183+ out : dict [str , CategoryInfo ] = {}
184+ for raw in data .get ("categories" , []):
185+ if not isinstance (raw , dict ):
186+ continue
187+ slug = str (raw .get ("slug" , "" )).strip ()
188+ if not slug :
189+ continue
190+ labels_raw = raw .get ("labels" , [])
191+ labels = (
192+ tuple (str (label ) for label in labels_raw )
193+ if isinstance (labels_raw , list )
194+ else ()
195+ )
196+ out [slug ] = CategoryInfo (
197+ slug = slug ,
198+ name = str (raw .get ("name" , slug )),
199+ description = str (raw .get ("description" , "" )).strip (),
200+ details = str (raw .get ("details" , "" )).strip (),
201+ labels = labels ,
202+ )
203+ return out
204+
205+
206+ def get_category_data (data : dict [str , Any ], slug : str ) -> CategoryInfo | None :
207+ return category_index (data ).get (slug )
208+
209+
210+ def format_category_choice_title (info : CategoryInfo , extension_count : int ) -> str :
211+ """Human title for the interactive extension-category picker."""
212+ noun = "extension" if extension_count == 1 else "extensions"
213+ title = f"{ info .name } ({ extension_count } { noun } )"
214+ if info .description :
215+ title = f"{ title } — { info .description } "
216+ if info .labels :
217+ title = f"{ title } · { ', ' .join (info .labels [:3 ])} "
218+ return title
219+
220+
171221def _category_map (data : dict [str , Any ]) -> dict [str , str ]:
172- return {
173- str (category .get ("slug" , "" )): str (category .get ("name" , "" ))
174- for category in data .get ("categories" , [])
175- }
222+ return {slug : info .name for slug , info in category_index (data ).items ()}
176223
177224
178225def _search_text (template : dict [str , Any ], category_name : str ) -> str :
@@ -471,44 +518,96 @@ def reset_catalog_cache_for_tests() -> None:
471518
472519def list_templates () -> None :
473520 data = get_catalog_data ()
474- table = Table (title = "Templates" )
475- table .add_column ("slug" )
476- table .add_column ("category" )
477- table .add_column ("type" )
478- for t in data .get ("templates" , []):
479- table .add_row (
480- str (t .get ("slug" , "" )),
481- str (t .get ("category" , "" )),
482- str (t .get ("type" , "" )),
483- )
484- console .print (table )
521+ categories = category_index (data )
522+ templates = [t for t in data .get ("templates" , []) if isinstance (t , dict )]
523+
524+ console .print ("[bold blue]\n Available Templates:[/bold blue]" )
525+ # Preserve catalog category order, then any unknown slugs.
526+ ordered_slugs = list (categories )
527+ seen : set [str ] = set ()
528+ for slug in ordered_slugs :
529+ seen .add (slug )
530+ group = [t for t in templates if str (t .get ("category" , "" )) == slug ]
531+ if not group :
532+ continue
533+ info = categories [slug ]
534+ console .print (f"[bold green]\n { info .name } :[/bold green]" )
535+ if info .description :
536+ console .print (f" { info .description } " )
537+ if info .details :
538+ console .print (f" [dim]{ info .details } [/dim]" )
539+ if info .labels :
540+ console .print (f" [dim]Keywords: { ', ' .join (info .labels )} [/dim]" )
541+ for template in group :
542+ name = str (template .get ("name" , template .get ("slug" , "" )))
543+ tslug = str (template .get ("slug" , "" ))
544+ console .print (f" [yellow]{ name } [/yellow] ([cyan]{ tslug } [/cyan])" )
545+ desc = str (template .get ("description" , "" )).strip ()
546+ if desc :
547+ console .print (f" { desc } " )
548+ labels = template .get ("labels" , [])
549+ if isinstance (labels , list ) and labels :
550+ console .print (f" Keywords: { ', ' .join (str (x ) for x in labels )} " )
551+
552+ orphans = [t for t in templates if str (t .get ("category" , "" )) not in seen ]
553+ if orphans :
554+ console .print ("[bold green]\n Other:[/bold green]" )
555+ for template in orphans :
556+ name = str (template .get ("name" , template .get ("slug" , "" )))
557+ tslug = str (template .get ("slug" , "" ))
558+ console .print (f" [yellow]{ name } [/yellow] ([cyan]{ tslug } [/cyan])" )
485559
486560
487561def list_addons (template_slug : str | None = None ) -> None :
488562 data = get_catalog_data ()
563+ categories = category_index (data )
489564 template_type : str | None = None
490565 if template_slug :
491566 for t in data .get ("templates" , []):
492- if t .get ("slug" ) == template_slug :
567+ if isinstance ( t , dict ) and t .get ("slug" ) == template_slug :
493568 template_type = str (t .get ("type" , "" ))
494569 break
495570
496- table = Table (title = "Extensions" )
497- table .add_column ("slug" )
498- table .add_column ("category" )
499- table .add_column ("type" )
500- for ext in data .get ("extensions" , data .get ("addons" , [])):
571+ console .print ("[bold blue]\n Available Addons:[/bold blue]" )
572+ if template_slug :
573+ console .print (
574+ f"[bold green]\n Compatible with template: { template_slug } [/bold green]"
575+ )
576+
577+ extensions = [
578+ ext
579+ for ext in data .get ("extensions" , data .get ("addons" , []))
580+ if isinstance (ext , dict )
581+ ]
582+ grouped : dict [str , list [dict [str , Any ]]] = {}
583+ for ext in extensions :
501584 ext_types = ext .get ("type" , [])
502585 if isinstance (ext_types , str ):
503586 ext_types = [ext_types ]
504587 if template_type and template_type not in ext_types :
505588 continue
506- type_label = (
507- ", " .join (ext_types ) if isinstance (ext_types , list ) else str (ext_types )
508- )
509- table .add_row (
510- str (ext .get ("slug" , "" )),
511- str (ext .get ("category" , "" )),
512- type_label ,
513- )
514- console .print (table )
589+ slug = str (ext .get ("category" , "custom" ))
590+ grouped .setdefault (slug , []).append (ext )
591+
592+ for slug in list (categories ) + [s for s in grouped if s not in categories ]:
593+ group = grouped .get (slug )
594+ if not group :
595+ continue
596+ info = categories .get (slug ) or CategoryInfo (slug = slug , name = slug )
597+ console .print (f"[bold green]\n { info .name } :[/bold green]" )
598+ if info .description :
599+ console .print (f" { info .description } " )
600+ if info .details :
601+ console .print (f" [dim]{ info .details } [/dim]" )
602+ if info .labels :
603+ console .print (f" [dim]Keywords: { ', ' .join (info .labels )} [/dim]" )
604+ for ext in group :
605+ name = str (ext .get ("name" , ext .get ("slug" , "" )))
606+ eslug = str (ext .get ("slug" , "" ))
607+ console .print (f" [yellow]{ name } [/yellow] ([cyan]{ eslug } [/cyan])" )
608+ desc = str (ext .get ("description" , "" )).strip ()
609+ if desc :
610+ console .print (f" { desc } " )
611+ labels = ext .get ("labels" , [])
612+ if isinstance (labels , list ) and labels :
613+ console .print (f" Keywords: { ', ' .join (str (x ) for x in labels )} " )
0 commit comments