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} # yapf: disable 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 237 # Brighten active pane toolbars. 238 'toolbar_active': 'bg:{} {}'.format(theme.active_bg, theme.active_fg), 239 'toolbar_inactive': 'bg:{} {}'.format(theme.inactive_bg, 240 theme.inactive_fg), 241 242 # Dimmer toolbar. 243 'toolbar_dim_active': 'bg:{} {}'.format(theme.active_bg, 244 theme.active_fg), 245 'toolbar_dim_inactive': 'bg:{} {}'.format(theme.default_bg, 246 theme.inactive_fg), 247 # Used for pane titles 248 'toolbar_accent': theme.cyan_accent, 249 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 258 # prompt_toolkit scrollbar styles: 259 'scrollbar.background': 'bg:{} {}'.format(theme.default_bg, 260 theme.default_fg), 261 # Scrollbar handle, bg is the bar color. 262 'scrollbar.button': 'bg:{} {}'.format(theme.purple_accent, 263 theme.default_bg), 264 'scrollbar.arrow': 'bg:{} {}'.format(theme.default_bg, 265 theme.blue_accent), 266 # Unstyled scrollbar classes: 267 # 'scrollbar.start' 268 # 'scrollbar.end' 269 270 # Top menu bar styles 271 'menu-bar': 'bg:{} {}'.format(theme.inactive_bg, theme.inactive_fg), 272 'menu-bar.selected-item': 'bg:{} {}'.format(theme.blue_accent, 273 theme.inactive_bg), 274 # Menu background 275 'menu': 'bg:{} {}'.format(theme.dialog_bg, theme.dim_fg), 276 # Menu item separator 277 'menu-border': theme.magenta_accent, 278 279 # Top bar logo + keyboard shortcuts 280 'logo': '{} bold'.format(theme.magenta_accent), 281 'keybind': '{} bold'.format(theme.purple_accent), 282 'keyhelp': theme.dim_fg, 283 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 288 'pane_indicator_active': 'bg:{}'.format(theme.magenta_accent), 289 'pane_indicator_inactive': 'bg:{}'.format(theme.inactive_bg), 290 291 'pane_title_active': '{} bold'.format(theme.magenta_accent), 292 'pane_title_inactive': '{}'.format(theme.purple_accent), 293 294 'window-tab-active': 'bg:{} {}'.format(theme.active_bg, 295 theme.cyan_accent), 296 'window-tab-inactive': 'bg:{} {}'.format(theme.inactive_bg, 297 theme.inactive_fg), 298 299 'pane_separator': 'bg:{} {}'.format(theme.default_bg, 300 theme.purple_accent), 301 302 # Search matches 303 'search': 'bg:{} {}'.format(theme.cyan_accent, theme.default_bg), 304 'search.current': 'bg:{} {}'.format(theme.cyan_accent, 305 theme.default_bg), 306 307 # Highlighted line styles 308 'selected-log-line': 'bg:{}'.format(theme.line_highlight_bg), 309 'marked-log-line': 'bg:{}'.format(theme.selected_line_bg), 310 'cursor-line': 'bg:{} nounderline'.format(theme.line_highlight_bg), 311 312 # Messages like 'Window too small' 313 'warning-text': 'bg:{} {}'.format(theme.default_bg, 314 theme.yellow_accent), 315 316 'log-time': 'bg:{} {}'.format(theme.default_fg, 317 theme.default_bg), 318 319 # Apply foreground only for level and column values. This way the text 320 # can inherit the background color of the parent window pane or line 321 # selection. 322 'log-level-{}'.format(logging.CRITICAL): '{} bold'.format( 323 theme.red_accent), 324 'log-level-{}'.format(logging.ERROR): '{}'.format(theme.red_accent), 325 'log-level-{}'.format(logging.WARNING): '{}'.format( 326 theme.yellow_accent), 327 'log-level-{}'.format(logging.INFO): '{}'.format(theme.purple_accent), 328 'log-level-{}'.format(logging.DEBUG): '{}'.format(theme.blue_accent), 329 330 'log-table-column-0': '{}'.format(theme.cyan_accent), 331 'log-table-column-1': '{}'.format(theme.green_accent), 332 'log-table-column-2': '{}'.format(theme.yellow_accent), 333 'log-table-column-3': '{}'.format(theme.magenta_accent), 334 'log-table-column-4': '{}'.format(theme.purple_accent), 335 'log-table-column-5': '{}'.format(theme.blue_accent), 336 'log-table-column-6': '{}'.format(theme.orange_accent), 337 'log-table-column-7': '{}'.format(theme.red_accent), 338 339 'search-bar': 'bg:{}'.format(theme.inactive_bg), 340 'search-bar-title': 'bg:{} {}'.format(theme.cyan_accent, 341 theme.default_bg), 342 'search-bar-setting': '{}'.format(theme.cyan_accent), 343 'search-bar-border': 'bg:{} {}'.format(theme.inactive_bg, 344 theme.cyan_accent), 345 'search-match-count-dialog': 'bg:{}'.format(theme.inactive_bg), 346 'search-match-count-dialog-title': '{}'.format(theme.cyan_accent), 347 'search-match-count-dialog-default-fg': '{}'.format(theme.default_fg), 348 'search-match-count-dialog-border': 'bg:{} {}'.format( 349 theme.inactive_bg, 350 theme.cyan_accent), 351 352 'filter-bar': 'bg:{}'.format(theme.inactive_bg), 353 'filter-bar-title': 'bg:{} {}'.format(theme.red_accent, 354 theme.default_bg), 355 'filter-bar-setting': '{}'.format(theme.cyan_accent), 356 'filter-bar-delete': '{}'.format(theme.red_accent), 357 'filter-bar-delimiter': '{}'.format(theme.purple_accent), 358 359 'saveas-dialog': 'bg:{}'.format(theme.inactive_bg), 360 'saveas-dialog-title': 'bg:{} {}'.format(theme.inactive_bg, 361 theme.default_fg), 362 'saveas-dialog-setting': '{}'.format(theme.cyan_accent), 363 'saveas-dialog-border': 'bg:{} {}'.format(theme.inactive_bg, 364 theme.cyan_accent), 365 366 'selection-dialog': 'bg:{}'.format(theme.inactive_bg), 367 'selection-dialog-title': '{}'.format(theme.yellow_accent), 368 'selection-dialog-default-fg': '{}'.format(theme.default_fg), 369 'selection-dialog-action-bg': 'bg:{}'.format(theme.yellow_accent), 370 'selection-dialog-action-fg': '{}'.format(theme.button_inactive_bg), 371 'selection-dialog-border': 'bg:{} {}'.format(theme.inactive_bg, 372 theme.yellow_accent), 373 374 'quit-dialog': 'bg:{}'.format(theme.inactive_bg), 375 'quit-dialog-border': 'bg:{} {}'.format(theme.inactive_bg, 376 theme.red_accent), 377 378 'command-runner': 'bg:{}'.format(theme.inactive_bg), 379 'command-runner-title': 'bg:{} {}'.format(theme.inactive_bg, 380 theme.default_fg), 381 'command-runner-setting': '{}'.format(theme.purple_accent), 382 'command-runner-border': 'bg:{} {}'.format(theme.inactive_bg, 383 theme.purple_accent), 384 'command-runner-selected-item': 'bg:{}'.format(theme.selected_line_bg), 385 'command-runner-fuzzy-highlight-0': '{}'.format(theme.blue_accent), 386 'command-runner-fuzzy-highlight-1': '{}'.format(theme.cyan_accent), 387 'command-runner-fuzzy-highlight-2': '{}'.format(theme.green_accent), 388 'command-runner-fuzzy-highlight-3': '{}'.format(theme.yellow_accent), 389 'command-runner-fuzzy-highlight-4': '{}'.format(theme.orange_accent), 390 'command-runner-fuzzy-highlight-5': '{}'.format(theme.red_accent), 391 392 # Progress Bar Styles 393 # Entire set of ProgressBars - no title is used in pw_console 394 'title': '', 395 # Actual bar title 396 'label': 'bold', 397 'percentage': '{}'.format(theme.green_accent), 398 'bar': '{}'.format(theme.magenta_accent), 399 # Filled part of the bar 400 'bar-a': '{} bold'.format(theme.cyan_accent), 401 # End of current progress 402 'bar-b': '{} bold'.format(theme.purple_accent), 403 # Empty part of the bar 404 'bar-c': '', 405 # current/total counts 406 'current': '{}'.format(theme.cyan_accent), 407 'total': '{}'.format(theme.cyan_accent), 408 'time-elapsed': '{}'.format(theme.purple_accent), 409 'time-left': '{}'.format(theme.magenta_accent), 410 411 # Named theme color classes for use in user plugins. 412 'theme-fg-red': '{}'.format(theme.red_accent), 413 'theme-fg-orange': '{}'.format(theme.orange_accent), 414 'theme-fg-yellow': '{}'.format(theme.yellow_accent), 415 'theme-fg-green': '{}'.format(theme.green_accent), 416 'theme-fg-cyan': '{}'.format(theme.cyan_accent), 417 'theme-fg-blue': '{}'.format(theme.blue_accent), 418 'theme-fg-purple': '{}'.format(theme.purple_accent), 419 'theme-fg-magenta': '{}'.format(theme.magenta_accent), 420 'theme-bg-red': 'bg:{}'.format(theme.red_accent), 421 'theme-bg-orange': 'bg:{}'.format(theme.orange_accent), 422 'theme-bg-yellow': 'bg:{}'.format(theme.yellow_accent), 423 'theme-bg-green': 'bg:{}'.format(theme.green_accent), 424 'theme-bg-cyan': 'bg:{}'.format(theme.cyan_accent), 425 'theme-bg-blue': 'bg:{}'.format(theme.blue_accent), 426 'theme-bg-purple': 'bg:{}'.format(theme.purple_accent), 427 'theme-bg-magenta': 'bg:{}'.format(theme.magenta_accent), 428 429 'theme-bg-active': 'bg:{}'.format(theme.active_bg), 430 'theme-fg-active': '{}'.format(theme.active_fg), 431 432 'theme-bg-inactive': 'bg:{}'.format(theme.inactive_bg), 433 'theme-fg-inactive': '{}'.format(theme.inactive_fg), 434 435 'theme-fg-default': '{}'.format(theme.default_fg), 436 'theme-bg-default': 'bg:{}'.format(theme.default_bg), 437 438 'theme-fg-dim': '{}'.format(theme.dim_fg), 439 'theme-bg-dim': 'bg:{}'.format(theme.dim_bg), 440 441 'theme-bg-dialog': 'bg:{}'.format(theme.dialog_bg), 442 'theme-bg-line-highlight': 'bg:{}'.format(theme.line_highlight_bg), 443 444 'theme-bg-button-active': 'bg:{}'.format(theme.button_active_bg), 445 'theme-bg-button-inactive': 'bg:{}'.format(theme.button_inactive_bg), 446 } # yapf: disable 447 448 return Style.from_dict(pw_console_styles) 449 450 451def get_toolbar_style(pt_container, dim=False) -> str: 452 """Return the style class for a toolbar if pt_container is in focus.""" 453 if has_focus(pt_container.__pt_container__())(): 454 return 'class:toolbar_dim_active' if dim else 'class:toolbar_active' 455 return 'class:toolbar_dim_inactive' if dim else 'class:toolbar_inactive' 456 457 458def get_button_style(pt_container) -> str: 459 """Return the style class for a toolbar if pt_container is in focus.""" 460 if has_focus(pt_container.__pt_container__())(): 461 return 'class:toolbar-button-active' 462 return 'class:toolbar-button-inactive' 463 464 465def get_pane_style(pt_container) -> str: 466 """Return the style class for a pane title if pt_container is in focus.""" 467 if has_focus(pt_container.__pt_container__())(): 468 return 'class:pane_active' 469 return 'class:pane_inactive' 470 471 472def get_pane_indicator(pt_container, 473 title, 474 mouse_handler=None, 475 hide_indicator=False) -> StyleAndTextTuples: 476 """Return formatted text for a pane indicator and title.""" 477 478 inactive_indicator: OneStyleAndTextTuple 479 active_indicator: OneStyleAndTextTuple 480 inactive_title: OneStyleAndTextTuple 481 active_title: OneStyleAndTextTuple 482 483 if mouse_handler: 484 inactive_indicator = ('class:pane_indicator_inactive', ' ', 485 mouse_handler) 486 active_indicator = ('class:pane_indicator_active', ' ', mouse_handler) 487 inactive_title = ('class:pane_title_inactive', title, mouse_handler) 488 active_title = ('class:pane_title_active', title, mouse_handler) 489 else: 490 inactive_indicator = ('class:pane_indicator_inactive', ' ') 491 active_indicator = ('class:pane_indicator_active', ' ') 492 inactive_title = ('class:pane_title_inactive', title) 493 active_title = ('class:pane_title_active', title) 494 495 fragments: StyleAndTextTuples = [] 496 if has_focus(pt_container.__pt_container__())(): 497 if not hide_indicator: 498 fragments.append(active_indicator) 499 fragments.append(active_title) 500 else: 501 if not hide_indicator: 502 fragments.append(inactive_indicator) 503 fragments.append(inactive_title) 504 return fragments 505