Skip to content

Commit f7a12cc

Browse files
fix: drop ANSI from autocomplete titles (questionary HTML) (#207)
questionary wraps choice text in HTML for match highlighting; ANSI category badges caused "not well-formed (invalid token)" while typing to search. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 47b167d commit f7a12cc

3 files changed

Lines changed: 46 additions & 35 deletions

File tree

docs/UIUX_BRANDING_HANDOFF.md

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -108,23 +108,19 @@ From `cpa.config.json` or catalog `customOptions`:
108108
- `text` for string options
109109
- Password/invisible types are skipped with a yellow warning
110110

111-
### Category badge colors
111+
### Category badges
112112

113-
Template list badges use deterministic ANSI colors from `_CATEGORY_PALETTE` in `catalog.py`:
113+
Interactive template choices use a plain fixed-width badge from
114+
`short_category_label()` (strips "Applications", "Application", "Boilerplate";
115+
abbreviates long names to initials). Titles stay plain text because
116+
`questionary.autocomplete` wraps choices in HTML for match highlighting — ANSI
117+
or other markup raises XML parse errors in prompt_toolkit.
114118

115-
| Index | ANSI color | Typical use |
116-
|-------|------------|-------------|
117-
| 0 | yellow (`\033[33m`) | category badge |
118-
| 1 | green (`\033[32m`) | category badge |
119-
| 2 | cyan (`\033[36m`) | category badge |
120-
| 3 | magenta (`\033[35m`) | category badge |
121-
| 4 | blue (`\033[34m`) | category badge |
119+
`--list-templates` uses Rich tables for color, not ANSI in choice strings.
122120

123-
Color selection: `sum(ord(char) for char in category_slug) % 5`. Respects `NO_COLOR` (plain text, no ANSI).
124-
125-
Badge label: compact form from `short_category_label()` (strips "Applications", "Application", "Boilerplate"; abbreviates long names to initials).
126-
127-
Design implication: terminal category colors are slug-hash-driven, not semantically mapped (e.g. "backend" is not always blue). A future brand system may want stable category-to-color mapping for docs and website cards.
121+
Design implication: if terminal category color returns, prefer Rich styling or
122+
a prompt library that does not HTML-parse choice titles (CNA uses `prompts` +
123+
picocolors).
128124

129125
### Rich semantic color usage
130126

@@ -195,7 +191,7 @@ The CLI experience is functional and CNA-aligned for catalog flows, but the broa
195191
Current CLI aesthetic:
196192

197193
- Rich semantic colors (red/yellow/green/cyan/dim).
198-
- Hash-based category badge colors in terminal.
194+
- Plain fixed-width category badges in autocomplete titles (HTML-safe for questionary).
199195
- Minimal hero SVG (slate + teal).
200196
- No startup banner or branded prompt chrome beyond questionary defaults.
201197

@@ -294,7 +290,7 @@ Before implementation, produce a complete audit answering:
294290
- What should the brand personality become?
295291
- Does the CLI first run explain the product clearly in the first 5 seconds?
296292
- Do autocomplete and checkbox flows feel premium and discoverable?
297-
- Should category badge colors become semantic instead of hash-based?
293+
- Should website/docs category badges use semantic colors (CLI titles stay plain for questionary)?
298294
- Does the PyPI package README convert visitors into users?
299295
- Does the root GitHub README convert visitors into contributors?
300296
- Are templates presented in a way that feels premium and trustworthy?
@@ -367,7 +363,7 @@ Visual identity:
367363
- Define illustration/hero style.
368364
- Define voice and tone (match CLI error copy guidelines).
369365
- Define how "cozy" and "developer infrastructure" coexist.
370-
- Map `_CATEGORY_PALETTE` colors to branded hex values for non-terminal surfaces.
366+
- Define category badge colors for website/docs cards (CLI autocomplete titles stay plain text).
371367

372368
## Constraints And Standards
373369

@@ -410,7 +406,7 @@ Environment variables affecting UX:
410406
| Variable | Effect |
411407
|----------|--------|
412408
| `CI` | Disables interactive prompts unless `--interactive` |
413-
| `NO_COLOR` | Disables category badge ANSI colors |
409+
| `NO_COLOR` | Honored by Rich output (list tables, messages) |
414410
| `CPA_CATALOG_URL` | Override catalog source |
415411
| `CPA_NO_CATALOG_CACHE` | Force catalog refresh |
416412
| `CPA_STRICT_VERSION` | Treat version mismatch as error |
@@ -424,7 +420,10 @@ We need to do a full UI/UX and branding review of the Create Python App ecosyste
424420
425421
Please start with discovery and audit before implementing. Review the root create-python-app repo, the package README, cpa-templates, and docs/BRAND.md. The goal is to improve engagement, attraction, branding, cozy developer experience, visual consistency, and conversion across GitHub, PyPI, docs, CLI, and generated starter UIs.
426422
427-
The CLI already uses Rich on stderr, questionary autocomplete/checkbox flows, and hash-based category badge colors from catalog.py. Evaluate whether those defaults should evolve into a cohesive brand system. Pay attention to error message tone, interactive vs CI non-interactive behavior, and the minimal hero SVG.
423+
The CLI already uses Rich on stderr and questionary autocomplete/checkbox flows
424+
with plain-text category badges (HTML-safe for prompt_toolkit). Evaluate whether
425+
those defaults should evolve into a cohesive brand system. Pay attention to error
426+
message tone, interactive vs CI non-interactive behavior, and the minimal hero SVG.
428427
429428
Previous work established basic READMEs and BRAND.md notes, but now I want a broader review and a stronger cohesive brand direction. Do not assume the current teal-on-slate hero or terminal colors are final.
430429

packages/create-awesome-python-app/src/create_awesome_python_app/catalog.py

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,6 @@
2020
console = Console(stderr=True)
2121

2222
CUSTOM_TEMPLATE_SENTINEL = "__custom_template__"
23-
_ANSI_RESET = "\033[0m"
24-
_CATEGORY_PALETTE = (
25-
"\033[33m", # yellow
26-
"\033[32m", # green
27-
"\033[36m", # cyan
28-
"\033[35m", # magenta
29-
"\033[34m", # blue
30-
)
3123

3224

3325
@dataclass(frozen=True)
@@ -172,13 +164,6 @@ def short_category_label(category_name: str) -> str:
172164
return " ".join(words[:2]) or category_name
173165

174166

175-
def _color_category(slug: str, label: str) -> str:
176-
if os.environ.get("NO_COLOR"):
177-
return label
178-
idx = sum(ord(char) for char in slug) % len(_CATEGORY_PALETTE)
179-
return f"{_CATEGORY_PALETTE[idx]}{label}{_ANSI_RESET}"
180-
181-
182167
def _category_map(data: dict[str, Any]) -> dict[str, str]:
183168
return {
184169
str(category.get("slug", "")): str(category.get("name", ""))
@@ -256,8 +241,10 @@ def build_template_choices(data: dict[str, Any]) -> list[TemplateChoice]:
256241
label_suffix = " · " + ", ".join(str(label) for label in labels[:3])
257242
description = str(template.get("description", "")).strip()
258243
description_suffix = f" — {description}" if description else ""
244+
# Plain text only: questionary.autocomplete wraps choices in HTML for
245+
# match highlighting, so ANSI / markup here raises XML parse errors.
259246
title = (
260-
f"{_color_category(category_slug, badge)} "
247+
f"{badge} "
261248
f"{template.get('name', slug)} ({slug})"
262249
f"{label_suffix}{description_suffix}"
263250
)

packages/create-awesome-python-app/tests/test_catalog_resolve.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,37 @@ def test_build_template_choices_are_searchable() -> None:
102102
assert "FastAPI Starter" in first.title
103103
assert "OpenAPI" in first.title
104104
assert "uv" in first.title
105+
assert "\033" not in first.title
105106
assert "openapi" in first.search
106107
assert "backend" in first.search
107108
assert "uv" in first.search
108109
assert choices[-1].value == CUSTOM_TEMPLATE_SENTINEL
109110

110111

112+
def test_template_choice_titles_are_html_safe_for_questionary() -> None:
113+
"""questionary.autocomplete formats choice text as HTML (match underline)."""
114+
from prompt_toolkit.formatted_text import HTML
115+
116+
catalog = {
117+
"categories": [
118+
{"slug": "backend-applications", "name": "Backend Applications"}
119+
],
120+
"templates": [
121+
{
122+
"slug": "fastapi-starter",
123+
"name": "FastAPI Starter",
124+
"description": "Async API with OpenAPI docs",
125+
"url": "file:///templates/fastapi",
126+
"category": "backend-applications",
127+
"labels": ["FastAPI"],
128+
}
129+
],
130+
}
131+
title = build_template_choices(catalog)[0].title
132+
# Must not raise "not well-formed (invalid token)" from ANSI escapes.
133+
HTML("{}<b><u>{}</u></b>{}").format(title[:3], title[3:6], title[6:])
134+
135+
111136
def test_build_extension_choices_filters_by_template_type() -> None:
112137
catalog = {
113138
"categories": [

0 commit comments

Comments
 (0)