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