xref: /aosp_15_r20/external/pigweed/pw_console/py/pw_console/style.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1# Copyright 2021 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""UI Color Styles for ConsoleApp."""
15
16import logging
17from dataclasses import dataclass
18
19from prompt_toolkit.formatted_text import StyleAndTextTuples
20from prompt_toolkit.formatted_text.base import OneStyleAndTextTuple
21from prompt_toolkit.styles import Style
22from prompt_toolkit.filters import has_focus
23
24_LOG = logging.getLogger(__package__)
25
26
27@dataclass
28class HighContrastDarkColors:
29    """Dark high contrast colors."""
30
31    # pylint: disable=too-many-instance-attributes
32    display_name = 'High Contrast'
33
34    default_bg = '#100f10'
35    default_fg = '#ffffff'
36
37    dim_bg = '#000000'
38    dim_fg = '#e0e6f0'
39
40    button_active_bg = '#4e4e4e'
41    button_inactive_bg = '#323232'
42
43    active_bg = '#323232'
44    active_fg = '#f4f4f4'
45
46    inactive_bg = '#1e1e1e'
47    inactive_fg = '#bfc0c4'
48
49    line_highlight_bg = '#2f2f2f'
50    selected_line_bg = '#4e4e4e'
51    dialog_bg = '#3c3c3c'
52
53    red_accent = '#ffc0bf'
54    orange_accent = '#f5ca80'
55    yellow_accent = '#eedc82'
56    green_accent = '#88ef88'
57    cyan_accent = '#60e7e0'
58    blue_accent = '#92d9ff'
59    purple_accent = '#cfcaff'
60    magenta_accent = '#ffb8ff'
61
62
63@dataclass
64class DarkColors:
65    """The default dark UI color theme."""
66
67    # pylint: disable=too-many-instance-attributes
68    display_name = 'Dark'
69
70    default_bg = '#2e2e2e'
71    default_fg = '#eeeeee'
72
73    dim_bg = '#262626'
74    dim_fg = '#dfdfdf'
75
76    button_active_bg = '#626262'
77    button_inactive_bg = '#525252'
78
79    active_bg = '#525252'
80    active_fg = '#dfdfdf'
81
82    inactive_bg = '#3f3f3f'
83    inactive_fg = '#bfbfbf'
84
85    line_highlight_bg = '#525252'
86    selected_line_bg = '#626262'
87    dialog_bg = '#3c3c3c'
88
89    red_accent = '#ff6c6b'
90    orange_accent = '#da8548'
91    yellow_accent = '#ffcc66'
92    green_accent = '#98be65'
93    cyan_accent = '#66cccc'
94    blue_accent = '#6699cc'
95    purple_accent = '#a9a1e1'
96    magenta_accent = '#c678dd'
97
98
99@dataclass
100class NordColors:
101    """Nord UI color theme."""
102
103    # pylint: disable=too-many-instance-attributes
104    display_name = 'Nord'
105
106    default_bg = '#2e3440'
107    default_fg = '#eceff4'
108
109    dim_bg = '#272c36'
110    dim_fg = '#e5e9f0'
111
112    button_active_bg = '#4c566a'
113    button_inactive_bg = '#434c5e'
114
115    active_bg = '#434c5e'
116    active_fg = '#eceff4'
117
118    inactive_bg = '#373e4c'
119    inactive_fg = '#d8dee9'
120
121    line_highlight_bg = '#191c25'
122    selected_line_bg = '#4c566a'
123    dialog_bg = '#2c333f'
124
125    red_accent = '#bf616a'
126    orange_accent = '#d08770'
127    yellow_accent = '#ebcb8b'
128    green_accent = '#a3be8c'
129    cyan_accent = '#88c0d0'
130    blue_accent = '#81a1c1'
131    purple_accent = '#a9a1e1'
132    magenta_accent = '#b48ead'
133
134
135@dataclass
136class NordLightColors:
137    """Nord light UI color theme."""
138
139    # pylint: disable=too-many-instance-attributes
140    display_name = 'Nord Light'
141
142    default_bg = '#e5e9f0'
143    default_fg = '#3b4252'
144    dim_bg = '#d8dee9'
145    dim_fg = '#2e3440'
146    button_active_bg = '#aebacf'
147    button_inactive_bg = '#b8c5db'
148    active_bg = '#b8c5db'
149    active_fg = '#3b4252'
150    inactive_bg = '#c2d0e7'
151    inactive_fg = '#60728c'
152    line_highlight_bg = '#f0f4fc'
153    selected_line_bg = '#f0f4fc'
154    dialog_bg = '#d8dee9'
155
156    red_accent = '#99324b'
157    orange_accent = '#ac4426'
158    yellow_accent = '#9a7500'
159    green_accent = '#4f894c'
160    cyan_accent = '#398eac'
161    blue_accent = '#3b6ea8'
162    purple_accent = '#842879'
163    magenta_accent = '#97365b'
164
165
166@dataclass
167class MoonlightColors:
168    """Moonlight UI color theme."""
169
170    # pylint: disable=too-many-instance-attributes
171    display_name = 'Moonlight'
172
173    default_bg = '#212337'
174    default_fg = '#c8d3f5'
175    dim_bg = '#191a2a'
176    dim_fg = '#b4c2f0'
177    button_active_bg = '#444a73'
178    button_inactive_bg = '#2f334d'
179    active_bg = '#2f334d'
180    active_fg = '#c8d3f5'
181    inactive_bg = '#222436'
182    inactive_fg = '#a9b8e8'
183    line_highlight_bg = '#383e5c'
184    selected_line_bg = '#444a73'
185    dialog_bg = '#1e2030'
186
187    red_accent = '#d95468'
188    orange_accent = '#d98e48'
189    yellow_accent = '#ebbf83'
190    green_accent = '#8bd49c'
191    cyan_accent = '#70e1e8'
192    blue_accent = '#5ec4ff'
193    purple_accent = '#b62d65'
194    magenta_accent = '#e27e8d'
195
196
197@dataclass
198class Synthwave84Colors:
199    """Synthwave84 UI color theme."""
200
201    # pylint: disable=too-many-instance-attributes
202    display_name = 'Synthwave84'
203
204    default_bg = '#252334'
205    default_fg = '#ffffff'
206    dim_bg = '#2a2139'
207    dim_fg = '#ffffff'
208    button_active_bg = '#614d85'
209    button_inactive_bg = '#2f334d'
210    active_bg = '#2f334d'
211    active_fg = '#c8d3f5'
212    inactive_bg = '#222436'
213    inactive_fg = '#a9b8e8'
214    line_highlight_bg = '#383e5c'
215    selected_line_bg = '#444a73'
216    dialog_bg = '#1e2030'
217
218    red_accent = '#fe4450'
219    orange_accent = '#f97e72'
220    yellow_accent = '#fede5d'
221    green_accent = '#72f1b8'
222    cyan_accent = '#03edf9'
223    blue_accent = '#2ee2fa'
224    purple_accent = '#9d8bca'
225    magenta_accent = '#ff7edb'
226
227
228@dataclass
229class AnsiTerm:
230    """Color theme that uses the default terminal color codes."""
231
232    # pylint: disable=too-many-instance-attributes
233    display_name = 'ANSI Term'
234
235    default_bg = 'default'
236    default_fg = 'default'
237
238    dim_bg = 'default'
239    dim_fg = 'default'
240
241    button_active_bg = 'default underline'
242    button_inactive_bg = 'default'
243
244    active_bg = 'default'
245    active_fg = 'default'
246
247    inactive_bg = 'default'
248    inactive_fg = 'default'
249
250    line_highlight_bg = 'ansidarkgray white'
251    selected_line_bg = 'default reverse'
252    dialog_bg = 'default'
253
254    red_accent = 'ansired'
255    orange_accent = 'orange'
256    yellow_accent = 'ansiyellow'
257    green_accent = 'ansigreen'
258    cyan_accent = 'ansicyan'
259    blue_accent = 'ansiblue'
260    purple_accent = 'ansipurple'
261    magenta_accent = 'ansimagenta'
262
263
264THEME_NAME_MAPPING = {
265    'dark': DarkColors(),
266    'high-contrast-dark': HighContrastDarkColors(),
267    'nord': NordColors(),
268    'nord-light': NordLightColors(),
269    'synthwave84': Synthwave84Colors(),
270    'moonlight': MoonlightColors(),
271    'ansi': AnsiTerm(),
272}
273
274
275def get_theme_colors(theme_name=''):
276    theme = THEME_NAME_MAPPING.get(theme_name, DarkColors())
277    return theme
278
279
280def generate_styles(theme_name='dark'):
281    """Return prompt_toolkit styles for the given theme name."""
282    # Use DarkColors() if name not found.
283    theme = THEME_NAME_MAPPING.get(theme_name, DarkColors())
284
285    pw_console_styles = {
286        # Default text and background.
287        'default': 'bg:{} {}'.format(theme.default_bg, theme.default_fg),
288        # Dim inactive panes.
289        'pane_inactive': 'bg:{} {}'.format(theme.dim_bg, theme.dim_fg),
290        # Use default for active panes.
291        'pane_active': 'bg:{} {}'.format(theme.default_bg, theme.default_fg),
292        # Brighten active pane toolbars.
293        'toolbar_active': 'bg:{} {}'.format(theme.active_bg, theme.active_fg),
294        'toolbar_inactive': 'bg:{} {}'.format(
295            theme.inactive_bg, theme.inactive_fg
296        ),
297        # Dimmer toolbar.
298        'toolbar_dim_active': 'bg:{} {}'.format(
299            theme.active_bg, theme.active_fg
300        ),
301        'toolbar_dim_inactive': 'bg:{} {}'.format(
302            theme.default_bg, theme.inactive_fg
303        ),
304        # Used for pane titles
305        'toolbar_accent': theme.cyan_accent,
306        'toolbar-button-decoration': '{}'.format(theme.cyan_accent),
307        'toolbar-setting-active': 'bg:{} {}'.format(
308            theme.green_accent,
309            theme.active_bg,
310        ),
311        'toolbar-button-active': 'bg:{}'.format(theme.button_active_bg),
312        'toolbar-button-inactive': 'bg:{}'.format(theme.button_inactive_bg),
313        # prompt_toolkit scrollbar styles:
314        'scrollbar.background': 'bg:{} {}'.format(
315            theme.default_bg, theme.default_fg
316        ),
317        # Scrollbar handle, bg is the bar color.
318        'scrollbar.button': 'bg:{} {}'.format(
319            theme.purple_accent, theme.default_bg
320        ),
321        'scrollbar.arrow': 'bg:{} {}'.format(
322            theme.default_bg, theme.blue_accent
323        ),
324        # Unstyled scrollbar classes:
325        # 'scrollbar.start'
326        # 'scrollbar.end'
327        # Top menu bar styles
328        'menu-bar': 'bg:{} {}'.format(theme.inactive_bg, theme.inactive_fg),
329        'menu-bar.selected-item': 'bg:{} {}'.format(
330            theme.blue_accent, theme.inactive_bg
331        ),
332        # Menu background
333        'menu': 'bg:{} {}'.format(theme.dialog_bg, theme.dim_fg),
334        # Menu item separator
335        'menu-border': theme.magenta_accent,
336        # Top bar logo + keyboard shortcuts
337        'logo': '{} bold'.format(theme.magenta_accent),
338        'keybind': '{} bold'.format(theme.purple_accent),
339        'keyhelp': theme.dim_fg,
340        # Help window styles
341        'help_window_content': 'bg:{} {}'.format(theme.dialog_bg, theme.dim_fg),
342        'frame.border': 'bg:{} {}'.format(theme.dialog_bg, theme.purple_accent),
343        'pane_indicator_active': 'bg:{}'.format(theme.magenta_accent),
344        'pane_indicator_inactive': 'bg:{}'.format(theme.inactive_bg),
345        'pane_title_active': '{} bold'.format(theme.magenta_accent),
346        'pane_title_inactive': '{}'.format(theme.purple_accent),
347        'window-tab-active': 'bg:{} {}'.format(
348            theme.active_bg, theme.cyan_accent
349        ),
350        'window-tab-inactive': 'bg:{} {}'.format(
351            theme.inactive_bg, theme.inactive_fg
352        ),
353        'pane_separator': 'bg:{} {}'.format(
354            theme.default_bg, theme.purple_accent
355        ),
356        # Search matches
357        'search': 'bg:{} {}'.format(theme.cyan_accent, theme.default_bg),
358        'search.current': 'bg:{} {}'.format(
359            theme.cyan_accent, theme.default_bg
360        ),
361        # Highlighted line styles
362        'selected-log-line': 'bg:{}'.format(theme.line_highlight_bg),
363        'marked-log-line': 'bg:{}'.format(theme.selected_line_bg),
364        'cursor-line': 'bg:{} nounderline'.format(theme.line_highlight_bg),
365        # Messages like 'Window too small'
366        'warning-text': 'bg:{} {}'.format(
367            theme.default_bg, theme.yellow_accent
368        ),
369        'log-time': 'bg:{} {}'.format(theme.default_fg, theme.default_bg),
370        # Apply foreground only for level and column values. This way the text
371        # can inherit the background color of the parent window pane or line
372        # selection.
373        'log-level-{}'.format(logging.CRITICAL): '{} bold'.format(
374            theme.red_accent
375        ),
376        'log-level-{}'.format(logging.ERROR): '{}'.format(theme.red_accent),
377        'log-level-{}'.format(logging.WARNING): '{}'.format(
378            theme.yellow_accent
379        ),
380        'log-level-{}'.format(logging.INFO): '{}'.format(theme.purple_accent),
381        'log-level-{}'.format(logging.DEBUG): '{}'.format(theme.blue_accent),
382        'log-table-column-0': '{}'.format(theme.cyan_accent),
383        'log-table-column-1': '{}'.format(theme.green_accent),
384        'log-table-column-2': '{}'.format(theme.yellow_accent),
385        'log-table-column-3': '{}'.format(theme.magenta_accent),
386        'log-table-column-4': '{}'.format(theme.purple_accent),
387        'log-table-column-5': '{}'.format(theme.blue_accent),
388        'log-table-column-6': '{}'.format(theme.orange_accent),
389        'log-table-column-7': '{}'.format(theme.red_accent),
390        'search-bar': 'bg:{}'.format(theme.inactive_bg),
391        'search-bar-title': 'bg:{} {}'.format(
392            theme.cyan_accent, theme.default_bg
393        ),
394        'search-bar-setting': '{}'.format(theme.cyan_accent),
395        'search-bar-border': 'bg:{} {}'.format(
396            theme.inactive_bg, theme.cyan_accent
397        ),
398        'search-match-count-dialog': 'bg:{}'.format(theme.inactive_bg),
399        'search-match-count-dialog-title': '{}'.format(theme.cyan_accent),
400        'search-match-count-dialog-default-fg': '{}'.format(theme.default_fg),
401        'search-match-count-dialog-border': 'bg:{} {}'.format(
402            theme.inactive_bg, theme.cyan_accent
403        ),
404        'filter-bar': 'bg:{}'.format(theme.inactive_bg),
405        'filter-bar-title': 'bg:{} {}'.format(
406            theme.red_accent, theme.default_bg
407        ),
408        'filter-bar-setting': '{}'.format(theme.cyan_accent),
409        'filter-bar-delete': '{}'.format(theme.red_accent),
410        'filter-bar-delimiter': '{}'.format(theme.purple_accent),
411        'saveas-dialog': 'bg:{}'.format(theme.inactive_bg),
412        'saveas-dialog-title': 'bg:{} {}'.format(
413            theme.inactive_bg, theme.default_fg
414        ),
415        'saveas-dialog-setting': '{}'.format(theme.cyan_accent),
416        'saveas-dialog-border': 'bg:{} {}'.format(
417            theme.inactive_bg, theme.cyan_accent
418        ),
419        'selection-dialog': 'bg:{}'.format(theme.inactive_bg),
420        'selection-dialog-title': '{}'.format(theme.yellow_accent),
421        'selection-dialog-default-fg': '{}'.format(theme.default_fg),
422        'selection-dialog-action-bg': 'bg:{}'.format(theme.yellow_accent),
423        'selection-dialog-action-fg': '{}'.format(theme.button_inactive_bg),
424        'selection-dialog-border': 'bg:{} {}'.format(
425            theme.inactive_bg, theme.yellow_accent
426        ),
427        'quit-dialog': 'bg:{}'.format(theme.inactive_bg),
428        'quit-dialog-border': 'bg:{} {}'.format(
429            theme.inactive_bg, theme.red_accent
430        ),
431        'command-runner': 'bg:{}'.format(theme.inactive_bg),
432        'command-runner-title': 'bg:{} {}'.format(
433            theme.inactive_bg, theme.default_fg
434        ),
435        'command-runner-setting': '{}'.format(theme.purple_accent),
436        'command-runner-border': 'bg:{} {}'.format(
437            theme.inactive_bg, theme.purple_accent
438        ),
439        'command-runner-selected-item': 'bg:{}'.format(theme.selected_line_bg),
440        'command-runner-fuzzy-highlight-0': '{}'.format(theme.blue_accent),
441        'command-runner-fuzzy-highlight-1': '{}'.format(theme.cyan_accent),
442        'command-runner-fuzzy-highlight-2': '{}'.format(theme.green_accent),
443        'command-runner-fuzzy-highlight-3': '{}'.format(theme.yellow_accent),
444        'command-runner-fuzzy-highlight-4': '{}'.format(theme.orange_accent),
445        'command-runner-fuzzy-highlight-5': '{}'.format(theme.red_accent),
446        # Progress Bar Styles
447        # Entire set of ProgressBars - no title is used in pw_console
448        'title': '',
449        # Actual bar title
450        'label': 'bold',
451        'percentage': '{}'.format(theme.green_accent),
452        'bar': '{}'.format(theme.magenta_accent),
453        # Filled part of the bar
454        'bar-a': '{} bold'.format(theme.cyan_accent),
455        # End of current progress
456        'bar-b': '{} bold'.format(theme.purple_accent),
457        # Empty part of the bar
458        'bar-c': '',
459        # current/total counts
460        'current': '{}'.format(theme.cyan_accent),
461        'total': '{}'.format(theme.cyan_accent),
462        'time-elapsed': '{}'.format(theme.purple_accent),
463        'time-left': '{}'.format(theme.magenta_accent),
464        # Named theme color classes for use in user plugins.
465        'theme-fg-red': '{}'.format(theme.red_accent),
466        'theme-fg-orange': '{}'.format(theme.orange_accent),
467        'theme-fg-yellow': '{}'.format(theme.yellow_accent),
468        'theme-fg-green': '{}'.format(theme.green_accent),
469        'theme-fg-cyan': '{}'.format(theme.cyan_accent),
470        'theme-fg-blue': '{}'.format(theme.blue_accent),
471        'theme-fg-purple': '{}'.format(theme.purple_accent),
472        'theme-fg-magenta': '{}'.format(theme.magenta_accent),
473        'theme-bg-red': 'bg:{}'.format(theme.red_accent),
474        'theme-bg-orange': 'bg:{}'.format(theme.orange_accent),
475        'theme-bg-yellow': 'bg:{}'.format(theme.yellow_accent),
476        'theme-bg-green': 'bg:{}'.format(theme.green_accent),
477        'theme-bg-cyan': 'bg:{}'.format(theme.cyan_accent),
478        'theme-bg-blue': 'bg:{}'.format(theme.blue_accent),
479        'theme-bg-purple': 'bg:{}'.format(theme.purple_accent),
480        'theme-bg-magenta': 'bg:{}'.format(theme.magenta_accent),
481        'theme-bg-active': 'bg:{}'.format(theme.active_bg),
482        'theme-fg-active': '{}'.format(theme.active_fg),
483        'theme-bg-inactive': 'bg:{}'.format(theme.inactive_bg),
484        'theme-fg-inactive': '{}'.format(theme.inactive_fg),
485        'theme-fg-default': '{}'.format(theme.default_fg),
486        'theme-bg-default': 'bg:{}'.format(theme.default_bg),
487        'theme-fg-dim': '{}'.format(theme.dim_fg),
488        'theme-bg-dim': 'bg:{}'.format(theme.dim_bg),
489        'theme-bg-dialog': 'bg:{}'.format(theme.dialog_bg),
490        'theme-bg-line-highlight': 'bg:{}'.format(theme.line_highlight_bg),
491        'theme-bg-button-active': 'bg:{}'.format(theme.button_active_bg),
492        'theme-bg-button-inactive': 'bg:{}'.format(theme.button_inactive_bg),
493    }
494
495    return Style.from_dict(pw_console_styles)
496
497
498def get_toolbar_style(pt_container, dim=False) -> str:
499    """Return the style class for a toolbar if pt_container is in focus."""
500    if has_focus(pt_container.__pt_container__())():
501        return 'class:toolbar_dim_active' if dim else 'class:toolbar_active'
502    return 'class:toolbar_dim_inactive' if dim else 'class:toolbar_inactive'
503
504
505def get_button_style(pt_container) -> str:
506    """Return the style class for a toolbar if pt_container is in focus."""
507    if has_focus(pt_container.__pt_container__())():
508        return 'class:toolbar-button-active'
509    return 'class:toolbar-button-inactive'
510
511
512def get_pane_style(pt_container) -> str:
513    """Return the style class for a pane title if pt_container is in focus."""
514    if has_focus(pt_container.__pt_container__())():
515        return 'class:pane_active'
516    return 'class:pane_inactive'
517
518
519def get_pane_indicator(
520    pt_container, title, mouse_handler=None, hide_indicator=False
521) -> StyleAndTextTuples:
522    """Return formatted text for a pane indicator and title."""
523
524    inactive_indicator: OneStyleAndTextTuple
525    active_indicator: OneStyleAndTextTuple
526    inactive_title: OneStyleAndTextTuple
527    active_title: OneStyleAndTextTuple
528
529    if mouse_handler:
530        inactive_indicator = (
531            'class:pane_indicator_inactive',
532            ' ',
533            mouse_handler,
534        )
535        active_indicator = ('class:pane_indicator_active', ' ', mouse_handler)
536        inactive_title = ('class:pane_title_inactive', title, mouse_handler)
537        active_title = ('class:pane_title_active', title, mouse_handler)
538    else:
539        inactive_indicator = ('class:pane_indicator_inactive', ' ')
540        active_indicator = ('class:pane_indicator_active', ' ')
541        inactive_title = ('class:pane_title_inactive', title)
542        active_title = ('class:pane_title_active', title)
543
544    fragments: StyleAndTextTuples = []
545    if has_focus(pt_container.__pt_container__())():
546        if not hide_indicator:
547            fragments.append(active_indicator)
548        fragments.append(active_title)
549    else:
550        if not hide_indicator:
551            fragments.append(inactive_indicator)
552        fragments.append(inactive_title)
553    return fragments
554