X Tutup
Skip to content

Commit 76ad7a7

Browse files
committed
cli: Separate category labels from "examples:" in help output
Category headings now use clean format without "examples:" suffix: Before: "Field-scoped search examples:" After: "Field-scoped search:" - build_description() formats headings as "{heading}:" not "{heading} examples:" - Formatter recognizes category headings within examples blocks - Add tests for category heading colorization - Update extract_examples_from_help() to match new format
1 parent e72bcd5 commit 76ad7a7

File tree

4 files changed

+72
-9
lines changed

4 files changed

+72
-9
lines changed

src/tmuxp/_internal/colors.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,7 @@ def build_description(
836836
example_blocks : sequence of (heading, commands) tuples
837837
Each tuple contains an optional heading and a sequence of example commands.
838838
If heading is None, the section is titled "examples:".
839+
If heading is provided, it becomes the section title (without "examples:").
839840
840841
Returns
841842
-------
@@ -849,7 +850,7 @@ def build_description(
849850
'My tool.\n\nexamples:\n mytool run'
850851
851852
>>> build_description("My tool.", [("sync", ["mytool sync repo"])])
852-
'My tool.\n\nsync examples:\n mytool sync repo'
853+
'My tool.\n\nsync:\n mytool sync repo'
853854
854855
>>> build_description("", [(None, ["cmd"])])
855856
'examples:\n cmd'
@@ -864,7 +865,7 @@ def build_description(
864865
for heading, commands in example_blocks:
865866
if not commands:
866867
continue
867-
title = "examples:" if heading is None else f"{heading} examples:"
868+
title = "examples:" if heading is None else f"{heading}:"
868869
lines = [title]
869870
lines.extend(f" {command}" for command in commands)
870871
sections.append("\n".join(lines))

src/tmuxp/cli/_formatter.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,18 @@ def _fill_text(self, text: str, width: int, indent: str) -> str:
129129
leading = stripped_line[:leading_length]
130130
content = stripped_line[leading_length:]
131131
content_lower = content.lower()
132-
is_section_heading = (
133-
content_lower.endswith("examples:") and content_lower != "examples:"
132+
# Recognize example section headings:
133+
# - "examples:" starts the examples block
134+
# - "X examples:" or "X:" are sub-section headings within examples
135+
is_examples_start = content_lower == "examples:"
136+
is_category_in_block = (
137+
in_examples_block and content.endswith(":") and not content[0].isspace()
134138
)
139+
is_section_heading = (
140+
content_lower.endswith("examples:") or is_category_in_block
141+
) and not is_examples_start
135142

136-
if is_section_heading or content_lower == "examples:":
143+
if is_section_heading or is_examples_start:
137144
formatted_content = f"{theme.heading}{content}{theme.reset}"
138145
in_examples_block = True
139146
expect_value = False

tests/cli/test_formatter.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,48 @@ def test_fill_text_without_theme_plain_text(self) -> None:
102102
assert "\033[" not in result
103103
assert "tmuxp load myproject" in result
104104

105+
def test_fill_text_category_headings_colorized(
106+
self,
107+
monkeypatch: pytest.MonkeyPatch,
108+
) -> None:
109+
"""Category headings within examples block are colorized."""
110+
monkeypatch.delenv("NO_COLOR", raising=False)
111+
colors = Colors(ColorMode.ALWAYS)
112+
formatter_cls = create_themed_formatter(colors)
113+
formatter = formatter_cls("tmuxp")
114+
115+
# Test category heading without "examples:" suffix
116+
text = "examples:\n tmuxp ls\n\nMachine-readable output:\n tmuxp ls --json"
117+
result = formatter._fill_text(text, 80, "")
118+
119+
# Both headings should be colorized
120+
assert "\033[" in result
121+
assert "examples:" in result
122+
assert "Machine-readable output:" in result
123+
# Commands should also be colorized
124+
assert "tmuxp" in result
125+
assert "--json" in result
126+
127+
def test_fill_text_category_heading_only_in_examples_block(
128+
self,
129+
monkeypatch: pytest.MonkeyPatch,
130+
) -> None:
131+
"""Category headings are only recognized within examples block."""
132+
monkeypatch.delenv("NO_COLOR", raising=False)
133+
colors = Colors(ColorMode.ALWAYS)
134+
formatter_cls = create_themed_formatter(colors)
135+
formatter = formatter_cls("tmuxp")
136+
137+
# Text before examples block should not be colorized as heading
138+
text = "Some heading:\n not a command\n\nexamples:\n tmuxp load"
139+
result = formatter._fill_text(text, 80, "")
140+
141+
# "Some heading:" should NOT be colorized (it's before examples block)
142+
# "examples:" and the command should be colorized
143+
lines = result.split("\n")
144+
# First line should be plain (no ANSI in "Some heading:")
145+
assert "Some heading:" in lines[0]
146+
105147

106148
class TestHelpOutputIntegration:
107149
"""Integration tests for help output colorization."""

tests/cli/test_help_examples.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from __future__ import annotations
44

55
import argparse
6-
import re
76
import subprocess
87

98
import pytest
@@ -55,19 +54,33 @@ def extract_examples_from_help(help_text: str) -> list[str]:
5554
5655
Examples
5756
--------
58-
>>> text = "load examples:\n tmuxp load myproject\n\npositions:"
57+
>>> text = "load:\n tmuxp load myproject\n\npositions:"
5958
>>> extract_examples_from_help(text)
6059
['tmuxp load myproject']
6160
6261
>>> text2 = "examples:\n tmuxp debug-info\n\noptions:"
6362
>>> extract_examples_from_help(text2)
6463
['tmuxp debug-info']
64+
65+
>>> text3 = "Field-scoped search:\n tmuxp search window:editor"
66+
>>> extract_examples_from_help(text3)
67+
['tmuxp search window:editor']
6568
"""
6669
examples = []
6770
in_examples = False
6871
for line in help_text.splitlines():
69-
# Match "examples:" or "load examples:" etc.
70-
if re.match(r"^(\S+\s+)?examples?:$", line, re.IGNORECASE):
72+
# Match example section headings:
73+
# - "examples:" (default examples section)
74+
# - "load examples:" or "load:" (category headings)
75+
# - "Field-scoped search:" (multi-word category headings)
76+
# Exclude argparse sections like "positional arguments:", "options:"
77+
stripped = line.strip()
78+
is_section_heading = (
79+
stripped.endswith(":")
80+
and stripped not in ("positional arguments:", "options:")
81+
and not stripped.startswith("-")
82+
)
83+
if is_section_heading:
7184
in_examples = True
7285
elif in_examples and line.startswith(" "):
7386
cmd = line.strip()

0 commit comments

Comments
 (0)
X Tutup