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 :
@@ -527,44 +574,96 @@ def reset_catalog_cache_for_tests() -> None:
527574
528575def list_templates () -> None :
529576 data = get_catalog_data ()
530- table = Table (title = "Templates" )
531- table .add_column ("slug" )
532- table .add_column ("category" )
533- table .add_column ("type" )
534- for t in data .get ("templates" , []):
535- table .add_row (
536- str (t .get ("slug" , "" )),
537- str (t .get ("category" , "" )),
538- str (t .get ("type" , "" )),
539- )
540- console .print (table )
577+ categories = category_index (data )
578+ templates = [t for t in data .get ("templates" , []) if isinstance (t , dict )]
579+
580+ console .print ("[bold blue]\n Available Templates:[/bold blue]" )
581+ # Preserve catalog category order, then any unknown slugs.
582+ ordered_slugs = list (categories )
583+ seen : set [str ] = set ()
584+ for slug in ordered_slugs :
585+ seen .add (slug )
586+ group = [t for t in templates if str (t .get ("category" , "" )) == slug ]
587+ if not group :
588+ continue
589+ info = categories [slug ]
590+ console .print (f"[bold green]\n { info .name } :[/bold green]" )
591+ if info .description :
592+ console .print (f" { info .description } " )
593+ if info .details :
594+ console .print (f" [dim]{ info .details } [/dim]" )
595+ if info .labels :
596+ console .print (f" [dim]Keywords: { ', ' .join (info .labels )} [/dim]" )
597+ for template in group :
598+ name = str (template .get ("name" , template .get ("slug" , "" )))
599+ tslug = str (template .get ("slug" , "" ))
600+ console .print (f" [yellow]{ name } [/yellow] ([cyan]{ tslug } [/cyan])" )
601+ desc = str (template .get ("description" , "" )).strip ()
602+ if desc :
603+ console .print (f" { desc } " )
604+ labels = template .get ("labels" , [])
605+ if isinstance (labels , list ) and labels :
606+ console .print (f" Keywords: { ', ' .join (str (x ) for x in labels )} " )
607+
608+ orphans = [t for t in templates if str (t .get ("category" , "" )) not in seen ]
609+ if orphans :
610+ console .print ("[bold green]\n Other:[/bold green]" )
611+ for template in orphans :
612+ name = str (template .get ("name" , template .get ("slug" , "" )))
613+ tslug = str (template .get ("slug" , "" ))
614+ console .print (f" [yellow]{ name } [/yellow] ([cyan]{ tslug } [/cyan])" )
541615
542616
543617def list_addons (template_slug : str | None = None ) -> None :
544618 data = get_catalog_data ()
619+ categories = category_index (data )
545620 template_type : str | None = None
546621 if template_slug :
547622 for t in data .get ("templates" , []):
548- if t .get ("slug" ) == template_slug :
623+ if isinstance ( t , dict ) and t .get ("slug" ) == template_slug :
549624 template_type = str (t .get ("type" , "" ))
550625 break
551626
552- table = Table (title = "Extensions" )
553- table .add_column ("slug" )
554- table .add_column ("category" )
555- table .add_column ("type" )
556- for ext in data .get ("extensions" , data .get ("addons" , [])):
627+ console .print ("[bold blue]\n Available Addons:[/bold blue]" )
628+ if template_slug :
629+ console .print (
630+ f"[bold green]\n Compatible with template: { template_slug } [/bold green]"
631+ )
632+
633+ extensions = [
634+ ext
635+ for ext in data .get ("extensions" , data .get ("addons" , []))
636+ if isinstance (ext , dict )
637+ ]
638+ grouped : dict [str , list [dict [str , Any ]]] = {}
639+ for ext in extensions :
557640 ext_types = ext .get ("type" , [])
558641 if isinstance (ext_types , str ):
559642 ext_types = [ext_types ]
560643 if template_type and template_type not in ext_types :
561644 continue
562- type_label = (
563- ", " .join (ext_types ) if isinstance (ext_types , list ) else str (ext_types )
564- )
565- table .add_row (
566- str (ext .get ("slug" , "" )),
567- str (ext .get ("category" , "" )),
568- type_label ,
569- )
570- console .print (table )
645+ slug = str (ext .get ("category" , "custom" ))
646+ grouped .setdefault (slug , []).append (ext )
647+
648+ for slug in list (categories ) + [s for s in grouped if s not in categories ]:
649+ group = grouped .get (slug )
650+ if not group :
651+ continue
652+ info = categories .get (slug ) or CategoryInfo (slug = slug , name = slug )
653+ console .print (f"[bold green]\n { info .name } :[/bold green]" )
654+ if info .description :
655+ console .print (f" { info .description } " )
656+ if info .details :
657+ console .print (f" [dim]{ info .details } [/dim]" )
658+ if info .labels :
659+ console .print (f" [dim]Keywords: { ', ' .join (info .labels )} [/dim]" )
660+ for ext in group :
661+ name = str (ext .get ("name" , ext .get ("slug" , "" )))
662+ eslug = str (ext .get ("slug" , "" ))
663+ console .print (f" [yellow]{ name } [/yellow] ([cyan]{ eslug } [/cyan])" )
664+ desc = str (ext .get ("description" , "" )).strip ()
665+ if desc :
666+ console .print (f" { desc } " )
667+ labels = ext .get ("labels" , [])
668+ if isinstance (labels , list ) and labels :
669+ console .print (f" Keywords: { ', ' .join (str (x ) for x in labels )} " )
0 commit comments