create-awesome-python-app 0.2.3__py3-none-any.whl → 0.2.4__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.
@@ -1,3 +1,3 @@
1
1
  """Create Awesome Python App CLI."""
2
2
 
3
- __version__ = "0.2.3"
3
+ __version__ = "0.2.4"
@@ -16,7 +16,10 @@ from rich.console import Console
16
16
  from rich.table import Table
17
17
 
18
18
  from create_awesome_python_app import __version__
19
- from create_awesome_python_app.prompt_style import bold, color_category, dim
19
+ from create_awesome_python_app.prompt_style import (
20
+ custom_template_title,
21
+ template_title_tokens,
22
+ )
20
23
 
21
24
  console = Console(stderr=True)
22
25
 
@@ -27,7 +30,7 @@ CUSTOM_TEMPLATE_SENTINEL = "__custom_template__"
27
30
  class TemplateChoice:
28
31
  """Searchable interactive template choice."""
29
32
 
30
- title: str
33
+ title: Any
31
34
  value: str
32
35
  search: str
33
36
 
@@ -236,35 +239,40 @@ def build_template_choices(data: dict[str, Any]) -> list[TemplateChoice]:
236
239
  category_name = categories.get(category_slug, category_slug)
237
240
  badge = short_category_label(category_name).ljust(10)[:10]
238
241
  slug = str(template.get("slug", ""))
239
- labels = template.get("labels", [])
240
- label_suffix = ""
241
- if isinstance(labels, list) and labels:
242
- label_suffix = dim(" · " + ", ".join(str(label) for label in labels[:3]))
242
+ labels_raw = template.get("labels", [])
243
+ labels = (
244
+ [str(label) for label in labels_raw[:3]]
245
+ if isinstance(labels_raw, list)
246
+ else []
247
+ )
243
248
  description = str(template.get("description", "")).strip()
244
- # Keep slug + short description in the title so select(use_search_filter)
245
- # can match them (filter scans Choice.title only).
246
- description_suffix = dim(f" — {description}") if description else ""
247
249
  name = str(template.get("name", slug))
248
- # ANSI is OK here: questionary.select renders titles as terminal text.
249
- # Do not pass these titles to autocomplete (HTML match highlighting).
250
- title = (
251
- f"{color_category(category_slug, badge)} "
252
- f"{bold(name)} ({slug})"
253
- f"{label_suffix}{description_suffix}"
250
+ search = _search_text(template, category_name)
251
+ # FormattedText tokens (not raw ANSI): select() prints str titles
252
+ # literally, which showed ^[[1;94m… in terminals.
253
+ title = template_title_tokens(
254
+ category_slug=category_slug,
255
+ badge=badge,
256
+ name=name,
257
+ slug=slug,
258
+ labels=labels,
259
+ description=description,
260
+ search=search,
254
261
  )
255
262
  choices.append(
256
263
  TemplateChoice(
257
264
  title=title,
258
265
  value=template_url,
259
- search=_search_text(template, category_name),
266
+ search=search,
260
267
  )
261
268
  )
262
269
 
270
+ custom_search = "custom own template url github file"
263
271
  choices.append(
264
272
  TemplateChoice(
265
- title=" " * 12 + dim("Use my own template URL"),
273
+ title=custom_template_title(custom_search),
266
274
  value=CUSTOM_TEMPLATE_SENTINEL,
267
- search="custom own template url github file",
275
+ search=custom_search,
268
276
  )
269
277
  )
270
278
  return choices
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
+ from typing import Any
6
7
 
7
8
  from questionary import Style
8
9
 
@@ -25,37 +26,88 @@ CPA_PROMPT_STYLE = Style.from_dict(
25
26
  }
26
27
  )
27
28
 
29
+ # Bright prompt_toolkit style strings (not raw ANSI — select() prints str titles
30
+ # literally, so escapes show as ^[[...m unless titles are FormattedText tokens).
31
+ _CATEGORY_STYLES = (
32
+ "fg:#facc15 bold", # yellow
33
+ "fg:#4ade80 bold", # green
34
+ "fg:#22d3ee bold", # cyan
35
+ "fg:#e879f9 bold", # magenta
36
+ "fg:#60a5fa bold", # blue
37
+ )
38
+
39
+
40
+ class SearchableFormattedText(list):
41
+ """FormattedText tokens with ``.lower()`` for questionary search filter.
42
+
43
+ ``select(use_search_filter=True)`` does ``needle in choice.title.lower()``.
44
+ A plain token list has no ``.lower()``; this keeps filter working while
45
+ titles render as styled FormattedText.
46
+ """
47
+
48
+ def __init__(self, tokens: list[tuple[str, str]], search: str) -> None:
49
+ super().__init__(tokens)
50
+ self._search = search
51
+
52
+ def lower(self) -> str:
53
+ return self._search.lower()
54
+
28
55
 
29
56
  def colors_enabled() -> bool:
30
57
  return not os.environ.get("NO_COLOR")
31
58
 
32
59
 
33
- def ansi(code: str, text: str) -> str:
34
- """Wrap *text* in an ANSI SGR sequence when colors are enabled."""
35
- if not colors_enabled():
36
- return text
37
- return f"\033[{code}m{text}\033[0m"
38
-
39
-
40
- # Bold bright ANSI — readable on dark terminals; select() renders these safely
41
- # (unlike autocomplete, which HTML-parses choice text).
42
- _CATEGORY_PALETTE = (
43
- "1;93", # bright yellow
44
- "1;92", # bright green
45
- "1;96", # bright cyan
46
- "1;95", # bright magenta
47
- "1;94", # bright blue
48
- )
49
-
60
+ def category_style(slug: str) -> str:
61
+ idx = sum(ord(char) for char in slug) % len(_CATEGORY_STYLES)
62
+ return _CATEGORY_STYLES[idx]
50
63
 
51
- def color_category(slug: str, label: str) -> str:
52
- idx = sum(ord(char) for char in slug) % len(_CATEGORY_PALETTE)
53
- return ansi(_CATEGORY_PALETTE[idx], label)
54
64
 
65
+ def plain_title_text(title: Any) -> str:
66
+ """Join FormattedText token text (or return a plain string title)."""
67
+ if isinstance(title, list):
68
+ return "".join(str(token[1]) for token in title)
69
+ return str(title)
55
70
 
56
- def bold(text: str) -> str:
57
- return ansi("1", text)
58
71
 
72
+ def template_title_tokens(
73
+ *,
74
+ category_slug: str,
75
+ badge: str,
76
+ name: str,
77
+ slug: str,
78
+ labels: list[str],
79
+ description: str,
80
+ search: str,
81
+ ) -> SearchableFormattedText | str:
82
+ """Build a select-safe title: FormattedText when colors on, else plain str."""
83
+ label_suffix = ""
84
+ if labels:
85
+ label_suffix = " · " + ", ".join(labels[:3])
86
+ description_suffix = f" — {description}" if description else ""
87
+ plain = f"{badge} {name} ({slug}){label_suffix}{description_suffix}"
59
88
 
60
- def dim(text: str) -> str:
61
- return ansi("2", text)
89
+ if not colors_enabled():
90
+ return plain
91
+
92
+ tokens: list[tuple[str, str]] = [
93
+ (category_style(category_slug), badge),
94
+ ("", " "),
95
+ ("bold", name),
96
+ ("class:instruction", f" ({slug})"),
97
+ ]
98
+ if label_suffix:
99
+ tokens.append(("class:instruction", label_suffix))
100
+ if description_suffix:
101
+ tokens.append(("fg:#94a3b8", description_suffix))
102
+ return SearchableFormattedText(tokens, search=search or plain)
103
+
104
+
105
+ def custom_template_title(search: str) -> SearchableFormattedText | str:
106
+ label = "Use my own template URL"
107
+ plain = " " * 12 + label
108
+ if not colors_enabled():
109
+ return plain
110
+ return SearchableFormattedText(
111
+ [("", " " * 12), ("italic fg:#94a3b8", label)],
112
+ search=search or plain,
113
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: create-awesome-python-app
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Composable scaffolding CLI for production-ready Python apps
5
5
  Project-URL: Homepage, https://github.com/Create-Python-App/create-python-app
6
6
  Project-URL: Repository, https://github.com/Create-Python-App/create-python-app
@@ -8,7 +8,7 @@ Project-URL: Issues, https://github.com/Create-Python-App/create-python-app/issu
8
8
  Project-URL: Changelog, https://github.com/Create-Python-App/create-python-app/blob/main/CHANGELOG.md
9
9
  License-Expression: MIT
10
10
  Requires-Python: >=3.12
11
- Requires-Dist: create-python-app-core>=0.2.3
11
+ Requires-Dist: create-python-app-core>=0.2.4
12
12
  Requires-Dist: questionary>=2.1.1
13
13
  Requires-Dist: rich>=15.0.0
14
14
  Requires-Dist: typer>=0.27.0
@@ -0,0 +1,9 @@
1
+ create_awesome_python_app/__init__.py,sha256=D04aKhr6EFJUIRigne5PNis9DoItI8X-4SC6lK_VfJE,60
2
+ create_awesome_python_app/cache.py,sha256=eHKUlunGexZKYJo0HDNoBoOVq0DjLyRYgEe_wUh4Fk0,11812
3
+ create_awesome_python_app/catalog.py,sha256=w5GS_kQ6MM37AilutXbYZ9Hk5JGa_12crzI0LYzKfRg,16821
4
+ create_awesome_python_app/cli.py,sha256=6og82WI-RZxTqHETqrieA8relgppwCqFntOEmOFrIPk,21195
5
+ create_awesome_python_app/prompt_style.py,sha256=pERX1qPQSLZXeJvPVFC7z85dvte73ZV4iuENPQduheE,3463
6
+ create_awesome_python_app-0.2.4.dist-info/METADATA,sha256=KD7yBBBomrwpw4hfqJm9aIBGWnjBukcHl48bOqjt8_M,1428
7
+ create_awesome_python_app-0.2.4.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
8
+ create_awesome_python_app-0.2.4.dist-info/entry_points.txt,sha256=rszFbUjY-lvMDZ96zX1lALJwgD1mI3CkUuNyF2qqjYo,81
9
+ create_awesome_python_app-0.2.4.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- create_awesome_python_app/__init__.py,sha256=DPsQbeW1110h7DEbWLxV7pLEEDAUnc5WkLiBamNsjSU,60
2
- create_awesome_python_app/cache.py,sha256=eHKUlunGexZKYJo0HDNoBoOVq0DjLyRYgEe_wUh4Fk0,11812
3
- create_awesome_python_app/catalog.py,sha256=_x6TqYAVlURM1dbNsflB9I4g8ku_anZzuK_ggN_OvKs,16909
4
- create_awesome_python_app/cli.py,sha256=6og82WI-RZxTqHETqrieA8relgppwCqFntOEmOFrIPk,21195
5
- create_awesome_python_app/prompt_style.py,sha256=A1RQn2hX7khu21bNL5KfD1ZNAHUJfCzmxXIRMgin658,1670
6
- create_awesome_python_app-0.2.3.dist-info/METADATA,sha256=Kvw2BT6Ff3YF3ZkH8DVJd5uSFyepcGGlMBqX6fgJAp4,1428
7
- create_awesome_python_app-0.2.3.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
8
- create_awesome_python_app-0.2.3.dist-info/entry_points.txt,sha256=rszFbUjY-lvMDZ96zX1lALJwgD1mI3CkUuNyF2qqjYo,81
9
- create_awesome_python_app-0.2.3.dist-info/RECORD,,