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