Skip to content

Commit 05ee4e3

Browse files
committed
feat(cli/utils): Add semantic colors to interactive prompts
Apply semantic color system to prompt utilities: - prompt(): default value [path] uses info() (cyan) - prompt_bool(): choice indicator [Y/n] uses muted() (blue) - prompt_choices(): options list (a, b) uses muted(), default uses info() Fix circular import between utils.py and _colors.py by using lazy import of style() inside _colorize() method. Includes 7 function-based tests for prompt color output.
1 parent 3d439b8 commit 05ee4e3

File tree

3 files changed

+109
-5
lines changed

3 files changed

+109
-5
lines changed

src/tmuxp/cli/_colors.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@
3434
import os
3535
import sys
3636

37-
from .utils import style
38-
3937

4038
class ColorMode(enum.Enum):
4139
"""Color output modes for CLI.
@@ -178,6 +176,9 @@ def _colorize(self, text: str, fg: str, bold: bool = False) -> str:
178176
Colorized text if enabled, plain text otherwise.
179177
"""
180178
if self._enabled:
179+
# Lazy import to avoid circular dependency with utils.py
180+
from .utils import style
181+
181182
return style(text, fg=fg, bold=bold)
182183
return text
183184

src/tmuxp/cli/utils.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
from tmuxp import log
1010

11+
from ._colors import ColorMode, Colors
12+
1113
if t.TYPE_CHECKING:
1214
from collections.abc import Callable, Sequence
1315
from typing import TypeAlias
@@ -59,7 +61,8 @@ def prompt(
5961
`flask-script <https://github.com/techniq/flask-script>`_. See the
6062
`flask-script license <https://github.com/techniq/flask-script/blob/master/LICENSE>`_.
6163
"""
62-
prompt_ = name + ((default and f" [{default}]") or "")
64+
colors = Colors(ColorMode.AUTO)
65+
prompt_ = name + ((default and " " + colors.info(f"[{default}]")) or "")
6366
prompt_ += (name.endswith("?") and " ") or ": "
6467
while True:
6568
rv = input(prompt_) or default
@@ -99,6 +102,7 @@ def prompt_bool(
99102
-------
100103
bool
101104
"""
105+
colors = Colors(ColorMode.AUTO)
102106
yes_choices = yes_choices or ("y", "yes", "1", "on", "true", "t")
103107
no_choices = no_choices or ("n", "no", "0", "off", "false", "f")
104108

@@ -109,7 +113,7 @@ def prompt_bool(
109113
else:
110114
prompt_choice = "y/N"
111115

112-
prompt_ = name + f" [{prompt_choice}]"
116+
prompt_ = name + " " + colors.muted(f"[{prompt_choice}]")
113117
prompt_ += (name.endswith("?") and " ") or ": "
114118

115119
while True:
@@ -151,6 +155,7 @@ def prompt_choices(
151155
-------
152156
str
153157
"""
158+
colors = Colors(ColorMode.AUTO)
154159
choices_: list[str] = []
155160
options: list[str] = []
156161

@@ -162,8 +167,13 @@ def prompt_choices(
162167
choice = choice[0]
163168
choices_.append(choice)
164169

170+
choices_str = colors.muted(f"({', '.join(options)})")
171+
default_str = " " + colors.info(f"[{default}]") if default else ""
172+
prompt_text = f"{name} - {choices_str}{default_str}"
173+
165174
while True:
166-
rv = prompt(name + " - ({})".format(", ".join(options)), default=default)
175+
prompt_ = prompt_text + ": "
176+
rv = input(prompt_) or default
167177
if not rv or rv == default:
168178
return default
169179
rv = rv.lower()

tests/cli/test_prompt_colors.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""Tests for colored prompt utilities."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
7+
from tmuxp.cli._colors import ColorMode, Colors
8+
9+
10+
def test_prompt_bool_choice_indicator_muted(monkeypatch: pytest.MonkeyPatch) -> None:
11+
"""Verify [Y/n] uses muted color (blue)."""
12+
monkeypatch.delenv("NO_COLOR", raising=False)
13+
colors = Colors(ColorMode.ALWAYS)
14+
15+
# Test the muted color is applied to choice indicators
16+
result = colors.muted("[Y/n]")
17+
assert "\033[34m" in result # blue foreground
18+
assert "[Y/n]" in result
19+
assert result.endswith("\033[0m")
20+
21+
22+
def test_prompt_bool_choice_indicator_variants(
23+
monkeypatch: pytest.MonkeyPatch,
24+
) -> None:
25+
"""Verify all choice indicator variants are colored."""
26+
monkeypatch.delenv("NO_COLOR", raising=False)
27+
colors = Colors(ColorMode.ALWAYS)
28+
29+
for indicator in ["[Y/n]", "[y/N]", "[y/n]"]:
30+
result = colors.muted(indicator)
31+
assert "\033[34m" in result
32+
assert indicator in result
33+
34+
35+
def test_prompt_default_value_uses_info(monkeypatch: pytest.MonkeyPatch) -> None:
36+
"""Verify default path uses info color (cyan)."""
37+
monkeypatch.delenv("NO_COLOR", raising=False)
38+
colors = Colors(ColorMode.ALWAYS)
39+
40+
path = "/home/user/.tmuxp/session.yaml"
41+
result = colors.info(f"[{path}]")
42+
assert "\033[36m" in result # cyan foreground
43+
assert path in result
44+
assert result.endswith("\033[0m")
45+
46+
47+
def test_prompt_choices_list_muted(monkeypatch: pytest.MonkeyPatch) -> None:
48+
"""Verify (yaml, json) uses muted color (blue)."""
49+
monkeypatch.delenv("NO_COLOR", raising=False)
50+
colors = Colors(ColorMode.ALWAYS)
51+
52+
choices = "(yaml, json)"
53+
result = colors.muted(choices)
54+
assert "\033[34m" in result # blue foreground
55+
assert choices in result
56+
57+
58+
def test_prompts_respect_no_color_env(monkeypatch: pytest.MonkeyPatch) -> None:
59+
"""Verify NO_COLOR disables prompt colors."""
60+
monkeypatch.setenv("NO_COLOR", "1")
61+
colors = Colors(ColorMode.AUTO)
62+
63+
assert colors.muted("[Y/n]") == "[Y/n]"
64+
assert colors.info("[default]") == "[default]"
65+
66+
67+
def test_prompt_combined_format(monkeypatch: pytest.MonkeyPatch) -> None:
68+
"""Verify combined prompt format with choices and default."""
69+
monkeypatch.delenv("NO_COLOR", raising=False)
70+
colors = Colors(ColorMode.ALWAYS)
71+
72+
name = "Convert to"
73+
choices_str = colors.muted("(yaml, json)")
74+
default_str = colors.info("[yaml]")
75+
prompt = f"{name} - {choices_str} {default_str}"
76+
77+
# Should contain both blue (muted) and cyan (info) ANSI codes
78+
assert "\033[34m" in prompt # blue for choices
79+
assert "\033[36m" in prompt # cyan for default
80+
assert "Convert to" in prompt
81+
assert "yaml, json" in prompt
82+
83+
84+
def test_prompt_colors_disabled_returns_plain_text(
85+
monkeypatch: pytest.MonkeyPatch,
86+
) -> None:
87+
"""Verify disabled colors return plain text without ANSI codes."""
88+
colors = Colors(ColorMode.NEVER)
89+
90+
assert colors.muted("[Y/n]") == "[Y/n]"
91+
assert colors.info("[/path/to/file]") == "[/path/to/file]"
92+
assert "\033[" not in colors.muted("test")
93+
assert "\033[" not in colors.info("test")

0 commit comments

Comments
 (0)