1#!/usr/bin/env python3 2 3# Copyright (c) 2018-2019, Nordic Semiconductor ASA and Ulf Magnusson 4# SPDX-License-Identifier: ISC 5 6""" 7Overview 8======== 9 10A curses-based Python 2/3 menuconfig implementation. The interface should feel 11familiar to people used to mconf ('make menuconfig'). 12 13Supports the same keys as mconf, and also supports a set of keybindings 14inspired by Vi: 15 16 J/K : Down/Up 17 L : Enter menu/Toggle item 18 H : Leave menu 19 Ctrl-D/U: Page Down/Page Up 20 G/End : Jump to end of list 21 g/Home : Jump to beginning of list 22 23[Space] toggles values if possible, and enters menus otherwise. [Enter] works 24the other way around. 25 26The mconf feature where pressing a key jumps to a menu entry with that 27character in it in the current menu isn't supported. A jump-to feature for 28jumping directly to any symbol (including invisible symbols), choice, menu or 29comment (as in a Kconfig 'comment "Foo"') is available instead. 30 31A few different modes are available: 32 33 F: Toggle show-help mode, which shows the help text of the currently selected 34 item in the window at the bottom of the menu display. This is handy when 35 browsing through options. 36 37 C: Toggle show-name mode, which shows the symbol name before each symbol menu 38 entry 39 40 A: Toggle show-all mode, which shows all items, including currently invisible 41 items and items that lack a prompt. Invisible items are drawn in a different 42 style to make them stand out. 43 44 45Running 46======= 47 48menuconfig.py can be run either as a standalone executable or by calling the 49menuconfig() function with an existing Kconfig instance. The second option is a 50bit inflexible in that it will still load and save .config, etc. 51 52When run in standalone mode, the top-level Kconfig file to load can be passed 53as a command-line argument. With no argument, it defaults to "Kconfig". 54 55The KCONFIG_CONFIG environment variable specifies the .config file to load (if 56it exists) and save. If KCONFIG_CONFIG is unset, ".config" is used. 57 58When overwriting a configuration file, the old version is saved to 59<filename>.old (e.g. .config.old). 60 61$srctree is supported through Kconfiglib. 62 63 64Color schemes 65============= 66 67It is possible to customize the color scheme by setting the MENUCONFIG_STYLE 68environment variable. For example, setting it to 'aquatic' will enable an 69alternative, less yellow, more 'make menuconfig'-like color scheme, contributed 70by Mitja Horvat (pinkfluid). 71 72This is the current list of built-in styles: 73 - default classic Kconfiglib theme with a yellow accent 74 - monochrome colorless theme (uses only bold and standout) attributes, 75 this style is used if the terminal doesn't support colors 76 - aquatic blue-tinted style loosely resembling the lxdialog theme 77 78It is possible to customize the current style by changing colors of UI 79elements on the screen. This is the list of elements that can be stylized: 80 81 - path Top row in the main display, with the menu path 82 - separator Separator lines between windows. Also used for the top line 83 in the symbol information display. 84 - list List of items, e.g. the main display 85 - selection Style for the selected item 86 - inv-list Like list, but for invisible items. Used in show-all mode. 87 - inv-selection Like selection, but for invisible items. Used in show-all 88 mode. 89 - help Help text windows at the bottom of various fullscreen 90 dialogs 91 - show-help Window showing the help text in show-help mode 92 - frame Frame around dialog boxes 93 - body Body of dialog boxes 94 - edit Edit box in pop-up dialogs 95 - jump-edit Edit box in jump-to dialog 96 - text Symbol information text 97 98The color definition is a comma separated list of attributes: 99 100 - fg:COLOR Set the foreground/background colors. COLOR can be one of 101 * or * the basic 16 colors (black, red, green, yellow, blue, 102 - bg:COLOR magenta, cyan, white and brighter versions, for example, 103 brightred). On terminals that support more than 8 colors, 104 you can also directly put in a color number, e.g. fg:123 105 (hexadecimal and octal constants are accepted as well). 106 Colors outside the range -1..curses.COLORS-1 (which is 107 terminal-dependent) are ignored (with a warning). The COLOR 108 can be also specified using a RGB value in the HTML 109 notation, for example #RRGGBB. If the terminal supports 110 color changing, the color is rendered accurately. 111 Otherwise, the visually nearest color is used. 112 113 If the background or foreground color of an element is not 114 specified, it defaults to -1, representing the default 115 terminal foreground or background color. 116 117 Note: On some terminals a bright version of the color 118 implies bold. 119 - bold Use bold text 120 - underline Use underline text 121 - standout Standout text attribute (reverse color) 122 123More often than not, some UI elements share the same color definition. In such 124cases the right value may specify an UI element from which the color definition 125will be copied. For example, "separator=help" will apply the current color 126definition for "help" to "separator". 127 128A keyword without the '=' is assumed to be a style template. The template name 129is looked up in the built-in styles list and the style definition is expanded 130in-place. With this, built-in styles can be used as basis for new styles. 131 132For example, take the aquatic theme and give it a red selection bar: 133 134MENUCONFIG_STYLE="aquatic selection=fg:white,bg:red" 135 136If there's an error in the style definition or if a missing style is assigned 137to, the assignment will be ignored, along with a warning being printed on 138stderr. 139 140The 'default' theme is always implicitly parsed first, so the following two 141settings have the same effect: 142 143 MENUCONFIG_STYLE="selection=fg:white,bg:red" 144 MENUCONFIG_STYLE="default selection=fg:white,bg:red" 145 146If the terminal doesn't support colors, the 'monochrome' theme is used, and 147MENUCONFIG_STYLE is ignored. The assumption is that the environment is broken 148somehow, and that the important thing is to get something usable. 149 150 151Other features 152============== 153 154 - Seamless terminal resizing 155 156 - No dependencies on *nix, as the 'curses' module is in the Python standard 157 library 158 159 - Unicode text entry 160 161 - Improved information screen compared to mconf: 162 163 * Expressions are split up by their top-level &&/|| operands to improve 164 readability 165 166 * Undefined symbols in expressions are pointed out 167 168 * Menus and comments have information displays 169 170 * Kconfig definitions are printed 171 172 * The include path is shown, listing the locations of the 'source' 173 statements that included the Kconfig file of the symbol (or other 174 item) 175 176 177Limitations 178=========== 179 180Doesn't work out of the box on Windows, but can be made to work with 181 182 pip install windows-curses 183 184See the https://github.com/zephyrproject-rtos/windows-curses repository. 185""" 186from __future__ import print_function 187 188import os 189import sys 190 191_IS_WINDOWS = os.name == "nt" # Are we running on Windows? 192 193try: 194 import curses 195except ImportError as e: 196 if not _IS_WINDOWS: 197 raise 198 sys.exit("""\ 199menuconfig failed to import the standard Python 'curses' library. Try 200installing a package like windows-curses 201(https://github.com/zephyrproject-rtos/windows-curses) by running this command 202in cmd.exe: 203 204 pip install windows-curses 205 206Starting with Kconfiglib 13.0.0, windows-curses is no longer automatically 207installed when installing Kconfiglib via pip on Windows (because it breaks 208installation on MSYS2). 209 210Exception: 211{}: {}""".format(type(e).__name__, e)) 212 213import errno 214import locale 215import re 216import textwrap 217 218from kconfiglib import Symbol, Choice, MENU, COMMENT, MenuNode, \ 219 BOOL, TRISTATE, STRING, INT, HEX, \ 220 AND, OR, \ 221 expr_str, expr_value, split_expr, \ 222 standard_sc_expr_str, \ 223 TRI_TO_STR, TYPE_TO_STR, \ 224 standard_kconfig, standard_config_filename 225 226 227# 228# Configuration variables 229# 230 231# If True, try to change LC_CTYPE to a UTF-8 locale if it is set to the C 232# locale (which implies ASCII). This fixes curses Unicode I/O issues on systems 233# with bad defaults. ncurses configures itself from the locale settings. 234# 235# Related PEP: https://www.python.org/dev/peps/pep-0538/ 236_CHANGE_C_LC_CTYPE_TO_UTF8 = True 237 238# How many steps an implicit submenu will be indented. Implicit submenus are 239# created when an item depends on the symbol before it. Note that symbols 240# defined with 'menuconfig' create a separate menu instead of indenting. 241_SUBMENU_INDENT = 4 242 243# Number of steps for Page Up/Down to jump 244_PG_JUMP = 6 245 246# Height of the help window in show-help mode 247_SHOW_HELP_HEIGHT = 8 248 249# How far the cursor needs to be from the edge of the window before it starts 250# to scroll. Used for the main menu display, the information display, the 251# search display, and for text boxes. 252_SCROLL_OFFSET = 5 253 254# Minimum width of dialogs that ask for text input 255_INPUT_DIALOG_MIN_WIDTH = 30 256 257# Number of arrows pointing up/down to draw when a window is scrolled 258_N_SCROLL_ARROWS = 14 259 260# Lines of help text shown at the bottom of the "main" display 261_MAIN_HELP_LINES = """ 262[Space/Enter] Toggle/enter [ESC] Leave menu [S] Save 263[O] Load [?] Symbol info [/] Jump to symbol 264[F] Toggle show-help mode [C] Toggle show-name mode [A] Toggle show-all mode 265[Q] Quit (prompts for save) [D] Save minimal config (advanced) 266"""[1:-1].split("\n") 267 268# Lines of help text shown at the bottom of the information dialog 269_INFO_HELP_LINES = """ 270[ESC/q] Return to menu [/] Jump to symbol 271"""[1:-1].split("\n") 272 273# Lines of help text shown at the bottom of the search dialog 274_JUMP_TO_HELP_LINES = """ 275Type text to narrow the search. Regexes are supported (via Python's 're' 276module). The up/down cursor keys step in the list. [Enter] jumps to the 277selected symbol. [ESC] aborts the search. Type multiple space-separated 278strings/regexes to find entries that match all of them. Type Ctrl-F to 279view the help of the selected item without leaving the dialog. 280"""[1:-1].split("\n") 281 282# 283# Styling 284# 285 286_STYLES = { 287 "default": """ 288 path=fg:black,bg:white,bold 289 separator=fg:black,bg:yellow,bold 290 list=fg:black,bg:white 291 selection=fg:white,bg:blue,bold 292 inv-list=fg:red,bg:white 293 inv-selection=fg:red,bg:blue 294 help=path 295 show-help=list 296 frame=fg:black,bg:yellow,bold 297 body=fg:white,bg:black 298 edit=fg:white,bg:blue 299 jump-edit=edit 300 text=list 301 """, 302 303 # This style is forced on terminals that do no support colors 304 "monochrome": """ 305 path=bold 306 separator=bold,standout 307 list= 308 selection=bold,standout 309 inv-list=bold 310 inv-selection=bold,standout 311 help=bold 312 show-help= 313 frame=bold,standout 314 body= 315 edit=standout 316 jump-edit= 317 text= 318 """, 319 320 # Blue-tinted style loosely resembling lxdialog 321 "aquatic": """ 322 path=fg:white,bg:blue 323 separator=fg:white,bg:cyan 324 help=path 325 frame=fg:white,bg:cyan 326 body=fg:white,bg:blue 327 edit=fg:black,bg:white 328 """ 329} 330 331_NAMED_COLORS = { 332 # Basic colors 333 "black": curses.COLOR_BLACK, 334 "red": curses.COLOR_RED, 335 "green": curses.COLOR_GREEN, 336 "yellow": curses.COLOR_YELLOW, 337 "blue": curses.COLOR_BLUE, 338 "magenta": curses.COLOR_MAGENTA, 339 "cyan": curses.COLOR_CYAN, 340 "white": curses.COLOR_WHITE, 341 342 # Bright versions 343 "brightblack": curses.COLOR_BLACK + 8, 344 "brightred": curses.COLOR_RED + 8, 345 "brightgreen": curses.COLOR_GREEN + 8, 346 "brightyellow": curses.COLOR_YELLOW + 8, 347 "brightblue": curses.COLOR_BLUE + 8, 348 "brightmagenta": curses.COLOR_MAGENTA + 8, 349 "brightcyan": curses.COLOR_CYAN + 8, 350 "brightwhite": curses.COLOR_WHITE + 8, 351 352 # Aliases 353 "purple": curses.COLOR_MAGENTA, 354 "brightpurple": curses.COLOR_MAGENTA + 8, 355} 356 357 358def _rgb_to_6cube(rgb): 359 # Converts an 888 RGB color to a 3-tuple (nice in that it's hashable) 360 # representing the closest xterm 256-color 6x6x6 color cube color. 361 # 362 # The xterm 256-color extension uses a RGB color palette with components in 363 # the range 0-5 (a 6x6x6 cube). The catch is that the mapping is nonlinear. 364 # Index 0 in the 6x6x6 cube is mapped to 0, index 1 to 95, then 135, 175, 365 # etc., in increments of 40. See the links below: 366 # 367 # https://commons.wikimedia.org/wiki/File:Xterm_256color_chart.svg 368 # https://github.com/tmux/tmux/blob/master/colour.c 369 370 # 48 is the middle ground between 0 and 95. 371 return tuple(0 if x < 48 else int(round(max(1, (x - 55)/40))) for x in rgb) 372 373 374def _6cube_to_rgb(r6g6b6): 375 # Returns the 888 RGB color for a 666 xterm color cube index 376 377 return tuple(0 if x == 0 else 40*x + 55 for x in r6g6b6) 378 379 380def _rgb_to_gray(rgb): 381 # Converts an 888 RGB color to the index of an xterm 256-color grayscale 382 # color with approx. the same perceived brightness 383 384 # Calculate the luminance (gray intensity) of the color. See 385 # https://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color 386 # and 387 # https://www.w3.org/TR/AERT/#color-contrast 388 luma = 0.299*rgb[0] + 0.587*rgb[1] + 0.114*rgb[2] 389 390 # Closest index in the grayscale palette, which starts at RGB 0x080808, 391 # with stepping 0x0A0A0A 392 index = int(round((luma - 8)/10)) 393 394 # Clamp the index to 0-23, corresponding to 232-255 395 return max(0, min(index, 23)) 396 397 398def _gray_to_rgb(index): 399 # Convert a grayscale index to its closet single RGB component 400 401 return 3*(10*index + 8,) # Returns a 3-tuple 402 403 404# Obscure Python: We never pass a value for rgb2index, and it keeps pointing to 405# the same dict. This avoids a global. 406def _alloc_rgb(rgb, rgb2index={}): 407 # Initialize a new entry in the xterm palette to the given RGB color, 408 # returning its index. If the color has already been initialized, the index 409 # of the existing entry is returned. 410 # 411 # ncurses is palette-based, so we need to overwrite palette entries to make 412 # new colors. 413 # 414 # The colors from 0 to 15 are user-defined, and there's no way to query 415 # their RGB values, so we better leave them untouched. Also leave any 416 # hypothetical colors above 255 untouched (though we're unlikely to 417 # allocate that many colors anyway). 418 419 if rgb in rgb2index: 420 return rgb2index[rgb] 421 422 # Many terminals allow the user to customize the first 16 colors. Avoid 423 # changing their values. 424 color_index = 16 + len(rgb2index) 425 if color_index >= 256: 426 _warn("Unable to allocate new RGB color ", rgb, ". Too many colors " 427 "allocated.") 428 return 0 429 430 # Map each RGB component from the range 0-255 to the range 0-1000, which is 431 # what curses uses 432 curses.init_color(color_index, *(int(round(1000*x/255)) for x in rgb)) 433 rgb2index[rgb] = color_index 434 435 return color_index 436 437 438def _color_from_num(num): 439 # Returns the index of a color that looks like color 'num' in the xterm 440 # 256-color palette (but that might not be 'num', if we're redefining 441 # colors) 442 443 # - _alloc_rgb() won't touch the first 16 colors or any (hypothetical) 444 # colors above 255, so we can always return them as-is 445 # 446 # - If the terminal doesn't support changing color definitions, or if 447 # curses.COLORS < 256, _alloc_rgb() won't touch any color, and all colors 448 # can be returned as-is 449 if num < 16 or num > 255 or not curses.can_change_color() or \ 450 curses.COLORS < 256: 451 return num 452 453 # _alloc_rgb() might redefine colors, so emulate the xterm 256-color 454 # palette by allocating new colors instead of returning color numbers 455 # directly 456 457 if num < 232: 458 num -= 16 459 return _alloc_rgb(_6cube_to_rgb(((num//36)%6, (num//6)%6, num%6))) 460 461 return _alloc_rgb(_gray_to_rgb(num - 232)) 462 463 464def _color_from_rgb(rgb): 465 # Returns the index of a color matching the 888 RGB color 'rgb'. The 466 # returned color might be an ~exact match or an approximation, depending on 467 # terminal capabilities. 468 469 # Calculates the Euclidean distance between two RGB colors 470 def dist(r1, r2): return sum((x - y)**2 for x, y in zip(r1, r2)) 471 472 if curses.COLORS >= 256: 473 # Assume we're dealing with xterm's 256-color extension 474 475 if curses.can_change_color(): 476 # Best case -- the terminal supports changing palette entries via 477 # curses.init_color(). Initialize an unused palette entry and 478 # return it. 479 return _alloc_rgb(rgb) 480 481 # Second best case -- pick between the xterm 256-color extension colors 482 483 # Closest 6-cube "color" color 484 c6 = _rgb_to_6cube(rgb) 485 # Closest gray color 486 gray = _rgb_to_gray(rgb) 487 488 if dist(rgb, _6cube_to_rgb(c6)) < dist(rgb, _gray_to_rgb(gray)): 489 # Use the "color" color from the 6x6x6 color palette. Calculate the 490 # color number from the 6-cube index triplet. 491 return 16 + 36*c6[0] + 6*c6[1] + c6[2] 492 493 # Use the color from the gray palette 494 return 232 + gray 495 496 # Terminal not in xterm 256-color mode. This is probably the best we can 497 # do, or is it? Submit patches. :) 498 min_dist = float('inf') 499 best = -1 500 for color in range(curses.COLORS): 501 # ncurses uses the range 0..1000. Scale that down to 0..255. 502 d = dist(rgb, tuple(int(round(255*c/1000)) 503 for c in curses.color_content(color))) 504 if d < min_dist: 505 min_dist = d 506 best = color 507 508 return best 509 510 511def _parse_style(style_str, parsing_default): 512 # Parses a string with '<element>=<style>' assignments. Anything not 513 # containing '=' is assumed to be a reference to a built-in style, which is 514 # treated as if all the assignments from the style were inserted at that 515 # point in the string. 516 # 517 # The parsing_default flag is set to True when we're implicitly parsing the 518 # 'default'/'monochrome' style, to prevent warnings. 519 520 for sline in style_str.split(): 521 # Words without a "=" character represents a style template 522 if "=" in sline: 523 key, data = sline.split("=", 1) 524 525 # The 'default' style template is assumed to define all keys. We 526 # run _style_to_curses() for non-existing keys as well, so that we 527 # print warnings for errors to the right of '=' for those too. 528 if key not in _style and not parsing_default: 529 _warn("Ignoring non-existent style", key) 530 531 # If data is a reference to another key, copy its style 532 if data in _style: 533 _style[key] = _style[data] 534 else: 535 _style[key] = _style_to_curses(data) 536 537 elif sline in _STYLES: 538 # Recursively parse style template. Ignore styles that don't exist, 539 # for backwards/forwards compatibility. 540 _parse_style(_STYLES[sline], parsing_default) 541 542 else: 543 _warn("Ignoring non-existent style template", sline) 544 545# Dictionary mapping element types to the curses attributes used to display 546# them 547_style = {} 548 549 550def _style_to_curses(style_def): 551 # Parses a style definition string (<element>=<style>), returning 552 # a (fg_color, bg_color, attributes) tuple. 553 554 def parse_color(color_def): 555 color_def = color_def.split(":", 1)[1] 556 557 # HTML format, #RRGGBB 558 if re.match("#[A-Fa-f0-9]{6}", color_def): 559 return _color_from_rgb(( 560 int(color_def[1:3], 16), 561 int(color_def[3:5], 16), 562 int(color_def[5:7], 16))) 563 564 if color_def in _NAMED_COLORS: 565 color_num = _color_from_num(_NAMED_COLORS[color_def]) 566 else: 567 try: 568 color_num = _color_from_num(int(color_def, 0)) 569 except ValueError: 570 _warn("Ignoring color", color_def, "that's neither " 571 "predefined nor a number") 572 return -1 573 574 if not -1 <= color_num < curses.COLORS: 575 _warn("Ignoring color {}, which is outside the range " 576 "-1..curses.COLORS-1 (-1..{})" 577 .format(color_def, curses.COLORS - 1)) 578 return -1 579 580 return color_num 581 582 fg_color = -1 583 bg_color = -1 584 attrs = 0 585 586 if style_def: 587 for field in style_def.split(","): 588 if field.startswith("fg:"): 589 fg_color = parse_color(field) 590 elif field.startswith("bg:"): 591 bg_color = parse_color(field) 592 elif field == "bold": 593 # A_BOLD tends to produce faint and hard-to-read text on the 594 # Windows console, especially with the old color scheme, before 595 # the introduction of 596 # https://blogs.msdn.microsoft.com/commandline/2017/08/02/updating-the-windows-console-colors/ 597 attrs |= curses.A_NORMAL if _IS_WINDOWS else curses.A_BOLD 598 elif field == "standout": 599 attrs |= curses.A_STANDOUT 600 elif field == "underline": 601 attrs |= curses.A_UNDERLINE 602 else: 603 _warn("Ignoring unknown style attribute", field) 604 605 return _style_attr(fg_color, bg_color, attrs) 606 607 608def _init_styles(): 609 if curses.has_colors(): 610 try: 611 curses.use_default_colors() 612 except curses.error: 613 # Ignore errors on funky terminals that support colors but not 614 # using default colors. Worst it can do is break transparency and 615 # the like. Ran across this with the MSYS2/winpty setup in 616 # https://github.com/msys2/MINGW-packages/issues/5823, though there 617 # seems to be a lot of general brokenness there. 618 pass 619 620 # Use the 'default' theme as the base, and add any user-defined style 621 # settings from the environment 622 _parse_style("default", True) 623 if "MENUCONFIG_STYLE" in os.environ: 624 _parse_style(os.environ["MENUCONFIG_STYLE"], False) 625 else: 626 # Force the 'monochrome' theme if the terminal doesn't support colors. 627 # MENUCONFIG_STYLE is likely to mess things up here (though any colors 628 # would be ignored), so ignore it. 629 _parse_style("monochrome", True) 630 631 632# color_attribs holds the color pairs we've already created, indexed by a 633# (<foreground color>, <background color>) tuple. 634# 635# Obscure Python: We never pass a value for color_attribs, and it keeps 636# pointing to the same dict. This avoids a global. 637def _style_attr(fg_color, bg_color, attribs, color_attribs={}): 638 # Returns an attribute with the specified foreground and background color 639 # and the attributes in 'attribs'. Reuses color pairs already created if 640 # possible, and creates a new color pair otherwise. 641 # 642 # Returns 'attribs' if colors aren't supported. 643 644 if not curses.has_colors(): 645 return attribs 646 647 if (fg_color, bg_color) not in color_attribs: 648 # Create new color pair. Color pair number 0 is hardcoded and cannot be 649 # changed, hence the +1s. 650 curses.init_pair(len(color_attribs) + 1, fg_color, bg_color) 651 color_attribs[(fg_color, bg_color)] = \ 652 curses.color_pair(len(color_attribs) + 1) 653 654 return color_attribs[(fg_color, bg_color)] | attribs 655 656 657# 658# Main application 659# 660 661 662def _main(): 663 menuconfig(standard_kconfig(__doc__)) 664 665 666def menuconfig(kconf): 667 """ 668 Launches the configuration interface, returning after the user exits. 669 670 kconf: 671 Kconfig instance to be configured 672 """ 673 global _kconf 674 global _conf_filename 675 global _conf_changed 676 global _minconf_filename 677 global _show_all 678 679 _kconf = kconf 680 681 # Filename to save configuration to 682 _conf_filename = standard_config_filename() 683 684 # Load existing configuration and set _conf_changed True if it is outdated 685 _conf_changed = _load_config() 686 687 # Filename to save minimal configuration to 688 _minconf_filename = "defconfig" 689 690 # Any visible items in the top menu? 691 _show_all = False 692 if not _shown_nodes(kconf.top_node): 693 # Nothing visible. Start in show-all mode and try again. 694 _show_all = True 695 if not _shown_nodes(kconf.top_node): 696 # Give up. The implementation relies on always having a selected 697 # node. 698 print("Empty configuration -- nothing to configure.\n" 699 "Check that environment variables are set properly.") 700 return 701 702 # Disable warnings. They get mangled in curses mode, and we deal with 703 # errors ourselves. 704 kconf.warn = False 705 706 # Make curses use the locale settings specified in the environment 707 locale.setlocale(locale.LC_ALL, "") 708 709 # Try to fix Unicode issues on systems with bad defaults 710 if _CHANGE_C_LC_CTYPE_TO_UTF8: 711 _change_c_lc_ctype_to_utf8() 712 713 # Get rid of the delay between pressing ESC and jumping to the parent menu, 714 # unless the user has set ESCDELAY (see ncurses(3)). This makes the UI much 715 # smoother to work with. 716 # 717 # Note: This is strictly pretty iffy, since escape codes for e.g. cursor 718 # keys start with ESC, but I've never seen it cause problems in practice 719 # (probably because it's unlikely that the escape code for a key would get 720 # split up across read()s, at least with a terminal emulator). Please 721 # report if you run into issues. Some suitable small default value could be 722 # used here instead in that case. Maybe it's silly to not put in the 723 # smallest imperceptible delay here already, though I don't like guessing. 724 # 725 # (From a quick glance at the ncurses source code, ESCDELAY might only be 726 # relevant for mouse events there, so maybe escapes are assumed to arrive 727 # in one piece already...) 728 os.environ.setdefault("ESCDELAY", "0") 729 730 # Enter curses mode. _menuconfig() returns a string to print on exit, after 731 # curses has been de-initialized. 732 print(curses.wrapper(_menuconfig)) 733 734 735def _load_config(): 736 # Loads any existing .config file. See the Kconfig.load_config() docstring. 737 # 738 # Returns True if .config is missing or outdated. We always prompt for 739 # saving the configuration in that case. 740 741 print(_kconf.load_config()) 742 if not os.path.exists(_conf_filename): 743 # No .config 744 return True 745 746 return _needs_save() 747 748 749def _needs_save(): 750 # Returns True if a just-loaded .config file is outdated (would get 751 # modified when saving) 752 753 if _kconf.missing_syms: 754 # Assignments to undefined symbols in the .config 755 return True 756 757 for sym in _kconf.unique_defined_syms: 758 if sym.user_value is None: 759 if sym.config_string: 760 # Unwritten symbol 761 return True 762 elif sym.orig_type in (BOOL, TRISTATE): 763 if sym.tri_value != sym.user_value: 764 # Written bool/tristate symbol, new value 765 return True 766 elif sym.str_value != sym.user_value: 767 # Written string/int/hex symbol, new value 768 return True 769 770 # No need to prompt for save 771 return False 772 773 774# Global variables used below: 775# 776# _stdscr: 777# stdscr from curses 778# 779# _cur_menu: 780# Menu node of the menu (or menuconfig symbol, or choice) currently being 781# shown 782# 783# _shown: 784# List of items in _cur_menu that are shown (ignoring scrolling). In 785# show-all mode, this list contains all items in _cur_menu. Otherwise, it 786# contains just the visible items. 787# 788# _sel_node_i: 789# Index in _shown of the currently selected node 790# 791# _menu_scroll: 792# Index in _shown of the top row of the main display 793# 794# _parent_screen_rows: 795# List/stack of the row numbers that the selections in the parent menus 796# appeared on. This is used to prevent the scrolling from jumping around 797# when going in and out of menus. 798# 799# _show_help/_show_name/_show_all: 800# If True, the corresponding mode is on. See the module docstring. 801# 802# _conf_filename: 803# File to save the configuration to 804# 805# _minconf_filename: 806# File to save minimal configurations to 807# 808# _conf_changed: 809# True if the configuration has been changed. If False, we don't bother 810# showing the save-and-quit dialog. 811# 812# We reset this to False whenever the configuration is saved explicitly 813# from the save dialog. 814 815 816def _menuconfig(stdscr): 817 # Logic for the main display, with the list of symbols, etc. 818 819 global _stdscr 820 global _conf_filename 821 global _conf_changed 822 global _minconf_filename 823 global _show_help 824 global _show_name 825 826 _stdscr = stdscr 827 828 _init() 829 830 while True: 831 _draw_main() 832 curses.doupdate() 833 834 835 c = _getch_compat(_menu_win) 836 837 if c == curses.KEY_RESIZE: 838 _resize_main() 839 840 elif c in (curses.KEY_DOWN, "j", "J"): 841 _select_next_menu_entry() 842 843 elif c in (curses.KEY_UP, "k", "K"): 844 _select_prev_menu_entry() 845 846 elif c in (curses.KEY_NPAGE, "\x04"): # Page Down/Ctrl-D 847 # Keep it simple. This way we get sane behavior for small windows, 848 # etc., for free. 849 for _ in range(_PG_JUMP): 850 _select_next_menu_entry() 851 852 elif c in (curses.KEY_PPAGE, "\x15"): # Page Up/Ctrl-U 853 for _ in range(_PG_JUMP): 854 _select_prev_menu_entry() 855 856 elif c in (curses.KEY_END, "G"): 857 _select_last_menu_entry() 858 859 elif c in (curses.KEY_HOME, "g"): 860 _select_first_menu_entry() 861 862 elif c == " ": 863 # Toggle the node if possible 864 sel_node = _shown[_sel_node_i] 865 if not _change_node(sel_node): 866 _enter_menu(sel_node) 867 868 elif c in (curses.KEY_RIGHT, "\n", "l", "L"): 869 # Enter the node if possible 870 sel_node = _shown[_sel_node_i] 871 if not _enter_menu(sel_node): 872 _change_node(sel_node) 873 874 elif c in ("n", "N"): 875 _set_sel_node_tri_val(0) 876 877 elif c in ("m", "M"): 878 _set_sel_node_tri_val(1) 879 880 elif c in ("y", "Y"): 881 _set_sel_node_tri_val(2) 882 883 elif c in (curses.KEY_LEFT, curses.KEY_BACKSPACE, _ERASE_CHAR, 884 "\x1B", "h", "H"): # \x1B = ESC 885 886 if c == "\x1B" and _cur_menu is _kconf.top_node: 887 res = _quit_dialog() 888 if res: 889 return res 890 else: 891 _leave_menu() 892 893 elif c in ("o", "O"): 894 _load_dialog() 895 896 elif c in ("s", "S"): 897 filename = _save_dialog(_kconf.write_config, _conf_filename, 898 "configuration") 899 if filename: 900 _conf_filename = filename 901 _conf_changed = False 902 903 elif c in ("d", "D"): 904 filename = _save_dialog(_kconf.write_min_config, _minconf_filename, 905 "minimal configuration") 906 if filename: 907 _minconf_filename = filename 908 909 elif c == "/": 910 _jump_to_dialog() 911 # The terminal might have been resized while the fullscreen jump-to 912 # dialog was open 913 _resize_main() 914 915 elif c == "?": 916 _info_dialog(_shown[_sel_node_i], False) 917 # The terminal might have been resized while the fullscreen info 918 # dialog was open 919 _resize_main() 920 921 elif c in ("f", "F"): 922 _show_help = not _show_help 923 _set_style(_help_win, "show-help" if _show_help else "help") 924 _resize_main() 925 926 elif c in ("c", "C"): 927 _show_name = not _show_name 928 929 elif c in ("a", "A"): 930 _toggle_show_all() 931 932 elif c in ("q", "Q"): 933 res = _quit_dialog() 934 if res: 935 return res 936 937 938def _quit_dialog(): 939 if not _conf_changed: 940 return "No changes to save (for '{}')".format(_conf_filename) 941 942 while True: 943 c = _key_dialog( 944 "Quit", 945 " Save configuration?\n" 946 "\n" 947 "(Y)es (N)o (C)ancel", 948 "ync") 949 950 if c is None or c == "c": 951 return None 952 953 if c == "y": 954 # Returns a message to print 955 msg = _try_save(_kconf.write_config, _conf_filename, "configuration") 956 if msg: 957 return msg 958 959 elif c == "n": 960 return "Configuration ({}) was not saved".format(_conf_filename) 961 962 963def _init(): 964 # Initializes the main display with the list of symbols, etc. Also does 965 # misc. global initialization that needs to happen after initializing 966 # curses. 967 968 global _ERASE_CHAR 969 970 global _path_win 971 global _top_sep_win 972 global _menu_win 973 global _bot_sep_win 974 global _help_win 975 976 global _parent_screen_rows 977 global _cur_menu 978 global _shown 979 global _sel_node_i 980 global _menu_scroll 981 982 global _show_help 983 global _show_name 984 985 # Looking for this in addition to KEY_BACKSPACE (which is unreliable) makes 986 # backspace work with TERM=vt100. That makes it likely to work in sane 987 # environments. 988 _ERASE_CHAR = curses.erasechar() 989 if sys.version_info[0] >= 3: 990 # erasechar() returns a one-byte bytes object on Python 3. This sets 991 # _ERASE_CHAR to a blank string if it can't be decoded, which should be 992 # harmless. 993 _ERASE_CHAR = _ERASE_CHAR.decode("utf-8", "ignore") 994 995 _init_styles() 996 997 # Hide the cursor 998 _safe_curs_set(0) 999 1000 # Initialize windows 1001 1002 # Top row, with menu path 1003 _path_win = _styled_win("path") 1004 1005 # Separator below menu path, with title and arrows pointing up 1006 _top_sep_win = _styled_win("separator") 1007 1008 # List of menu entries with symbols, etc. 1009 _menu_win = _styled_win("list") 1010 _menu_win.keypad(True) 1011 1012 # Row below menu list, with arrows pointing down 1013 _bot_sep_win = _styled_win("separator") 1014 1015 # Help window with keys at the bottom. Shows help texts in show-help mode. 1016 _help_win = _styled_win("help") 1017 1018 # The rows we'd like the nodes in the parent menus to appear on. This 1019 # prevents the scroll from jumping around when going in and out of menus. 1020 _parent_screen_rows = [] 1021 1022 # Initial state 1023 1024 _cur_menu = _kconf.top_node 1025 _shown = _shown_nodes(_cur_menu) 1026 _sel_node_i = _menu_scroll = 0 1027 1028 _show_help = _show_name = False 1029 1030 # Give windows their initial size 1031 _resize_main() 1032 1033 1034def _resize_main(): 1035 # Resizes the main display, with the list of symbols, etc., to fill the 1036 # terminal 1037 1038 global _menu_scroll 1039 1040 screen_height, screen_width = _stdscr.getmaxyx() 1041 1042 _path_win.resize(1, screen_width) 1043 _top_sep_win.resize(1, screen_width) 1044 _bot_sep_win.resize(1, screen_width) 1045 1046 help_win_height = _SHOW_HELP_HEIGHT if _show_help else \ 1047 len(_MAIN_HELP_LINES) 1048 1049 menu_win_height = screen_height - help_win_height - 3 1050 1051 if menu_win_height >= 1: 1052 _menu_win.resize(menu_win_height, screen_width) 1053 _help_win.resize(help_win_height, screen_width) 1054 1055 _top_sep_win.mvwin(1, 0) 1056 _menu_win.mvwin(2, 0) 1057 _bot_sep_win.mvwin(2 + menu_win_height, 0) 1058 _help_win.mvwin(2 + menu_win_height + 1, 0) 1059 else: 1060 # Degenerate case. Give up on nice rendering and just prevent errors. 1061 1062 menu_win_height = 1 1063 1064 _menu_win.resize(1, screen_width) 1065 _help_win.resize(1, screen_width) 1066 1067 for win in _top_sep_win, _menu_win, _bot_sep_win, _help_win: 1068 win.mvwin(0, 0) 1069 1070 # Adjust the scroll so that the selected node is still within the window, 1071 # if needed 1072 if _sel_node_i - _menu_scroll >= menu_win_height: 1073 _menu_scroll = _sel_node_i - menu_win_height + 1 1074 1075 1076def _height(win): 1077 # Returns the height of 'win' 1078 1079 return win.getmaxyx()[0] 1080 1081 1082def _width(win): 1083 # Returns the width of 'win' 1084 1085 return win.getmaxyx()[1] 1086 1087 1088def _enter_menu(menu): 1089 # Makes 'menu' the currently displayed menu. In addition to actual 'menu's, 1090 # "menu" here includes choices and symbols defined with the 'menuconfig' 1091 # keyword. 1092 # 1093 # Returns False if 'menu' can't be entered. 1094 1095 global _cur_menu 1096 global _shown 1097 global _sel_node_i 1098 global _menu_scroll 1099 1100 if not menu.is_menuconfig: 1101 return False # Not a menu 1102 1103 shown_sub = _shown_nodes(menu) 1104 # Never enter empty menus. We depend on having a current node. 1105 if not shown_sub: 1106 return False 1107 1108 # Remember where the current node appears on the screen, so we can try 1109 # to get it to appear in the same place when we leave the menu 1110 _parent_screen_rows.append(_sel_node_i - _menu_scroll) 1111 1112 # Jump into menu 1113 _cur_menu = menu 1114 _shown = shown_sub 1115 _sel_node_i = _menu_scroll = 0 1116 1117 if isinstance(menu.item, Choice): 1118 _select_selected_choice_sym() 1119 1120 return True 1121 1122 1123def _select_selected_choice_sym(): 1124 # Puts the cursor on the currently selected (y-valued) choice symbol, if 1125 # any. Does nothing if if the choice has no selection (is not visible/in y 1126 # mode). 1127 1128 global _sel_node_i 1129 1130 choice = _cur_menu.item 1131 if choice.selection: 1132 # Search through all menu nodes to handle choice symbols being defined 1133 # in multiple locations 1134 for node in choice.selection.nodes: 1135 if node in _shown: 1136 _sel_node_i = _shown.index(node) 1137 _center_vertically() 1138 return 1139 1140 1141def _jump_to(node): 1142 # Jumps directly to the menu node 'node' 1143 1144 global _cur_menu 1145 global _shown 1146 global _sel_node_i 1147 global _menu_scroll 1148 global _show_all 1149 global _parent_screen_rows 1150 1151 # Clear remembered menu locations. We might not even have been in the 1152 # parent menus before. 1153 _parent_screen_rows = [] 1154 1155 old_show_all = _show_all 1156 jump_into = (isinstance(node.item, Choice) or node.item == MENU) and \ 1157 node.list 1158 1159 # If we're jumping to a non-empty choice or menu, jump to the first entry 1160 # in it instead of jumping to its menu node 1161 if jump_into: 1162 _cur_menu = node 1163 node = node.list 1164 else: 1165 _cur_menu = _parent_menu(node) 1166 1167 _shown = _shown_nodes(_cur_menu) 1168 if node not in _shown: 1169 # The node wouldn't be shown. Turn on show-all to show it. 1170 _show_all = True 1171 _shown = _shown_nodes(_cur_menu) 1172 1173 _sel_node_i = _shown.index(node) 1174 1175 if jump_into and not old_show_all and _show_all: 1176 # If we're jumping into a choice or menu and were forced to turn on 1177 # show-all because the first entry wasn't visible, try turning it off. 1178 # That will land us at the first visible node if there are visible 1179 # nodes, and is a no-op otherwise. 1180 _toggle_show_all() 1181 1182 _center_vertically() 1183 1184 # If we're jumping to a non-empty choice, jump to the selected symbol, if 1185 # any 1186 if jump_into and isinstance(_cur_menu.item, Choice): 1187 _select_selected_choice_sym() 1188 1189 1190def _leave_menu(): 1191 # Jumps to the parent menu of the current menu. Does nothing if we're in 1192 # the top menu. 1193 1194 global _cur_menu 1195 global _shown 1196 global _sel_node_i 1197 global _menu_scroll 1198 1199 if _cur_menu is _kconf.top_node: 1200 return 1201 1202 # Jump to parent menu 1203 parent = _parent_menu(_cur_menu) 1204 _shown = _shown_nodes(parent) 1205 _sel_node_i = _shown.index(_cur_menu) 1206 _cur_menu = parent 1207 1208 # Try to make the menu entry appear on the same row on the screen as it did 1209 # before we entered the menu. 1210 1211 if _parent_screen_rows: 1212 # The terminal might have shrunk since we were last in the parent menu 1213 screen_row = min(_parent_screen_rows.pop(), _height(_menu_win) - 1) 1214 _menu_scroll = max(_sel_node_i - screen_row, 0) 1215 else: 1216 # No saved parent menu locations, meaning we jumped directly to some 1217 # node earlier 1218 _center_vertically() 1219 1220 1221def _select_next_menu_entry(): 1222 # Selects the menu entry after the current one, adjusting the scroll if 1223 # necessary. Does nothing if we're already at the last menu entry. 1224 1225 global _sel_node_i 1226 global _menu_scroll 1227 1228 if _sel_node_i < len(_shown) - 1: 1229 # Jump to the next node 1230 _sel_node_i += 1 1231 1232 # If the new node is sufficiently close to the edge of the menu window 1233 # (as determined by _SCROLL_OFFSET), increase the scroll by one. This 1234 # gives nice and non-jumpy behavior even when 1235 # _SCROLL_OFFSET >= _height(_menu_win). 1236 if _sel_node_i >= _menu_scroll + _height(_menu_win) - _SCROLL_OFFSET \ 1237 and _menu_scroll < _max_scroll(_shown, _menu_win): 1238 1239 _menu_scroll += 1 1240 1241 1242def _select_prev_menu_entry(): 1243 # Selects the menu entry before the current one, adjusting the scroll if 1244 # necessary. Does nothing if we're already at the first menu entry. 1245 1246 global _sel_node_i 1247 global _menu_scroll 1248 1249 if _sel_node_i > 0: 1250 # Jump to the previous node 1251 _sel_node_i -= 1 1252 1253 # See _select_next_menu_entry() 1254 if _sel_node_i < _menu_scroll + _SCROLL_OFFSET: 1255 _menu_scroll = max(_menu_scroll - 1, 0) 1256 1257 1258def _select_last_menu_entry(): 1259 # Selects the last menu entry in the current menu 1260 1261 global _sel_node_i 1262 global _menu_scroll 1263 1264 _sel_node_i = len(_shown) - 1 1265 _menu_scroll = _max_scroll(_shown, _menu_win) 1266 1267 1268def _select_first_menu_entry(): 1269 # Selects the first menu entry in the current menu 1270 1271 global _sel_node_i 1272 global _menu_scroll 1273 1274 _sel_node_i = _menu_scroll = 0 1275 1276 1277def _toggle_show_all(): 1278 # Toggles show-all mode on/off. If turning it off would give no visible 1279 # items in the current menu, it is left on. 1280 1281 global _show_all 1282 global _shown 1283 global _sel_node_i 1284 global _menu_scroll 1285 1286 # Row on the screen the cursor is on. Preferably we want the same row to 1287 # stay highlighted. 1288 old_row = _sel_node_i - _menu_scroll 1289 1290 _show_all = not _show_all 1291 # List of new nodes to be shown after toggling _show_all 1292 new_shown = _shown_nodes(_cur_menu) 1293 1294 # Find a good node to select. The selected node might disappear if show-all 1295 # mode is turned off. 1296 1297 # Select the previously selected node itself if it is still visible. If 1298 # there are visible nodes before it, select the closest one. 1299 for node in _shown[_sel_node_i::-1]: 1300 if node in new_shown: 1301 _sel_node_i = new_shown.index(node) 1302 break 1303 else: 1304 # No visible nodes before the previously selected node. Select the 1305 # closest visible node after it instead. 1306 for node in _shown[_sel_node_i + 1:]: 1307 if node in new_shown: 1308 _sel_node_i = new_shown.index(node) 1309 break 1310 else: 1311 # No visible nodes at all, meaning show-all was turned off inside 1312 # an invisible menu. Don't allow that, as the implementation relies 1313 # on always having a selected node. 1314 _show_all = True 1315 return 1316 1317 _shown = new_shown 1318 1319 # Try to make the cursor stay on the same row in the menu window. This 1320 # might be impossible if too many nodes have disappeared above the node. 1321 _menu_scroll = max(_sel_node_i - old_row, 0) 1322 1323 1324def _center_vertically(): 1325 # Centers the selected node vertically, if possible 1326 1327 global _menu_scroll 1328 1329 _menu_scroll = min(max(_sel_node_i - _height(_menu_win)//2, 0), 1330 _max_scroll(_shown, _menu_win)) 1331 1332 1333def _draw_main(): 1334 # Draws the "main" display, with the list of symbols, the header, and the 1335 # footer. 1336 # 1337 # This could be optimized to only update the windows that have actually 1338 # changed, but keep it simple for now and let curses sort it out. 1339 1340 term_width = _width(_stdscr) 1341 1342 # 1343 # Update the separator row below the menu path 1344 # 1345 1346 _top_sep_win.erase() 1347 1348 # Draw arrows pointing up if the symbol window is scrolled down. Draw them 1349 # before drawing the title, so the title ends up on top for small windows. 1350 if _menu_scroll > 0: 1351 _safe_hline(_top_sep_win, 0, 4, curses.ACS_UARROW, _N_SCROLL_ARROWS) 1352 1353 # Add the 'mainmenu' text as the title, centered at the top 1354 _safe_addstr(_top_sep_win, 1355 0, max((term_width - len(_kconf.mainmenu_text))//2, 0), 1356 _kconf.mainmenu_text) 1357 1358 _top_sep_win.noutrefresh() 1359 1360 # Note: The menu path at the top is deliberately updated last. See below. 1361 1362 # 1363 # Update the symbol window 1364 # 1365 1366 _menu_win.erase() 1367 1368 # Draw the _shown nodes starting from index _menu_scroll up to either as 1369 # many as fit in the window, or to the end of _shown 1370 for i in range(_menu_scroll, 1371 min(_menu_scroll + _height(_menu_win), len(_shown))): 1372 1373 node = _shown[i] 1374 1375 # The 'not _show_all' test avoids showing invisible items in red 1376 # outside show-all mode, which could look confusing/broken. Invisible 1377 # symbols show up outside show-all mode if an invisible symbol has 1378 # visible children in an implicit (indented) menu. 1379 if _visible(node) or not _show_all: 1380 style = _style["selection" if i == _sel_node_i else "list"] 1381 else: 1382 style = _style["inv-selection" if i == _sel_node_i else "inv-list"] 1383 1384 _safe_addstr(_menu_win, i - _menu_scroll, 0, _node_str(node), style) 1385 1386 _menu_win.noutrefresh() 1387 1388 # 1389 # Update the bottom separator window 1390 # 1391 1392 _bot_sep_win.erase() 1393 1394 # Draw arrows pointing down if the symbol window is scrolled up 1395 if _menu_scroll < _max_scroll(_shown, _menu_win): 1396 _safe_hline(_bot_sep_win, 0, 4, curses.ACS_DARROW, _N_SCROLL_ARROWS) 1397 1398 # Indicate when show-name/show-help/show-all mode is enabled 1399 enabled_modes = [] 1400 if _show_help: 1401 enabled_modes.append("show-help (toggle with [F])") 1402 if _show_name: 1403 enabled_modes.append("show-name") 1404 if _show_all: 1405 enabled_modes.append("show-all") 1406 if enabled_modes: 1407 s = " and ".join(enabled_modes) + " mode enabled" 1408 _safe_addstr(_bot_sep_win, 0, max(term_width - len(s) - 2, 0), s) 1409 1410 _bot_sep_win.noutrefresh() 1411 1412 # 1413 # Update the help window, which shows either key bindings or help texts 1414 # 1415 1416 _help_win.erase() 1417 1418 if _show_help: 1419 node = _shown[_sel_node_i] 1420 if isinstance(node.item, (Symbol, Choice)) and node.help: 1421 help_lines = textwrap.wrap(node.help, _width(_help_win)) 1422 for i in range(min(_height(_help_win), len(help_lines))): 1423 _safe_addstr(_help_win, i, 0, help_lines[i]) 1424 else: 1425 _safe_addstr(_help_win, 0, 0, "(no help)") 1426 else: 1427 for i, line in enumerate(_MAIN_HELP_LINES): 1428 _safe_addstr(_help_win, i, 0, line) 1429 1430 _help_win.noutrefresh() 1431 1432 # 1433 # Update the top row with the menu path. 1434 # 1435 # Doing this last leaves the cursor on the top row, which avoids some minor 1436 # annoying jumpiness in gnome-terminal when reducing the height of the 1437 # terminal. It seems to happen whenever the row with the cursor on it 1438 # disappears. 1439 # 1440 1441 _path_win.erase() 1442 1443 # Draw the menu path ("(Top) -> Menu -> Submenu -> ...") 1444 1445 menu_prompts = [] 1446 1447 menu = _cur_menu 1448 while menu is not _kconf.top_node: 1449 # Promptless choices can be entered in show-all mode. Use 1450 # standard_sc_expr_str() for them, so they show up as 1451 # '<choice (name if any)>'. 1452 menu_prompts.append(menu.prompt[0] if menu.prompt else 1453 standard_sc_expr_str(menu.item)) 1454 menu = menu.parent 1455 menu_prompts.append("(Top)") 1456 menu_prompts.reverse() 1457 1458 # Hack: We can't put ACS_RARROW directly in the string. Temporarily 1459 # represent it with NULL. 1460 menu_path_str = " \0 ".join(menu_prompts) 1461 1462 # Scroll the menu path to the right if needed to make the current menu's 1463 # title visible 1464 if len(menu_path_str) > term_width: 1465 menu_path_str = menu_path_str[len(menu_path_str) - term_width:] 1466 1467 # Print the path with the arrows reinserted 1468 split_path = menu_path_str.split("\0") 1469 _safe_addstr(_path_win, split_path[0]) 1470 for s in split_path[1:]: 1471 _safe_addch(_path_win, curses.ACS_RARROW) 1472 _safe_addstr(_path_win, s) 1473 1474 _path_win.noutrefresh() 1475 1476 1477def _parent_menu(node): 1478 # Returns the menu node of the menu that contains 'node'. In addition to 1479 # proper 'menu's, this might also be a 'menuconfig' symbol or a 'choice'. 1480 # "Menu" here means a menu in the interface. 1481 1482 menu = node.parent 1483 while not menu.is_menuconfig: 1484 menu = menu.parent 1485 return menu 1486 1487 1488def _shown_nodes(menu): 1489 # Returns the list of menu nodes from 'menu' (see _parent_menu()) that 1490 # would be shown when entering it 1491 1492 def rec(node): 1493 res = [] 1494 1495 while node: 1496 if _visible(node) or _show_all: 1497 res.append(node) 1498 if node.list and not node.is_menuconfig: 1499 # Nodes from implicit menu created from dependencies. Will 1500 # be shown indented. Note that is_menuconfig is True for 1501 # menus and choices as well as 'menuconfig' symbols. 1502 res += rec(node.list) 1503 1504 elif node.list and isinstance(node.item, Symbol): 1505 # Show invisible symbols if they have visible children. This 1506 # can happen for an m/y-valued symbol with an optional prompt 1507 # ('prompt "foo" is COND') that is currently disabled. Note 1508 # that it applies to both 'config' and 'menuconfig' symbols. 1509 shown_children = rec(node.list) 1510 if shown_children: 1511 res.append(node) 1512 if not node.is_menuconfig: 1513 res += shown_children 1514 1515 node = node.next 1516 1517 return res 1518 1519 if isinstance(menu.item, Choice): 1520 # For named choices defined in multiple locations, entering the choice 1521 # at a particular menu node would normally only show the choice symbols 1522 # defined there (because that's what the MenuNode tree looks like). 1523 # 1524 # That might look confusing, and makes extending choices by defining 1525 # them in multiple locations less useful. Instead, gather all the child 1526 # menu nodes for all the choices whenever a choice is entered. That 1527 # makes all choice symbols visible at all locations. 1528 # 1529 # Choices can contain non-symbol items (people do all sorts of weird 1530 # stuff with them), hence the generality here. We really need to 1531 # preserve the menu tree at each choice location. 1532 # 1533 # Note: Named choices are pretty broken in the C tools, and this is 1534 # super obscure, so you probably won't find much that relies on this. 1535 # This whole 'if' could be deleted if you don't care about defining 1536 # choices in multiple locations to add symbols (which will still work, 1537 # just with things being displayed in a way that might be unexpected). 1538 1539 # Do some additional work to avoid listing choice symbols twice if all 1540 # or part of the choice is copied in multiple locations (e.g. by 1541 # including some Kconfig file multiple times). We give the prompts at 1542 # the current location precedence. 1543 seen_syms = {node.item for node in rec(menu.list) 1544 if isinstance(node.item, Symbol)} 1545 res = [] 1546 for choice_node in menu.item.nodes: 1547 for node in rec(choice_node.list): 1548 # 'choice_node is menu' checks if we're dealing with the 1549 # current location 1550 if node.item not in seen_syms or choice_node is menu: 1551 res.append(node) 1552 if isinstance(node.item, Symbol): 1553 seen_syms.add(node.item) 1554 return res 1555 1556 return rec(menu.list) 1557 1558 1559def _visible(node): 1560 # Returns True if the node should appear in the menu (outside show-all 1561 # mode) 1562 1563 return node.prompt and expr_value(node.prompt[1]) and not \ 1564 (node.item == MENU and not expr_value(node.visibility)) 1565 1566 1567def _change_node(node): 1568 # Changes the value of the menu node 'node' if it is a symbol. Bools and 1569 # tristates are toggled, while other symbol types pop up a text entry 1570 # dialog. 1571 # 1572 # Returns False if the value of 'node' can't be changed. 1573 1574 if not _changeable(node): 1575 return False 1576 1577 # sc = symbol/choice 1578 sc = node.item 1579 1580 if sc.orig_type in (INT, HEX, STRING): 1581 s = sc.str_value 1582 1583 while True: 1584 s = _input_dialog( 1585 "{} ({})".format(node.prompt[0], TYPE_TO_STR[sc.orig_type]), 1586 s, _range_info(sc)) 1587 1588 if s is None: 1589 break 1590 1591 if sc.orig_type in (INT, HEX): 1592 s = s.strip() 1593 1594 # 'make menuconfig' does this too. Hex values not starting with 1595 # '0x' are accepted when loading .config files though. 1596 if sc.orig_type == HEX and not s.startswith(("0x", "0X")): 1597 s = "0x" + s 1598 1599 if _check_valid(sc, s): 1600 _set_val(sc, s) 1601 break 1602 1603 elif len(sc.assignable) == 1: 1604 # Handles choice symbols for choices in y mode, which are a special 1605 # case: .assignable can be (2,) while .tri_value is 0. 1606 _set_val(sc, sc.assignable[0]) 1607 1608 else: 1609 # Set the symbol to the value after the current value in 1610 # sc.assignable, with wrapping 1611 val_index = sc.assignable.index(sc.tri_value) 1612 _set_val(sc, sc.assignable[(val_index + 1) % len(sc.assignable)]) 1613 1614 1615 if _is_y_mode_choice_sym(sc) and not node.list: 1616 # Immediately jump to the parent menu after making a choice selection, 1617 # like 'make menuconfig' does, except if the menu node has children 1618 # (which can happen if a symbol 'depends on' a choice symbol that 1619 # immediately precedes it). 1620 _leave_menu() 1621 1622 1623 return True 1624 1625 1626def _changeable(node): 1627 # Returns True if the value if 'node' can be changed 1628 1629 sc = node.item 1630 1631 if not isinstance(sc, (Symbol, Choice)): 1632 return False 1633 1634 # This will hit for invisible symbols, which appear in show-all mode and 1635 # when an invisible symbol has visible children (which can happen e.g. for 1636 # symbols with optional prompts) 1637 if not (node.prompt and expr_value(node.prompt[1])): 1638 return False 1639 1640 return sc.orig_type in (STRING, INT, HEX) or len(sc.assignable) > 1 \ 1641 or _is_y_mode_choice_sym(sc) 1642 1643 1644def _set_sel_node_tri_val(tri_val): 1645 # Sets the value of the currently selected menu entry to 'tri_val', if that 1646 # value can be assigned 1647 1648 sc = _shown[_sel_node_i].item 1649 if isinstance(sc, (Symbol, Choice)) and tri_val in sc.assignable: 1650 _set_val(sc, tri_val) 1651 1652 1653def _set_val(sc, val): 1654 # Wrapper around Symbol/Choice.set_value() for updating the menu state and 1655 # _conf_changed 1656 1657 global _conf_changed 1658 1659 # Use the string representation of tristate values. This makes the format 1660 # consistent for all symbol types. 1661 if val in TRI_TO_STR: 1662 val = TRI_TO_STR[val] 1663 1664 if val != sc.str_value: 1665 sc.set_value(val) 1666 _conf_changed = True 1667 1668 # Changing the value of the symbol might have changed what items in the 1669 # current menu are visible. Recalculate the state. 1670 _update_menu() 1671 1672 1673def _update_menu(): 1674 # Updates the current menu after the value of a symbol or choice has been 1675 # changed. Changing a value might change which items in the menu are 1676 # visible. 1677 # 1678 # If possible, preserves the location of the cursor on the screen when 1679 # items are added/removed above the selected item. 1680 1681 global _shown 1682 global _sel_node_i 1683 global _menu_scroll 1684 1685 # Row on the screen the cursor was on 1686 old_row = _sel_node_i - _menu_scroll 1687 1688 sel_node = _shown[_sel_node_i] 1689 1690 # New visible nodes 1691 _shown = _shown_nodes(_cur_menu) 1692 1693 # New index of selected node 1694 _sel_node_i = _shown.index(sel_node) 1695 1696 # Try to make the cursor stay on the same row in the menu window. This 1697 # might be impossible if too many nodes have disappeared above the node. 1698 _menu_scroll = max(_sel_node_i - old_row, 0) 1699 1700 1701def _input_dialog(title, initial_text, info_text=None): 1702 # Pops up a dialog that prompts the user for a string 1703 # 1704 # title: 1705 # Title to display at the top of the dialog window's border 1706 # 1707 # initial_text: 1708 # Initial text to prefill the input field with 1709 # 1710 # info_text: 1711 # String to show next to the input field. If None, just the input field 1712 # is shown. 1713 1714 win = _styled_win("body") 1715 win.keypad(True) 1716 1717 info_lines = info_text.split("\n") if info_text else [] 1718 1719 # Give the input dialog its initial size 1720 _resize_input_dialog(win, title, info_lines) 1721 1722 _safe_curs_set(2) 1723 1724 # Input field text 1725 s = initial_text 1726 1727 # Cursor position 1728 i = len(initial_text) 1729 1730 def edit_width(): 1731 return _width(win) - 4 1732 1733 # Horizontal scroll offset 1734 hscroll = max(i - edit_width() + 1, 0) 1735 1736 while True: 1737 # Draw the "main" display with the menu, etc., so that resizing still 1738 # works properly. This is like a stack of windows, only hardcoded for 1739 # now. 1740 _draw_main() 1741 _draw_input_dialog(win, title, info_lines, s, i, hscroll) 1742 curses.doupdate() 1743 1744 1745 c = _getch_compat(win) 1746 1747 if c == curses.KEY_RESIZE: 1748 # Resize the main display too. The dialog floats above it. 1749 _resize_main() 1750 _resize_input_dialog(win, title, info_lines) 1751 1752 elif c == "\n": 1753 _safe_curs_set(0) 1754 return s 1755 1756 elif c == "\x1B": # \x1B = ESC 1757 _safe_curs_set(0) 1758 return None 1759 1760 else: 1761 s, i, hscroll = _edit_text(c, s, i, hscroll, edit_width()) 1762 1763 1764def _resize_input_dialog(win, title, info_lines): 1765 # Resizes the input dialog to a size appropriate for the terminal size 1766 1767 screen_height, screen_width = _stdscr.getmaxyx() 1768 1769 win_height = 5 1770 if info_lines: 1771 win_height += len(info_lines) + 1 1772 win_height = min(win_height, screen_height) 1773 1774 win_width = max(_INPUT_DIALOG_MIN_WIDTH, 1775 len(title) + 4, 1776 *(len(line) + 4 for line in info_lines)) 1777 win_width = min(win_width, screen_width) 1778 1779 win.resize(win_height, win_width) 1780 win.mvwin((screen_height - win_height)//2, 1781 (screen_width - win_width)//2) 1782 1783 1784def _draw_input_dialog(win, title, info_lines, s, i, hscroll): 1785 edit_width = _width(win) - 4 1786 1787 win.erase() 1788 1789 # Note: Perhaps having a separate window for the input field would be nicer 1790 visible_s = s[hscroll:hscroll + edit_width] 1791 _safe_addstr(win, 2, 2, visible_s + " "*(edit_width - len(visible_s)), 1792 _style["edit"]) 1793 1794 for linenr, line in enumerate(info_lines): 1795 _safe_addstr(win, 4 + linenr, 2, line) 1796 1797 # Draw the frame last so that it overwrites the body text for small windows 1798 _draw_frame(win, title) 1799 1800 _safe_move(win, 2, 2 + i - hscroll) 1801 1802 win.noutrefresh() 1803 1804 1805def _load_dialog(): 1806 # Dialog for loading a new configuration 1807 1808 global _conf_changed 1809 global _conf_filename 1810 global _show_all 1811 1812 if _conf_changed: 1813 c = _key_dialog( 1814 "Load", 1815 "You have unsaved changes. Load new\n" 1816 "configuration anyway?\n" 1817 "\n" 1818 " (O)K (C)ancel", 1819 "oc") 1820 1821 if c is None or c == "c": 1822 return 1823 1824 filename = _conf_filename 1825 while True: 1826 filename = _input_dialog("File to load", filename, _load_save_info()) 1827 if filename is None: 1828 return 1829 1830 filename = os.path.expanduser(filename) 1831 1832 if _try_load(filename): 1833 _conf_filename = filename 1834 _conf_changed = _needs_save() 1835 1836 # Turn on show-all mode if the selected node is not visible after 1837 # loading the new configuration. _shown still holds the old state. 1838 if _shown[_sel_node_i] not in _shown_nodes(_cur_menu): 1839 _show_all = True 1840 1841 _update_menu() 1842 1843 # The message dialog indirectly updates the menu display, so _msg() 1844 # must be called after the new state has been initialized 1845 _msg("Success", "Loaded " + filename) 1846 return 1847 1848 1849def _try_load(filename): 1850 # Tries to load a configuration file. Pops up an error and returns False on 1851 # failure. 1852 # 1853 # filename: 1854 # Configuration file to load 1855 1856 try: 1857 _kconf.load_config(filename) 1858 return True 1859 except EnvironmentError as e: 1860 _error("Error loading '{}'\n\n{} (errno: {})" 1861 .format(filename, e.strerror, errno.errorcode[e.errno])) 1862 return False 1863 1864 1865def _save_dialog(save_fn, default_filename, description): 1866 # Dialog for saving the current configuration 1867 # 1868 # save_fn: 1869 # Function to call with 'filename' to save the file 1870 # 1871 # default_filename: 1872 # Prefilled filename in the input field 1873 # 1874 # description: 1875 # String describing the thing being saved 1876 # 1877 # Return value: 1878 # The path to the saved file, or None if no file was saved 1879 1880 filename = default_filename 1881 while True: 1882 filename = _input_dialog("Filename to save {} to".format(description), 1883 filename, _load_save_info()) 1884 if filename is None: 1885 return None 1886 1887 filename = os.path.expanduser(filename) 1888 1889 msg = _try_save(save_fn, filename, description) 1890 if msg: 1891 _msg("Success", msg) 1892 return filename 1893 1894 1895def _try_save(save_fn, filename, description): 1896 # Tries to save a configuration file. Returns a message to print on 1897 # success. 1898 # 1899 # save_fn: 1900 # Function to call with 'filename' to save the file 1901 # 1902 # description: 1903 # String describing the thing being saved 1904 # 1905 # Return value: 1906 # A message to print on success, and None on failure 1907 1908 try: 1909 # save_fn() returns a message to print 1910 return save_fn(filename) 1911 except EnvironmentError as e: 1912 _error("Error saving {} to '{}'\n\n{} (errno: {})" 1913 .format(description, e.filename, e.strerror, 1914 errno.errorcode[e.errno])) 1915 return None 1916 1917 1918def _key_dialog(title, text, keys): 1919 # Pops up a dialog that can be closed by pressing a key 1920 # 1921 # title: 1922 # Title to display at the top of the dialog window's border 1923 # 1924 # text: 1925 # Text to show in the dialog 1926 # 1927 # keys: 1928 # List of keys that will close the dialog. Other keys (besides ESC) are 1929 # ignored. The caller is responsible for providing a hint about which 1930 # keys can be pressed in 'text'. 1931 # 1932 # Return value: 1933 # The key that was pressed to close the dialog. Uppercase characters are 1934 # converted to lowercase. ESC will always close the dialog, and returns 1935 # None. 1936 1937 win = _styled_win("body") 1938 win.keypad(True) 1939 1940 _resize_key_dialog(win, text) 1941 1942 while True: 1943 # See _input_dialog() 1944 _draw_main() 1945 _draw_key_dialog(win, title, text) 1946 curses.doupdate() 1947 1948 1949 c = _getch_compat(win) 1950 1951 if c == curses.KEY_RESIZE: 1952 # Resize the main display too. The dialog floats above it. 1953 _resize_main() 1954 _resize_key_dialog(win, text) 1955 1956 elif c == "\x1B": # \x1B = ESC 1957 return None 1958 1959 elif isinstance(c, str): 1960 c = c.lower() 1961 if c in keys: 1962 return c 1963 1964 1965def _resize_key_dialog(win, text): 1966 # Resizes the key dialog to a size appropriate for the terminal size 1967 1968 screen_height, screen_width = _stdscr.getmaxyx() 1969 1970 lines = text.split("\n") 1971 1972 win_height = min(len(lines) + 4, screen_height) 1973 win_width = min(max(len(line) for line in lines) + 4, screen_width) 1974 1975 win.resize(win_height, win_width) 1976 win.mvwin((screen_height - win_height)//2, 1977 (screen_width - win_width)//2) 1978 1979 1980def _draw_key_dialog(win, title, text): 1981 win.erase() 1982 1983 for i, line in enumerate(text.split("\n")): 1984 _safe_addstr(win, 2 + i, 2, line) 1985 1986 # Draw the frame last so that it overwrites the body text for small windows 1987 _draw_frame(win, title) 1988 1989 win.noutrefresh() 1990 1991 1992def _draw_frame(win, title): 1993 # Draw a frame around the inner edges of 'win', with 'title' at the top 1994 1995 win_height, win_width = win.getmaxyx() 1996 1997 win.attron(_style["frame"]) 1998 1999 # Draw top/bottom edge 2000 _safe_hline(win, 0, 0, " ", win_width) 2001 _safe_hline(win, win_height - 1, 0, " ", win_width) 2002 2003 # Draw left/right edge 2004 _safe_vline(win, 0, 0, " ", win_height) 2005 _safe_vline(win, 0, win_width - 1, " ", win_height) 2006 2007 # Draw title 2008 _safe_addstr(win, 0, max((win_width - len(title))//2, 0), title) 2009 2010 win.attroff(_style["frame"]) 2011 2012 2013def _jump_to_dialog(): 2014 # Implements the jump-to dialog, where symbols can be looked up via 2015 # incremental search and jumped to. 2016 # 2017 # Returns True if the user jumped to a symbol, and False if the dialog was 2018 # canceled. 2019 2020 s = "" # Search text 2021 prev_s = None # Previous search text 2022 s_i = 0 # Search text cursor position 2023 hscroll = 0 # Horizontal scroll offset 2024 2025 sel_node_i = 0 # Index of selected row 2026 scroll = 0 # Index in 'matches' of the top row of the list 2027 2028 # Edit box at the top 2029 edit_box = _styled_win("jump-edit") 2030 edit_box.keypad(True) 2031 2032 # List of matches 2033 matches_win = _styled_win("list") 2034 2035 # Bottom separator, with arrows pointing down 2036 bot_sep_win = _styled_win("separator") 2037 2038 # Help window with instructions at the bottom 2039 help_win = _styled_win("help") 2040 2041 # Give windows their initial size 2042 _resize_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, 2043 sel_node_i, scroll) 2044 2045 _safe_curs_set(2) 2046 2047 # Logic duplication with _select_{next,prev}_menu_entry(), except we do a 2048 # functional variant that returns the new (sel_node_i, scroll) values to 2049 # avoid 'nonlocal'. TODO: Can this be factored out in some nice way? 2050 2051 def select_next_match(): 2052 if sel_node_i == len(matches) - 1: 2053 return sel_node_i, scroll 2054 2055 if sel_node_i + 1 >= scroll + _height(matches_win) - _SCROLL_OFFSET \ 2056 and scroll < _max_scroll(matches, matches_win): 2057 2058 return sel_node_i + 1, scroll + 1 2059 2060 return sel_node_i + 1, scroll 2061 2062 def select_prev_match(): 2063 if sel_node_i == 0: 2064 return sel_node_i, scroll 2065 2066 if sel_node_i - 1 < scroll + _SCROLL_OFFSET: 2067 return sel_node_i - 1, max(scroll - 1, 0) 2068 2069 return sel_node_i - 1, scroll 2070 2071 while True: 2072 if s != prev_s: 2073 # The search text changed. Find new matching nodes. 2074 2075 prev_s = s 2076 2077 try: 2078 # We could use re.IGNORECASE here instead of lower(), but this 2079 # is noticeably less jerky while inputting regexes like 2080 # '.*debug$' (though the '.*' is redundant there). Those 2081 # probably have bad interactions with re.search(), which 2082 # matches anywhere in the string. 2083 # 2084 # It's not horrible either way. Just a bit smoother. 2085 regex_searches = [re.compile(regex).search 2086 for regex in s.lower().split()] 2087 2088 # No exception thrown, so the regexes are okay 2089 bad_re = None 2090 2091 # List of matching nodes 2092 matches = [] 2093 add_match = matches.append 2094 2095 # Search symbols and choices 2096 2097 for node in _sorted_sc_nodes(): 2098 # Symbol/choice 2099 sc = node.item 2100 2101 for search in regex_searches: 2102 # Both the name and the prompt might be missing, since 2103 # we're searching both symbols and choices 2104 2105 # Does the regex match either the symbol name or the 2106 # prompt (if any)? 2107 if not (sc.name and search(sc.name.lower()) or 2108 node.prompt and search(node.prompt[0].lower())): 2109 2110 # Give up on the first regex that doesn't match, to 2111 # speed things up a bit when multiple regexes are 2112 # entered 2113 break 2114 2115 else: 2116 add_match(node) 2117 2118 # Search menus and comments 2119 2120 for node in _sorted_menu_comment_nodes(): 2121 for search in regex_searches: 2122 if not search(node.prompt[0].lower()): 2123 break 2124 else: 2125 add_match(node) 2126 2127 except re.error as e: 2128 # Bad regex. Remember the error message so we can show it. 2129 bad_re = "Bad regular expression" 2130 # re.error.msg was added in Python 3.5 2131 if hasattr(e, "msg"): 2132 bad_re += ": " + e.msg 2133 2134 matches = [] 2135 2136 # Reset scroll and jump to the top of the list of matches 2137 sel_node_i = scroll = 0 2138 2139 _draw_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, 2140 s, s_i, hscroll, 2141 bad_re, matches, sel_node_i, scroll) 2142 curses.doupdate() 2143 2144 2145 c = _getch_compat(edit_box) 2146 2147 if c == "\n": 2148 if matches: 2149 _jump_to(matches[sel_node_i]) 2150 _safe_curs_set(0) 2151 return True 2152 2153 elif c == "\x1B": # \x1B = ESC 2154 _safe_curs_set(0) 2155 return False 2156 2157 elif c == curses.KEY_RESIZE: 2158 # We adjust the scroll so that the selected node stays visible in 2159 # the list when the terminal is resized, hence the 'scroll' 2160 # assignment 2161 scroll = _resize_jump_to_dialog( 2162 edit_box, matches_win, bot_sep_win, help_win, 2163 sel_node_i, scroll) 2164 2165 elif c == "\x06": # \x06 = Ctrl-F 2166 if matches: 2167 _safe_curs_set(0) 2168 _info_dialog(matches[sel_node_i], True) 2169 _safe_curs_set(2) 2170 2171 scroll = _resize_jump_to_dialog( 2172 edit_box, matches_win, bot_sep_win, help_win, 2173 sel_node_i, scroll) 2174 2175 elif c == curses.KEY_DOWN: 2176 sel_node_i, scroll = select_next_match() 2177 2178 elif c == curses.KEY_UP: 2179 sel_node_i, scroll = select_prev_match() 2180 2181 elif c in (curses.KEY_NPAGE, "\x04"): # Page Down/Ctrl-D 2182 # Keep it simple. This way we get sane behavior for small windows, 2183 # etc., for free. 2184 for _ in range(_PG_JUMP): 2185 sel_node_i, scroll = select_next_match() 2186 2187 # Page Up (no Ctrl-U, as it's already used by the edit box) 2188 elif c == curses.KEY_PPAGE: 2189 for _ in range(_PG_JUMP): 2190 sel_node_i, scroll = select_prev_match() 2191 2192 elif c == curses.KEY_END: 2193 sel_node_i = len(matches) - 1 2194 scroll = _max_scroll(matches, matches_win) 2195 2196 elif c == curses.KEY_HOME: 2197 sel_node_i = scroll = 0 2198 2199 else: 2200 s, s_i, hscroll = _edit_text(c, s, s_i, hscroll, 2201 _width(edit_box) - 2) 2202 2203 2204# Obscure Python: We never pass a value for cached_nodes, and it keeps pointing 2205# to the same list. This avoids a global. 2206def _sorted_sc_nodes(cached_nodes=[]): 2207 # Returns a sorted list of symbol and choice nodes to search. The symbol 2208 # nodes appear first, sorted by name, and then the choice nodes, sorted by 2209 # prompt and (secondarily) name. 2210 2211 if not cached_nodes: 2212 # Add symbol nodes 2213 for sym in sorted(_kconf.unique_defined_syms, 2214 key=lambda sym: sym.name): 2215 # += is in-place for lists 2216 cached_nodes += sym.nodes 2217 2218 # Add choice nodes 2219 2220 choices = sorted(_kconf.unique_choices, 2221 key=lambda choice: choice.name or "") 2222 2223 cached_nodes += sorted( 2224 [node for choice in choices for node in choice.nodes], 2225 key=lambda node: node.prompt[0] if node.prompt else "") 2226 2227 return cached_nodes 2228 2229 2230def _sorted_menu_comment_nodes(cached_nodes=[]): 2231 # Returns a list of menu and comment nodes to search, sorted by prompt, 2232 # with the menus first 2233 2234 if not cached_nodes: 2235 def prompt_text(mc): 2236 return mc.prompt[0] 2237 2238 cached_nodes += sorted(_kconf.menus, key=prompt_text) 2239 cached_nodes += sorted(_kconf.comments, key=prompt_text) 2240 2241 return cached_nodes 2242 2243 2244def _resize_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, 2245 sel_node_i, scroll): 2246 # Resizes the jump-to dialog to fill the terminal. 2247 # 2248 # Returns the new scroll index. We adjust the scroll if needed so that the 2249 # selected node stays visible. 2250 2251 screen_height, screen_width = _stdscr.getmaxyx() 2252 2253 bot_sep_win.resize(1, screen_width) 2254 2255 help_win_height = len(_JUMP_TO_HELP_LINES) 2256 matches_win_height = screen_height - help_win_height - 4 2257 2258 if matches_win_height >= 1: 2259 edit_box.resize(3, screen_width) 2260 matches_win.resize(matches_win_height, screen_width) 2261 help_win.resize(help_win_height, screen_width) 2262 2263 matches_win.mvwin(3, 0) 2264 bot_sep_win.mvwin(3 + matches_win_height, 0) 2265 help_win.mvwin(3 + matches_win_height + 1, 0) 2266 else: 2267 # Degenerate case. Give up on nice rendering and just prevent errors. 2268 2269 matches_win_height = 1 2270 2271 edit_box.resize(screen_height, screen_width) 2272 matches_win.resize(1, screen_width) 2273 help_win.resize(1, screen_width) 2274 2275 for win in matches_win, bot_sep_win, help_win: 2276 win.mvwin(0, 0) 2277 2278 # Adjust the scroll so that the selected row is still within the window, if 2279 # needed 2280 if sel_node_i - scroll >= matches_win_height: 2281 return sel_node_i - matches_win_height + 1 2282 return scroll 2283 2284 2285def _draw_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, 2286 s, s_i, hscroll, 2287 bad_re, matches, sel_node_i, scroll): 2288 2289 edit_width = _width(edit_box) - 2 2290 2291 # 2292 # Update list of matches 2293 # 2294 2295 matches_win.erase() 2296 2297 if matches: 2298 for i in range(scroll, 2299 min(scroll + _height(matches_win), len(matches))): 2300 2301 node = matches[i] 2302 2303 if isinstance(node.item, (Symbol, Choice)): 2304 node_str = _name_and_val_str(node.item) 2305 if node.prompt: 2306 node_str += ' "{}"'.format(node.prompt[0]) 2307 elif node.item == MENU: 2308 node_str = 'menu "{}"'.format(node.prompt[0]) 2309 else: # node.item == COMMENT 2310 node_str = 'comment "{}"'.format(node.prompt[0]) 2311 2312 _safe_addstr(matches_win, i - scroll, 0, node_str, 2313 _style["selection" if i == sel_node_i else "list"]) 2314 2315 else: 2316 # bad_re holds the error message from the re.error exception on errors 2317 _safe_addstr(matches_win, 0, 0, bad_re or "No matches") 2318 2319 matches_win.noutrefresh() 2320 2321 # 2322 # Update bottom separator line 2323 # 2324 2325 bot_sep_win.erase() 2326 2327 # Draw arrows pointing down if the symbol list is scrolled up 2328 if scroll < _max_scroll(matches, matches_win): 2329 _safe_hline(bot_sep_win, 0, 4, curses.ACS_DARROW, _N_SCROLL_ARROWS) 2330 2331 bot_sep_win.noutrefresh() 2332 2333 # 2334 # Update help window at bottom 2335 # 2336 2337 help_win.erase() 2338 2339 for i, line in enumerate(_JUMP_TO_HELP_LINES): 2340 _safe_addstr(help_win, i, 0, line) 2341 2342 help_win.noutrefresh() 2343 2344 # 2345 # Update edit box. We do this last since it makes it handy to position the 2346 # cursor. 2347 # 2348 2349 edit_box.erase() 2350 2351 _draw_frame(edit_box, "Jump to symbol/choice/menu/comment") 2352 2353 # Draw arrows pointing up if the symbol list is scrolled down 2354 if scroll > 0: 2355 # TODO: Bit ugly that _style["frame"] is repeated here 2356 _safe_hline(edit_box, 2, 4, curses.ACS_UARROW, _N_SCROLL_ARROWS, 2357 _style["frame"]) 2358 2359 visible_s = s[hscroll:hscroll + edit_width] 2360 _safe_addstr(edit_box, 1, 1, visible_s) 2361 2362 _safe_move(edit_box, 1, 1 + s_i - hscroll) 2363 2364 edit_box.noutrefresh() 2365 2366 2367def _info_dialog(node, from_jump_to_dialog): 2368 # Shows a fullscreen window with information about 'node'. 2369 # 2370 # If 'from_jump_to_dialog' is True, the information dialog was opened from 2371 # within the jump-to-dialog. In this case, we make '/' from within the 2372 # information dialog just return, to avoid a confusing recursive invocation 2373 # of the jump-to-dialog. 2374 2375 # Top row, with title and arrows point up 2376 top_line_win = _styled_win("separator") 2377 2378 # Text display 2379 text_win = _styled_win("text") 2380 text_win.keypad(True) 2381 2382 # Bottom separator, with arrows pointing down 2383 bot_sep_win = _styled_win("separator") 2384 2385 # Help window with keys at the bottom 2386 help_win = _styled_win("help") 2387 2388 # Give windows their initial size 2389 _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win) 2390 2391 2392 # Get lines of help text 2393 lines = _info_str(node).split("\n") 2394 2395 # Index of first row in 'lines' to show 2396 scroll = 0 2397 2398 while True: 2399 _draw_info_dialog(node, lines, scroll, top_line_win, text_win, 2400 bot_sep_win, help_win) 2401 curses.doupdate() 2402 2403 2404 c = _getch_compat(text_win) 2405 2406 if c == curses.KEY_RESIZE: 2407 _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win) 2408 2409 elif c in (curses.KEY_DOWN, "j", "J"): 2410 if scroll < _max_scroll(lines, text_win): 2411 scroll += 1 2412 2413 elif c in (curses.KEY_NPAGE, "\x04"): # Page Down/Ctrl-D 2414 scroll = min(scroll + _PG_JUMP, _max_scroll(lines, text_win)) 2415 2416 elif c in (curses.KEY_PPAGE, "\x15"): # Page Up/Ctrl-U 2417 scroll = max(scroll - _PG_JUMP, 0) 2418 2419 elif c in (curses.KEY_END, "G"): 2420 scroll = _max_scroll(lines, text_win) 2421 2422 elif c in (curses.KEY_HOME, "g"): 2423 scroll = 0 2424 2425 elif c in (curses.KEY_UP, "k", "K"): 2426 if scroll > 0: 2427 scroll -= 1 2428 2429 elif c == "/": 2430 # Support starting a search from within the information dialog 2431 2432 if from_jump_to_dialog: 2433 return # Avoid recursion 2434 2435 if _jump_to_dialog(): 2436 return # Jumped to a symbol. Cancel the information dialog. 2437 2438 # Stay in the information dialog if the jump-to dialog was 2439 # canceled. Resize it in case the terminal was resized while the 2440 # fullscreen jump-to dialog was open. 2441 _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win) 2442 2443 elif c in (curses.KEY_LEFT, curses.KEY_BACKSPACE, _ERASE_CHAR, 2444 "\x1B", # \x1B = ESC 2445 "q", "Q", "h", "H"): 2446 2447 return 2448 2449 2450def _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win): 2451 # Resizes the info dialog to fill the terminal 2452 2453 screen_height, screen_width = _stdscr.getmaxyx() 2454 2455 top_line_win.resize(1, screen_width) 2456 bot_sep_win.resize(1, screen_width) 2457 2458 help_win_height = len(_INFO_HELP_LINES) 2459 text_win_height = screen_height - help_win_height - 2 2460 2461 if text_win_height >= 1: 2462 text_win.resize(text_win_height, screen_width) 2463 help_win.resize(help_win_height, screen_width) 2464 2465 text_win.mvwin(1, 0) 2466 bot_sep_win.mvwin(1 + text_win_height, 0) 2467 help_win.mvwin(1 + text_win_height + 1, 0) 2468 else: 2469 # Degenerate case. Give up on nice rendering and just prevent errors. 2470 2471 text_win.resize(1, screen_width) 2472 help_win.resize(1, screen_width) 2473 2474 for win in text_win, bot_sep_win, help_win: 2475 win.mvwin(0, 0) 2476 2477 2478def _draw_info_dialog(node, lines, scroll, top_line_win, text_win, 2479 bot_sep_win, help_win): 2480 2481 text_win_height, text_win_width = text_win.getmaxyx() 2482 2483 # Note: The top row is deliberately updated last. See _draw_main(). 2484 2485 # 2486 # Update text display 2487 # 2488 2489 text_win.erase() 2490 2491 for i, line in enumerate(lines[scroll:scroll + text_win_height]): 2492 _safe_addstr(text_win, i, 0, line) 2493 2494 text_win.noutrefresh() 2495 2496 # 2497 # Update bottom separator line 2498 # 2499 2500 bot_sep_win.erase() 2501 2502 # Draw arrows pointing down if the symbol window is scrolled up 2503 if scroll < _max_scroll(lines, text_win): 2504 _safe_hline(bot_sep_win, 0, 4, curses.ACS_DARROW, _N_SCROLL_ARROWS) 2505 2506 bot_sep_win.noutrefresh() 2507 2508 # 2509 # Update help window at bottom 2510 # 2511 2512 help_win.erase() 2513 2514 for i, line in enumerate(_INFO_HELP_LINES): 2515 _safe_addstr(help_win, i, 0, line) 2516 2517 help_win.noutrefresh() 2518 2519 # 2520 # Update top row 2521 # 2522 2523 top_line_win.erase() 2524 2525 # Draw arrows pointing up if the information window is scrolled down. Draw 2526 # them before drawing the title, so the title ends up on top for small 2527 # windows. 2528 if scroll > 0: 2529 _safe_hline(top_line_win, 0, 4, curses.ACS_UARROW, _N_SCROLL_ARROWS) 2530 2531 title = ("Symbol" if isinstance(node.item, Symbol) else 2532 "Choice" if isinstance(node.item, Choice) else 2533 "Menu" if node.item == MENU else 2534 "Comment") + " information" 2535 _safe_addstr(top_line_win, 0, max((text_win_width - len(title))//2, 0), 2536 title) 2537 2538 top_line_win.noutrefresh() 2539 2540 2541def _info_str(node): 2542 # Returns information about the menu node 'node' as a string. 2543 # 2544 # The helper functions are responsible for adding newlines. This allows 2545 # them to return "" if they don't want to add any output. 2546 2547 if isinstance(node.item, Symbol): 2548 sym = node.item 2549 2550 return ( 2551 _name_info(sym) + 2552 _prompt_info(sym) + 2553 "Type: {}\n".format(TYPE_TO_STR[sym.type]) + 2554 _value_info(sym) + 2555 _help_info(sym) + 2556 _direct_dep_info(sym) + 2557 _defaults_info(sym) + 2558 _select_imply_info(sym) + 2559 _kconfig_def_info(sym) 2560 ) 2561 2562 if isinstance(node.item, Choice): 2563 choice = node.item 2564 2565 return ( 2566 _name_info(choice) + 2567 _prompt_info(choice) + 2568 "Type: {}\n".format(TYPE_TO_STR[choice.type]) + 2569 'Mode: {}\n'.format(choice.str_value) + 2570 _help_info(choice) + 2571 _choice_syms_info(choice) + 2572 _direct_dep_info(choice) + 2573 _defaults_info(choice) + 2574 _kconfig_def_info(choice) 2575 ) 2576 2577 return _kconfig_def_info(node) # node.item in (MENU, COMMENT) 2578 2579 2580def _name_info(sc): 2581 # Returns a string with the name of the symbol/choice. Names are optional 2582 # for choices. 2583 2584 return "Name: {}\n".format(sc.name) if sc.name else "" 2585 2586 2587def _prompt_info(sc): 2588 # Returns a string listing the prompts of 'sc' (Symbol or Choice) 2589 2590 s = "" 2591 2592 for node in sc.nodes: 2593 if node.prompt: 2594 s += "Prompt: {}\n".format(node.prompt[0]) 2595 2596 return s 2597 2598 2599def _value_info(sym): 2600 # Returns a string showing 'sym's value 2601 2602 # Only put quotes around the value for string symbols 2603 return "Value: {}\n".format( 2604 '"{}"'.format(sym.str_value) 2605 if sym.orig_type == STRING 2606 else sym.str_value) 2607 2608 2609def _choice_syms_info(choice): 2610 # Returns a string listing the choice symbols in 'choice'. Adds 2611 # "(selected)" next to the selected one. 2612 2613 s = "Choice symbols:\n" 2614 2615 for sym in choice.syms: 2616 s += " - " + sym.name 2617 if sym is choice.selection: 2618 s += " (selected)" 2619 s += "\n" 2620 2621 return s + "\n" 2622 2623 2624def _help_info(sc): 2625 # Returns a string with the help text(s) of 'sc' (Symbol or Choice). 2626 # Symbols and choices defined in multiple locations can have multiple help 2627 # texts. 2628 2629 s = "\n" 2630 2631 for node in sc.nodes: 2632 if node.help is not None: 2633 s += "Help:\n\n{}\n\n".format(_indent(node.help, 2)) 2634 2635 return s 2636 2637 2638def _direct_dep_info(sc): 2639 # Returns a string describing the direct dependencies of 'sc' (Symbol or 2640 # Choice). The direct dependencies are the OR of the dependencies from each 2641 # definition location. The dependencies at each definition location come 2642 # from 'depends on' and dependencies inherited from parent items. 2643 2644 return "" if sc.direct_dep is _kconf.y else \ 2645 'Direct dependencies (={}):\n{}\n' \ 2646 .format(TRI_TO_STR[expr_value(sc.direct_dep)], 2647 _split_expr_info(sc.direct_dep, 2)) 2648 2649 2650def _defaults_info(sc): 2651 # Returns a string describing the defaults of 'sc' (Symbol or Choice) 2652 2653 if not sc.defaults: 2654 return "" 2655 2656 s = "Default" 2657 if len(sc.defaults) > 1: 2658 s += "s" 2659 s += ":\n" 2660 2661 for val, cond in sc.orig_defaults: 2662 s += " - " 2663 if isinstance(sc, Symbol): 2664 s += _expr_str(val) 2665 2666 # Skip the tristate value hint if the expression is just a single 2667 # symbol. _expr_str() already shows its value as a string. 2668 # 2669 # This also avoids showing the tristate value for string/int/hex 2670 # defaults, which wouldn't make any sense. 2671 if isinstance(val, tuple): 2672 s += ' (={})'.format(TRI_TO_STR[expr_value(val)]) 2673 else: 2674 # Don't print the value next to the symbol name for choice 2675 # defaults, as it looks a bit confusing 2676 s += val.name 2677 s += "\n" 2678 2679 if cond is not _kconf.y: 2680 s += " Condition (={}):\n{}" \ 2681 .format(TRI_TO_STR[expr_value(cond)], 2682 _split_expr_info(cond, 4)) 2683 2684 return s + "\n" 2685 2686 2687def _split_expr_info(expr, indent): 2688 # Returns a string with 'expr' split into its top-level && or || operands, 2689 # with one operand per line, together with the operand's value. This is 2690 # usually enough to get something readable for long expressions. A fancier 2691 # recursive thingy would be possible too. 2692 # 2693 # indent: 2694 # Number of leading spaces to add before the split expression. 2695 2696 if len(split_expr(expr, AND)) > 1: 2697 split_op = AND 2698 op_str = "&&" 2699 else: 2700 split_op = OR 2701 op_str = "||" 2702 2703 s = "" 2704 for i, term in enumerate(split_expr(expr, split_op)): 2705 s += "{}{} {}".format(indent*" ", 2706 " " if i == 0 else op_str, 2707 _expr_str(term)) 2708 2709 # Don't bother showing the value hint if the expression is just a 2710 # single symbol. _expr_str() already shows its value. 2711 if isinstance(term, tuple): 2712 s += " (={})".format(TRI_TO_STR[expr_value(term)]) 2713 2714 s += "\n" 2715 2716 return s 2717 2718 2719def _select_imply_info(sym): 2720 # Returns a string with information about which symbols 'select' or 'imply' 2721 # 'sym'. The selecting/implying symbols are grouped according to which 2722 # value they select/imply 'sym' to (n/m/y). 2723 2724 def sis(expr, val, title): 2725 # sis = selects/implies 2726 sis = [si for si in split_expr(expr, OR) if expr_value(si) == val] 2727 if not sis: 2728 return "" 2729 2730 res = title 2731 for si in sis: 2732 res += " - {}\n".format(split_expr(si, AND)[0].name) 2733 return res + "\n" 2734 2735 s = "" 2736 2737 if sym.rev_dep is not _kconf.n: 2738 s += sis(sym.rev_dep, 2, 2739 "Symbols currently y-selecting this symbol:\n") 2740 s += sis(sym.rev_dep, 1, 2741 "Symbols currently m-selecting this symbol:\n") 2742 s += sis(sym.rev_dep, 0, 2743 "Symbols currently n-selecting this symbol (no effect):\n") 2744 2745 if sym.weak_rev_dep is not _kconf.n: 2746 s += sis(sym.weak_rev_dep, 2, 2747 "Symbols currently y-implying this symbol:\n") 2748 s += sis(sym.weak_rev_dep, 1, 2749 "Symbols currently m-implying this symbol:\n") 2750 s += sis(sym.weak_rev_dep, 0, 2751 "Symbols currently n-implying this symbol (no effect):\n") 2752 2753 return s 2754 2755 2756def _kconfig_def_info(item): 2757 # Returns a string with the definition of 'item' in Kconfig syntax, 2758 # together with the definition location(s) and their include and menu paths 2759 2760 nodes = [item] if isinstance(item, MenuNode) else item.nodes 2761 2762 s = "Kconfig definition{}, with parent deps. propagated to 'depends on'\n" \ 2763 .format("s" if len(nodes) > 1 else "") 2764 s += (len(s) - 1)*"=" 2765 2766 for node in nodes: 2767 s += "\n\n" \ 2768 "At {}:{}\n" \ 2769 "{}" \ 2770 "Menu path: {}\n\n" \ 2771 "{}" \ 2772 .format(node.filename, node.linenr, 2773 _include_path_info(node), 2774 _menu_path_info(node), 2775 _indent(node.custom_str(_name_and_val_str), 2)) 2776 2777 return s 2778 2779 2780def _include_path_info(node): 2781 if not node.include_path: 2782 # In the top-level Kconfig file 2783 return "" 2784 2785 return "Included via {}\n".format( 2786 " -> ".join("{}:{}".format(filename, linenr) 2787 for filename, linenr in node.include_path)) 2788 2789 2790def _menu_path_info(node): 2791 # Returns a string describing the menu path leading up to 'node' 2792 2793 path = "" 2794 2795 while node.parent is not _kconf.top_node: 2796 node = node.parent 2797 2798 # Promptless choices might appear among the parents. Use 2799 # standard_sc_expr_str() for them, so that they show up as 2800 # '<choice (name if any)>'. 2801 path = " -> " + (node.prompt[0] if node.prompt else 2802 standard_sc_expr_str(node.item)) + path 2803 2804 return "(Top)" + path 2805 2806 2807def _indent(s, n): 2808 # Returns 's' with each line indented 'n' spaces. textwrap.indent() is not 2809 # available in Python 2 (it's 3.3+). 2810 2811 return "\n".join(n*" " + line for line in s.split("\n")) 2812 2813 2814def _name_and_val_str(sc): 2815 # Custom symbol/choice printer that shows symbol values after symbols 2816 2817 # Show the values of non-constant (non-quoted) symbols that don't look like 2818 # numbers. Things like 123 are actually symbol references, and only work as 2819 # expected due to undefined symbols getting their name as their value. 2820 # Showing the symbol value for those isn't helpful though. 2821 if isinstance(sc, Symbol) and not sc.is_constant and not _is_num(sc.name): 2822 if not sc.nodes: 2823 # Undefined symbol reference 2824 return "{}(undefined/n)".format(sc.name) 2825 2826 return '{}(={})'.format(sc.name, sc.str_value) 2827 2828 # For other items, use the standard format 2829 return standard_sc_expr_str(sc) 2830 2831 2832def _expr_str(expr): 2833 # Custom expression printer that shows symbol values 2834 return expr_str(expr, _name_and_val_str) 2835 2836 2837def _styled_win(style): 2838 # Returns a new curses window with style 'style' and space as the fill 2839 # character. The initial dimensions are (1, 1), so the window needs to be 2840 # sized and positioned separately. 2841 2842 win = curses.newwin(1, 1) 2843 _set_style(win, style) 2844 return win 2845 2846 2847def _set_style(win, style): 2848 # Changes the style of an existing window 2849 2850 win.bkgdset(" ", _style[style]) 2851 2852 2853def _max_scroll(lst, win): 2854 # Assuming 'lst' is a list of items to be displayed in 'win', 2855 # returns the maximum number of steps 'win' can be scrolled down. 2856 # We stop scrolling when the bottom item is visible. 2857 2858 return max(0, len(lst) - _height(win)) 2859 2860 2861def _edit_text(c, s, i, hscroll, width): 2862 # Implements text editing commands for edit boxes. Takes a character (which 2863 # could also be e.g. curses.KEY_LEFT) and the edit box state, and returns 2864 # the new state after the character has been processed. 2865 # 2866 # c: 2867 # Character from user 2868 # 2869 # s: 2870 # Current contents of string 2871 # 2872 # i: 2873 # Current cursor index in string 2874 # 2875 # hscroll: 2876 # Index in s of the leftmost character in the edit box, for horizontal 2877 # scrolling 2878 # 2879 # width: 2880 # Width in characters of the edit box 2881 # 2882 # Return value: 2883 # An (s, i, hscroll) tuple for the new state 2884 2885 if c == curses.KEY_LEFT: 2886 if i > 0: 2887 i -= 1 2888 2889 elif c == curses.KEY_RIGHT: 2890 if i < len(s): 2891 i += 1 2892 2893 elif c in (curses.KEY_HOME, "\x01"): # \x01 = CTRL-A 2894 i = 0 2895 2896 elif c in (curses.KEY_END, "\x05"): # \x05 = CTRL-E 2897 i = len(s) 2898 2899 elif c in (curses.KEY_BACKSPACE, _ERASE_CHAR): 2900 if i > 0: 2901 s = s[:i-1] + s[i:] 2902 i -= 1 2903 2904 elif c == curses.KEY_DC: 2905 s = s[:i] + s[i+1:] 2906 2907 elif c == "\x17": # \x17 = CTRL-W 2908 # The \W removes characters like ',' one at a time 2909 new_i = re.search(r"(?:\w*|\W)\s*$", s[:i]).start() 2910 s = s[:new_i] + s[i:] 2911 i = new_i 2912 2913 elif c == "\x0B": # \x0B = CTRL-K 2914 s = s[:i] 2915 2916 elif c == "\x15": # \x15 = CTRL-U 2917 s = s[i:] 2918 i = 0 2919 2920 elif isinstance(c, str): 2921 # Insert character 2922 s = s[:i] + c + s[i:] 2923 i += 1 2924 2925 # Adjust the horizontal scroll so that the cursor never touches the left or 2926 # right edges of the edit box, except when it's at the beginning or the end 2927 # of the string 2928 if i < hscroll + _SCROLL_OFFSET: 2929 hscroll = max(i - _SCROLL_OFFSET, 0) 2930 elif i >= hscroll + width - _SCROLL_OFFSET: 2931 max_scroll = max(len(s) - width + 1, 0) 2932 hscroll = min(i - width + _SCROLL_OFFSET + 1, max_scroll) 2933 2934 return s, i, hscroll 2935 2936 2937def _load_save_info(): 2938 # Returns an information string for load/save dialog boxes 2939 2940 return "(Relative to {})\n\nRefer to your home directory with ~" \ 2941 .format(os.path.join(os.getcwd(), "")) 2942 2943 2944def _msg(title, text): 2945 # Pops up a message dialog that can be dismissed with Space/Enter/ESC 2946 2947 _key_dialog(title, text, " \n") 2948 2949 2950def _error(text): 2951 # Pops up an error dialog that can be dismissed with Space/Enter/ESC 2952 2953 _msg("Error", text) 2954 2955 2956def _node_str(node): 2957 # Returns the complete menu entry text for a menu node. 2958 # 2959 # Example return value: "[*] Support for X" 2960 2961 # Calculate the indent to print the item with by checking how many levels 2962 # above it the closest 'menuconfig' item is (this includes menus and 2963 # choices as well as menuconfig symbols) 2964 indent = 0 2965 parent = node.parent 2966 while not parent.is_menuconfig: 2967 indent += _SUBMENU_INDENT 2968 parent = parent.parent 2969 2970 # This approach gives nice alignment for empty string symbols ("() Foo") 2971 s = "{:{}}".format(_value_str(node), 3 + indent) 2972 2973 if _should_show_name(node): 2974 if isinstance(node.item, Symbol): 2975 s += " <{}>".format(node.item.name) 2976 else: 2977 # For choices, use standard_sc_expr_str(). That way they show up as 2978 # '<choice (name if any)>'. 2979 s += " " + standard_sc_expr_str(node.item) 2980 2981 if node.prompt: 2982 if node.item == COMMENT: 2983 s += " *** {} ***".format(node.prompt[0]) 2984 else: 2985 s += " " + node.prompt[0] 2986 2987 if isinstance(node.item, Symbol): 2988 sym = node.item 2989 2990 # Print "(NEW)" next to symbols without a user value (from e.g. a 2991 # .config), but skip it for choice symbols in choices in y mode, 2992 # and for symbols of UNKNOWN type (which generate a warning though) 2993 if sym.user_value is None and sym.orig_type and \ 2994 not (sym.choice and sym.choice.tri_value == 2): 2995 2996 s += " (NEW)" 2997 2998 if isinstance(node.item, Choice) and node.item.tri_value == 2: 2999 # Print the prompt of the selected symbol after the choice for 3000 # choices in y mode 3001 sym = node.item.selection 3002 if sym: 3003 for sym_node in sym.nodes: 3004 # Use the prompt used at this choice location, in case the 3005 # choice symbol is defined in multiple locations 3006 if sym_node.parent is node and sym_node.prompt: 3007 s += " ({})".format(sym_node.prompt[0]) 3008 break 3009 else: 3010 # If the symbol isn't defined at this choice location, then 3011 # just use whatever prompt we can find for it 3012 for sym_node in sym.nodes: 3013 if sym_node.prompt: 3014 s += " ({})".format(sym_node.prompt[0]) 3015 break 3016 3017 # Print "--->" next to nodes that have menus that can potentially be 3018 # entered. Print "----" if the menu is empty. We don't allow those to be 3019 # entered. 3020 if node.is_menuconfig: 3021 s += " --->" if _shown_nodes(node) else " ----" 3022 3023 return s 3024 3025 3026def _should_show_name(node): 3027 # Returns True if 'node' is a symbol or choice whose name should shown (if 3028 # any, as names are optional for choices) 3029 3030 # The 'not node.prompt' case only hits in show-all mode, for promptless 3031 # symbols and choices 3032 return not node.prompt or \ 3033 (_show_name and isinstance(node.item, (Symbol, Choice))) 3034 3035 3036def _value_str(node): 3037 # Returns the value part ("[*]", "<M>", "(foo)" etc.) of a menu node 3038 3039 item = node.item 3040 3041 if item in (MENU, COMMENT): 3042 return "" 3043 3044 # Wouldn't normally happen, and generates a warning 3045 if not item.orig_type: 3046 return "" 3047 3048 if item.orig_type in (STRING, INT, HEX): 3049 return "({})".format(item.str_value) 3050 3051 # BOOL or TRISTATE 3052 3053 if _is_y_mode_choice_sym(item): 3054 return "(X)" if item.choice.selection is item else "( )" 3055 3056 tri_val_str = (" ", "M", "*")[item.tri_value] 3057 3058 if len(item.assignable) <= 1: 3059 # Pinned to a single value 3060 return "" if isinstance(item, Choice) else "-{}-".format(tri_val_str) 3061 3062 if item.type == BOOL: 3063 return "[{}]".format(tri_val_str) 3064 3065 # item.type == TRISTATE 3066 if item.assignable == (1, 2): 3067 return "{{{}}}".format(tri_val_str) # {M}/{*} 3068 return "<{}>".format(tri_val_str) 3069 3070 3071def _is_y_mode_choice_sym(item): 3072 # The choice mode is an upper bound on the visibility of choice symbols, so 3073 # we can check the choice symbols' own visibility to see if the choice is 3074 # in y mode 3075 return isinstance(item, Symbol) and item.choice and item.visibility == 2 3076 3077 3078def _check_valid(sym, s): 3079 # Returns True if the string 's' is a well-formed value for 'sym'. 3080 # Otherwise, displays an error and returns False. 3081 3082 if sym.orig_type not in (INT, HEX): 3083 return True # Anything goes for non-int/hex symbols 3084 3085 base = 10 if sym.orig_type == INT else 16 3086 try: 3087 int(s, base) 3088 except ValueError: 3089 _error("'{}' is a malformed {} value" 3090 .format(s, TYPE_TO_STR[sym.orig_type])) 3091 return False 3092 3093 for low_sym, high_sym, cond in sym.ranges: 3094 if expr_value(cond): 3095 low_s = low_sym.str_value 3096 high_s = high_sym.str_value 3097 3098 if not int(low_s, base) <= int(s, base) <= int(high_s, base): 3099 _error("{} is outside the range {}-{}" 3100 .format(s, low_s, high_s)) 3101 return False 3102 3103 break 3104 3105 return True 3106 3107 3108def _range_info(sym): 3109 # Returns a string with information about the valid range for the symbol 3110 # 'sym', or None if 'sym' doesn't have a range 3111 3112 if sym.orig_type in (INT, HEX): 3113 for low, high, cond in sym.ranges: 3114 if expr_value(cond): 3115 return "Range: {}-{}".format(low.str_value, high.str_value) 3116 3117 return None 3118 3119 3120def _is_num(name): 3121 # Heuristic to see if a symbol name looks like a number, for nicer output 3122 # when printing expressions. Things like 16 are actually symbol names, only 3123 # they get their name as their value when the symbol is undefined. 3124 3125 try: 3126 int(name) 3127 except ValueError: 3128 if not name.startswith(("0x", "0X")): 3129 return False 3130 3131 try: 3132 int(name, 16) 3133 except ValueError: 3134 return False 3135 3136 return True 3137 3138 3139def _getch_compat(win): 3140 # Uses get_wch() if available (Python 3.3+) and getch() otherwise. 3141 # 3142 # Also falls back on getch() if get_wch() raises curses.error, to work 3143 # around an issue when resizing the terminal on at least macOS Catalina. 3144 # See https://github.com/ulfalizer/Kconfiglib/issues/84. 3145 # 3146 # Also handles a PDCurses resizing quirk. 3147 3148 try: 3149 c = win.get_wch() 3150 except (AttributeError, curses.error): 3151 c = win.getch() 3152 if 0 <= c <= 255: 3153 c = chr(c) 3154 3155 # Decent resizing behavior on PDCurses requires calling resize_term(0, 0) 3156 # after receiving KEY_RESIZE, while ncurses (usually) handles terminal 3157 # resizing automatically in get(_w)ch() (see the end of the 3158 # resizeterm(3NCURSES) man page). 3159 # 3160 # resize_term(0, 0) reliably fails and does nothing on ncurses, so this 3161 # hack gives ncurses/PDCurses compatibility for resizing. I don't know 3162 # whether it would cause trouble for other implementations. 3163 if c == curses.KEY_RESIZE: 3164 try: 3165 curses.resize_term(0, 0) 3166 except curses.error: 3167 pass 3168 3169 return c 3170 3171 3172def _warn(*args): 3173 # Temporarily returns from curses to shell mode and prints a warning to 3174 # stderr. The warning would get lost in curses mode. 3175 curses.endwin() 3176 print("menuconfig warning: ", end="", file=sys.stderr) 3177 print(*args, file=sys.stderr) 3178 curses.doupdate() 3179 3180 3181# Ignore exceptions from some functions that might fail, e.g. for small 3182# windows. They usually do reasonable things anyway. 3183 3184 3185def _safe_curs_set(visibility): 3186 try: 3187 curses.curs_set(visibility) 3188 except curses.error: 3189 pass 3190 3191 3192def _safe_addstr(win, *args): 3193 # Clip the line to avoid wrapping to the next line, which looks glitchy. 3194 # addchstr() would do it for us, but it's not available in the 'curses' 3195 # module. 3196 3197 attr = None 3198 if isinstance(args[0], str): 3199 y, x = win.getyx() 3200 s = args[0] 3201 if len(args) == 2: 3202 attr = args[1] 3203 else: 3204 y, x, s = args[:3] # pylint: disable=unbalanced-tuple-unpacking 3205 if len(args) == 4: 3206 attr = args[3] 3207 3208 maxlen = _width(win) - x 3209 s = s.expandtabs() 3210 3211 try: 3212 # The 'curses' module uses wattr_set() internally if you pass 'attr', 3213 # overwriting the background style, so setting 'attr' to 0 in the first 3214 # case won't do the right thing 3215 if attr is None: 3216 win.addnstr(y, x, s, maxlen) 3217 else: 3218 win.addnstr(y, x, s, maxlen, attr) 3219 except curses.error: 3220 pass 3221 3222 3223def _safe_addch(win, *args): 3224 try: 3225 win.addch(*args) 3226 except curses.error: 3227 pass 3228 3229 3230def _safe_hline(win, *args): 3231 try: 3232 win.hline(*args) 3233 except curses.error: 3234 pass 3235 3236 3237def _safe_vline(win, *args): 3238 try: 3239 win.vline(*args) 3240 except curses.error: 3241 pass 3242 3243 3244def _safe_move(win, *args): 3245 try: 3246 win.move(*args) 3247 except curses.error: 3248 pass 3249 3250 3251def _change_c_lc_ctype_to_utf8(): 3252 # See _CHANGE_C_LC_CTYPE_TO_UTF8 3253 3254 if _IS_WINDOWS: 3255 # Windows rarely has issues here, and the PEP 538 implementation avoids 3256 # changing the locale on it. None of the UTF-8 locales below were 3257 # supported from some quick testing either. Play it safe. 3258 return 3259 3260 def try_set_locale(loc): 3261 try: 3262 locale.setlocale(locale.LC_CTYPE, loc) 3263 return True 3264 except locale.Error: 3265 return False 3266 3267 # Is LC_CTYPE set to the C locale? 3268 if locale.setlocale(locale.LC_CTYPE) == "C": 3269 # This list was taken from the PEP 538 implementation in the CPython 3270 # code, in Python/pylifecycle.c 3271 for loc in "C.UTF-8", "C.utf8", "UTF-8": 3272 if try_set_locale(loc): 3273 # LC_CTYPE successfully changed 3274 return 3275 3276 3277if __name__ == "__main__": 3278 _main() 3279