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

1from typing import Any, Sequence, Type # noqa: F401 

2 

3from enum import Enum 

4 

5import click 

6# ------------------------------------------------------------------------------ 

7 

8 

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. 

18 

19 Returns: 

20 dict: (name, value) dictionary. 

21 ''' 

22 return {x.name: x.value for x in cls.__members__.values()} 

23 

24 

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' 

52 

53 

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' 

74 

75 

76# ------------------------------------------------------------------------------ 

77def get_plotly_template(colorscheme=Colorscheme): 

78 # type: (Type[Colorscheme]) -> dict 

79 ''' 

80 Create a plotly template from a given color scheme. 

81 

82 Args: 

83 colorscheme (colorscheme): colorscheme enum. 

84 

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 ] 

95 

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 

143 

144 

145# ------------------------------------------------------------------------------ 

146class ThemeFormatter(click.HelpFormatter): 

147 ''' 

148 ThemeFormatter makes click CLI output prettier. 

149 

150 Include the following code to add it to click: 

151 

152 .. code-block:: python 

153 

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. 

169 

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] 

189 

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. 

195 

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') 

210 

211 def write_usage(self, prog, *args, **kwargs): 

212 # type: (str, Any, Any) -> None 

213 r''' 

214 Writes a usage line into the buffer. 

215 

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) 

228 

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. 

234 

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) 

250 

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) 

257 

258 def write_heading(self, heading): 

259 # type: (str) -> None 

260 ''' 

261 Write section heading into buffer. 

262 

263 Commands is converted to COMMANDS. 

264 Options is converted to FLAGS. 

265 

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)