1"""Handles mapping between color names and ANSI codes and determining auto color codes."""
2
3import sys
4try:
5    from collections import Mapping
6except ImportError:
7    from collections.abc import Mapping
8
9BASE_CODES = {
10    '/all': 0, 'b': 1, 'f': 2, 'i': 3, 'u': 4, 'flash': 5, 'outline': 6, 'negative': 7, 'invis': 8, 'strike': 9,
11    '/b': 22, '/f': 22, '/i': 23, '/u': 24, '/flash': 25, '/outline': 26, '/negative': 27, '/invis': 28,
12    '/strike': 29, '/fg': 39, '/bg': 49,
13
14    'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37,
15
16    'bgblack': 40, 'bgred': 41, 'bggreen': 42, 'bgyellow': 43, 'bgblue': 44, 'bgmagenta': 45, 'bgcyan': 46,
17    'bgwhite': 47,
18
19    'hiblack': 90, 'hired': 91, 'higreen': 92, 'hiyellow': 93, 'hiblue': 94, 'himagenta': 95, 'hicyan': 96,
20    'hiwhite': 97,
21
22    'hibgblack': 100, 'hibgred': 101, 'hibggreen': 102, 'hibgyellow': 103, 'hibgblue': 104, 'hibgmagenta': 105,
23    'hibgcyan': 106, 'hibgwhite': 107,
24
25    'autored': None, 'autoblack': None, 'automagenta': None, 'autowhite': None, 'autoblue': None, 'autoyellow': None,
26    'autogreen': None, 'autocyan': None,
27
28    'autobgred': None, 'autobgblack': None, 'autobgmagenta': None, 'autobgwhite': None, 'autobgblue': None,
29    'autobgyellow': None, 'autobggreen': None, 'autobgcyan': None,
30
31    '/black': 39, '/red': 39, '/green': 39, '/yellow': 39, '/blue': 39, '/magenta': 39, '/cyan': 39, '/white': 39,
32    '/hiblack': 39, '/hired': 39, '/higreen': 39, '/hiyellow': 39, '/hiblue': 39, '/himagenta': 39, '/hicyan': 39,
33    '/hiwhite': 39,
34
35    '/bgblack': 49, '/bgred': 49, '/bggreen': 49, '/bgyellow': 49, '/bgblue': 49, '/bgmagenta': 49, '/bgcyan': 49,
36    '/bgwhite': 49, '/hibgblack': 49, '/hibgred': 49, '/hibggreen': 49, '/hibgyellow': 49, '/hibgblue': 49,
37    '/hibgmagenta': 49, '/hibgcyan': 49, '/hibgwhite': 49,
38
39    '/autored': 39, '/autoblack': 39, '/automagenta': 39, '/autowhite': 39, '/autoblue': 39, '/autoyellow': 39,
40    '/autogreen': 39, '/autocyan': 39,
41
42    '/autobgred': 49, '/autobgblack': 49, '/autobgmagenta': 49, '/autobgwhite': 49, '/autobgblue': 49,
43    '/autobgyellow': 49, '/autobggreen': 49, '/autobgcyan': 49,
44}
45
46
47class ANSICodeMapping(Mapping):
48    """Read-only dictionary, resolves closing tags and automatic colors. Iterates only used color tags.
49
50    :cvar bool DISABLE_COLORS: Disable colors (strip color codes).
51    :cvar bool LIGHT_BACKGROUND: Use low intensity color codes.
52    """
53
54    DISABLE_COLORS = False
55    LIGHT_BACKGROUND = False
56
57    def __init__(self, value_markup):
58        """Constructor.
59
60        :param str value_markup: String with {color} tags.
61        """
62        self.whitelist = [k for k in BASE_CODES if '{' + k + '}' in value_markup]
63
64    def __getitem__(self, item):
65        """Return value for key or None if colors are disabled.
66
67        :param str item: Key.
68
69        :return: Color code integer.
70        :rtype: int
71        """
72        if item not in self.whitelist:
73            raise KeyError(item)
74        if self.DISABLE_COLORS:
75            return None
76        return getattr(self, item, BASE_CODES[item])
77
78    def __iter__(self):
79        """Iterate dictionary."""
80        return iter(self.whitelist)
81
82    def __len__(self):
83        """Dictionary length."""
84        return len(self.whitelist)
85
86    @classmethod
87    def disable_all_colors(cls):
88        """Disable all colors. Strips any color tags or codes."""
89        cls.DISABLE_COLORS = True
90
91    @classmethod
92    def enable_all_colors(cls):
93        """Enable all colors. Strips any color tags or codes."""
94        cls.DISABLE_COLORS = False
95
96    @classmethod
97    def disable_if_no_tty(cls):
98        """Disable all colors only if there is no TTY available.
99
100        :return: True if colors are disabled, False if stderr or stdout is a TTY.
101        :rtype: bool
102        """
103        if sys.stdout.isatty() or sys.stderr.isatty():
104            return False
105        cls.disable_all_colors()
106        return True
107
108    @classmethod
109    def set_dark_background(cls):
110        """Choose dark colors for all 'auto'-prefixed codes for readability on light backgrounds."""
111        cls.LIGHT_BACKGROUND = False
112
113    @classmethod
114    def set_light_background(cls):
115        """Choose dark colors for all 'auto'-prefixed codes for readability on light backgrounds."""
116        cls.LIGHT_BACKGROUND = True
117
118    @property
119    def autoblack(self):
120        """Return automatic black foreground color depending on background color."""
121        return BASE_CODES['black' if ANSICodeMapping.LIGHT_BACKGROUND else 'hiblack']
122
123    @property
124    def autored(self):
125        """Return automatic red foreground color depending on background color."""
126        return BASE_CODES['red' if ANSICodeMapping.LIGHT_BACKGROUND else 'hired']
127
128    @property
129    def autogreen(self):
130        """Return automatic green foreground color depending on background color."""
131        return BASE_CODES['green' if ANSICodeMapping.LIGHT_BACKGROUND else 'higreen']
132
133    @property
134    def autoyellow(self):
135        """Return automatic yellow foreground color depending on background color."""
136        return BASE_CODES['yellow' if ANSICodeMapping.LIGHT_BACKGROUND else 'hiyellow']
137
138    @property
139    def autoblue(self):
140        """Return automatic blue foreground color depending on background color."""
141        return BASE_CODES['blue' if ANSICodeMapping.LIGHT_BACKGROUND else 'hiblue']
142
143    @property
144    def automagenta(self):
145        """Return automatic magenta foreground color depending on background color."""
146        return BASE_CODES['magenta' if ANSICodeMapping.LIGHT_BACKGROUND else 'himagenta']
147
148    @property
149    def autocyan(self):
150        """Return automatic cyan foreground color depending on background color."""
151        return BASE_CODES['cyan' if ANSICodeMapping.LIGHT_BACKGROUND else 'hicyan']
152
153    @property
154    def autowhite(self):
155        """Return automatic white foreground color depending on background color."""
156        return BASE_CODES['white' if ANSICodeMapping.LIGHT_BACKGROUND else 'hiwhite']
157
158    @property
159    def autobgblack(self):
160        """Return automatic black background color depending on background color."""
161        return BASE_CODES['bgblack' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibgblack']
162
163    @property
164    def autobgred(self):
165        """Return automatic red background color depending on background color."""
166        return BASE_CODES['bgred' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibgred']
167
168    @property
169    def autobggreen(self):
170        """Return automatic green background color depending on background color."""
171        return BASE_CODES['bggreen' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibggreen']
172
173    @property
174    def autobgyellow(self):
175        """Return automatic yellow background color depending on background color."""
176        return BASE_CODES['bgyellow' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibgyellow']
177
178    @property
179    def autobgblue(self):
180        """Return automatic blue background color depending on background color."""
181        return BASE_CODES['bgblue' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibgblue']
182
183    @property
184    def autobgmagenta(self):
185        """Return automatic magenta background color depending on background color."""
186        return BASE_CODES['bgmagenta' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibgmagenta']
187
188    @property
189    def autobgcyan(self):
190        """Return automatic cyan background color depending on background color."""
191        return BASE_CODES['bgcyan' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibgcyan']
192
193    @property
194    def autobgwhite(self):
195        """Return automatic white background color depending on background color."""
196        return BASE_CODES['bgwhite' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibgwhite']
197
198
199def list_tags():
200    """List the available tags.
201
202    :return: List of 4-item tuples: opening tag, closing tag, main ansi value, closing ansi value.
203    :rtype: list
204    """
205    # Build reverse dictionary. Keys are closing tags, values are [closing ansi, opening tag, opening ansi].
206    reverse_dict = dict()
207    for tag, ansi in sorted(BASE_CODES.items()):
208        if tag.startswith('/'):
209            reverse_dict[tag] = [ansi, None, None]
210        else:
211            reverse_dict['/' + tag][1:] = [tag, ansi]
212
213    # Collapse
214    four_item_tuples = [(v[1], k, v[2], v[0]) for k, v in reverse_dict.items()]
215
216    # Sort.
217    def sorter(four_item):
218        """Sort /all /fg /bg first, then b i u flash, then auto colors, then dark colors, finally light colors.
219
220        :param iter four_item: [opening tag, closing tag, main ansi value, closing ansi value]
221
222        :return Sorting weight.
223        :rtype: int
224        """
225        if not four_item[2]:  # /all /fg /bg
226            return four_item[3] - 200
227        if four_item[2] < 10 or four_item[0].startswith('auto'):  # b f i u or auto colors
228            return four_item[2] - 100
229        return four_item[2]
230    four_item_tuples.sort(key=sorter)
231
232    return four_item_tuples
233