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