Skip to content

Commit 6082ce9

Browse files
ctruedenclaude
andcommitted
Fix emoji substitution in Rich output and add coordinate formatting
Rich's emoji feature was converting patterns like `:fiji:` in Maven coordinates to flag emojis (🇫🇯). This affected logging and console output, making coordinates difficult to read and breaking text parsing. Changes: - Set RichHandler markup=False to disable emoji/markup in log messages - Add Coordinate.rich() method for styled coordinate formatting - Update coord2str() with rich parameter for semantic coloring - groupId: bold cyan, artifactId: bold, version: green - Colons: dim (prevents emoji substitution as side effect) - Update search.py and versions.py to use Coordinate.rich() - Add tests in color.t verifying emoji prevention with :bear: pattern - Update docs/output-subsystem.md with markup behavior explanation The dim colons in rich formatting break emoji patterns (e.g., `:bear:` becomes `[dim]:[/]bear[dim]:[/]` which Rich doesn't recognize as 🐻). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
1 parent 87bb7d3 commit 6082ce9

File tree

7 files changed

+138
-37
lines changed

7 files changed

+138
-37
lines changed

docs/output-subsystem.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,45 @@ Provides structured logging with Rich formatting.
183183
- `2+`: DEBUG (with `-vv` or more)
184184
- Uses stderr console from `get_err_console()`
185185

186+
#### Rich Markup in Log Messages
187+
188+
**RichHandler is configured with `markup=False`**, which has important implications:
189+
190+
**✅ What IS enabled** (built-in RichHandler features):
191+
- Log level colors (DEBUG=blue, INFO=green, WARNING=yellow, ERROR=red)
192+
- Module:line formatting when verbose >= 2 (`show_path=True`)
193+
- Rich tracebacks for better exception formatting
194+
- Nice layout and styling
195+
196+
**❌ What is NOT enabled** (markup processing in message content):
197+
- Rich markup tags like `[red]text[/]`, `[bold]text[/]` - will be shown literally
198+
- Emoji substitution like `:emoji_name:` (e.g., `:fire:`, `:fiji:`) - will be shown literally
199+
200+
**Rationale:**
201+
1. **Prevents collisions with user data**: Maven coordinates like `sc.fiji:fiji:2.17.0` contain the pattern `:fiji:` which Rich would interpret as the Fiji flag emoji 🇫🇯 if markup were enabled
202+
2. **Keeps logs simple**: Log messages should be plain text for debugging and grepping
203+
3. **No need for markup in logs**: RichHandler already provides sufficient styling via log levels
204+
205+
**Example:**
206+
```python
207+
import logging
208+
_log = logging.getLogger(__name__)
209+
210+
# ✅ Good: Plain text logging
211+
_log.debug("Processing sc.fiji:fiji:2.17.0")
212+
_log.info("Added 3 dependencies")
213+
214+
# ❌ Don't use markup in log messages (won't work)
215+
_log.debug("[cyan]Processing[/] sc.fiji:fiji:2.17.0") # Tags shown literally
216+
217+
# ✅ For styled user messages, use console.print() instead
218+
from ..util.console import get_console
219+
_console = get_console()
220+
_console.print("[cyan]Processing:[/] sc.fiji:fiji:2.17.0")
221+
```
222+
223+
**Note:** If you need to include text with literal square brackets (e.g., `[settings]` section names) in console output, use `rich.markup.escape()` to prevent misinterpretation as markup tags.
224+
186225
**`get_log(name="jgo") -> logging.Logger`**
187226
- Get a logger instance for a module
188227
- All module loggers should be children of "jgo" root logger
@@ -444,6 +483,7 @@ After refactoring and testing, here's the **verified clean output approach**:
444483
- ✅ Warnings about potential issues
445484
- ✅ Errors with context
446485
- ✅ Debug information
486+
- ⚠️ **Note**: Log messages do NOT support Rich markup (see [Rich Markup in Log Messages](#rich-markup-in-log-messages))
447487

448488
#### Systematic Approach
449489

src/jgo/cli/commands/search.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,10 @@ def _display_results(results: list[dict]) -> None:
218218
version = result["latest_version"]
219219

220220
# Basic format: coordinate and latest version
221-
coordinate = f"{group_id}:{artifact_id}"
222-
_console.print(f"{i}. {coordinate}")
223-
_console.print(f" Latest version: {version}")
221+
from ...parse.coordinate import Coordinate
222+
223+
coord = Coordinate(group_id, artifact_id, version)
224+
_console.print(f"{i}. {coord.rich()}")
224225

225226
if verbose:
226227
# Show additional details in verbose mode

src/jgo/cli/commands/versions.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,17 @@ def execute(args: ParsedArgs, config: dict) -> int:
7272
# Get available versions
7373
metadata = project.metadata
7474
if not metadata or not metadata.versions:
75-
_console.print(f"No versions found for {coord.groupId}:{coord.artifactId}")
75+
# Use rich() for styled output (emoji-safe)
76+
_console.print(f"No versions found for {coord.rich()}")
7677
return 0
7778

7879
# Use project's smart release/latest resolution
7980
release_version = project.release
8081
latest_version = project.latest
8182

8283
# Data output - keep as print() for parseable output
83-
print(f"Available versions for {coord.groupId}:{coord.artifactId}:")
84+
# Use str() for plain text (no Rich markup)
85+
print(f"Available versions for {coord}:")
8486
for version in metadata.versions:
8587
marker = ""
8688
if release_version and version == release_version:

src/jgo/parse/coordinate.py

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class Coordinate:
6363
placement: Literal["class-path", "module-path"] | None = None
6464

6565
def __str__(self) -> str:
66-
"""Return string representation using coord2str."""
66+
"""Return plain string representation using coord2str."""
6767
return coord2str(
6868
self.groupId,
6969
self.artifactId,
@@ -76,6 +76,42 @@ def __str__(self) -> str:
7676
self.placement,
7777
)
7878

79+
def rich(self) -> str:
80+
"""
81+
Return Rich-formatted string representation with semantic colors.
82+
83+
Uses Rich markup to colorize components based on their semantic meaning:
84+
- groupId: bold cyan
85+
- artifactId: bold
86+
- version: green
87+
- packaging: default color
88+
- classifier: default color
89+
- scope: dim
90+
- colons: dim (prevents emoji substitution like :fiji: → 🇫🇯)
91+
92+
The markup is automatically stripped by Rich when --color=plain is used.
93+
94+
Returns:
95+
Formatted string with Rich markup
96+
97+
Examples:
98+
>>> coord = Coordinate("sc.fiji", "fiji", "2.17.0")
99+
>>> coord.rich()
100+
'[bold cyan]sc.fiji[/][dim]:[/][bold]fiji[/][dim]:[/][green]2.17.0[/]'
101+
"""
102+
return coord2str(
103+
self.groupId,
104+
self.artifactId,
105+
self.version,
106+
self.classifier,
107+
self.packaging,
108+
self.scope,
109+
self.optional,
110+
self.raw,
111+
self.placement,
112+
rich=True,
113+
)
114+
79115
@classmethod
80116
def parse(cls, coordinate: "Coordinate" | str) -> "Coordinate":
81117
"""
@@ -248,6 +284,7 @@ def coord2str(
248284
optional: bool = False,
249285
raw: bool | None = None,
250286
placement: str | None = None,
287+
rich: bool = False,
251288
) -> str:
252289
"""
253290
Convert Maven coordinate components to a string.
@@ -262,41 +299,57 @@ def coord2str(
262299
optional: Whether this is an optional dependency
263300
raw: Whether to use raw/strict resolution (appends ! if True)
264301
placement: Module path placement ("class-path" or "module-path")
302+
rich: If True, format with Rich markup for semantic coloring
265303
266304
Returns:
267305
A formatted coordinate string (e.g., "g:a:p:c:v:s" or "g:a:v!" for raw)
306+
If rich=True, includes Rich markup tags for coloring
268307
"""
269-
parts = [groupId, artifactId]
308+
tag = lambda s, t: f"[{t}]{s}[/]" if rich and t is not None else s
309+
310+
rich_g = tag(groupId, "bold cyan")
311+
rich_a = tag(artifactId, "bold")
312+
rich_v = tag(version, "green")
313+
rich_c = tag(classifier, None)
314+
rich_p = tag(packaging, None)
315+
rich_s = tag(scope, None)
316+
opt = tag("(optional)", "dim")
317+
colon = tag(":", "dim") # Prevents emojification (e.g., :fiji: → 🇫🇯)
318+
bang = tag("!", "dim")
319+
cp = tag("(c)", "dim")
320+
mp = tag("(m)", "dim")
321+
322+
# Start with groupId:artifactId
323+
result = f"{rich_g}{colon}{rich_a}"
270324

271325
# Add packaging if present (comes before version in Maven format)
272326
if packaging:
273-
parts.append(packaging)
327+
result += f"{colon}{rich_p}"
274328

275329
# Add classifier if present
276330
if classifier:
277-
parts.append(classifier)
331+
result += f"{colon}{rich_c}"
278332

279333
# Add version if present
280334
if version:
281-
parts.append(version)
335+
result += f"{colon}{rich_v}"
282336

283337
# Add scope if present
284338
if scope:
285-
parts.append(scope)
339+
scope_text = rich_s
286340
if optional:
287-
parts[-1] += " (optional)"
288-
289-
result = ":".join(parts)
341+
scope_text += f" {opt}"
342+
result += f"{colon}{scope_text}"
290343

291344
# Append placement suffix (before raw flag)
292345
if placement == "class-path":
293-
result += "(c)"
346+
result += cp
294347
elif placement == "module-path":
295-
result += "(m)"
348+
result += mp
296349

297350
# Append ! for raw/strict resolution (comes last)
298351
if raw:
299-
result += "!"
352+
result += bang
300353

301354
return result
302355

src/jgo/util/logging.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def setup_logging(verbose: int = 0) -> logging.Logger:
5050
console=console,
5151
show_time=False,
5252
show_path=(verbose >= 2), # Show module:line in debug mode
53-
markup=True, # Allow rich markup in log messages
53+
markup=False, # Disable markup to prevent emoji conversion (e.g., :fiji: → 🇫🇯)
5454
rich_tracebacks=True, # Better exception formatting
5555
)
5656
handler.setLevel(level)

tests/cli/color.t

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,3 +258,17 @@ Test --color respects environment variable COLOR.
258258
259259
[2]
260260
261+
Test that coordinate formatting prevents emoji substitution.
262+
Coordinates use dim colons in rich mode to prevent patterns like :bear:
263+
from being converted to emoji (🐻). Without proper markup, Rich would
264+
interpret such patterns as emoji codes.
265+
266+
Verify that "bear" appears as text, not emoji (would be 🐻 without escaping).
267+
If emoji substitution were happening, grep wouldn't find the text "bear".
268+
And filter to known artifact to avoid brittleness from search result changes.
269+
270+
$ jgo --color=plain search bear | grep bear | grep com.github.qydq:
271+
*. com.github.qydq:bear:* (glob)
272+
273+
$ jgo --color=rich search bear | grep bear | grep com.github.qydq:
274+
*. com.github.qydq:bear:* (glob)

tests/cli/search.t

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,36 +32,27 @@ Test search with query.
3232
$ jgo search scijava-ops
3333
Found 9 artifacts:
3434

35-
1. org.scijava:scijava-ops-indexer
36-
Latest version: [0-9.]+ (re)
35+
1. org.scijava:scijava-ops-indexer:1.0.0
3736

38-
2. org.scijava:scijava-ops-api
39-
Latest version: [0-9.]+ (re)
37+
2. org.scijava:scijava-ops-api:1.0.0
4038

41-
3. org.scijava:scijava-ops-engine
42-
Latest version: [0-9.]+ (re)
39+
3. org.scijava:scijava-ops-engine:1.0.0
4340

44-
4. org.scijava:scijava-ops-spi
45-
Latest version: [0-9.]+ (re)
41+
4. org.scijava:scijava-ops-spi:1.0.0
4642

47-
5. org.scijava:scijava-ops-flim
48-
Latest version: [0-9.]+ (re)
43+
5. org.scijava:scijava-ops-flim:1.0.0
4944

50-
6. org.scijava:scijava-ops-opencv
51-
Latest version: [0-9.]+ (re)
45+
6. org.scijava:scijava-ops-opencv:1.0.0
5246

53-
7. org.scijava:scijava-ops-image
54-
Latest version: [0-9.]+ (re)
47+
7. org.scijava:scijava-ops-image:1.0.0
5548

56-
8. org.scijava:scijava-ops-tutorial
57-
Latest version: [0-9.]+ (re)
49+
8. org.scijava:scijava-ops-tutorial:1.0.0
5850

59-
9. org.scijava:scijava-ops-ext-parser
60-
Latest version: [0-9.]+ (re)
51+
9. org.scijava:scijava-ops-ext-parser:1.0.0
6152

6253

6354

6455
Test search with --dry-run.
6556

6657
$ jgo --dry-run search jython
67-
\[DRY-RUN\] Would search Maven Central for 'jython' with limit 20 (re)
58+
[DRY-RUN] Would search Maven Central for 'jython' with limit 20

0 commit comments

Comments
 (0)