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