Skip to content

Commit bd545f9

Browse files
committed
Add enhanced terminal color support with fallback options
1 parent 0c76ccb commit bd545f9

File tree

2 files changed

+188
-0
lines changed

2 files changed

+188
-0
lines changed

rich/terminal.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"""Terminal color support detection and fallback options."""
2+
3+
import os
4+
import sys
5+
from typing import Dict, Optional, Set, Tuple
6+
7+
# ANSI color codes
8+
ANSI_COLORS = {
9+
'black': 30,
10+
'red': 31,
11+
'green': 32,
12+
'yellow': 33,
13+
'blue': 34,
14+
'magenta': 35,
15+
'cyan': 36,
16+
'white': 37,
17+
'bright_black': 90,
18+
'bright_red': 91,
19+
'bright_green': 92,
20+
'bright_yellow': 93,
21+
'bright_blue': 94,
22+
'bright_magenta': 95,
23+
'bright_cyan': 96,
24+
'bright_white': 97,
25+
}
26+
27+
class TerminalColorSupport:
28+
"""Detect and manage terminal color support."""
29+
30+
def __init__(self) -> None:
31+
self._color_support: Optional[bool] = None
32+
self._supported_colors: Set[str] = set()
33+
self._fallback_colors: Dict[str, str] = {}
34+
35+
def detect_color_support(self) -> bool:
36+
"""Detect if the terminal supports colors."""
37+
if self._color_support is not None:
38+
return self._color_support
39+
40+
# Check environment variables
41+
if 'NO_COLOR' in os.environ:
42+
self._color_support = False
43+
return False
44+
45+
if 'FORCE_COLOR' in os.environ:
46+
self._color_support = True
47+
return True
48+
49+
# Check if we're in a terminal
50+
if not sys.stdout.isatty():
51+
self._color_support = False
52+
return False
53+
54+
# Check terminal type
55+
term = os.environ.get('TERM', '').lower()
56+
if term in ('dumb', 'unknown'):
57+
self._color_support = False
58+
return False
59+
60+
# Windows specific checks
61+
if sys.platform == 'win32':
62+
try:
63+
import ctypes
64+
kernel32 = ctypes.windll.kernel32
65+
if kernel32.GetConsoleMode(kernel32.GetStdHandle(-11), None):
66+
self._color_support = True
67+
return True
68+
except Exception:
69+
pass
70+
71+
# Default to True for modern terminals
72+
self._color_support = True
73+
return True
74+
75+
def get_supported_colors(self) -> Set[str]:
76+
"""Get the set of supported colors."""
77+
if not self._supported_colors:
78+
if self.detect_color_support():
79+
# Test each color
80+
for color in ANSI_COLORS:
81+
if self._test_color(color):
82+
self._supported_colors.add(color)
83+
84+
return self._supported_colors
85+
86+
def _test_color(self, color: str) -> bool:
87+
"""Test if a specific color is supported."""
88+
# Implementation would test actual color support
89+
# For now, return True for all colors if color support is enabled
90+
return self.detect_color_support()
91+
92+
def get_fallback_color(self, color: str) -> str:
93+
"""Get a fallback color if the requested color is not supported."""
94+
if color in self._fallback_colors:
95+
return self._fallback_colors[color]
96+
97+
# Define fallback mappings
98+
fallbacks = {
99+
'bright_black': 'black',
100+
'bright_red': 'red',
101+
'bright_green': 'green',
102+
'bright_yellow': 'yellow',
103+
'bright_blue': 'blue',
104+
'bright_magenta': 'magenta',
105+
'bright_cyan': 'cyan',
106+
'bright_white': 'white',
107+
}
108+
109+
fallback = fallbacks.get(color, 'white')
110+
self._fallback_colors[color] = fallback
111+
return fallback
112+
113+
def get_color_code(self, color: str) -> Tuple[int, bool]:
114+
"""Get the ANSI color code and whether it's supported."""
115+
if not self.detect_color_support():
116+
return (0, False)
117+
118+
if color not in self.get_supported_colors():
119+
color = self.get_fallback_color(color)
120+
121+
return (ANSI_COLORS[color], True)
122+
123+
# Global instance
124+
terminal_color = TerminalColorSupport()

tests/test_terminal_color.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Tests for terminal color support."""
2+
3+
import os
4+
import sys
5+
from unittest import mock
6+
7+
import pytest
8+
9+
from rich.terminal import TerminalColorSupport, terminal_color
10+
11+
def test_color_support_detection():
12+
"""Test color support detection."""
13+
# Test NO_COLOR environment variable
14+
with mock.patch.dict(os.environ, {'NO_COLOR': '1'}):
15+
assert not terminal_color.detect_color_support()
16+
17+
# Test FORCE_COLOR environment variable
18+
with mock.patch.dict(os.environ, {'FORCE_COLOR': '1'}):
19+
assert terminal_color.detect_color_support()
20+
21+
# Test dumb terminal
22+
with mock.patch.dict(os.environ, {'TERM': 'dumb'}):
23+
assert not terminal_color.detect_color_support()
24+
25+
# Test unknown terminal
26+
with mock.patch.dict(os.environ, {'TERM': 'unknown'}):
27+
assert not terminal_color.detect_color_support()
28+
29+
def test_supported_colors():
30+
"""Test getting supported colors."""
31+
with mock.patch.object(terminal_color, 'detect_color_support', return_value=True):
32+
colors = terminal_color.get_supported_colors()
33+
assert isinstance(colors, set)
34+
assert len(colors) > 0
35+
36+
def test_fallback_colors():
37+
"""Test fallback color mapping."""
38+
# Test bright color fallbacks
39+
assert terminal_color.get_fallback_color('bright_red') == 'red'
40+
assert terminal_color.get_fallback_color('bright_blue') == 'blue'
41+
42+
# Test unknown color fallback
43+
assert terminal_color.get_fallback_color('unknown_color') == 'white'
44+
45+
def test_color_code():
46+
"""Test getting color codes."""
47+
with mock.patch.object(terminal_color, 'detect_color_support', return_value=True):
48+
code, supported = terminal_color.get_color_code('red')
49+
assert code == 31 # ANSI code for red
50+
assert supported is True
51+
52+
with mock.patch.object(terminal_color, 'detect_color_support', return_value=False):
53+
code, supported = terminal_color.get_color_code('red')
54+
assert code == 0
55+
assert supported is False
56+
57+
def test_windows_specific():
58+
"""Test Windows-specific color support."""
59+
if sys.platform == 'win32':
60+
with mock.patch('ctypes.windll.kernel32.GetConsoleMode', return_value=1):
61+
assert terminal_color.detect_color_support()
62+
63+
with mock.patch('ctypes.windll.kernel32.GetConsoleMode', return_value=0):
64+
assert not terminal_color.detect_color_support()

0 commit comments

Comments
 (0)