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