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