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 elif c == "\0": # \0 = NUL, ignore 1761 pass 1762 1763 else: 1764 s, i, hscroll = _edit_text(c, s, i, hscroll, edit_width()) 1765 1766 1767def _resize_input_dialog(win, title, info_lines): 1768 # Resizes the input dialog to a size appropriate for the terminal size 1769 1770 screen_height, screen_width = _stdscr.getmaxyx() 1771 1772 win_height = 5 1773 if info_lines: 1774 win_height += len(info_lines) + 1 1775 win_height = min(win_height, screen_height) 1776 1777 win_width = max(_INPUT_DIALOG_MIN_WIDTH, 1778 len(title) + 4, 1779 *(len(line) + 4 for line in info_lines)) 1780 win_width = min(win_width, screen_width) 1781 1782 win.resize(win_height, win_width) 1783 win.mvwin((screen_height - win_height)//2, 1784 (screen_width - win_width)//2) 1785 1786 1787def _draw_input_dialog(win, title, info_lines, s, i, hscroll): 1788 edit_width = _width(win) - 4 1789 1790 win.erase() 1791 1792 # Note: Perhaps having a separate window for the input field would be nicer 1793 visible_s = s[hscroll:hscroll + edit_width] 1794 _safe_addstr(win, 2, 2, visible_s + " "*(edit_width - len(visible_s)), 1795 _style["edit"]) 1796 1797 for linenr, line in enumerate(info_lines): 1798 _safe_addstr(win, 4 + linenr, 2, line) 1799 1800 # Draw the frame last so that it overwrites the body text for small windows 1801 _draw_frame(win, title) 1802 1803 _safe_move(win, 2, 2 + i - hscroll) 1804 1805 win.noutrefresh() 1806 1807 1808def _load_dialog(): 1809 # Dialog for loading a new configuration 1810 1811 global _conf_changed 1812 global _conf_filename 1813 global _show_all 1814 1815 if _conf_changed: 1816 c = _key_dialog( 1817 "Load", 1818 "You have unsaved changes. Load new\n" 1819 "configuration anyway?\n" 1820 "\n" 1821 " (O)K (C)ancel", 1822 "oc") 1823 1824 if c is None or c == "c": 1825 return 1826 1827 filename = _conf_filename 1828 while True: 1829 filename = _input_dialog("File to load", filename, _load_save_info()) 1830 if filename is None: 1831 return 1832 1833 filename = os.path.expanduser(filename) 1834 1835 if _try_load(filename): 1836 _conf_filename = filename 1837 _conf_changed = _needs_save() 1838 1839 # Turn on show-all mode if the selected node is not visible after 1840 # loading the new configuration. _shown still holds the old state. 1841 if _shown[_sel_node_i] not in _shown_nodes(_cur_menu): 1842 _show_all = True 1843 1844 _update_menu() 1845 1846 # The message dialog indirectly updates the menu display, so _msg() 1847 # must be called after the new state has been initialized 1848 _msg("Success", "Loaded " + filename) 1849 return 1850 1851 1852def _try_load(filename): 1853 # Tries to load a configuration file. Pops up an error and returns False on 1854 # failure. 1855 # 1856 # filename: 1857 # Configuration file to load 1858 1859 try: 1860 _kconf.load_config(filename) 1861 return True 1862 except EnvironmentError as e: 1863 _error("Error loading '{}'\n\n{} (errno: {})" 1864 .format(filename, e.strerror, errno.errorcode[e.errno])) 1865 return False 1866 1867 1868def _save_dialog(save_fn, default_filename, description): 1869 # Dialog for saving the current configuration 1870 # 1871 # save_fn: 1872 # Function to call with 'filename' to save the file 1873 # 1874 # default_filename: 1875 # Prefilled filename in the input field 1876 # 1877 # description: 1878 # String describing the thing being saved 1879 # 1880 # Return value: 1881 # The path to the saved file, or None if no file was saved 1882 1883 filename = default_filename 1884 while True: 1885 filename = _input_dialog("Filename to save {} to".format(description), 1886 filename, _load_save_info()) 1887 if filename is None: 1888 return None 1889 1890 filename = os.path.expanduser(filename) 1891 1892 msg = _try_save(save_fn, filename, description) 1893 if msg: 1894 _msg("Success", msg) 1895 return filename 1896 1897 1898def _try_save(save_fn, filename, description): 1899 # Tries to save a configuration file. Returns a message to print on 1900 # success. 1901 # 1902 # save_fn: 1903 # Function to call with 'filename' to save the file 1904 # 1905 # description: 1906 # String describing the thing being saved 1907 # 1908 # Return value: 1909 # A message to print on success, and None on failure 1910 1911 try: 1912 # save_fn() returns a message to print 1913 return save_fn(filename) 1914 except EnvironmentError as e: 1915 _error("Error saving {} to '{}'\n\n{} (errno: {})" 1916 .format(description, e.filename, e.strerror, 1917 errno.errorcode[e.errno])) 1918 return None 1919 1920 1921def _key_dialog(title, text, keys): 1922 # Pops up a dialog that can be closed by pressing a key 1923 # 1924 # title: 1925 # Title to display at the top of the dialog window's border 1926 # 1927 # text: 1928 # Text to show in the dialog 1929 # 1930 # keys: 1931 # List of keys that will close the dialog. Other keys (besides ESC) are 1932 # ignored. The caller is responsible for providing a hint about which 1933 # keys can be pressed in 'text'. 1934 # 1935 # Return value: 1936 # The key that was pressed to close the dialog. Uppercase characters are 1937 # converted to lowercase. ESC will always close the dialog, and returns 1938 # None. 1939 1940 win = _styled_win("body") 1941 win.keypad(True) 1942 1943 _resize_key_dialog(win, text) 1944 1945 while True: 1946 # See _input_dialog() 1947 _draw_main() 1948 _draw_key_dialog(win, title, text) 1949 curses.doupdate() 1950 1951 1952 c = _getch_compat(win) 1953 1954 if c == curses.KEY_RESIZE: 1955 # Resize the main display too. The dialog floats above it. 1956 _resize_main() 1957 _resize_key_dialog(win, text) 1958 1959 elif c == "\x1B": # \x1B = ESC 1960 return None 1961 1962 elif isinstance(c, str): 1963 c = c.lower() 1964 if c in keys: 1965 return c 1966 1967 1968def _resize_key_dialog(win, text): 1969 # Resizes the key dialog to a size appropriate for the terminal size 1970 1971 screen_height, screen_width = _stdscr.getmaxyx() 1972 1973 lines = text.split("\n") 1974 1975 win_height = min(len(lines) + 4, screen_height) 1976 win_width = min(max(len(line) for line in lines) + 4, screen_width) 1977 1978 win.resize(win_height, win_width) 1979 win.mvwin((screen_height - win_height)//2, 1980 (screen_width - win_width)//2) 1981 1982 1983def _draw_key_dialog(win, title, text): 1984 win.erase() 1985 1986 for i, line in enumerate(text.split("\n")): 1987 _safe_addstr(win, 2 + i, 2, line) 1988 1989 # Draw the frame last so that it overwrites the body text for small windows 1990 _draw_frame(win, title) 1991 1992 win.noutrefresh() 1993 1994 1995def _draw_frame(win, title): 1996 # Draw a frame around the inner edges of 'win', with 'title' at the top 1997 1998 win_height, win_width = win.getmaxyx() 1999 2000 win.attron(_style["frame"]) 2001 2002 # Draw top/bottom edge 2003 _safe_hline(win, 0, 0, " ", win_width) 2004 _safe_hline(win, win_height - 1, 0, " ", win_width) 2005 2006 # Draw left/right edge 2007 _safe_vline(win, 0, 0, " ", win_height) 2008 _safe_vline(win, 0, win_width - 1, " ", win_height) 2009 2010 # Draw title 2011 _safe_addstr(win, 0, max((win_width - len(title))//2, 0), title) 2012 2013 win.attroff(_style["frame"]) 2014 2015 2016def _jump_to_dialog(): 2017 # Implements the jump-to dialog, where symbols can be looked up via 2018 # incremental search and jumped to. 2019 # 2020 # Returns True if the user jumped to a symbol, and False if the dialog was 2021 # canceled. 2022 2023 s = "" # Search text 2024 prev_s = None # Previous search text 2025 s_i = 0 # Search text cursor position 2026 hscroll = 0 # Horizontal scroll offset 2027 2028 sel_node_i = 0 # Index of selected row 2029 scroll = 0 # Index in 'matches' of the top row of the list 2030 2031 # Edit box at the top 2032 edit_box = _styled_win("jump-edit") 2033 edit_box.keypad(True) 2034 2035 # List of matches 2036 matches_win = _styled_win("list") 2037 2038 # Bottom separator, with arrows pointing down 2039 bot_sep_win = _styled_win("separator") 2040 2041 # Help window with instructions at the bottom 2042 help_win = _styled_win("help") 2043 2044 # Give windows their initial size 2045 _resize_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, 2046 sel_node_i, scroll) 2047 2048 _safe_curs_set(2) 2049 2050 # Logic duplication with _select_{next,prev}_menu_entry(), except we do a 2051 # functional variant that returns the new (sel_node_i, scroll) values to 2052 # avoid 'nonlocal'. TODO: Can this be factored out in some nice way? 2053 2054 def select_next_match(): 2055 if sel_node_i == len(matches) - 1: 2056 return sel_node_i, scroll 2057 2058 if sel_node_i + 1 >= scroll + _height(matches_win) - _SCROLL_OFFSET \ 2059 and scroll < _max_scroll(matches, matches_win): 2060 2061 return sel_node_i + 1, scroll + 1 2062 2063 return sel_node_i + 1, scroll 2064 2065 def select_prev_match(): 2066 if sel_node_i == 0: 2067 return sel_node_i, scroll 2068 2069 if sel_node_i - 1 < scroll + _SCROLL_OFFSET: 2070 return sel_node_i - 1, max(scroll - 1, 0) 2071 2072 return sel_node_i - 1, scroll 2073 2074 while True: 2075 if s != prev_s: 2076 # The search text changed. Find new matching nodes. 2077 2078 prev_s = s 2079 2080 try: 2081 # We could use re.IGNORECASE here instead of lower(), but this 2082 # is noticeably less jerky while inputting regexes like 2083 # '.*debug$' (though the '.*' is redundant there). Those 2084 # probably have bad interactions with re.search(), which 2085 # matches anywhere in the string. 2086 # 2087 # It's not horrible either way. Just a bit smoother. 2088 regex_searches = [re.compile(regex).search 2089 for regex in s.lower().split()] 2090 2091 # No exception thrown, so the regexes are okay 2092 bad_re = None 2093 2094 # List of matching nodes 2095 matches = [] 2096 add_match = matches.append 2097 2098 # Search symbols and choices 2099 2100 for node in _sorted_sc_nodes(): 2101 # Symbol/choice 2102 sc = node.item 2103 2104 for search in regex_searches: 2105 # Both the name and the prompt might be missing, since 2106 # we're searching both symbols and choices 2107 2108 # Does the regex match either the symbol name or the 2109 # prompt (if any)? 2110 if not (sc.name and search(sc.name.lower()) or 2111 node.prompt and search(node.prompt[0].lower())): 2112 2113 # Give up on the first regex that doesn't match, to 2114 # speed things up a bit when multiple regexes are 2115 # entered 2116 break 2117 2118 else: 2119 add_match(node) 2120 2121 # Search menus and comments 2122 2123 for node in _sorted_menu_comment_nodes(): 2124 for search in regex_searches: 2125 if not search(node.prompt[0].lower()): 2126 break 2127 else: 2128 add_match(node) 2129 2130 except re.error as e: 2131 # Bad regex. Remember the error message so we can show it. 2132 bad_re = "Bad regular expression" 2133 # re.error.msg was added in Python 3.5 2134 if hasattr(e, "msg"): 2135 bad_re += ": " + e.msg 2136 2137 matches = [] 2138 2139 # Reset scroll and jump to the top of the list of matches 2140 sel_node_i = scroll = 0 2141 2142 _draw_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, 2143 s, s_i, hscroll, 2144 bad_re, matches, sel_node_i, scroll) 2145 curses.doupdate() 2146 2147 2148 c = _getch_compat(edit_box) 2149 2150 if c == "\n": 2151 if matches: 2152 _jump_to(matches[sel_node_i]) 2153 _safe_curs_set(0) 2154 return True 2155 2156 elif c == "\x1B": # \x1B = ESC 2157 _safe_curs_set(0) 2158 return False 2159 2160 elif c == curses.KEY_RESIZE: 2161 # We adjust the scroll so that the selected node stays visible in 2162 # the list when the terminal is resized, hence the 'scroll' 2163 # assignment 2164 scroll = _resize_jump_to_dialog( 2165 edit_box, matches_win, bot_sep_win, help_win, 2166 sel_node_i, scroll) 2167 2168 elif c == "\x06": # \x06 = Ctrl-F 2169 if matches: 2170 _safe_curs_set(0) 2171 _info_dialog(matches[sel_node_i], True) 2172 _safe_curs_set(2) 2173 2174 scroll = _resize_jump_to_dialog( 2175 edit_box, matches_win, bot_sep_win, help_win, 2176 sel_node_i, scroll) 2177 2178 elif c == curses.KEY_DOWN: 2179 sel_node_i, scroll = select_next_match() 2180 2181 elif c == curses.KEY_UP: 2182 sel_node_i, scroll = select_prev_match() 2183 2184 elif c in (curses.KEY_NPAGE, "\x04"): # Page Down/Ctrl-D 2185 # Keep it simple. This way we get sane behavior for small windows, 2186 # etc., for free. 2187 for _ in range(_PG_JUMP): 2188 sel_node_i, scroll = select_next_match() 2189 2190 # Page Up (no Ctrl-U, as it's already used by the edit box) 2191 elif c == curses.KEY_PPAGE: 2192 for _ in range(_PG_JUMP): 2193 sel_node_i, scroll = select_prev_match() 2194 2195 elif c == curses.KEY_END: 2196 sel_node_i = len(matches) - 1 2197 scroll = _max_scroll(matches, matches_win) 2198 2199 elif c == curses.KEY_HOME: 2200 sel_node_i = scroll = 0 2201 2202 elif c == "\0": # \0 = NUL, ignore 2203 pass 2204 2205 else: 2206 s, s_i, hscroll = _edit_text(c, s, s_i, hscroll, 2207 _width(edit_box) - 2) 2208 2209 2210# Obscure Python: We never pass a value for cached_nodes, and it keeps pointing 2211# to the same list. This avoids a global. 2212def _sorted_sc_nodes(cached_nodes=[]): 2213 # Returns a sorted list of symbol and choice nodes to search. The symbol 2214 # nodes appear first, sorted by name, and then the choice nodes, sorted by 2215 # prompt and (secondarily) name. 2216 2217 if not cached_nodes: 2218 # Add symbol nodes 2219 for sym in sorted(_kconf.unique_defined_syms, 2220 key=lambda sym: sym.name): 2221 # += is in-place for lists 2222 cached_nodes += sym.nodes 2223 2224 # Add choice nodes 2225 2226 choices = sorted(_kconf.unique_choices, 2227 key=lambda choice: choice.name or "") 2228 2229 cached_nodes += sorted( 2230 [node for choice in choices for node in choice.nodes], 2231 key=lambda node: node.prompt[0] if node.prompt else "") 2232 2233 return cached_nodes 2234 2235 2236def _sorted_menu_comment_nodes(cached_nodes=[]): 2237 # Returns a list of menu and comment nodes to search, sorted by prompt, 2238 # with the menus first 2239 2240 if not cached_nodes: 2241 def prompt_text(mc): 2242 return mc.prompt[0] 2243 2244 cached_nodes += sorted(_kconf.menus, key=prompt_text) 2245 cached_nodes += sorted(_kconf.comments, key=prompt_text) 2246 2247 return cached_nodes 2248 2249 2250def _resize_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, 2251 sel_node_i, scroll): 2252 # Resizes the jump-to dialog to fill the terminal. 2253 # 2254 # Returns the new scroll index. We adjust the scroll if needed so that the 2255 # selected node stays visible. 2256 2257 screen_height, screen_width = _stdscr.getmaxyx() 2258 2259 bot_sep_win.resize(1, screen_width) 2260 2261 help_win_height = len(_JUMP_TO_HELP_LINES) 2262 matches_win_height = screen_height - help_win_height - 4 2263 2264 if matches_win_height >= 1: 2265 edit_box.resize(3, screen_width) 2266 matches_win.resize(matches_win_height, screen_width) 2267 help_win.resize(help_win_height, screen_width) 2268 2269 matches_win.mvwin(3, 0) 2270 bot_sep_win.mvwin(3 + matches_win_height, 0) 2271 help_win.mvwin(3 + matches_win_height + 1, 0) 2272 else: 2273 # Degenerate case. Give up on nice rendering and just prevent errors. 2274 2275 matches_win_height = 1 2276 2277 edit_box.resize(screen_height, screen_width) 2278 matches_win.resize(1, screen_width) 2279 help_win.resize(1, screen_width) 2280 2281 for win in matches_win, bot_sep_win, help_win: 2282 win.mvwin(0, 0) 2283 2284 # Adjust the scroll so that the selected row is still within the window, if 2285 # needed 2286 if sel_node_i - scroll >= matches_win_height: 2287 return sel_node_i - matches_win_height + 1 2288 return scroll 2289 2290 2291def _draw_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, 2292 s, s_i, hscroll, 2293 bad_re, matches, sel_node_i, scroll): 2294 2295 edit_width = _width(edit_box) - 2 2296 2297 # 2298 # Update list of matches 2299 # 2300 2301 matches_win.erase() 2302 2303 if matches: 2304 for i in range(scroll, 2305 min(scroll + _height(matches_win), len(matches))): 2306 2307 node = matches[i] 2308 2309 if isinstance(node.item, (Symbol, Choice)): 2310 node_str = _name_and_val_str(node.item) 2311 if node.prompt: 2312 node_str += ' "{}"'.format(node.prompt[0]) 2313 elif node.item == MENU: 2314 node_str = 'menu "{}"'.format(node.prompt[0]) 2315 else: # node.item == COMMENT 2316 node_str = 'comment "{}"'.format(node.prompt[0]) 2317 2318 _safe_addstr(matches_win, i - scroll, 0, node_str, 2319 _style["selection" if i == sel_node_i else "list"]) 2320 2321 else: 2322 # bad_re holds the error message from the re.error exception on errors 2323 _safe_addstr(matches_win, 0, 0, bad_re or "No matches") 2324 2325 matches_win.noutrefresh() 2326 2327 # 2328 # Update bottom separator line 2329 # 2330 2331 bot_sep_win.erase() 2332 2333 # Draw arrows pointing down if the symbol list is scrolled up 2334 if scroll < _max_scroll(matches, matches_win): 2335 _safe_hline(bot_sep_win, 0, 4, curses.ACS_DARROW, _N_SCROLL_ARROWS) 2336 2337 bot_sep_win.noutrefresh() 2338 2339 # 2340 # Update help window at bottom 2341 # 2342 2343 help_win.erase() 2344 2345 for i, line in enumerate(_JUMP_TO_HELP_LINES): 2346 _safe_addstr(help_win, i, 0, line) 2347 2348 help_win.noutrefresh() 2349 2350 # 2351 # Update edit box. We do this last since it makes it handy to position the 2352 # cursor. 2353 # 2354 2355 edit_box.erase() 2356 2357 _draw_frame(edit_box, "Jump to symbol/choice/menu/comment") 2358 2359 # Draw arrows pointing up if the symbol list is scrolled down 2360 if scroll > 0: 2361 # TODO: Bit ugly that _style["frame"] is repeated here 2362 _safe_hline(edit_box, 2, 4, curses.ACS_UARROW, _N_SCROLL_ARROWS, 2363 _style["frame"]) 2364 2365 visible_s = s[hscroll:hscroll + edit_width] 2366 _safe_addstr(edit_box, 1, 1, visible_s) 2367 2368 _safe_move(edit_box, 1, 1 + s_i - hscroll) 2369 2370 edit_box.noutrefresh() 2371 2372 2373def _info_dialog(node, from_jump_to_dialog): 2374 # Shows a fullscreen window with information about 'node'. 2375 # 2376 # If 'from_jump_to_dialog' is True, the information dialog was opened from 2377 # within the jump-to-dialog. In this case, we make '/' from within the 2378 # information dialog just return, to avoid a confusing recursive invocation 2379 # of the jump-to-dialog. 2380 2381 # Top row, with title and arrows point up 2382 top_line_win = _styled_win("separator") 2383 2384 # Text display 2385 text_win = _styled_win("text") 2386 text_win.keypad(True) 2387 2388 # Bottom separator, with arrows pointing down 2389 bot_sep_win = _styled_win("separator") 2390 2391 # Help window with keys at the bottom 2392 help_win = _styled_win("help") 2393 2394 # Give windows their initial size 2395 _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win) 2396 2397 2398 # Get lines of help text 2399 lines = _info_str(node).split("\n") 2400 2401 # Index of first row in 'lines' to show 2402 scroll = 0 2403 2404 while True: 2405 _draw_info_dialog(node, lines, scroll, top_line_win, text_win, 2406 bot_sep_win, help_win) 2407 curses.doupdate() 2408 2409 2410 c = _getch_compat(text_win) 2411 2412 if c == curses.KEY_RESIZE: 2413 _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win) 2414 2415 elif c in (curses.KEY_DOWN, "j", "J"): 2416 if scroll < _max_scroll(lines, text_win): 2417 scroll += 1 2418 2419 elif c in (curses.KEY_NPAGE, "\x04"): # Page Down/Ctrl-D 2420 scroll = min(scroll + _PG_JUMP, _max_scroll(lines, text_win)) 2421 2422 elif c in (curses.KEY_PPAGE, "\x15"): # Page Up/Ctrl-U 2423 scroll = max(scroll - _PG_JUMP, 0) 2424 2425 elif c in (curses.KEY_END, "G"): 2426 scroll = _max_scroll(lines, text_win) 2427 2428 elif c in (curses.KEY_HOME, "g"): 2429 scroll = 0 2430 2431 elif c in (curses.KEY_UP, "k", "K"): 2432 if scroll > 0: 2433 scroll -= 1 2434 2435 elif c == "/": 2436 # Support starting a search from within the information dialog 2437 2438 if from_jump_to_dialog: 2439 return # Avoid recursion 2440 2441 if _jump_to_dialog(): 2442 return # Jumped to a symbol. Cancel the information dialog. 2443 2444 # Stay in the information dialog if the jump-to dialog was 2445 # canceled. Resize it in case the terminal was resized while the 2446 # fullscreen jump-to dialog was open. 2447 _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win) 2448 2449 elif c in (curses.KEY_LEFT, curses.KEY_BACKSPACE, _ERASE_CHAR, 2450 "\x1B", # \x1B = ESC 2451 "q", "Q", "h", "H"): 2452 2453 return 2454 2455 2456def _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win): 2457 # Resizes the info dialog to fill the terminal 2458 2459 screen_height, screen_width = _stdscr.getmaxyx() 2460 2461 top_line_win.resize(1, screen_width) 2462 bot_sep_win.resize(1, screen_width) 2463 2464 help_win_height = len(_INFO_HELP_LINES) 2465 text_win_height = screen_height - help_win_height - 2 2466 2467 if text_win_height >= 1: 2468 text_win.resize(text_win_height, screen_width) 2469 help_win.resize(help_win_height, screen_width) 2470 2471 text_win.mvwin(1, 0) 2472 bot_sep_win.mvwin(1 + text_win_height, 0) 2473 help_win.mvwin(1 + text_win_height + 1, 0) 2474 else: 2475 # Degenerate case. Give up on nice rendering and just prevent errors. 2476 2477 text_win.resize(1, screen_width) 2478 help_win.resize(1, screen_width) 2479 2480 for win in text_win, bot_sep_win, help_win: 2481 win.mvwin(0, 0) 2482 2483 2484def _draw_info_dialog(node, lines, scroll, top_line_win, text_win, 2485 bot_sep_win, help_win): 2486 2487 text_win_height, text_win_width = text_win.getmaxyx() 2488 2489 # Note: The top row is deliberately updated last. See _draw_main(). 2490 2491 # 2492 # Update text display 2493 # 2494 2495 text_win.erase() 2496 2497 for i, line in enumerate(lines[scroll:scroll + text_win_height]): 2498 _safe_addstr(text_win, i, 0, line) 2499 2500 text_win.noutrefresh() 2501 2502 # 2503 # Update bottom separator line 2504 # 2505 2506 bot_sep_win.erase() 2507 2508 # Draw arrows pointing down if the symbol window is scrolled up 2509 if scroll < _max_scroll(lines, text_win): 2510 _safe_hline(bot_sep_win, 0, 4, curses.ACS_DARROW, _N_SCROLL_ARROWS) 2511 2512 bot_sep_win.noutrefresh() 2513 2514 # 2515 # Update help window at bottom 2516 # 2517 2518 help_win.erase() 2519 2520 for i, line in enumerate(_INFO_HELP_LINES): 2521 _safe_addstr(help_win, i, 0, line) 2522 2523 help_win.noutrefresh() 2524 2525 # 2526 # Update top row 2527 # 2528 2529 top_line_win.erase() 2530 2531 # Draw arrows pointing up if the information window is scrolled down. Draw 2532 # them before drawing the title, so the title ends up on top for small 2533 # windows. 2534 if scroll > 0: 2535 _safe_hline(top_line_win, 0, 4, curses.ACS_UARROW, _N_SCROLL_ARROWS) 2536 2537 title = ("Symbol" if isinstance(node.item, Symbol) else 2538 "Choice" if isinstance(node.item, Choice) else 2539 "Menu" if node.item == MENU else 2540 "Comment") + " information" 2541 _safe_addstr(top_line_win, 0, max((text_win_width - len(title))//2, 0), 2542 title) 2543 2544 top_line_win.noutrefresh() 2545 2546 2547def _info_str(node): 2548 # Returns information about the menu node 'node' as a string. 2549 # 2550 # The helper functions are responsible for adding newlines. This allows 2551 # them to return "" if they don't want to add any output. 2552 2553 if isinstance(node.item, Symbol): 2554 sym = node.item 2555 2556 return ( 2557 _name_info(sym) + 2558 _prompt_info(sym) + 2559 "Type: {}\n".format(TYPE_TO_STR[sym.type]) + 2560 _value_info(sym) + 2561 _help_info(sym) + 2562 _direct_dep_info(sym) + 2563 _defaults_info(sym) + 2564 _select_imply_info(sym) + 2565 _kconfig_def_info(sym) 2566 ) 2567 2568 if isinstance(node.item, Choice): 2569 choice = node.item 2570 2571 return ( 2572 _name_info(choice) + 2573 _prompt_info(choice) + 2574 "Type: {}\n".format(TYPE_TO_STR[choice.type]) + 2575 'Mode: {}\n'.format(choice.str_value) + 2576 _help_info(choice) + 2577 _choice_syms_info(choice) + 2578 _direct_dep_info(choice) + 2579 _defaults_info(choice) + 2580 _kconfig_def_info(choice) 2581 ) 2582 2583 return _kconfig_def_info(node) # node.item in (MENU, COMMENT) 2584 2585 2586def _name_info(sc): 2587 # Returns a string with the name of the symbol/choice. Names are optional 2588 # for choices. 2589 2590 return "Name: {}\n".format(sc.name) if sc.name else "" 2591 2592 2593def _prompt_info(sc): 2594 # Returns a string listing the prompts of 'sc' (Symbol or Choice) 2595 2596 s = "" 2597 2598 for node in sc.nodes: 2599 if node.prompt: 2600 s += "Prompt: {}\n".format(node.prompt[0]) 2601 2602 return s 2603 2604 2605def _value_info(sym): 2606 # Returns a string showing 'sym's value 2607 2608 # Only put quotes around the value for string symbols 2609 return "Value: {}\n".format( 2610 '"{}"'.format(sym.str_value) 2611 if sym.orig_type == STRING 2612 else sym.str_value) 2613 2614 2615def _choice_syms_info(choice): 2616 # Returns a string listing the choice symbols in 'choice'. Adds 2617 # "(selected)" next to the selected one. 2618 2619 s = "Choice symbols:\n" 2620 2621 for sym in choice.syms: 2622 s += " - " + sym.name 2623 if sym is choice.selection: 2624 s += " (selected)" 2625 s += "\n" 2626 2627 return s + "\n" 2628 2629 2630def _help_info(sc): 2631 # Returns a string with the help text(s) of 'sc' (Symbol or Choice). 2632 # Symbols and choices defined in multiple locations can have multiple help 2633 # texts. 2634 2635 s = "\n" 2636 2637 for node in sc.nodes: 2638 if node.help is not None: 2639 s += "Help:\n\n{}\n\n".format(_indent(node.help, 2)) 2640 2641 return s 2642 2643 2644def _direct_dep_info(sc): 2645 # Returns a string describing the direct dependencies of 'sc' (Symbol or 2646 # Choice). The direct dependencies are the OR of the dependencies from each 2647 # definition location. The dependencies at each definition location come 2648 # from 'depends on' and dependencies inherited from parent items. 2649 2650 return "" if sc.direct_dep is _kconf.y else \ 2651 'Direct dependencies (={}):\n{}\n' \ 2652 .format(TRI_TO_STR[expr_value(sc.direct_dep)], 2653 _split_expr_info(sc.direct_dep, 2)) 2654 2655 2656def _defaults_info(sc): 2657 # Returns a string describing the defaults of 'sc' (Symbol or Choice) 2658 2659 if not sc.defaults: 2660 return "" 2661 2662 s = "Default" 2663 if len(sc.defaults) > 1: 2664 s += "s" 2665 s += ":\n" 2666 2667 for val, cond in sc.orig_defaults: 2668 s += " - " 2669 if isinstance(sc, Symbol): 2670 s += _expr_str(val) 2671 2672 # Skip the tristate value hint if the expression is just a single 2673 # symbol. _expr_str() already shows its value as a string. 2674 # 2675 # This also avoids showing the tristate value for string/int/hex 2676 # defaults, which wouldn't make any sense. 2677 if isinstance(val, tuple): 2678 s += ' (={})'.format(TRI_TO_STR[expr_value(val)]) 2679 else: 2680 # Don't print the value next to the symbol name for choice 2681 # defaults, as it looks a bit confusing 2682 s += val.name 2683 s += "\n" 2684 2685 if cond is not _kconf.y: 2686 s += " Condition (={}):\n{}" \ 2687 .format(TRI_TO_STR[expr_value(cond)], 2688 _split_expr_info(cond, 4)) 2689 2690 return s + "\n" 2691 2692 2693def _split_expr_info(expr, indent): 2694 # Returns a string with 'expr' split into its top-level && or || operands, 2695 # with one operand per line, together with the operand's value. This is 2696 # usually enough to get something readable for long expressions. A fancier 2697 # recursive thingy would be possible too. 2698 # 2699 # indent: 2700 # Number of leading spaces to add before the split expression. 2701 2702 if len(split_expr(expr, AND)) > 1: 2703 split_op = AND 2704 op_str = "&&" 2705 else: 2706 split_op = OR 2707 op_str = "||" 2708 2709 s = "" 2710 for i, term in enumerate(split_expr(expr, split_op)): 2711 s += "{}{} {}".format(indent*" ", 2712 " " if i == 0 else op_str, 2713 _expr_str(term)) 2714 2715 # Don't bother showing the value hint if the expression is just a 2716 # single symbol. _expr_str() already shows its value. 2717 if isinstance(term, tuple): 2718 s += " (={})".format(TRI_TO_STR[expr_value(term)]) 2719 2720 s += "\n" 2721 2722 return s 2723 2724 2725def _select_imply_info(sym): 2726 # Returns a string with information about which symbols 'select' or 'imply' 2727 # 'sym'. The selecting/implying symbols are grouped according to which 2728 # value they select/imply 'sym' to (n/m/y). 2729 2730 def sis(expr, val, title): 2731 # sis = selects/implies 2732 sis = [si for si in split_expr(expr, OR) if expr_value(si) == val] 2733 if not sis: 2734 return "" 2735 2736 res = title 2737 for si in sis: 2738 res += " - {}\n".format(split_expr(si, AND)[0].name) 2739 return res + "\n" 2740 2741 s = "" 2742 2743 if sym.rev_dep is not _kconf.n: 2744 s += sis(sym.rev_dep, 2, 2745 "Symbols currently y-selecting this symbol:\n") 2746 s += sis(sym.rev_dep, 1, 2747 "Symbols currently m-selecting this symbol:\n") 2748 s += sis(sym.rev_dep, 0, 2749 "Symbols currently n-selecting this symbol (no effect):\n") 2750 2751 if sym.weak_rev_dep is not _kconf.n: 2752 s += sis(sym.weak_rev_dep, 2, 2753 "Symbols currently y-implying this symbol:\n") 2754 s += sis(sym.weak_rev_dep, 1, 2755 "Symbols currently m-implying this symbol:\n") 2756 s += sis(sym.weak_rev_dep, 0, 2757 "Symbols currently n-implying this symbol (no effect):\n") 2758 2759 return s 2760 2761 2762def _kconfig_def_info(item): 2763 # Returns a string with the definition of 'item' in Kconfig syntax, 2764 # together with the definition location(s) and their include and menu paths 2765 2766 nodes = [item] if isinstance(item, MenuNode) else item.nodes 2767 2768 s = "Kconfig definition{}, with parent deps. propagated to 'depends on'\n" \ 2769 .format("s" if len(nodes) > 1 else "") 2770 s += (len(s) - 1)*"=" 2771 2772 for node in nodes: 2773 s += "\n\n" \ 2774 "At {}:{}\n" \ 2775 "{}" \ 2776 "Menu path: {}\n\n" \ 2777 "{}" \ 2778 .format(node.filename, node.linenr, 2779 _include_path_info(node), 2780 _menu_path_info(node), 2781 _indent(node.custom_str(_name_and_val_str), 2)) 2782 2783 return s 2784 2785 2786def _include_path_info(node): 2787 if not node.include_path: 2788 # In the top-level Kconfig file 2789 return "" 2790 2791 return "Included via {}\n".format( 2792 " -> ".join("{}:{}".format(filename, linenr) 2793 for filename, linenr in node.include_path)) 2794 2795 2796def _menu_path_info(node): 2797 # Returns a string describing the menu path leading up to 'node' 2798 2799 path = "" 2800 2801 while node.parent is not _kconf.top_node: 2802 node = node.parent 2803 2804 # Promptless choices might appear among the parents. Use 2805 # standard_sc_expr_str() for them, so that they show up as 2806 # '<choice (name if any)>'. 2807 path = " -> " + (node.prompt[0] if node.prompt else 2808 standard_sc_expr_str(node.item)) + path 2809 2810 return "(Top)" + path 2811 2812 2813def _indent(s, n): 2814 # Returns 's' with each line indented 'n' spaces. textwrap.indent() is not 2815 # available in Python 2 (it's 3.3+). 2816 2817 return "\n".join(n*" " + line for line in s.split("\n")) 2818 2819 2820def _name_and_val_str(sc): 2821 # Custom symbol/choice printer that shows symbol values after symbols 2822 2823 # Show the values of non-constant (non-quoted) symbols that don't look like 2824 # numbers. Things like 123 are actually symbol references, and only work as 2825 # expected due to undefined symbols getting their name as their value. 2826 # Showing the symbol value for those isn't helpful though. 2827 if isinstance(sc, Symbol) and not sc.is_constant and not _is_num(sc.name): 2828 if not sc.nodes: 2829 # Undefined symbol reference 2830 return "{}(undefined/n)".format(sc.name) 2831 2832 return '{}(={})'.format(sc.name, sc.str_value) 2833 2834 # For other items, use the standard format 2835 return standard_sc_expr_str(sc) 2836 2837 2838def _expr_str(expr): 2839 # Custom expression printer that shows symbol values 2840 return expr_str(expr, _name_and_val_str) 2841 2842 2843def _styled_win(style): 2844 # Returns a new curses window with style 'style' and space as the fill 2845 # character. The initial dimensions are (1, 1), so the window needs to be 2846 # sized and positioned separately. 2847 2848 win = curses.newwin(1, 1) 2849 _set_style(win, style) 2850 return win 2851 2852 2853def _set_style(win, style): 2854 # Changes the style of an existing window 2855 2856 win.bkgdset(" ", _style[style]) 2857 2858 2859def _max_scroll(lst, win): 2860 # Assuming 'lst' is a list of items to be displayed in 'win', 2861 # returns the maximum number of steps 'win' can be scrolled down. 2862 # We stop scrolling when the bottom item is visible. 2863 2864 return max(0, len(lst) - _height(win)) 2865 2866 2867def _edit_text(c, s, i, hscroll, width): 2868 # Implements text editing commands for edit boxes. Takes a character (which 2869 # could also be e.g. curses.KEY_LEFT) and the edit box state, and returns 2870 # the new state after the character has been processed. 2871 # 2872 # c: 2873 # Character from user 2874 # 2875 # s: 2876 # Current contents of string 2877 # 2878 # i: 2879 # Current cursor index in string 2880 # 2881 # hscroll: 2882 # Index in s of the leftmost character in the edit box, for horizontal 2883 # scrolling 2884 # 2885 # width: 2886 # Width in characters of the edit box 2887 # 2888 # Return value: 2889 # An (s, i, hscroll) tuple for the new state 2890 2891 if c == curses.KEY_LEFT: 2892 if i > 0: 2893 i -= 1 2894 2895 elif c == curses.KEY_RIGHT: 2896 if i < len(s): 2897 i += 1 2898 2899 elif c in (curses.KEY_HOME, "\x01"): # \x01 = CTRL-A 2900 i = 0 2901 2902 elif c in (curses.KEY_END, "\x05"): # \x05 = CTRL-E 2903 i = len(s) 2904 2905 elif c in (curses.KEY_BACKSPACE, _ERASE_CHAR): 2906 if i > 0: 2907 s = s[:i-1] + s[i:] 2908 i -= 1 2909 2910 elif c == curses.KEY_DC: 2911 s = s[:i] + s[i+1:] 2912 2913 elif c == "\x17": # \x17 = CTRL-W 2914 # The \W removes characters like ',' one at a time 2915 new_i = re.search(r"(?:\w*|\W)\s*$", s[:i]).start() 2916 s = s[:new_i] + s[i:] 2917 i = new_i 2918 2919 elif c == "\x0B": # \x0B = CTRL-K 2920 s = s[:i] 2921 2922 elif c == "\x15": # \x15 = CTRL-U 2923 s = s[i:] 2924 i = 0 2925 2926 elif isinstance(c, str): 2927 # Insert character 2928 s = s[:i] + c + s[i:] 2929 i += 1 2930 2931 # Adjust the horizontal scroll so that the cursor never touches the left or 2932 # right edges of the edit box, except when it's at the beginning or the end 2933 # of the string 2934 if i < hscroll + _SCROLL_OFFSET: 2935 hscroll = max(i - _SCROLL_OFFSET, 0) 2936 elif i >= hscroll + width - _SCROLL_OFFSET: 2937 max_scroll = max(len(s) - width + 1, 0) 2938 hscroll = min(i - width + _SCROLL_OFFSET + 1, max_scroll) 2939 2940 return s, i, hscroll 2941 2942 2943def _load_save_info(): 2944 # Returns an information string for load/save dialog boxes 2945 2946 return "(Relative to {})\n\nRefer to your home directory with ~" \ 2947 .format(os.path.join(os.getcwd(), "")) 2948 2949 2950def _msg(title, text): 2951 # Pops up a message dialog that can be dismissed with Space/Enter/ESC 2952 2953 _key_dialog(title, text, " \n") 2954 2955 2956def _error(text): 2957 # Pops up an error dialog that can be dismissed with Space/Enter/ESC 2958 2959 _msg("Error", text) 2960 2961 2962def _node_str(node): 2963 # Returns the complete menu entry text for a menu node. 2964 # 2965 # Example return value: "[*] Support for X" 2966 2967 # Calculate the indent to print the item with by checking how many levels 2968 # above it the closest 'menuconfig' item is (this includes menus and 2969 # choices as well as menuconfig symbols) 2970 indent = 0 2971 parent = node.parent 2972 while not parent.is_menuconfig: 2973 indent += _SUBMENU_INDENT 2974 parent = parent.parent 2975 2976 # This approach gives nice alignment for empty string symbols ("() Foo") 2977 s = "{:{}}".format(_value_str(node), 3 + indent) 2978 2979 if _should_show_name(node): 2980 if isinstance(node.item, Symbol): 2981 s += " <{}>".format(node.item.name) 2982 else: 2983 # For choices, use standard_sc_expr_str(). That way they show up as 2984 # '<choice (name if any)>'. 2985 s += " " + standard_sc_expr_str(node.item) 2986 2987 if node.prompt: 2988 if node.item == COMMENT: 2989 s += " *** {} ***".format(node.prompt[0]) 2990 else: 2991 s += " " + node.prompt[0] 2992 2993 if isinstance(node.item, Symbol): 2994 sym = node.item 2995 2996 # Print "(NEW)" next to symbols without a user value (from e.g. a 2997 # .config), but skip it for choice symbols in choices in y mode, 2998 # and for symbols of UNKNOWN type (which generate a warning though) 2999 if sym.user_value is None and sym.orig_type and \ 3000 not (sym.choice and sym.choice.tri_value == 2): 3001 3002 s += " (NEW)" 3003 3004 if isinstance(node.item, Choice) and node.item.tri_value == 2: 3005 # Print the prompt of the selected symbol after the choice for 3006 # choices in y mode 3007 sym = node.item.selection 3008 if sym: 3009 for sym_node in sym.nodes: 3010 # Use the prompt used at this choice location, in case the 3011 # choice symbol is defined in multiple locations 3012 if sym_node.parent is node and sym_node.prompt: 3013 s += " ({})".format(sym_node.prompt[0]) 3014 break 3015 else: 3016 # If the symbol isn't defined at this choice location, then 3017 # just use whatever prompt we can find for it 3018 for sym_node in sym.nodes: 3019 if sym_node.prompt: 3020 s += " ({})".format(sym_node.prompt[0]) 3021 break 3022 3023 # Print "--->" next to nodes that have menus that can potentially be 3024 # entered. Print "----" if the menu is empty. We don't allow those to be 3025 # entered. 3026 if node.is_menuconfig: 3027 s += " --->" if _shown_nodes(node) else " ----" 3028 3029 return s 3030 3031 3032def _should_show_name(node): 3033 # Returns True if 'node' is a symbol or choice whose name should shown (if 3034 # any, as names are optional for choices) 3035 3036 # The 'not node.prompt' case only hits in show-all mode, for promptless 3037 # symbols and choices 3038 return not node.prompt or \ 3039 (_show_name and isinstance(node.item, (Symbol, Choice))) 3040 3041 3042def _value_str(node): 3043 # Returns the value part ("[*]", "<M>", "(foo)" etc.) of a menu node 3044 3045 item = node.item 3046 3047 if item in (MENU, COMMENT): 3048 return "" 3049 3050 # Wouldn't normally happen, and generates a warning 3051 if not item.orig_type: 3052 return "" 3053 3054 if item.orig_type in (STRING, INT, HEX): 3055 return "({})".format(item.str_value) 3056 3057 # BOOL or TRISTATE 3058 3059 if _is_y_mode_choice_sym(item): 3060 return "(X)" if item.choice.selection is item else "( )" 3061 3062 tri_val_str = (" ", "M", "*")[item.tri_value] 3063 3064 if len(item.assignable) <= 1: 3065 # Pinned to a single value 3066 return "" if isinstance(item, Choice) else "-{}-".format(tri_val_str) 3067 3068 if item.type == BOOL: 3069 return "[{}]".format(tri_val_str) 3070 3071 # item.type == TRISTATE 3072 if item.assignable == (1, 2): 3073 return "{{{}}}".format(tri_val_str) # {M}/{*} 3074 return "<{}>".format(tri_val_str) 3075 3076 3077def _is_y_mode_choice_sym(item): 3078 # The choice mode is an upper bound on the visibility of choice symbols, so 3079 # we can check the choice symbols' own visibility to see if the choice is 3080 # in y mode 3081 return isinstance(item, Symbol) and item.choice and item.visibility == 2 3082 3083 3084def _check_valid(sym, s): 3085 # Returns True if the string 's' is a well-formed value for 'sym'. 3086 # Otherwise, displays an error and returns False. 3087 3088 if sym.orig_type not in (INT, HEX): 3089 return True # Anything goes for non-int/hex symbols 3090 3091 base = 10 if sym.orig_type == INT else 16 3092 try: 3093 int(s, base) 3094 except ValueError: 3095 _error("'{}' is a malformed {} value" 3096 .format(s, TYPE_TO_STR[sym.orig_type])) 3097 return False 3098 3099 for low_sym, high_sym, cond in sym.ranges: 3100 if expr_value(cond): 3101 low_s = low_sym.str_value 3102 high_s = high_sym.str_value 3103 3104 if not int(low_s, base) <= int(s, base) <= int(high_s, base): 3105 _error("{} is outside the range {}-{}" 3106 .format(s, low_s, high_s)) 3107 return False 3108 3109 break 3110 3111 return True 3112 3113 3114def _range_info(sym): 3115 # Returns a string with information about the valid range for the symbol 3116 # 'sym', or None if 'sym' doesn't have a range 3117 3118 if sym.orig_type in (INT, HEX): 3119 for low, high, cond in sym.ranges: 3120 if expr_value(cond): 3121 return "Range: {}-{}".format(low.str_value, high.str_value) 3122 3123 return None 3124 3125 3126def _is_num(name): 3127 # Heuristic to see if a symbol name looks like a number, for nicer output 3128 # when printing expressions. Things like 16 are actually symbol names, only 3129 # they get their name as their value when the symbol is undefined. 3130 3131 try: 3132 int(name) 3133 except ValueError: 3134 if not name.startswith(("0x", "0X")): 3135 return False 3136 3137 try: 3138 int(name, 16) 3139 except ValueError: 3140 return False 3141 3142 return True 3143 3144 3145def _getch_compat(win): 3146 # Uses get_wch() if available (Python 3.3+) and getch() otherwise. 3147 # 3148 # Also falls back on getch() if get_wch() raises curses.error, to work 3149 # around an issue when resizing the terminal on at least macOS Catalina. 3150 # See https://github.com/ulfalizer/Kconfiglib/issues/84. 3151 # 3152 # Also handles a PDCurses resizing quirk. 3153 3154 try: 3155 c = win.get_wch() 3156 except (AttributeError, curses.error): 3157 c = win.getch() 3158 if 0 <= c <= 255: 3159 c = chr(c) 3160 3161 # Decent resizing behavior on PDCurses requires calling resize_term(0, 0) 3162 # after receiving KEY_RESIZE, while ncurses (usually) handles terminal 3163 # resizing automatically in get(_w)ch() (see the end of the 3164 # resizeterm(3NCURSES) man page). 3165 # 3166 # resize_term(0, 0) reliably fails and does nothing on ncurses, so this 3167 # hack gives ncurses/PDCurses compatibility for resizing. I don't know 3168 # whether it would cause trouble for other implementations. 3169 if c == curses.KEY_RESIZE: 3170 try: 3171 curses.resize_term(0, 0) 3172 except curses.error: 3173 pass 3174 3175 return c 3176 3177 3178def _warn(*args): 3179 # Temporarily returns from curses to shell mode and prints a warning to 3180 # stderr. The warning would get lost in curses mode. 3181 curses.endwin() 3182 print("menuconfig warning: ", end="", file=sys.stderr) 3183 print(*args, file=sys.stderr) 3184 curses.doupdate() 3185 3186 3187# Ignore exceptions from some functions that might fail, e.g. for small 3188# windows. They usually do reasonable things anyway. 3189 3190 3191def _safe_curs_set(visibility): 3192 try: 3193 curses.curs_set(visibility) 3194 except curses.error: 3195 pass 3196 3197 3198def _safe_addstr(win, *args): 3199 # Clip the line to avoid wrapping to the next line, which looks glitchy. 3200 # addchstr() would do it for us, but it's not available in the 'curses' 3201 # module. 3202 3203 attr = None 3204 if isinstance(args[0], str): 3205 y, x = win.getyx() 3206 s = args[0] 3207 if len(args) == 2: 3208 attr = args[1] 3209 else: 3210 y, x, s = args[:3] # pylint: disable=unbalanced-tuple-unpacking 3211 if len(args) == 4: 3212 attr = args[3] 3213 3214 maxlen = _width(win) - x 3215 s = s.expandtabs() 3216 3217 try: 3218 # The 'curses' module uses wattr_set() internally if you pass 'attr', 3219 # overwriting the background style, so setting 'attr' to 0 in the first 3220 # case won't do the right thing 3221 if attr is None: 3222 win.addnstr(y, x, s, maxlen) 3223 else: 3224 win.addnstr(y, x, s, maxlen, attr) 3225 except curses.error: 3226 pass 3227 3228 3229def _safe_addch(win, *args): 3230 try: 3231 win.addch(*args) 3232 except curses.error: 3233 pass 3234 3235 3236def _safe_hline(win, *args): 3237 try: 3238 win.hline(*args) 3239 except curses.error: 3240 pass 3241 3242 3243def _safe_vline(win, *args): 3244 try: 3245 win.vline(*args) 3246 except curses.error: 3247 pass 3248 3249 3250def _safe_move(win, *args): 3251 try: 3252 win.move(*args) 3253 except curses.error: 3254 pass 3255 3256 3257def _change_c_lc_ctype_to_utf8(): 3258 # See _CHANGE_C_LC_CTYPE_TO_UTF8 3259 3260 if _IS_WINDOWS: 3261 # Windows rarely has issues here, and the PEP 538 implementation avoids 3262 # changing the locale on it. None of the UTF-8 locales below were 3263 # supported from some quick testing either. Play it safe. 3264 return 3265 3266 def try_set_locale(loc): 3267 try: 3268 locale.setlocale(locale.LC_CTYPE, loc) 3269 return True 3270 except locale.Error: 3271 return False 3272 3273 # Is LC_CTYPE set to the C locale? 3274 if locale.setlocale(locale.LC_CTYPE) == "C": 3275 # This list was taken from the PEP 538 implementation in the CPython 3276 # code, in Python/pylifecycle.c 3277 for loc in "C.UTF-8", "C.utf8", "UTF-8": 3278 if try_set_locale(loc): 3279 # LC_CTYPE successfully changed 3280 return 3281 3282 3283if __name__ == "__main__": 3284 _main() 3285