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

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

2 

3from enum import Enum 

4 

5import click 

6 

7from lunchbox.enum import EnumBase 

8# ------------------------------------------------------------------------------ 

9 

10 

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' 

38 

39 

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' 

60 

61 

62# ------------------------------------------------------------------------------ 

63def get_plotly_template(colorscheme=Colorscheme): 

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

65 ''' 

66 Create a plotly template from a given color scheme. 

67 

68 Args: 

69 colorscheme (colorscheme): colorscheme enum. 

70 

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] 

81 

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 

129 

130 

131# ------------------------------------------------------------------------------ 

132class ThemeFormatter(click.HelpFormatter): 

133 ''' 

134 ThemeFormatter makes click CLI output prettier. 

135 

136 Include the following code to add it to click: 

137 

138 .. code-block:: python 

139 

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. 

155 

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] 

175 

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. 

181 

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

196 

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

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

199 r''' 

200 Writes a usage line into the buffer. 

201 

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) 

214 

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. 

220 

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) 

236 

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) 

243 

244 def write_heading(self, heading): 

245 # type: (str) -> None 

246 ''' 

247 Write section heading into buffer. 

248 

249 Commands is converted to COMMANDS. 

250 Options is converted to FLAGS. 

251 

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)