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