scripts: kconfig: Add incremental search to menuconfig

Pressing [/] brings up a dialog with an edit box where a regex can be
entered. The list of matching symbols is always shown below it.
Selecting a symbol and pressing [Enter] jumps directly to it in the menu
tree. If the symbol is invisible, show-all mode is turned on
automatically.

This commit also includes a bunch of more-or-less unrelated changes from
poking around with the code:

  - Some redundant styles were merged. Probably wouldn't want to have a
    different style for each separator line, for example...

  - [ESC] in the top menu now works like [Q]

  - Returning to a parent menu now makes sure that the selected row is
    visible, even if the terminal was shrunk between entering the child
    menu and leaving it.

  - A _max_scroll() helper was factored out to reduce code duplication.
    It takes a list of items and a window in which the list is
    displayed, with one row per item, and returns the minimum scroll
    value that will make the final item visible.

  - The save dialog now pops up a message to confirm that the save was
    successful.

  - Lots of minor code nits all over (renamings, etc.)

Signed-off-by: Ulf Magnusson <ulfalizer@gmail.com>
diff --git a/scripts/kconfig/menuconfig.py b/scripts/kconfig/menuconfig.py
index 3f639fbfc..760839d 100755
--- a/scripts/kconfig/menuconfig.py
+++ b/scripts/kconfig/menuconfig.py
@@ -18,9 +18,9 @@
   g/Home  : Jump to beginning of list
 
 The mconf feature where pressing a key jumps to a menu entry with that
-character in it in the current menu isn't supported. A search feature with a
-"jump to" function for jumping directly to a particular symbol regardless of
-where it is defined will be added later instead.
+character in it in the current menu isn't supported. A jump-to feature for
+jumping directly to any symbol (including invisible symbols) is available
+instead.
 
 Space and Enter are "smart" and try to do what you'd expect for the given
 menu entry.
@@ -78,6 +78,25 @@
     https://www.lfd.uci.edu/~gohlke/pythonlibs/#curses though.
 """
 
+import curses
+import errno
+import locale
+import os
+import platform
+import re
+import sys
+import textwrap
+
+# We need this double import for the _expr_str() override below
+import kconfiglib
+
+from kconfiglib import Kconfig, \
+                       Symbol, Choice, MENU, COMMENT, \
+                       BOOL, TRISTATE, STRING, INT, HEX, UNKNOWN, \
+                       AND, OR, NOT, \
+                       expr_value, split_expr, \
+                       TRI_TO_STR, TYPE_TO_STR
+
 
 #
 # Configuration variables
@@ -98,31 +117,38 @@
 
 # Lines of help text shown at the bottom of the "main" display
 _MAIN_HELP_LINES = """
-[Space/Enter] Toggle/enter   [ESC] Leave menu   [S] Save
-[M] Save minimal config      [?] Symbol info    [Q] Quit (prompts for save)
+[Space/Enter] Toggle/enter   [ESC] Leave menu    [S] Save
+[?] Symbol info              [/] Jump to symbol  [A] Toggle show-all mode
+[Q] Quit (prompts for save)  [D] Save minimal config (advanced)
 """[1:-1].split("\n")
 
-# Lines of help text shown at the bottom of the information display
+# Lines of help text shown at the bottom of the information dialog
 _INFO_HELP_LINES = """
 [ESC/q] Return to menu
 """[1:-1].split("\n")
 
+# Lines of help text shown at the bottom of the search dialog
+_JUMP_TO_HELP_LINES = """
+Type text to narrow the search. Regular expressions are supported (anything
+available in the Python 're' module). Use the up/down cursor keys to step in
+the list. [Enter] jumps to the selected symbol. [ESC] aborts the search.
+"""[1:-1].split("\n")
+
 def _init_styles():
-    global _PATH_STYLE
-    global _TOP_SEP_STYLE
-    global _MENU_LIST_STYLE
-    global _MENU_LIST_SEL_STYLE
-    global _BOT_SEP_STYLE
+    global _SEPARATOR_STYLE
     global _HELP_STYLE
+    global _LIST_STYLE
+    global _LIST_SEL_STYLE
+    global _LIST_INVISIBLE_STYLE
+    global _LIST_INVISIBLE_SEL_STYLE
+    global _INPUT_FIELD_STYLE
+
+    global _PATH_STYLE
 
     global _DIALOG_FRAME_STYLE
     global _DIALOG_BODY_STYLE
-    global _INPUT_FIELD_STYLE
 
-    global _INFO_TOP_LINE_STYLE
     global _INFO_TEXT_STYLE
-    global _INFO_BOT_SEP_STYLE
-    global _INFO_HELP_STYLE
 
     # Initialize styles for different parts of the application. The arguments
     # are ordered as follows:
@@ -140,71 +166,41 @@
     BOLD = curses.A_NORMAL if platform.system() == "Windows" else curses.A_BOLD
 
 
-    # Top row, with menu path
-    _PATH_STYLE          = _style(curses.COLOR_BLACK, curses.COLOR_WHITE,  BOLD                              )
+    # Separator lines between windows. Also used for the top line in the symbol
+    # information dialog.
+    _SEPARATOR_STYLE          = _style(curses.COLOR_BLACK, curses.COLOR_YELLOW, BOLD,            curses.A_STANDOUT)
 
-    # Separator below menu path, with title and arrows pointing up
-    _TOP_SEP_STYLE       = _style(curses.COLOR_BLACK, curses.COLOR_YELLOW, BOLD,            curses.A_STANDOUT)
+    # Edit boxes
+    _INPUT_FIELD_STYLE        = _style(curses.COLOR_WHITE, curses.COLOR_BLUE,   curses.A_NORMAL, curses.A_STANDOUT)
 
-    # The "main" menu display with the list of symbols, etc.
-    _MENU_LIST_STYLE     = _style(curses.COLOR_BLACK, curses.COLOR_WHITE,  curses.A_NORMAL                   )
+    # List of items, e.g. the main display
+    _LIST_STYLE               = _style(curses.COLOR_BLACK, curses.COLOR_WHITE,  curses.A_NORMAL                   )
+    # Style for the selected item
+    _LIST_SEL_STYLE           = _style(curses.COLOR_WHITE, curses.COLOR_BLUE,   curses.A_NORMAL, curses.A_STANDOUT)
 
-    # Selected menu entry
-    _MENU_LIST_SEL_STYLE = _style(curses.COLOR_WHITE, curses.COLOR_BLUE,   curses.A_NORMAL, curses.A_STANDOUT)
+    # Like _LIST_(SEL_)STYLE, for invisible items. Used in show-all mode.
+    _LIST_INVISIBLE_STYLE     = _style(curses.COLOR_RED,   curses.COLOR_WHITE,  curses.A_NORMAL, BOLD             )
+    _LIST_INVISIBLE_SEL_STYLE = _style(curses.COLOR_RED,   curses.COLOR_BLUE,   curses.A_NORMAL, curses.A_STANDOUT)
 
-    # Row below menu list, with arrows pointing down
-    _BOT_SEP_STYLE       = _style(curses.COLOR_BLACK, curses.COLOR_YELLOW, BOLD,            curses.A_STANDOUT)
+    # Help text windows at the bottom of various fullscreen dialogs
+    _HELP_STYLE               = _style(curses.COLOR_BLACK, curses.COLOR_WHITE,  BOLD                              )
 
-    # Help window with keys at the bottom
-    _HELP_STYLE          = _style(curses.COLOR_BLACK, curses.COLOR_WHITE,  BOLD                              )
+    # Top row in the main display, with the menu path
+    _PATH_STYLE               = _style(curses.COLOR_BLACK, curses.COLOR_WHITE,  BOLD                              )
 
+    # Symbol information text
+    _INFO_TEXT_STYLE          = _LIST_STYLE
 
     # Frame around dialog boxes
-    _DIALOG_FRAME_STYLE  = _style(curses.COLOR_BLACK, curses.COLOR_YELLOW, BOLD,            curses.A_STANDOUT)
-
+    _DIALOG_FRAME_STYLE       = _style(curses.COLOR_BLACK, curses.COLOR_YELLOW, BOLD,            curses.A_STANDOUT)
     # Body of dialog boxes
-    _DIALOG_BODY_STYLE   = _style(curses.COLOR_WHITE, curses.COLOR_BLACK,  curses.A_NORMAL                   )
-
-    # Text input field in dialog boxes
-    _INPUT_FIELD_STYLE   = _style(curses.COLOR_WHITE, curses.COLOR_BLUE,   curses.A_NORMAL, curses.A_STANDOUT)
-
-
-    # Top line of information display, with title and arrows pointing up
-    _INFO_TOP_LINE_STYLE = _style(curses.COLOR_BLACK, curses.COLOR_YELLOW, BOLD,            curses.A_STANDOUT)
-
-    # Main information display window
-    _INFO_TEXT_STYLE     = _style(curses.COLOR_BLACK, curses.COLOR_WHITE,  curses.A_NORMAL                   )
-
-    # Separator below information display, with arrows pointing down
-    _INFO_BOT_SEP_STYLE  = _style(curses.COLOR_BLACK, curses.COLOR_YELLOW, BOLD,            curses.A_STANDOUT)
-
-    # Help window with keys at the bottom of the information display
-    _INFO_HELP_STYLE     = _style(curses.COLOR_BLACK, curses.COLOR_WHITE,  BOLD                              )
+    _DIALOG_BODY_STYLE        = _style(curses.COLOR_WHITE, curses.COLOR_BLACK,  curses.A_NORMAL                   )
 
 
 #
 # Main application
 #
 
-from kconfiglib import Kconfig, \
-                       Symbol, Choice, MENU, COMMENT, \
-                       BOOL, TRISTATE, STRING, INT, HEX, UNKNOWN, \
-                       AND, OR, NOT, \
-                       expr_value, split_expr, \
-                       TRI_TO_STR, TYPE_TO_STR
-
-# We need this double import for the _expr_str() override below
-import kconfiglib
-
-import curses
-import errno
-import locale
-import os
-import platform
-import sys
-import textwrap
-
-
 # Color pairs we've already created, indexed by a
 # (<foreground color>, <background color>) tuple
 _color_attribs = {}
@@ -231,7 +227,7 @@
     return _color_attribs[(fg_color, bg_color)] | attribs
 
 # "Extend" the standard kconfiglib.expr_str() to show values for symbols
-# appearing in expressions, for the information display.
+# appearing in expressions, for the information dialog.
 #
 # This is a bit hacky, but officially supported. It beats having to reimplement
 # expression printing just to tweak it a bit.
@@ -277,6 +273,7 @@
 
     globals()["_kconf"] = kconf
     global _config_filename
+    global _show_all
 
 
     _config_filename = os.environ.get("KCONFIG_CONFIG")
@@ -294,12 +291,17 @@
     else:
         print("Using default symbol values as base")
 
-
-    # We rely on having a selected node
-    if not _visible_nodes(_kconf.top_node):
-        print("No visible symbols in the top menu -- nothing to configure.\n"
-              "Check that environment variables are set properly.")
-        return
+    # Any visible items in the top menu?
+    _show_all = False
+    if not _shown_nodes(_kconf.top_node):
+        # Nothing visible. Start in show-all mode and try again.
+        _show_all = True
+        if not _shown_nodes(_kconf.top_node):
+            # Give up. The implementation relies on always having a selected
+            # node.
+            print("Empty configuration -- nothing to configure.\n"
+                  "Check that environment variables are set properly.")
+            return
 
     # Disable warnings. They get mangled in curses mode, and we deal with
     # errors ourselves.
@@ -321,24 +323,41 @@
 #     Menu node of the menu (or menuconfig symbol, or choice) currently being
 #     shown
 #
-#   _visible:
-#     List of visible symbols in _cur_menu
+#   _shown:
+#     List of items in _cur_menu that are shown (ignoring scrolling). In
+#     show-all mode, this list contains all items in _cur_menu. Otherwise, it
+#     contains just the visible items.
 #
 #   _sel_node_i:
-#     Index in _visible of the currently selected node
+#     Index in _shown of the currently selected node
 #
 #   _menu_scroll:
-#     Index in _visible of the top row of the menu display
+#     Index in _shown of the top row of the main display
 #
 #   _parent_screen_rows:
 #     List/stack of the row numbers that the selections in the parent menus
 #     appeared on. This is used to prevent the scrolling from jumping around
 #     when going in and out of menus.
+#
+#   _show_all:
+#     If True, "show-all" mode is on. Show-all mode shows all symbols and other
+#     items in the current menu, including those that lack a prompt or aren't
+#     currently visible.
+#
+#     Invisible items are drawn in a different style to make them stand out.
+#
+#   _conf_changed:
+#     True if the configuration has been changed. If False, we don't bother
+#     showing the save-and-quit dialog.
+#
+#     We reset this to False whenever the configuration is saved explicitly
+#     from the save dialog.
 
 def _menuconfig(stdscr):
-    # Logic for the "main" display, with the list of symbols, etc.
+    # Logic for the main display, with the list of symbols, etc.
 
     globals()["stdscr"] = stdscr
+    global _conf_changed
 
     _init()
 
@@ -378,7 +397,7 @@
             # Do appropriate node action. Only Space is treated specially,
             # preferring to toggle nodes rather than enter menus.
 
-            sel_node = _visible[_sel_node_i]
+            sel_node = _shown[_sel_node_i]
 
             if sel_node.is_menuconfig and not \
                (c == " " and _prefer_toggle(sel_node.item)):
@@ -392,44 +411,74 @@
                     # selection, like 'make menuconfig' does
                     _leave_menu()
 
+        elif c in ("n", "N"):
+            _set_node_tri_val(_shown[_sel_node_i], 0)
+
+        elif c in ("m", "M"):
+            _set_node_tri_val(_shown[_sel_node_i], 1)
+
+        elif c in ("y", "Y"):
+            _set_node_tri_val(_shown[_sel_node_i], 2)
+
         elif c in (curses.KEY_LEFT, curses.KEY_BACKSPACE, _ERASE_CHAR,
                    "\x1B",  # \x1B = ESC
                    "h", "H"):
 
-            _leave_menu()
+            if c == "\x1B" and _cur_menu is _kconf.top_node:
+                res = quit_dialog()
+                if res:
+                    return res
+            else:
+                _leave_menu()
 
         elif c in ("s", "S"):
-            _save_dialog(_kconf.write_config, _config_filename,
-                         "configuration")
+            if _save_dialog(_kconf.write_config, _config_filename,
+                            "configuration"):
 
-        elif c in ("m", "M"):
+                _conf_changed = False
+
+        elif c in ("d", "D"):
             _save_dialog(_kconf.write_min_config, "defconfig",
                          "minimal configuration")
 
+        elif c == "/":
+            _jump_to_dialog()
+
         elif c == "?":
-            _display_info(_visible[_sel_node_i])
+            _info_dialog(_shown[_sel_node_i])
+
+        elif c in ("a", "A"):
+            _toggle_show_all()
 
         elif c in ("q", "Q"):
-            while True:
-                c = _key_dialog(
-                    "Quit",
-                    " Save configuration?\n"
-                    "\n"
-                    "(Y)es  (N)o  (C)ancel",
-                    "ync")
+            res = quit_dialog()
+            if res:
+                return res
 
-                if c is None or c == "c":
-                    break
+def quit_dialog():
+   if not _conf_changed:
+       return "No changes to save"
 
-                if c == "y":
-                    if _try_save(_kconf.write_config, _config_filename,
-                                 "configuration"):
+   while True:
+       c = _key_dialog(
+           "Quit",
+           " Save configuration?\n"
+           "\n"
+           "(Y)es  (N)o  (C)ancel",
+           "ync")
 
-                        return "Configuration saved to '{}'" \
-                               .format(_config_filename)
+       if c is None or c == "c":
+           return None
 
-                elif c == "n":
-                    return "Configuration was not saved"
+       if c == "y":
+           if _try_save(_kconf.write_config, _config_filename,
+                        "configuration"):
+
+               return "Configuration saved to '{}'" \
+                      .format(_config_filename)
+
+       elif c == "n":
+           return "Configuration was not saved"
 
 def _init():
     # Initializes the main display with the list of symbols, etc. Also does
@@ -446,10 +495,12 @@
 
     global _parent_screen_rows
     global _cur_menu
-    global _visible
+    global _shown
     global _sel_node_i
     global _menu_scroll
 
+    global _conf_changed
+
     # Looking for this in addition to KEY_BACKSPACE (which is unreliable) makes
     # backspace work with TERM=vt100. That makes it likely to work in sane
     # environments.
@@ -469,14 +520,14 @@
     _path_win = _styled_win(_PATH_STYLE)
 
     # Separator below menu path, with title and arrows pointing up
-    _top_sep_win = _styled_win(_TOP_SEP_STYLE)
+    _top_sep_win = _styled_win(_SEPARATOR_STYLE)
 
     # List of menu entries with symbols, etc.
-    _menu_win = _styled_win(_MENU_LIST_STYLE)
+    _menu_win = _styled_win(_LIST_STYLE)
     _menu_win.keypad(True)
 
     # Row below menu list, with arrows pointing down
-    _bot_sep_win = _styled_win(_BOT_SEP_STYLE)
+    _bot_sep_win = _styled_win(_SEPARATOR_STYLE)
 
     # Help window with keys at the bottom
     _help_win = _styled_win(_HELP_STYLE)
@@ -487,16 +538,19 @@
 
     # Initial state
     _cur_menu = _kconf.top_node
-    _visible = _visible_nodes(_cur_menu)
+    _shown = _shown_nodes(_cur_menu)
     _sel_node_i = 0
     _menu_scroll = 0
 
     # Give windows their initial size
     _resize_main()
 
+    # No changes yet
+    _conf_changed = False
+
 def _resize_main():
-    # Resizes the "main" display, with the list of menu entries, etc., to a
-    # size appropriate for the terminal size
+    # Resizes the main display, with the list of symbols, etc., to fill the
+    # terminal
 
     global _menu_scroll
 
@@ -528,8 +582,8 @@
         for win in _top_sep_win, _menu_win, _bot_sep_win, _help_win:
             win.mvwin(0, 0)
 
-    # Adjust the scroll so that the selected node is still within the
-    # window, if needed
+    # Adjust the scroll so that the selected node is still within the window,
+    # if needed
     if _sel_node_i - _menu_scroll >= menu_win_height:
         _menu_scroll = _sel_node_i - menu_win_height + 1
 
@@ -538,12 +592,6 @@
 
     return _menu_win.getmaxyx()[0]
 
-def _max_menu_scroll():
-    # Returns the maximum amount the menu display can be scrolled down. We stop
-    # scrolling when the bottom node is visible.
-
-    return max(0, len(_visible) - _menu_win_height())
-
 def _prefer_toggle(item):
     # For nodes with menus, determines whether Space should change the value of
     # the node's item or enter its menu. We toggle symbols (which have menus
@@ -557,29 +605,54 @@
     # Makes 'menu' the currently displayed menu
 
     global _cur_menu
-    global _visible
+    global _shown
     global _sel_node_i
     global _menu_scroll
 
-    visible_sub = _visible_nodes(menu)
+    shown_sub = _shown_nodes(menu)
     # Never enter empty menus. We depend on having a current node.
-    if visible_sub:
+    if shown_sub:
         # Remember where the current node appears on the screen, so we can try
         # to get it to appear in the same place when we leave the menu
         _parent_screen_rows.append(_sel_node_i - _menu_scroll)
 
         # Jump into menu
         _cur_menu = menu
-        _visible = visible_sub
+        _shown = shown_sub
         _sel_node_i = 0
         _menu_scroll = 0
 
+def _jump_to(node):
+    # Jumps directly to the menu node 'node'
+
+    global _cur_menu
+    global _shown
+    global _sel_node_i
+    global _menu_scroll
+    global _show_all
+    global _parent_screen_rows
+
+    # Clear remembered menu locations. We might not even have been in the
+    # parent menus before.
+    _parent_screen_rows = []
+
+    # Turn on show-all mode if the node isn't visible
+    if not (node.prompt and expr_value(node.prompt[1])):
+        _show_all = True
+
+    _cur_menu = _parent_menu(node)
+    _shown = _shown_nodes(_cur_menu)
+    _sel_node_i = _shown.index(node)
+
+    # Center the jumped-to node vertically, if possible
+    _menu_scroll = max(_sel_node_i - _menu_win_height()//2, 0)
+
 def _leave_menu():
     # Jumps to the parent menu of the current menu. Does nothing if we're in
     # the top menu.
 
     global _cur_menu
-    global _visible
+    global _shown
     global _sel_node_i
     global _menu_scroll
 
@@ -588,13 +661,21 @@
 
     # Jump to parent menu
     parent = _parent_menu(_cur_menu)
-    _visible = _visible_nodes(parent)
-    _sel_node_i = _visible.index(_cur_menu)
+    _shown = _shown_nodes(parent)
+    _sel_node_i = _shown.index(_cur_menu)
     _cur_menu = parent
 
     # Try to make the menu entry appear on the same row on the screen as it did
-    # before we entered the menu
-    _menu_scroll = max(_sel_node_i - _parent_screen_rows.pop(), 0)
+    # before we entered the menu.
+
+    if _parent_screen_rows:
+        # The terminal might have shrunk since we were last in the parent menu
+        screen_row = min(_parent_screen_rows.pop(), _menu_win_height() - 1)
+        _menu_scroll = max(_sel_node_i - screen_row, 0)
+    else:
+        # No saved parent menu locations, meaning we jumped directly to some
+        # node earlier. Just center the node vertically if possible.
+        _menu_scroll = max(_sel_node_i - _menu_win_height()//2, 0)
 
 def _select_next_menu_entry():
     # Selects the menu entry after the current one, adjusting the scroll if
@@ -603,7 +684,7 @@
     global _sel_node_i
     global _menu_scroll
 
-    if _sel_node_i < len(_visible) - 1:
+    if _sel_node_i < len(_shown) - 1:
         # Jump to the next node
         _sel_node_i += 1
 
@@ -612,7 +693,8 @@
         # gives nice and non-jumpy behavior even when
         # _SCROLL_OFFSET >= _menu_win_height().
         if _sel_node_i >= _menu_scroll + _menu_win_height() - _SCROLL_OFFSET:
-            _menu_scroll = min(_menu_scroll + 1, _max_menu_scroll())
+            _menu_scroll = min(_menu_scroll + 1,
+                               _max_scroll(_shown, _menu_win))
 
 def _select_prev_menu_entry():
     # Selects the menu entry before the current one, adjusting the scroll if
@@ -635,8 +717,8 @@
     global _sel_node_i
     global _menu_scroll
 
-    _sel_node_i = len(_visible) - 1
-    _menu_scroll = _max_menu_scroll()
+    _sel_node_i = len(_shown) - 1
+    _menu_scroll = _max_scroll(_shown, _menu_win)
 
 def _select_first_menu_entry():
     # Selects the first menu entry in the current menu
@@ -646,6 +728,53 @@
 
     _sel_node_i = _menu_scroll = 0
 
+def _toggle_show_all():
+    # Toggles show-all mode on/off
+
+    global _show_all
+    global _shown
+    global _sel_node_i
+    global _menu_scroll
+
+    # Row on the screen the cursor is on. Preferably we want the same row to
+    # stay highlighted.
+    old_row = _sel_node_i - _menu_scroll
+
+    _show_all = not _show_all
+    # List of new nodes to be shown after toggling _show_all
+    new_shown = _shown_nodes(_cur_menu)
+
+    # Find a good node to select. The selected node might disappear if show-all
+    # mode is turned off.
+
+    # If there are visible nodes before the previously selected node, select
+    # the closest one. This will select the previously selected node itself if
+    # it is still visible.
+    for node in reversed(_shown[:_sel_node_i + 1]):
+        if node in new_shown:
+            _sel_node_i = new_shown.index(node)
+            break
+    else:
+        # No visible nodes before the previously selected node. Select the
+        # closest visible node after it instead.
+        for node in _shown[_sel_node_i + 1:]:
+            if node in new_shown:
+                _sel_node_i = new_shown.index(node)
+                break
+        else:
+            # No visible nodes at all, meaning show-all was turned off inside
+            # an invisible menu. Don't allow that, as the implementation relies
+            # on always having a selected node.
+            _show_all = True
+
+            return
+
+    _shown = new_shown
+
+    # Try to make the cursor stay on the same row in the menu window. This
+    # might be impossible if too many nodes have disappeared above the node.
+    _menu_scroll = max(_sel_node_i - old_row, 0)
+
 def _draw_main():
     # Draws the "main" display, with the list of symbols, the header, and the
     # footer.
@@ -653,6 +782,8 @@
     # This could be optimized to only update the windows that have actually
     # changed, but keep it simple for now and let curses sort it out.
 
+    term_width = stdscr.getmaxyx()[1]
+
 
     #
     # Update the top row with the menu path
@@ -666,14 +797,26 @@
 
     menu = _cur_menu
     while menu is not _kconf.top_node:
-        menu_prompts.insert(0, menu.prompt[0])
+        menu_prompts.append(menu.prompt[0])
         menu = menu.parent
+    menu_prompts.append("(top menu)")
+    menu_prompts.reverse()
 
-    _safe_addstr(_path_win, 0, 0, "(top menu)")
-    for prompt in menu_prompts:
-        _safe_addch(_path_win, " ")
+    # Hack: We can't put ACS_RARROW directly in the string. Temporarily
+    # represent it with NULL. Maybe using a Unicode character would be better.
+    menu_path_str = " \0 ".join(menu_prompts)
+
+    # Scroll the menu path to the right if needed to make the current menu's
+    # title visible
+    if len(menu_path_str) > term_width:
+        menu_path_str = menu_path_str[len(menu_path_str) - term_width:]
+
+    # Print the path with the arrows reinserted
+    split_path = menu_path_str.split("\0")
+    _safe_addstr(_path_win, split_path[0])
+    for s in split_path[1:]:
         _safe_addch(_path_win, curses.ACS_RARROW)
-        _safe_addstr(_path_win, " " + prompt)
+        _safe_addstr(_path_win, s)
 
     _path_win.noutrefresh()
 
@@ -691,7 +834,7 @@
 
     # Add the 'mainmenu' text as the title, centered at the top
     _safe_addstr(_top_sep_win,
-                 0, (stdscr.getmaxyx()[1] - len(_kconf.mainmenu_text))//2,
+                 0, (term_width - len(_kconf.mainmenu_text))//2,
                  _kconf.mainmenu_text)
 
     _top_sep_win.noutrefresh()
@@ -703,16 +846,20 @@
 
     _menu_win.erase()
 
-    # Draw the _visible nodes starting from index _menu_scroll up to either as
-    # many as fit in the window, or to the end of _visible
+    # Draw the _shown nodes starting from index _menu_scroll up to either as
+    # many as fit in the window, or to the end of _shown
     for i in range(_menu_scroll,
-                   min(_menu_scroll + _menu_win_height(), len(_visible))):
+                   min(_menu_scroll + _menu_win_height(), len(_shown))):
 
-        _safe_addstr(_menu_win, i - _menu_scroll, 0,
-                     _node_str(_visible[i]),
-                     # Highlight the selected entry
-                     _MENU_LIST_SEL_STYLE
-                         if i == _sel_node_i else curses.A_NORMAL)
+        node = _shown[i]
+
+        if node.prompt and expr_value(node.prompt[1]):
+            style = _LIST_SEL_STYLE if i == _sel_node_i else _LIST_STYLE
+        else:
+            style = _LIST_INVISIBLE_SEL_STYLE if i == _sel_node_i else \
+                    _LIST_INVISIBLE_STYLE
+
+        _safe_addstr(_menu_win, i - _menu_scroll, 0, _node_str(node), style)
 
     _menu_win.noutrefresh()
 
@@ -724,9 +871,14 @@
     _bot_sep_win.erase()
 
     # Draw arrows pointing down if the symbol window is scrolled up
-    if _menu_scroll < _max_menu_scroll():
+    if _menu_scroll < _max_scroll(_shown, _menu_win):
         _safe_hline(_bot_sep_win, 0, 4, curses.ACS_DARROW, _N_SCROLL_ARROWS)
 
+    # Indicate when show-all mode is enabled
+    if _show_all:
+        s = "Show-all mode enabled"
+        _safe_addstr(_bot_sep_win, 0, term_width - len(s) - 2, s)
+
     _bot_sep_win.noutrefresh()
 
 
@@ -751,45 +903,47 @@
         menu = menu.parent
     return menu
 
-def _visible_nodes(menu):
+def _shown_nodes(menu):
     # Returns a list of the nodes in 'menu' (see _parent_menu()) that should be
-    # visible in the menu window
+    # shown in the menu window
+
+    res = []
 
     def rec(node):
-        res = []
+        nonlocal res
 
         while node:
             # Show the node if its prompt is visible. For menus, also check
-            # 'visible if'.
-            if node.prompt and expr_value(node.prompt[1]) and not \
-               (node.item == MENU and not expr_value(node.visibility)):
+            # 'visible if'. In show-all mode, show everything.
+            if _show_all or \
+               (node.prompt and expr_value(node.prompt[1]) and not \
+                (node.item == MENU and not expr_value(node.visibility))):
+
                 res.append(node)
 
                 # If a node has children but doesn't have the is_menuconfig
                 # flag set, the children come from a submenu created implicitly
                 # from dependencies. Show those in this menu too.
                 if node.list and not node.is_menuconfig:
-                    res.extend(rec(node.list))
+                    rec(node.list)
 
             node = node.next
 
-        return res
-
-    return rec(menu.list)
+    rec(menu.list)
+    return res
 
 def _change_node(node):
     # Changes the value of the menu node 'node' if it is a symbol. Bools and
     # tristates are toggled, while other symbol types pop up a text entry
     # dialog.
 
-    global _cur_menu
-    global _visible
-    global _sel_node_i
-    global _menu_scroll
-
     if not isinstance(node.item, (Symbol, Choice)):
         return
 
+    # This will hit for invisible symbols in show-all mode
+    if not (node.prompt and expr_value(node.prompt[1])):
+        return
+
     # sc = symbol/choice
     sc = node.item
 
@@ -813,34 +967,71 @@
                     s = "0x" + s
 
             if _check_validity(sc, s):
-                sc.set_value(s)
+                _set_val(sc, s)
                 break
 
     elif len(sc.assignable) == 1:
         # Handles choice symbols for choices in y mode, which are a special
         # case: .assignable can be (2,) while .tri_value is 0.
-        sc.set_value(sc.assignable[0])
+        _set_val(sc, sc.assignable[0])
 
     else:
         # Set the symbol to the value after the current value in
         # sc.assignable, with wrapping
         val_index = sc.assignable.index(sc.tri_value)
-        sc.set_value(
-            sc.assignable[(val_index + 1) % len(sc.assignable)])
+        _set_val(sc, sc.assignable[(val_index + 1) % len(sc.assignable)])
 
-    # Changing the value of the symbol might have changed what items in the
-    # current menu are visible. Recalculate the state.
+    _update_menu()
+
+def _set_node_tri_val(node, tri_val):
+    # Sets 'node' to 'tri_val', if that value can be assigned
+
+    if isinstance(node.item, (Symbol, Choice)) and \
+       tri_val in node.item.assignable:
+
+        _set_val(node.item, tri_val)
+
+def _set_val(sc, val):
+    # Wrapper around Symbol/Choice.set_value() for updating the menu state and
+    # _conf_changed
+
+    global _conf_changed
+
+    # Use the string representation of tristate values. This makes the format
+    # consistent for all symbol types.
+    if val in TRI_TO_STR:
+        val = TRI_TO_STR[val]
+
+    if val != sc.str_value:
+        sc.set_value(val)
+        _conf_changed = True
+
+        # Changing the value of the symbol might have changed what items in the
+        # current menu are visible. Recalculate the state.
+        _update_menu()
+
+def _update_menu():
+    # Updates the current menu after the value of a symbol or choice has been
+    # changed. Changing a value might change which items in the menu are
+    # visible.
+    #
+    # Tries to preserve the location of the cursor when items disappear above
+    # it.
+
+    global _shown
+    global _sel_node_i
+    global _menu_scroll
 
     # Row on the screen the cursor was on
     old_row = _sel_node_i - _menu_scroll
 
-    sel_node = _visible[_sel_node_i]
+    sel_node = _shown[_sel_node_i]
 
     # New visible nodes
-    _visible = _visible_nodes(_cur_menu)
+    _shown = _shown_nodes(_cur_menu)
 
     # New index of selected node
-    _sel_node_i = _visible.index(sel_node)
+    _sel_node_i = _shown.index(sel_node)
 
     # Try to make the cursor stay on the same row in the menu window. This
     # might be impossible if too many nodes have disappeared above the node.
@@ -877,21 +1068,10 @@
     hscroll = 0
 
     while True:
-        # Width of input field
-        edit_width = win.getmaxyx()[1] - 4
-
-        # Adjust horizontal scroll if the cursor would be outside the input
-        # field
-        if i < hscroll:
-            hscroll = i
-        elif i >= hscroll + edit_width:
-            hscroll = i - edit_width + 1
-
         # Draw the "main" display with the menu, etc., so that resizing still
         # works properly. This is like a stack of windows, only hardcoded for
         # now.
         _draw_main()
-
         _draw_input_dialog(win, title, info_text, s, i, hscroll)
         curses.doupdate()
 
@@ -910,43 +1090,10 @@
         if c == curses.KEY_RESIZE:
             # Resize the main display too. The dialog floats above it.
             _resize_main()
-
             _resize_input_dialog(win, title, info_text)
 
-        elif c == curses.KEY_LEFT:
-            if i > 0:
-                i -= 1
-
-        elif c == curses.KEY_RIGHT:
-            if i < len(s):
-                i += 1
-
-        elif c in (curses.KEY_HOME, "\x01"):  # \x01 = CTRL-A
-            i = 0
-
-        elif c in (curses.KEY_END, "\x05"):  # \x05 = CTRL-E
-            i = len(s)
-
-        elif c in (curses.KEY_BACKSPACE, _ERASE_CHAR):
-            if i > 0:
-                s = s[:i-1] + s[i:]
-                i -= 1
-
-        elif c == curses.KEY_DC:
-            s = s[:i] + s[i+1:]
-
-        elif c == "\x0B":  # \x0B = CTRL-K
-            s = s[:i]
-
-        elif c == "\x15":  # \x15 = CTRL-U
-            s = s[i:]
-            i = 0
-
-        elif isinstance(c, str):
-            # Insert character
-
-            s = s[:i] + c + s[i:]
-            i += 1
+        else:
+            s, i, hscroll = _edit_text(c, s, i, hscroll, win.getmaxyx()[1] - 4)
 
 def _resize_input_dialog(win, title, info_text):
     # Resizes the input dialog to a size appropriate for the terminal size
@@ -994,6 +1141,10 @@
     #
     # description:
     #   String describing the thing being saved
+    #
+    # Return value:
+    #   Returns True if the configuration was saved, and False if the user
+    #   canceled the dialog
 
     filename = default_filename
     while True:
@@ -1001,10 +1152,12 @@
             "Filename to save {} to".format(description),
             filename)
 
-        if filename is None or \
-           _try_save(save_fn, filename, description):
+        if filename is None:
+            return False
 
-            return
+        if _try_save(save_fn, filename, description):
+            _msg("Success", "{} saved to {}".format(description, filename))
+            return True
 
 def _try_save(save_fn, filename, description):
     # Tries to save a file. Pops up an error and returns False on failure.
@@ -1051,7 +1204,6 @@
     while True:
         # See _input_dialog()
         _draw_main()
-
         _draw_key_dialog(win, title, text)
         curses.doupdate()
 
@@ -1065,7 +1217,6 @@
         if c == curses.KEY_RESIZE:
             # Resize the main display too. The dialog floats above it.
             _resize_main()
-
             _resize_key_dialog(win, text)
 
         elif isinstance(c, str):
@@ -1096,11 +1247,6 @@
 
     win.noutrefresh()
 
-def _error(text):
-    # Pops up an error dialog that can be dismissed with Space/Enter/ESC
-
-    _key_dialog("Error", text, " \n")
-
 def _draw_frame(win, title):
     # Draw a frame around the inner edges of 'win', with 'title' at the top
 
@@ -1121,35 +1267,308 @@
 
     win.attroff(_DIALOG_FRAME_STYLE)
 
-def _display_info(node):
+def _jump_to_dialog():
+    # Search text
+    s = ""
+    # Previous search text
+    prev_s = None
+    # Search text cursor position
+    s_i = 0
+    # Horizontal scroll offset
+    hscroll = 0
+
+    # Index of selected row
+    sel_node_i = 0
+    # Index in 'matches' of the top row of the list
+    scroll = 0
+
+    # Edit box at the top
+    edit_box = _styled_win(_INPUT_FIELD_STYLE)
+    edit_box.keypad(True)
+
+    # List of matches
+    matches_win = _styled_win(_LIST_STYLE)
+
+    # Bottom separator, with arrows pointing down
+    bot_sep_win = _styled_win(_SEPARATOR_STYLE)
+
+    # Help window with instructions at the bottom
+    help_win = _styled_win(_HELP_STYLE)
+
+    # Give windows their initial size
+    _resize_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win,
+                           sel_node_i, scroll)
+
+    _safe_curs_set(2)
+
+    # Defined symbols sorted by name, with duplicates removed
+    sorted_syms = sorted(set(_kconf.defined_syms), key=lambda sym: sym.name)
+
+    # TODO: Code duplication with _select_{next,prev}_menu_entry(). Can this be
+    # factored out in some nice way?
+
+    def select_next_match():
+        nonlocal sel_node_i
+        nonlocal scroll
+
+        if sel_node_i < len(matches) - 1:
+            sel_node_i += 1
+
+            if sel_node_i >= scroll + matches_win.getmaxyx()[0] - _SCROLL_OFFSET:
+                scroll = min(scroll + 1, _max_scroll(matches, matches_win))
+
+    def select_prev_match():
+        nonlocal sel_node_i
+        nonlocal scroll
+
+        if sel_node_i > 0:
+           sel_node_i -= 1
+
+           if sel_node_i <= scroll + _SCROLL_OFFSET:
+               scroll = max(scroll - 1, 0)
+
+    while True:
+        if s != prev_s:
+            # The search text changed. Find new matching nodes.
+
+            prev_s = s
+
+            try:
+                re_search = re.compile(s, re.IGNORECASE).search
+
+                # No exception thrown, so the regex is okay
+                bad_re = None
+
+                # 'matches' holds a list of matching menu nodes.
+
+                # This is a bit faster than the loop equivalent. At a high
+                # level, the syntax of list comprehensions is
+                # [<item> <loop template>].
+                matches = [node
+                           for sym in sorted_syms
+                               if re_search(sym.name)
+                                   for node in sym.nodes]
+
+            except re.error as e:
+                # Bad regex. Remember the error message so we can show it.
+                bad_re = e.msg
+                matches = []
+
+            # Reset scroll and jump to the top of the list of matches
+            sel_node_i = scroll = 0
+
+        _draw_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win,
+                             s, s_i, hscroll,
+                             bad_re, matches, sel_node_i, scroll)
+        curses.doupdate()
+
+
+        c = _get_wch_compat(edit_box)
+
+        if c == "\n":
+            if not matches:
+                continue
+
+            _jump_to(matches[sel_node_i])
+
+            _safe_curs_set(0)
+            # Resize the main display before returning in case the terminal was
+            # resized while the search dialog was open
+            _resize_main()
+            return
+
+        if c == "\x1B":  # \x1B = ESC
+            _safe_curs_set(0)
+            _resize_main()
+            return
+
+
+        if c == curses.KEY_RESIZE:
+            # No need to call _resize_main(), because the search window is
+            # fullscreen.
+
+            # We adjust the scroll so that the selected node stays visible in
+            # the list when the terminal is resized, hence the 'scroll'
+            # assignment
+            scroll = _resize_jump_to_dialog(
+                edit_box, matches_win, bot_sep_win, help_win,
+                sel_node_i, scroll)
+
+        elif c == curses.KEY_DOWN:
+            select_next_match()
+
+        elif c == curses.KEY_UP:
+            select_prev_match()
+
+        elif c == curses.KEY_NPAGE:  # Page Down
+            # Keep it simple. This way we get sane behavior for small windows,
+            # etc., for free.
+            for _ in range(_PG_JUMP):
+                select_next_match()
+
+        elif c == curses.KEY_PPAGE:  # Page Up
+            for _ in range(_PG_JUMP):
+                select_prev_match()
+
+        else:
+            s, s_i, hscroll = _edit_text(c, s, s_i, hscroll,
+                                         edit_box.getmaxyx()[1] - 2)
+
+def _resize_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win,
+                           sel_node_i, scroll):
+    # Resizes the jump-to dialog to fill the terminal.
+    #
+    # Returns the new scroll index. We adjust the scroll if needed so that the
+    # selected node stays visible.
+
+    screen_height, screen_width = stdscr.getmaxyx()
+
+    bot_sep_win.resize(1, screen_width)
+
+    help_win_height = len(_JUMP_TO_HELP_LINES)
+    matches_win_height = screen_height - help_win_height - 4
+
+    if matches_win_height >= 1:
+        edit_box.resize(3, screen_width)
+        matches_win.resize(matches_win_height, screen_width)
+        help_win.resize(help_win_height, screen_width)
+
+        matches_win.mvwin(3, 0)
+        bot_sep_win.mvwin(3 + matches_win_height, 0)
+        help_win.mvwin(3 + matches_win_height + 1, 0)
+    else:
+        # Degenerate case. Give up on nice rendering and just prevent errors.
+
+        matches_win_height = 1
+
+        edit_box.resize(screen_height, screen_width)
+        matches_win.resize(1, screen_width)
+        help_win.resize(1, screen_width)
+
+        for win in matches_win, bot_sep_win, help_win:
+            win.mvwin(0, 0)
+
+    # Adjust the scroll so that the selected row is still within the window, if
+    # needed
+    if sel_node_i - scroll >= matches_win_height:
+        return sel_node_i - matches_win_height + 1
+    return scroll
+
+def _draw_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win,
+                         s, s_i, hscroll,
+                         bad_re, matches, sel_node_i, scroll):
+    edit_width = edit_box.getmaxyx()[1] - 2
+
+
+    #
+    # Update list of matches
+    #
+
+    matches_win.erase()
+
+    if bad_re is not None:
+        # bad_re holds the error message from the re.error exception on errors
+        _safe_addstr(matches_win, 0, 0,
+                     "Bad regular expression: " + bad_re)
+    elif not matches:
+        _safe_addstr(matches_win, 0, 0, "No matches")
+    else:
+        for i in range(scroll,
+                       min(scroll + matches_win.getmaxyx()[0], len(matches))):
+            style = _LIST_SEL_STYLE if i == sel_node_i else _LIST_STYLE
+
+            sym = matches[i].item
+
+            s2 = sym.name
+            if len(sym.nodes) > 1:
+                # Give menu locations as well for symbols that are defined in
+                # multiple locations. The different menu locations will be
+                # listed next to one another.
+                s2 += " (in menu {})".format(
+                    _parent_menu(matches[i]).prompt[0])
+
+            _safe_addstr(matches_win, i - scroll, 0, s2, style)
+
+    matches_win.noutrefresh()
+
+
+    #
+    # Update bottom separator line
+    #
+
+    bot_sep_win.erase()
+
+    # Draw arrows pointing down if the symbol list is scrolled up
+    if scroll < _max_scroll(matches, matches_win):
+        _safe_hline(bot_sep_win, 0, 4, curses.ACS_DARROW, _N_SCROLL_ARROWS)
+
+    bot_sep_win.noutrefresh()
+
+
+    #
+    # Update help window at bottom
+    #
+
+    help_win.erase()
+
+    for i, line in enumerate(_JUMP_TO_HELP_LINES):
+        _safe_addstr(help_win, i, 0, line)
+
+    help_win.noutrefresh()
+
+
+    #
+    # Update edit box. We do this last since it makes it handy to position the
+    # cursor.
+    #
+
+    edit_box.erase()
+
+    _draw_frame(edit_box, "Jump to symbol")
+
+    # Draw arrows pointing up if the symbol list is scrolled down
+    if scroll > 0:
+        # TODO: Bit ugly that _DIALOG_FRAME_STYLE is repeated here
+        _safe_hline(edit_box, 2, 4, curses.ACS_UARROW, _N_SCROLL_ARROWS,
+                    _DIALOG_FRAME_STYLE)
+
+    # Note: Perhaps having a separate window for the input field would be nicer
+    visible_s = s[hscroll:hscroll + edit_width]
+    _safe_addstr(edit_box, 1, 1, visible_s, _INPUT_FIELD_STYLE)
+
+    _safe_move(edit_box, 1, 1 + s_i - hscroll)
+
+    edit_box.noutrefresh()
+
+def _info_dialog(node):
     # Shows a fullscreen window with information about 'node'
 
     # Top row, with title and arrows point up
-    top_line_win = _styled_win(_INFO_TOP_LINE_STYLE)
+    top_line_win = _styled_win(_SEPARATOR_STYLE)
 
     # Text display
     text_win = _styled_win(_INFO_TEXT_STYLE)
     text_win.keypad(True)
 
     # Bottom separator, with arrows pointing down
-    bot_sep_win = _styled_win(_INFO_BOT_SEP_STYLE)
+    bot_sep_win = _styled_win(_SEPARATOR_STYLE)
 
     # Help window with keys at the bottom
-    help_win = _styled_win(_INFO_HELP_STYLE)
+    help_win = _styled_win(_HELP_STYLE)
 
     # Give windows their initial size
-    _resize_info_display(top_line_win, text_win, bot_sep_win, help_win)
+    _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win)
 
 
     # Get lines of help text
-    lines = _info(node).split("\n")
+    lines = _info_str(node).split("\n")
 
     # Index of first row in 'lines' to show
     scroll = 0
 
     while True:
-        _draw_info_display(node, lines, scroll, top_line_win, text_win,
-                           bot_sep_win, help_win)
+        _draw_info_dialog(node, lines, scroll, top_line_win, text_win,
+                          bot_sep_win, help_win)
         curses.doupdate()
 
 
@@ -1158,20 +1577,20 @@
         if c == curses.KEY_RESIZE:
             # No need to call _resize_main(), because the help window is
             # fullscreen
-            _resize_info_display(top_line_win, text_win, bot_sep_win, help_win)
+            _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win)
 
         elif c in (curses.KEY_DOWN, "j", "J"):
-            if scroll < _max_info_scroll(text_win, lines):
+            if scroll < _max_scroll(lines, text_win):
                 scroll += 1
 
         elif c in (curses.KEY_NPAGE, "\x04"):  # Page Down/Ctrl-D
-            scroll = min(scroll + _PG_JUMP, _max_info_scroll(text_win, lines))
+            scroll = min(scroll + _PG_JUMP, _max_scroll(lines, text_win))
 
         elif c in (curses.KEY_PPAGE, "\x15"):  # Page Up/Ctrl-U
             scroll = max(scroll - _PG_JUMP, 0)
 
         elif c in (curses.KEY_END, "G"):
-            scroll = _max_info_scroll(text_win, lines)
+            scroll = _max_scroll(lines, text_win)
 
         elif c in (curses.KEY_HOME, "g"):
             scroll = 0
@@ -1184,15 +1603,14 @@
                    "\x1B",  # \x1B = ESC
                    "q", "Q", "h", "H"):
 
-            # Resize the main display before returning so that it gets the
-            # right size in case the terminal was resized while the help
-            # display was open
+            # Resize the main display before returning in case the terminal was
+            # resized while the help dialog was open
             _resize_main()
 
             return
 
-def _resize_info_display(top_line_win, text_win, bot_sep_win, help_win):
-    # Resizes the help display to a size appropriate for the terminal size
+def _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win):
+    # Resizes the info dialog to fill the terminal
 
     screen_height, screen_width = stdscr.getmaxyx()
 
@@ -1218,8 +1636,8 @@
         for win in text_win, bot_sep_win, help_win:
             win.mvwin(0, 0)
 
-def _draw_info_display(node, lines, scroll, top_line_win, text_win,
-                       bot_sep_win, help_win):
+def _draw_info_dialog(node, lines, scroll, top_line_win, text_win,
+                      bot_sep_win, help_win):
 
     text_win_height, text_win_width = text_win.getmaxyx()
 
@@ -1274,7 +1692,7 @@
     bot_sep_win.erase()
 
     # Draw arrows pointing down if the symbol window is scrolled up
-    if scroll < _max_info_scroll(text_win, lines):
+    if scroll < _max_scroll(lines, text_win):
         _safe_hline(bot_sep_win, 0, 4, curses.ACS_DARROW, _N_SCROLL_ARROWS)
 
     bot_sep_win.noutrefresh()
@@ -1291,13 +1709,7 @@
 
     help_win.noutrefresh()
 
-def _max_info_scroll(text_win, lines):
-    # Returns the maximum amount the information display can be scrolled down.
-    # We stop scrolling when the last line of the help text is visible.
-
-    return max(0, len(lines) - text_win.getmaxyx()[0])
-
-def _info(node):
+def _info_str(node):
     # Returns information about the menu node 'node' as a string.
     #
     # The helper functions are responsible for adding newlines. This allows
@@ -1398,14 +1810,15 @@
 
     s = "Defaults:\n"
 
-    for value, cond in sc.defaults:
+    for val, cond in sc.defaults:
         s += "  - "
         if isinstance(sc, Symbol):
-            s += _expr_str(value)
+            s += '{} (value: "{}")' \
+                 .format(_expr_str(val), TRI_TO_STR[expr_value(val)])
         else:
             # Don't print the value next to the symbol name for choice
             # defaults, as it looks a bit confusing
-            s += value.name
+            s += val.name
         s += "\n"
 
         if cond is not _kconf.y:
@@ -1511,6 +1924,93 @@
     win.bkgdset(" ", style)
     return win
 
+def _max_scroll(lst, win):
+    # Assuming 'lst' is a list of items to be displayed in 'win',
+    # returns the maximum number of steps 'win' can be scrolled down.
+    # We stop scrolling when the bottom item is visible.
+
+    return max(0, len(lst) - win.getmaxyx()[0])
+
+def _edit_text(c, s, i, hscroll, width):
+    # Implements text editing commands for edit boxes. Takes a character (which
+    # could also be e.g. curses.KEY_LEFT) and the edit box state, and returns
+    # the new state after the character has been processed.
+    #
+    # c:
+    #   Character from user
+    #
+    # s:
+    #   Current contents of string
+    #
+    # i:
+    #   Current cursor index in string
+    #
+    # hscroll:
+    #   Index in s of the leftmost character in the edit box, for horizontal
+    #   scrolling
+    #
+    # width:
+    #   Width in characters of the edit box
+    #
+    # Return value:
+    #   An (s, i, hscroll) tuple for the new state
+
+    if c == curses.KEY_LEFT:
+        if i > 0:
+            i -= 1
+
+    elif c == curses.KEY_RIGHT:
+        if i < len(s):
+            i += 1
+
+    elif c in (curses.KEY_HOME, "\x01"):  # \x01 = CTRL-A
+        i = 0
+
+    elif c in (curses.KEY_END, "\x05"):  # \x05 = CTRL-E
+        i = len(s)
+
+    elif c in (curses.KEY_BACKSPACE, _ERASE_CHAR):
+        if i > 0:
+            s = s[:i-1] + s[i:]
+            i -= 1
+
+    elif c == curses.KEY_DC:
+        s = s[:i] + s[i+1:]
+
+    elif c == "\x0B":  # \x0B = CTRL-K
+        s = s[:i]
+
+    elif c == "\x15":  # \x15 = CTRL-U
+        s = s[i:]
+        i = 0
+
+    elif isinstance(c, str):
+        # Insert character
+
+        s = s[:i] + c + s[i:]
+        i += 1
+
+
+    # Adjust the horizontal scroll if the cursor would be outside the input
+    # field
+    if i < hscroll:
+        hscroll = i
+    elif i >= hscroll + width:
+        hscroll = i - width + 1
+
+
+    return s, i, hscroll
+
+def _msg(title, text):
+    # Pops up a message dialog that can be dismissed with Space/Enter/ESC
+
+    _key_dialog(title, text, " \n")
+
+def _error(text):
+    # Pops up an error dialog that can be dismissed with Space/Enter/ESC
+
+    _msg("Error", text)
+
 def _node_str(node):
     # Returns the complete menu entry text for a menu node.
     #
@@ -1528,7 +2028,11 @@
     # This approach gives nice alignment for empty string symbols ("()  Foo")
     s = "{:{}} ".format(_value_str(node), 3 + indent)
 
-    if node.prompt:
+    if not node.prompt:
+        # Show the symbol/choice name in <> brackets if it has no prompt. This
+        # path can only hit in show-all mode.
+        s += "<{}>".format(node.item.name)
+    else:
         if node.item == COMMENT:
             s += "*** {} ***".format(node.prompt[0])
         else:
@@ -1557,7 +2061,7 @@
     # entered. Add "(empty)" if the menu is empty. We don't allow those to be
     # entered.
     if node.is_menuconfig:
-        s += "  --->" if _visible_nodes(node) else "  ---> (empty)"
+        s += "  --->" if _shown_nodes(node) else "  ---> (empty)"
 
     return s