Coverage for /home/ubuntu/lunchbox/python/lunchbox/theme.py: 100%
103 statements
« prev ^ index » next coverage.py v7.9.0, created at 2025-06-13 03:03 +0000
« prev ^ index » next coverage.py v7.9.0, created at 2025-06-13 03:03 +0000
1from typing import Any, Sequence, Type # noqa: F401
3from enum import Enum
5import click
6# ------------------------------------------------------------------------------
9class EnumBase(Enum):
10 '''
11 Base class for enums.
12 '''
13 @classmethod
14 def to_dict(cls):
15 # type: () -> dict
16 '''
17 Convert enum to a dictionary.
19 Returns:
20 dict: (name, value) dictionary.
21 '''
22 return {x.name: x.value for x in cls.__members__.values()}
25class Colorscheme(EnumBase):
26 '''
27 Henanigans color scheme.
28 '''
29 DARK1 = '#040404'
30 DARK2 = '#181818'
31 BG = '#242424'
32 GREY1 = '#343434'
33 GREY2 = '#444444'
34 LIGHT1 = '#A4A4A4'
35 LIGHT2 = '#F4F4F4'
36 DIALOG1 = '#444459'
37 DIALOG2 = '#5D5D7A'
38 RED1 = '#F77E70'
39 RED2 = '#DE958E'
40 ORANGE1 = '#EB9E58'
41 ORANGE2 = '#EBB483'
42 YELLOW1 = '#E8EA7E'
43 YELLOW2 = '#E9EABE'
44 GREEN1 = '#8BD155'
45 GREEN2 = '#A0D17B'
46 CYAN1 = '#7EC4CF'
47 CYAN2 = '#B6ECF3'
48 BLUE1 = '#5F95DE'
49 BLUE2 = '#93B6E6'
50 PURPLE1 = '#C98FDE'
51 PURPLE2 = '#AC92DE'
54class TerminalColorscheme(EnumBase):
55 '''
56 Terminal color scheme.
57 '''
58 BLUE1 = '\033[0;34m'
59 BLUE2 = '\033[0;94m'
60 CYAN1 = '\033[0;36m'
61 CYAN2 = '\033[0;96m'
62 GREEN1 = '\033[0;32m'
63 GREEN2 = '\033[0;92m'
64 GREY1 = '\033[0;90m'
65 GREY2 = '\033[0;37m'
66 PURPLE1 = '\033[0;35m'
67 PURPLE2 = '\033[0;95m'
68 RED1 = '\033[0;31m'
69 RED2 = '\033[0;91m'
70 WHITE = '\033[1;97m'
71 YELLOW1 = '\033[0;33m'
72 YELLOW2 = '\033[0;93m'
73 CLEAR = '\033[0m'
76# ------------------------------------------------------------------------------
77def get_plotly_template(colorscheme=Colorscheme):
78 # type: (Type[Colorscheme]) -> dict
79 '''
80 Create a plotly template from a given color scheme.
82 Args:
83 colorscheme (colorscheme): colorscheme enum.
85 Returns:
86 dict: Plotly template.
87 '''
88 cs = colorscheme
89 colors = [
90 cs.CYAN2, cs.RED2, cs.GREEN2, cs.BLUE2, cs.ORANGE2, cs.PURPLE2,
91 cs.YELLOW2, cs.LIGHT2, cs.DARK2, cs.GREY2, cs.CYAN1, cs.RED1, cs.GREEN1,
92 cs.BLUE1, cs.ORANGE1, cs.PURPLE1, cs.YELLOW1, cs.LIGHT1, cs.DARK1,
93 cs.GREY1,
94 ]
96 template = dict(
97 layout=dict(
98 colorway=[x.value for x in colors],
99 plot_bgcolor=cs.DARK2.value,
100 paper_bgcolor=cs.DARK2.value,
101 bargap=0.15,
102 bargroupgap=0.05,
103 autosize=True,
104 margin=dict(t=80, b=65, l=80, r=105),
105 title=dict(font=dict(
106 color=cs.LIGHT2.value,
107 size=30,
108 )),
109 legend=dict(
110 font=dict(color=cs.LIGHT2.value),
111 bgcolor=cs.BG.value,
112 bordercolor=cs.BG.value,
113 indentation=5,
114 borderwidth=4,
115 ),
116 xaxis=dict(
117 title=dict(font=dict(
118 color=cs.LIGHT2.value,
119 size=16,
120 )),
121 gridcolor=cs.BG.value,
122 zerolinecolor=cs.GREY1.value,
123 zerolinewidth=5,
124 tickfont=dict(color=cs.LIGHT1.value),
125 showgrid=True,
126 autorange=True,
127 ),
128 yaxis=dict(
129 title=dict(font=dict(
130 color=cs.LIGHT2.value,
131 size=16,
132 )),
133 gridcolor=cs.BG.value,
134 zerolinecolor=cs.GREY1.value,
135 zerolinewidth=5,
136 tickfont=dict(color=cs.LIGHT1.value),
137 showgrid=True,
138 autorange=True,
139 )
140 )
141 )
142 return template
145# ------------------------------------------------------------------------------
146class ThemeFormatter(click.HelpFormatter):
147 '''
148 ThemeFormatter makes click CLI output prettier.
150 Include the following code to add it to click:
152 .. code-block:: python
154 import lunchbox.theme as lbc
155 click.Context.formatter_class = lbc.ThemeFormatter
156 '''
157 def __init__(
158 self,
159 *args,
160 heading_color='blue2',
161 command_color='cyan2',
162 flag_color='green2',
163 grayscale=False,
164 **kwargs,
165 ):
166 # type: (Any, str, str, str, bool, Any) -> None
167 r'''
168 Constructs a ThemeFormatter instance for use with click.
170 Args:
171 \*args (optional): Positional arguments.
172 heading_color (str, optional): Heading color. Default: blue2.
173 command_color (str, optional): Command color. Default: cyan2.
174 flag_color (str, optional): Flag color. Default: green2.
175 grayscale (bool, optional): Grayscale colors only. Default: False.
176 \*\*kwargs (optional): Keyword arguments.
177 '''
178 super().__init__(*args, **kwargs)
179 self.current_indent = 4
180 self._sep = '='
181 self._line_width = 80
182 self._write_calls = 0
183 self._colors = {k.lower(): v for k, v in TerminalColorscheme.to_dict().items()}
184 if grayscale:
185 self._colors = {k: '' for k in self._colors.keys()}
186 self._heading_color = self._colors[heading_color]
187 self._command_color = self._colors[command_color]
188 self._flag_color = self._colors[flag_color]
190 def write_text(self, text):
191 # type: (str) -> None
192 '''
193 Writes re-indented text into the buffer. This rewraps and preserves
194 paragraphs.
196 Args:
197 text (str): Text to write.
198 '''
199 self._write_calls += 1
200 self.write(
201 click.formatting.wrap_text(
202 text.format(**self._colors),
203 self.width,
204 initial_indent=' ',
205 subsequent_indent=' ',
206 preserve_paragraphs=True,
207 )
208 )
209 self.write('\n')
211 def write_usage(self, prog, *args, **kwargs):
212 # type: (str, Any, Any) -> None
213 r'''
214 Writes a usage line into the buffer.
216 Args:
217 prog (str): Program name.
218 \*args (optional): Positional arguments.
219 \*\*kwargs (optional): Keyword arguments.
220 '''
221 self._write_calls += 1
222 text = prog.split(' ')[-1].upper() + ' '
223 text = text.ljust(self._line_width, self._sep)
224 text = '{h}{text}{clear}\n'.format(
225 text=text, h=self._heading_color, **self._colors
226 )
227 self.write(text)
229 def write_dl(self, rows, col_max=30, col_spacing=2):
230 # type: (Sequence[tuple[str, str]], int, int) -> None
231 '''
232 Writes a definition list into the buffer. This is how options and
233 commands are usually formatted.
235 Args:
236 rows (list): List of (term, value) tuples.
237 col_max (int, optional): Maximum width of first column. Default: 30.
238 col_spacing (int, optional): Spacing between first and second
239 columns. Default: 2.
240 '''
241 self._write_calls += 1
242 data = []
243 for k, v in rows:
244 k = ' {f}{k}{clear}'.format(
245 f=self._flag_color, k=k, **self._colors
246 )
247 v = v.format(**self._colors)
248 data.append((k, v))
249 super().write_dl(data, col_max, col_spacing)
251 if self._write_calls in [4, 5]:
252 line = self._sep * self._line_width
253 line = '\n{h}{line}{clear}\n'.format(
254 line=line, h=self._heading_color, **self._colors
255 )
256 self.write(line)
258 def write_heading(self, heading):
259 # type: (str) -> None
260 '''
261 Write section heading into buffer.
263 Commands is converted to COMMANDS.
264 Options is converted to FLAGS.
266 Args:
267 heading (str): Heading text.
268 '''
269 self._write_calls += 1
270 color = self._heading_color
271 if heading == 'Options':
272 heading = 'FLAGS'
273 color = self._flag_color
274 elif heading == 'Commands':
275 heading = 'COMMANDS'
276 color = self._command_color
277 self._flag_color = color
278 heading += ' '
279 buff = f"{'':>{self.current_indent}}"
280 text = "{color}{buff}{heading}{clear}\n"
281 text = text.format(
282 buff=buff, heading=heading, color=color, **self._colors
283 )
284 self.write(text)